Texture resource¶
change a sprite’s image at runtime.
▶ Run in browserTags: 2d
A plain path, PNG bytes or RGBA NumPy array is the right way to hand a sprite an image that never changes. When the image does change, there are three routes, shown here side by side:
Reassign the sprite’s
textureto another source:sprite.texture = other_image. The sprite resolves the new source and draws it on the next frame.Edit the pixels in place behind a
Textureand callupdate():pixels = rings(0.0) tex = Texture(pixels) ... pixels[:] = rings(t) tex.update()
Editing the array alone changes nothing on screen, because the renderer keeps the upload it already made.
update()bumps the texture’sversion, and the renderer writes the new pixels into the same GPU slot, so nothing is reassigned.Swap the source with
tex.update(new_pixels), the shorter route when each frame is a fresh array rather than an edit. The new array must be the same size: a texture’ssizeis fixed, and a bigger image is a newTextureassigned to the sprite, which is the first route.
The left panel flips between two checkerboards, the middle one animates rings
by editing its array, and the right one by handing update() a new array. The
footer shows the middle texture’s version climbing. Press SPACE, click or tap
to pause and resume: the panels keep their last frame.
Source¶
1# /// simvx
2# web = { width = 900, height = 620 }
3# ///
4"""Texture resource: change a sprite's image at runtime.
5
6A plain path, PNG bytes or RGBA NumPy array is the right way to hand a sprite an
7image that never changes. When the image does change, there are three routes,
8shown here side by side:
9
10- **Reassign** the sprite's `texture` to another source:
11 `sprite.texture = other_image`. The sprite resolves the new source and draws
12 it on the next frame.
13- **Edit the pixels in place** behind a `Texture` and call `update()`:
14
15 pixels = rings(0.0)
16 tex = Texture(pixels)
17 ...
18 pixels[:] = rings(t)
19 tex.update()
20
21 Editing the array alone changes nothing on screen, because the renderer keeps
22 the upload it already made. `update()` bumps the texture's `version`, and the
23 renderer writes the new pixels into the same GPU slot, so nothing is
24 reassigned.
25- **Swap the source** with `tex.update(new_pixels)`, the shorter route when each
26 frame is a fresh array rather than an edit. The new array must be the same
27 size: a texture's `size` is fixed, and a bigger image is a new `Texture`
28 assigned to the sprite, which is the first route.
29
30The left panel flips between two checkerboards, the middle one animates rings
31by editing its array, and the right one by handing `update()` a new array. The
32footer shows the middle texture's `version` climbing. Press SPACE, click or tap
33to pause and resume: the panels keep their last frame.
34"""
35
36import numpy as np
37
38from simvx.core import Anchor, CanvasLayer, Key, Label, Node2D, Pointer, Sprite2D, Text2D, Texture, on_input
39from simvx.graphics import App
40
41SIZE = 64
42PANEL = 180
43
44
45def checker(a: tuple[int, int, int], b: tuple[int, int, int], cell: int = 8) -> np.ndarray:
46 ys, xs = np.mgrid[:SIZE, :SIZE]
47 even = ((xs // cell + ys // cell) % 2 == 0)[..., None]
48 return np.where(even, (*a, 255), (*b, 255)).astype(np.uint8)
49
50
51def rings(phase: float) -> np.ndarray:
52 ys, xs = np.mgrid[:SIZE, :SIZE]
53 band = (np.sin(np.hypot(xs - SIZE / 2, ys - SIZE / 2) * 0.5 - phase) * 0.5 + 0.5) ** 2
54 img = np.empty((SIZE, SIZE, 4), dtype=np.uint8)
55 img[..., 0] = band * 240
56 img[..., 1] = band * 120
57 img[..., 2] = (1.0 - band) * 220
58 img[..., 3] = 255
59 return img
60
61
62class TextureResourceScene(Node2D):
63 input_context = {"pause": [Key.SPACE, Pointer.PRIMARY]}
64
65 def on_ready(self):
66 self.time = 0.0
67 self.running = True
68
69 # Reassignment: two plain arrays, swapped on the sprite's property.
70 self.sources = (checker((235, 90, 70), (40, 40, 55)), checker((70, 200, 130), (25, 45, 40)))
71 self.shown = 0
72 self.reassigned = Sprite2D(
73 texture=self.sources[0], width=PANEL, height=PANEL, filter="nearest", name="Reassigned"
74 )
75
76 # In-place edit: one Texture wrapping one array that is rewritten every frame.
77 self.pixels = rings(0.0)
78 self.live = Texture(self.pixels)
79 self.mutated = Sprite2D(texture=self.live, width=PANEL, height=PANEL, name="Mutated")
80
81 # Source swap: one Texture handed a fresh array every frame.
82 self.swapped_tex = Texture(rings(0.0))
83 self.swapped = Sprite2D(texture=self.swapped_tex, width=PANEL, height=PANEL, name="Swapped")
84
85 captions = ("sprite.texture = other_image", "pixels[:] = ...; tex.update()", "tex.update(new_pixels)")
86 self.panels = (self.reassigned, self.mutated, self.swapped)
87 for sprite, caption in zip(self.panels, captions, strict=True):
88 self.add_child(sprite)
89 sprite.add_child(Text2D(text=caption, position=(0, PANEL / 2 + 30), align="centre"))
90
91 hud = self.add_child(CanvasLayer())
92 hud.add_child(
93 Label("Texture resource: SPACE, click or tap to pause", font_size=24, anchor=Anchor.TOP_LEFT, inset=16)
94 )
95 self.status = hud.add_child(Label("", anchor=Anchor.BOTTOM_LEFT, inset=16))
96
97 @on_input("pause")
98 def toggle_pause(self, event):
99 self.running = not self.running
100
101 def on_update(self, dt: float):
102 w, h = self.viewport_size
103 gap = (w - 3 * PANEL) / 4
104 for i, sprite in enumerate(self.panels):
105 sprite.position = (gap * (i + 1) + PANEL * (i + 0.5), h * 0.42)
106
107 if not self.running:
108 return
109 self.time += dt
110
111 shown = int(self.time * 1.5) % 2
112 if shown != self.shown:
113 self.shown = shown
114 self.reassigned.texture = self.sources[shown]
115
116 self.pixels[:] = rings(self.time * 4.0)
117 self.live.update()
118
119 self.swapped_tex.update(rings(-self.time * 2.5))
120
121 self.status.text = f"live version: {self.live.version} size: {tuple(self.live.size)}"
122
123
124if __name__ == "__main__":
125 App(width=900, height=620, title="Texture Resource").run(TextureResourceScene())