Pixelgrafik für die Dinge der Vitrine

Toter Boden hat Risse, lebendiger Boden Gras, Blumen und einen
unregelmäßigen Rand. Moos funkelt, die Ranke trägt Blätter, die
Faserpflanze wächst in sechs Stufen bis zu den Fasern und wiegt sich,
das Weidetier zieht im Sprung die Beine an, und die Ruine leuchtet,
wenn sie erwacht.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-10-02 13:29:32 +02:00
parent d189a358cd
commit deba758e87
3 changed files with 387 additions and 0 deletions
+327
View File
@@ -14,6 +14,8 @@ const TILE := 16
const FACE := 6
## Images of a burst of chips, from the hit to its end.
const HIT_FRAMES := 6
## Stages of a growing plant, from the seed to the fibres.
const GROWTH_STAGES := 6
## Size of a figure: a tile wide and one and a half tiles tall.
const FIGURE_SIZE := Vector2i(16, 24)
const CLEAR := Color(0, 0, 0, 0)
@@ -25,6 +27,9 @@ var players: Array[Color] = []
## True for outlines in a darker shade of what they surround, false for one
## dark outline colour.
var tinted_outlines := false
## How strongly living things glow, from 0 (not at all) to 1; the subclass
## chooses it.
var glow := 0.0
var _textures: Dictionary[String, Texture2D] = {}
@@ -471,3 +476,325 @@ func _figure_image(colour: int, frame: int, left: bool) -> Pixels:
if left:
pixels.image.flip_x()
return pixels
# Things of later milestones, shown in the showcase.
func _dead_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var shadow: int = state.get("solid", 0) & (TileShape.N | TileShape.W | TileShape.NW)
var variant: int = state.get("variant", 0) % 8
var key := "dead/%d/%d" % [shadow, variant]
_put(canvas, _image(key, func() -> Pixels: return _crack_soil(_earth(variant, shadow), variant)), rect.position)
## Dry cracks in every other tile of dead soil. Returns `pixels`.
func _crack_soil(pixels: Pixels, variant: int) -> Pixels:
if variant % 2 == 0:
var at := _spot(12, variant, 3, 7)
for step in 5:
pixels.put(at.x, at.y, _c("dead_crack"))
if step == 2:
pixels.put(at.x, at.y + 1, _c("dead_crack"))
pixels.put(at.x - 1, at.y + 2, _c("dead_crack"))
at += Vector2i(1, -1 if _noise(step, 5, variant) < 0.4 else 0)
return pixels
func _living_soil(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var living := _shape(state.get("living", 0))
var variant: int = state.get("variant", 0) % 6
_ground(canvas, rect, {"variant": state.get("variant", 0)})
_put(canvas, _image("living/%d/%d" % [living, variant], func() -> Pixels: return _grass_image(living, variant)), rect.position)
## Grass with blades and a few flowers, with a soft edge toward the sides
## that are not living.
func _grass_image(living: int, variant: int) -> Pixels:
var open_n := (living & TileShape.N) == 0
var open_e := (living & TileShape.E) == 0
var open_s := (living & TileShape.S) == 0
var open_w := (living & TileShape.W) == 0
var pixels := Pixels.new(TILE, TILE)
pixels.rect(0, 0, TILE, TILE, _c("grass"))
for tuft in 6:
var at := _spot(tuft + 10, variant, 2, 12)
pixels.put(at.x, at.y, _c("grass_dark"))
pixels.put(at.x - 1, at.y - 1, _c("grass_dark"))
pixels.put(at.x + 1, at.y - 1, _c("grass_dark"))
for blade in 4:
var at := _spot(blade + 20, variant, 1, 14)
pixels.put(at.x, at.y, _c("grass_light"))
if variant % 3 == 0:
var x := 4 + variant % 7
var y := 5 + variant % 5
var petal := _c("flower_b") if variant % 2 == 0 else _c("flower_c")
for side in Pixels.SIDES:
pixels.put(x + side.x, y + side.y, petal)
pixels.put(x, y, _c("flower_a"))
if glow > 0.0:
for spark in 2:
var light := _c("spore")
light.a = glow * 0.8
pixels.put(2 + int(_noise(spark, 21, variant) * 12.0), 2 + int(_noise(spark, 22, variant) * 12.0), light)
# A ragged edge toward bare ground.
for i in TILE:
var ragged := 1 if _noise(i, 23, variant) < 0.45 else 0
if open_n:
_edge(pixels, Vector2i(i, 0), Vector2i(0, 1), ragged)
if open_s:
_edge(pixels, Vector2i(i, TILE - 1), Vector2i(0, -1), ragged)
if open_w:
_edge(pixels, Vector2i(0, i), Vector2i(1, 0), ragged)
if open_e:
_edge(pixels, Vector2i(TILE - 1, i), Vector2i(-1, 0), ragged)
if open_n and open_w:
_round(pixels, Vector2i(0, 0), Vector2i(1, 1), _c("grass_edge"))
if open_n and open_e:
_round(pixels, Vector2i(TILE - 1, 0), Vector2i(-1, 1), _c("grass_edge"))
if open_s and open_w:
_round(pixels, Vector2i(0, TILE - 1), Vector2i(1, -1), _c("grass_edge"))
if open_s and open_e:
_round(pixels, Vector2i(TILE - 1, TILE - 1), Vector2i(-1, -1), _c("grass_edge"))
return pixels
## Clears `cut` pixels at `at` and draws the darker edge of the grass behind
## them, one step further in the direction `inward`.
func _edge(pixels: Pixels, at: Vector2i, inward: Vector2i, cut: int) -> void:
for depth in cut:
var gone := at + inward * depth
pixels.put(gone.x, gone.y, CLEAR)
var edge := at + inward * cut
pixels.put(edge.x, edge.y, _c("grass_edge"))
func _moss(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var variant: int = state.get("variant", 0) % 3
var twinkle := int(float(state.get("time", 0.0)) * 2.5) % 4
var foot := Vector2(rect.get_center().x, rect.end.y)
_put(canvas, _image("moss/%d/%d" % [variant, twinkle], func() -> Pixels: return _moss_image(variant, twinkle)), foot - Vector2(8, 13))
## A cushion of moss whose spores twinkle one after another.
func _moss_image(variant: int, twinkle: int) -> Pixels:
var pixels := Pixels.new(TILE, 13)
var shift := variant - 1
for bump: Vector4 in [Vector4(4.5 + shift, 9.5, 4, 3.2), Vector4(11, 9.2, 4.3, 3.5), Vector4(7.8 - shift, 6.8, 3.8, 3.4)]:
pixels.ellipse(bump.x, bump.y, bump.z, bump.w, _c("moss"))
for y in pixels.size.y:
for x in pixels.size.x:
if not pixels.filled(x, y):
continue
if y >= 11:
pixels.put(x, y, _c("moss_dark"))
elif not pixels.filled(x, y - 1) or not pixels.filled(x, y - 2):
pixels.put(x, y, _c("moss_light"))
pixels.outline(_ink(_c("moss")), tinted_outlines)
var spores: Array[Vector2i] = [Vector2i(4 + shift, 8), Vector2i(8 - shift, 5), Vector2i(12, 8)]
for index in spores.size():
var at := spores[index]
var bright := index == twinkle
var spore := _c("spore") if bright else _c("spore").darkened(0.2)
pixels.put(at.x, at.y, spore)
pixels.put(at.x + 1, at.y, spore)
if bright:
pixels.put(at.x, at.y - 1, _c("spore"))
if glow > 0.0:
var halo := _c("spore")
halo.a = 0.45 * glow
for side in Pixels.SIDES:
pixels.shade(at.x + side.x, at.y + side.y + 1, halo)
return pixels
func _vine(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var points: PackedVector2Array = state.get("points", PackedVector2Array())
if points.size() < 2:
return
var origin := (rect.position - Vector2(6, 6)).floor()
var size := Vector2i(ceili(rect.size.x) + 13, ceili(rect.size.y) + 13)
var key := "vine/%d/%d/%d" % [hash(points), size.x, size.y]
_put(canvas, _image(key, func() -> Pixels: return _vine_image(points, origin, size)), origin)
## A vine two pixels thick along `points`, with leaves on alternate sides.
func _vine_image(points: PackedVector2Array, origin: Vector2, size: Vector2i) -> Pixels:
var pixels := Pixels.new(size.x, size.y)
for i in points.size() - 1:
var from := Vector2i((points[i] - origin).round())
var to := Vector2i((points[i + 1] - origin).round())
pixels.line(from.x, from.y - 1, to.x, to.y - 1, _c("leaf"))
pixels.line(from.x, from.y, to.x, to.y, _c("vine"))
pixels.line(from.x, from.y + 1, to.x, to.y + 1, _c("vine_dark"))
var length := 0.0
for i in points.size() - 1:
length += points[i].distance_to(points[i + 1])
var leaves := int(length / 9.0)
for leaf in leaves:
var at := Vector2i((along(points, (leaf + 0.5) / leaves) - origin).round())
var up := leaf % 2 == 0
var dy := -2 if up else 2
var tip := -1 if up else 1
pixels.put(at.x, at.y + dy, _c("leaf"))
pixels.put(at.x + 1, at.y + dy, _c("leaf"))
pixels.put(at.x + 1, at.y + dy + tip, _c("leaf"))
pixels.put(at.x + 2, at.y + dy + tip, _c("leaf_dark"))
pixels.outline(_ink(_c("vine")), tinted_outlines)
return pixels
func _fiber_plant(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var stage := clampi(int(float(state.get("growth", 1.0)) * GROWTH_STAGES), 0, GROWTH_STAGES - 1)
var lean := clampi(roundi(sway(state.get("time", 0.0), 3) * 1.4), -1, 1)
var foot := Vector2(rect.get_center().x, rect.end.y)
_put(canvas, _image("plant/%d/%d" % [stage, lean], func() -> Pixels: return _plant_image(stage, lean)), foot - Vector2(8, 28))
## A fibre plant at growth stage `stage`, its top leaning by `lean` pixels:
## a seed in a small mound, a sprout, a stalk with leaves, buds and at last
## white tufts of fibre.
func _plant_image(stage: int, lean: int) -> Pixels:
var pixels := Pixels.new(TILE, 28)
pixels.rect(5, 26, 6, 1, _c("ground_dark"))
pixels.rect(4, 27, 8, 1, _c("ground_dark"))
if stage == 0:
pixels.put(7, 25, _c("seed"))
pixels.put(8, 25, _c("seed"))
pixels.outline(_ink(_c("seed")), tinted_outlines)
return pixels
var heights: Array[int] = [0, 4, 8, 13, 17, 19]
var height := heights[stage]
var top := Vector2i(8, 25)
for i in height:
var x := 8 + roundi(lean * 2.0 * i / 19.0)
top = Vector2i(x, 25 - i)
pixels.put(top.x, top.y, _c("stem"))
@warning_ignore("integer_division")
for pair in range(1, height / 3 + 1):
var y := 26 - pair * 3
var x := 8 + roundi(lean * 2.0 * (25 - y) / 19.0)
var side := 1 if pair % 2 == 0 else -1
pixels.put(x + side, y, _c("leaf"))
pixels.put(x + side * 2, y, _c("leaf"))
pixels.put(x + side * 2, y - 1, _c("leaf"))
pixels.put(x + side * 3, y - 1, _c("leaf_dark"))
pixels.put(x - side, y - 1, _c("leaf_dark"))
if stage == 1:
pixels.put(top.x - 1, top.y, _c("leaf"))
pixels.put(top.x + 1, top.y, _c("leaf"))
if stage == 4:
pixels.ellipse(top.x + 0.5, top.y - 0.5, 1.6, 1.6, _c("bud"))
if stage == 5:
for tuft: Vector3 in [Vector3(0, -2, 2.3), Vector3(-3, 1, 1.8), Vector3(3, 2, 1.8)]:
pixels.ellipse(top.x + 0.5 + tuft.x, top.y + tuft.y, tuft.z, tuft.z, _c("fibre"))
pixels.put(top.x + int(tuft.x), top.y + int(tuft.y) + 1, _c("fibre_shade"))
pixels.outline(_ink(_c("stem")), tinted_outlines)
return pixels
func _stone(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
_put(canvas, _image("stone", func() -> Pixels: return _stone_image()), Vector2(rect.get_center().x - 4, rect.end.y - 8))
func _stone_image() -> Pixels:
var pixels := Pixels.new(8, 8)
pixels.ellipse(4, 4.2, 3.2, 2.7, _c("stone"))
pixels.rect(2, 6, 4, 1, _c("stone_dark"))
pixels.put(2, 3, _c("stone_light"))
pixels.put(3, 2, _c("stone_light"))
pixels.outline(_ink(_c("stone")), tinted_outlines)
return pixels
func _grazer(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var height := roundi(sin(PI * clampf(state.get("hop", 0.0), 0.0, 1.0)) * 5.0)
var left: bool = float(state.get("facing", 1.0)) < 0.0
var foot := Vector2(rect.get_center().x, rect.end.y)
var width := 14 - height
_put(canvas, _image("shadow/%d" % width, func() -> Pixels: return _shadow_image(width)), foot - Vector2(width / 2.0, 2))
var tucked := height > 1
_put(canvas, _image("grazer/%s/%s" % [tucked, left], func() -> Pixels: return _grazer_image(tucked, left)), foot - Vector2(10, 16 + height))
## A woolly animal, facing right or, with `left`, left; in the air its legs
## are `tucked`.
func _grazer_image(tucked: bool, left: bool) -> Pixels:
var pixels := Pixels.new(20, 16)
for leg in [5, 7, 11, 13]:
pixels.rect(leg, 12, 1, 1 if tucked else 3, _c("hoof"))
for puff: Vector4 in [Vector4(9, 8, 6.5, 4.5), Vector4(5.5, 9.5, 4, 3.5), Vector4(12.5, 9.5, 4.5, 3.5), Vector4(8, 5.8, 4, 3), Vector4(11.5, 6.3, 3.5, 3)]:
pixels.ellipse(puff.x, puff.y, puff.z, puff.w, _c("wool"))
for y in pixels.size.y:
for x in pixels.size.x:
if pixels.at(x, y) == _c("wool"):
if y >= 11:
pixels.put(x, y, _c("wool_shade"))
elif not pixels.filled(x, y - 1) and _noise(x, y, 4) < 0.6:
pixels.put(x, y, _c("wool_light"))
pixels.ellipse(16.2, 8.0, 2.6, 3.2, _c("face"))
pixels.put(14, 5, _c("face"))
pixels.put(15, 5, _c("face"))
pixels.put(17, 7, _c("wool_light"))
pixels.outline(_ink(_c("wool")), tinted_outlines)
if left:
pixels.image.flip_x()
return pixels
func _ruin(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var awake := clampf(state.get("awake", 0.0), 0.0, 1.0)
_put(canvas, _image("ruin", func() -> Pixels: return _ruin_image()), rect.position)
_put(canvas, _image("ruin_glow", func() -> Pixels: return _ruin_glow()), rect.position, Color(1, 1, 1, awake))
## A gate of old stone blocks, three tiles wide, with moss on top and a dark
## doorway.
func _ruin_image() -> Pixels:
var pixels := Pixels.new(TILE * 3, TILE * 3)
var stone := _c("ruin")
pixels.rect(3, 12, 12, 34, stone)
pixels.rect(33, 12, 12, 34, stone)
pixels.rect(1, 6, 46, 9, stone)
pixels.rect(1, 3, 46, 3, _c("ruin_light"))
pixels.rect(15, 15, 18, 31, _c("door"))
pixels.rect(9, 44, 30, 2, _c("ruin_dark"))
pixels.rect(7, 46, 34, 2, stone)
# Blocks: a joint every five rows, and between the blocks of each row.
for course in 8:
var y := 8 + course * 5
for x in range(1, 47):
if pixels.at(x, y) == stone:
pixels.put(x, y, _c("ruin_dark"))
var offset := 0 if course % 2 == 0 else 4
for x in range(3 + offset, 47, 8):
for row in range(y + 1, y + 5):
if pixels.at(x, row) == stone:
pixels.put(x, row, _c("ruin_dark"))
for x in range(1, 47):
if _noise(x, 40, 1) < 0.45:
pixels.put(x, 3, _c("ruin_moss"))
if _noise(x, 41, 1) < 0.25:
pixels.put(x, 4, _c("ruin_moss"))
for strand: Vector2i in [Vector2i(5, 4), Vector2i(12, 3), Vector2i(36, 5), Vector2i(43, 2)]:
pixels.rect(strand.x, 15, 1, strand.y, _c("ruin_moss"))
pixels.outline(_ink(stone), tinted_outlines)
return pixels
## The light in the doorway and the runes above it, shown as the ruin wakes.
func _ruin_glow() -> Pixels:
var pixels := Pixels.new(TILE * 3, TILE * 3)
var center := Vector2(24, 31)
for y in range(15, 46):
for x in range(15, 33):
var distance := Vector2(x + 0.5, y + 0.5).distance_to(center)
var light := _c("glow_light") if distance < 4.0 else _c("glow")
light.a = clampf(1.0 - distance / 13.0, 0.0, 1.0) * (0.8 + 0.2 * glow)
if light.a > 0.05:
pixels.put(x, y, light)
for rune in 3:
var x := 16 + rune * 7
for dot: Vector2i in [Vector2i(0, 0), Vector2i(1, 0), Vector2i(1, 1), Vector2i(0, 2), Vector2i(1, 2)]:
pixels.put(x + dot.x + (rune % 2), 8 + dot.y, _c("glow"))
return pixels