d189a358cd
In zwei der vier Laufbilder hob sich der Körper um ein Pixel, und der dunkle Umriss oben am Kopf fiel aus dem Bild. Laufende Figuren flackerten dadurch am Kopf. Jetzt sinkt der Körper, solange die Beine gespreizt sind, und über dem Kopf bleibt immer eine Zeile frei. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
474 lines
18 KiB
GDScript
474 lines
18 KiB
GDScript
class_name PixelStyle
|
|
extends ArtStyle
|
|
## Pixel art in the manner of classic farming games, drawn in code. A tile
|
|
## is 16 by 16 pixels, seen a little from the front: rock shows its face
|
|
## toward open ground, and figures, animals and plants stand upright. Every
|
|
## graphic is made once as a small image, pixel by pixel from the palette,
|
|
## and kept; the camera enlarges each pixel to a crisp square.
|
|
##
|
|
## The subclasses choose the palette and the outlines.
|
|
|
|
## Pixels along the edge of a tile.
|
|
const TILE := 16
|
|
## Rows of the face that rock shows toward open ground below it.
|
|
const FACE := 6
|
|
## Images of a burst of chips, from the hit to its end.
|
|
const HIT_FRAMES := 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)
|
|
|
|
## Colours by name; the subclass fills them.
|
|
var palette: Dictionary[String, Color] = {}
|
|
## The colours of the players, in the order of their numbers.
|
|
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
|
|
var _textures: Dictionary[String, Texture2D] = {}
|
|
|
|
|
|
## A small image that the drawings put their pixels on.
|
|
class Pixels:
|
|
const SIDES: Array[Vector2i] = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]
|
|
const CLEAR := Color(0, 0, 0, 0)
|
|
|
|
var image: Image
|
|
var size: Vector2i
|
|
|
|
func _init(width: int, height: int) -> void:
|
|
size = Vector2i(width, height)
|
|
image = Image.create_empty(width, height, false, Image.FORMAT_RGBA8)
|
|
|
|
func has(x: int, y: int) -> bool:
|
|
return x >= 0 and y >= 0 and x < size.x and y < size.y
|
|
|
|
func put(x: int, y: int, color: Color) -> void:
|
|
if has(x, y):
|
|
image.set_pixel(x, y, color)
|
|
|
|
## The colour at x, y, transparent outside the image.
|
|
func at(x: int, y: int) -> Color:
|
|
return image.get_pixel(x, y) if has(x, y) else CLEAR
|
|
|
|
func filled(x: int, y: int) -> bool:
|
|
return at(x, y).a > 0.0
|
|
|
|
## Lays the translucent `color` over the pixel at x, y.
|
|
func shade(x: int, y: int, color: Color) -> void:
|
|
if has(x, y):
|
|
image.set_pixel(x, y, image.get_pixel(x, y).blend(color))
|
|
|
|
func rect(x: int, y: int, width: int, height: int, color: Color) -> void:
|
|
for row in range(y, y + height):
|
|
for column in range(x, x + width):
|
|
put(column, row, color)
|
|
|
|
## Fills the ellipse around cx, cy with the half axes rx and ry.
|
|
func ellipse(cx: float, cy: float, rx: float, ry: float, color: Color) -> void:
|
|
for row in range(floori(cy - ry), ceili(cy + ry) + 1):
|
|
for column in range(floori(cx - rx), ceili(cx + rx) + 1):
|
|
var dx := (column + 0.5 - cx) / rx
|
|
var dy := (row + 0.5 - cy) / ry
|
|
if dx * dx + dy * dy <= 1.0:
|
|
put(column, row, color)
|
|
|
|
## A line of single pixels from x0, y0 to x1, y1.
|
|
func line(x0: int, y0: int, x1: int, y1: int, color: Color) -> void:
|
|
var dx := absi(x1 - x0)
|
|
var dy := -absi(y1 - y0)
|
|
var sx := 1 if x0 < x1 else -1
|
|
var sy := 1 if y0 < y1 else -1
|
|
var error := dx + dy
|
|
var x := x0
|
|
var y := y0
|
|
while true:
|
|
put(x, y, color)
|
|
if x == x1 and y == y1:
|
|
return
|
|
var twice := 2 * error
|
|
if twice >= dy:
|
|
error += dy
|
|
x += sx
|
|
if twice <= dx:
|
|
error += dx
|
|
y += sy
|
|
|
|
## Surrounds everything solid drawn so far with a line one pixel wide: in
|
|
## `color`, or, with `tinted`, in a darker shade of the pixel beside it.
|
|
func outline(color: Color, tinted: bool) -> void:
|
|
var found: Array[Vector3i] = []
|
|
var colors: Array[Color] = []
|
|
for y in size.y:
|
|
for x in size.x:
|
|
if filled(x, y):
|
|
continue
|
|
for side in SIDES:
|
|
var beside := at(x + side.x, y + side.y)
|
|
if beside.a >= 1.0:
|
|
found.append(Vector3i(x, y, 0))
|
|
colors.append(beside.darkened(0.5) if tinted else color)
|
|
break
|
|
for index in found.size():
|
|
put(found[index].x, found[index].y, colors[index])
|
|
|
|
|
|
func player_color(player: int) -> Color:
|
|
return players[maxi(0, player) % players.size()]
|
|
|
|
|
|
func _c(name: String) -> Color:
|
|
return palette[name]
|
|
|
|
|
|
## The dark line around things, or around a thing of colour `fill` when
|
|
## outlines are tinted.
|
|
func _ink(fill: Color) -> Color:
|
|
return fill.darkened(0.5) if tinted_outlines else _c("outline")
|
|
|
|
|
|
## A number from 0 to 1 that looks random and is the same for the same
|
|
## arguments.
|
|
static func _noise(x: int, y: int, salt: int) -> float:
|
|
return float(TileShape.variant(Vector2i(x * 31 + salt * 977, y * 17 - salt * 131)) % 1000) / 1000.0
|
|
|
|
|
|
## The texture made by `make` under `key`, made only the first time.
|
|
func _image(key: String, make: Callable) -> Texture2D:
|
|
var texture: Texture2D = _textures.get(key)
|
|
if texture == null:
|
|
var pixels: Pixels = make.call()
|
|
texture = ImageTexture.create_from_image(pixels.image)
|
|
_textures[key] = texture
|
|
return texture
|
|
|
|
|
|
## Draws `texture` with its top left corner at `at`, on whole pixels.
|
|
static func _put(canvas: CanvasItem, texture: Texture2D, at: Vector2, modulate := Color.WHITE) -> void:
|
|
canvas.draw_texture(texture, at.round(), modulate)
|
|
|
|
|
|
## The neighbours of `mask` that change how a tile looks: the four sides,
|
|
## and a corner only where both sides beside it are set.
|
|
static func _shape(mask: int) -> int:
|
|
var result := mask & (TileShape.N | TileShape.E | TileShape.S | TileShape.W)
|
|
var corners := [
|
|
[TileShape.NE, TileShape.N | TileShape.E], [TileShape.SE, TileShape.S | TileShape.E],
|
|
[TileShape.SW, TileShape.S | TileShape.W], [TileShape.NW, TileShape.N | TileShape.W],
|
|
]
|
|
for corner: Array in corners:
|
|
var sides: int = corner[1]
|
|
if (mask & corner[0]) != 0 and (mask & sides) == sides:
|
|
result |= corner[0]
|
|
return result
|
|
|
|
|
|
# Terrain.
|
|
|
|
|
|
func _ground(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
|
|
_put(canvas, _image("ground/%d/%d" % [shadow, variant], func() -> Pixels: return _earth(variant, shadow)), rect.position)
|
|
|
|
|
|
## The bare earth of the dead world: up to two soft patches of darker earth,
|
|
## a light grain or two, now and then a pebble.
|
|
func _earth(variant: int, shadow: int) -> Pixels:
|
|
var pixels := Pixels.new(TILE, TILE)
|
|
pixels.rect(0, 0, TILE, TILE, _c("ground"))
|
|
var patches: int = [0, 1, 2, 1, 0, 1, 1, 2][variant]
|
|
for patch in patches:
|
|
var at := _spot(patch, variant, 2, 11)
|
|
for dy in 2:
|
|
for dx in range(-1, 2):
|
|
if _noise(at.x + dx, at.y + dy, variant + 40) < 0.6:
|
|
pixels.put(at.x + dx, at.y + dy, _c("ground_dark"))
|
|
for grain in 1 + variant % 2:
|
|
var at := _spot(grain + 4, variant, 1, 14)
|
|
pixels.put(at.x, at.y, _c("ground_light"))
|
|
if variant % 4 == 1:
|
|
var at := _spot(9, variant, 3, 9)
|
|
_pebble(pixels, at.x, at.y)
|
|
_cast_shadow(pixels, shadow)
|
|
return pixels
|
|
|
|
|
|
## A point in the tile that is the same for the same `index` and `variant`,
|
|
## from `low` to `low + span - 1` on both axes.
|
|
static func _spot(index: int, variant: int, low: int, span: int) -> Vector2i:
|
|
return Vector2i(low + int(_noise(index, 1, variant) * span), low + int(_noise(index, 2, variant) * span))
|
|
|
|
|
|
func _pebble(pixels: Pixels, x: int, y: int) -> void:
|
|
pixels.put(x, y, _c("pebble_light"))
|
|
pixels.put(x + 1, y, _c("pebble"))
|
|
pixels.put(x, y + 1, _c("pebble"))
|
|
pixels.put(x + 1, y + 1, _c("pebble_dark"))
|
|
|
|
|
|
## Darkens the ground where rock to the north or west casts its shadow; the
|
|
## light falls from the top left.
|
|
func _cast_shadow(pixels: Pixels, shadow: int) -> void:
|
|
var deep := Color(0, 0, 0, 0.32)
|
|
var faint := Color(0, 0, 0, 0.16)
|
|
if (shadow & TileShape.N) != 0:
|
|
for x in TILE:
|
|
pixels.shade(x, 0, deep)
|
|
pixels.shade(x, 1, deep if x % 2 == 0 else faint)
|
|
pixels.shade(x, 2, faint if x % 2 == 1 else CLEAR)
|
|
if (shadow & TileShape.W) != 0:
|
|
for y in range(1 if (shadow & TileShape.N) != 0 else 0, TILE):
|
|
pixels.shade(0, y, faint)
|
|
elif (shadow & (TileShape.N | TileShape.NW)) == TileShape.NW:
|
|
pixels.shade(0, 0, deep)
|
|
|
|
|
|
func _soft_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
|
_rock(canvas, rect, state, "soft")
|
|
|
|
|
|
func _hard_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
|
_rock(canvas, rect, state, "hard")
|
|
|
|
|
|
func _bedrock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
|
_rock(canvas, rect, state, "bed")
|
|
|
|
|
|
## Rock of `kind`: its top, and its face where the ground below is open.
|
|
## The ground shows where the outer corners are round.
|
|
func _rock(canvas: CanvasItem, rect: Rect2, state: Dictionary, kind: String) -> void:
|
|
var mask := _shape(state.get("solid", 0))
|
|
var variant: int = state.get("variant", 0) % 4
|
|
_ground(canvas, rect, {"variant": state.get("variant", 0)})
|
|
var key := "%s/%d/%d" % [kind, mask, variant]
|
|
_put(canvas, _image(key, func() -> Pixels: return _rock_image(kind, mask, variant)), rect.position)
|
|
|
|
|
|
func _rock_image(kind: String, mask: int, variant: int) -> Pixels:
|
|
var open_n := (mask & TileShape.N) == 0
|
|
var open_e := (mask & TileShape.E) == 0
|
|
var open_s := (mask & TileShape.S) == 0
|
|
var open_w := (mask & TileShape.W) == 0
|
|
var top_rows := TILE - FACE if open_s else TILE
|
|
var ink := _c("outline")
|
|
var pixels := Pixels.new(TILE, TILE)
|
|
for y in TILE:
|
|
for x in TILE:
|
|
var color := _c(kind + "_top")
|
|
if y >= top_rows:
|
|
color = _c(kind + "_face")
|
|
if y == top_rows or ((x + variant) % 4 == 0 and y < TILE - 2):
|
|
color = _c(kind + "_face_dark")
|
|
else:
|
|
if (open_n and y == 1) or (open_w and x == 1):
|
|
color = _c(kind + "_top_light")
|
|
elif open_e and x == TILE - 2:
|
|
color = _c(kind + "_top_dark")
|
|
if open_s and y == top_rows - 1:
|
|
color = _c(kind + "_lip")
|
|
pixels.put(x, y, color)
|
|
_rock_marks(pixels, kind, variant, top_rows)
|
|
if open_n:
|
|
pixels.rect(0, 0, TILE, 1, ink)
|
|
if open_w:
|
|
pixels.rect(0, 0, 1, TILE, ink)
|
|
if open_e:
|
|
pixels.rect(TILE - 1, 0, 1, TILE, ink)
|
|
if open_s:
|
|
pixels.rect(0, TILE - 1, TILE, 1, ink)
|
|
# Inner corners, where the mass turns around open ground.
|
|
if not open_n and not open_w and (mask & TileShape.NW) == 0:
|
|
pixels.put(0, 0, ink)
|
|
if not open_n and not open_e and (mask & TileShape.NE) == 0:
|
|
pixels.put(TILE - 1, 0, ink)
|
|
if not open_s and not open_w and (mask & TileShape.SW) == 0:
|
|
pixels.rect(0, TILE - FACE, 1, FACE, ink)
|
|
if not open_s and not open_e and (mask & TileShape.SE) == 0:
|
|
pixels.rect(TILE - 1, TILE - FACE, 1, FACE, ink)
|
|
# Round outer corners.
|
|
if open_n and open_w:
|
|
_round(pixels, Vector2i(0, 0), Vector2i(1, 1), ink)
|
|
if open_n and open_e:
|
|
_round(pixels, Vector2i(TILE - 1, 0), Vector2i(-1, 1), ink)
|
|
if open_s and open_w:
|
|
_round(pixels, Vector2i(0, TILE - 1), Vector2i(1, -1), ink)
|
|
if open_s and open_e:
|
|
_round(pixels, Vector2i(TILE - 1, TILE - 1), Vector2i(-1, -1), ink)
|
|
return pixels
|
|
|
|
|
|
## Cuts the corner at `corner` and closes the outline one pixel further in,
|
|
## in the direction `inward`.
|
|
static func _round(pixels: Pixels, corner: Vector2i, inward: Vector2i, ink: Color) -> void:
|
|
pixels.put(corner.x, corner.y, CLEAR)
|
|
pixels.put(corner.x + inward.x, corner.y, CLEAR)
|
|
pixels.put(corner.x, corner.y + inward.y, CLEAR)
|
|
pixels.put(corner.x + inward.x, corner.y + inward.y, ink)
|
|
|
|
|
|
## Small details on the top of rock: pores and chips on soft rock, glinting
|
|
## crystals in hard rock, layers in bedrock.
|
|
func _rock_marks(pixels: Pixels, kind: String, variant: int, top_rows: int) -> void:
|
|
var room := maxi(1, top_rows - 6)
|
|
for pore in 2:
|
|
var spot := Vector2i(3 + int(_noise(pore, 8, variant) * 10.0), 3 + int(_noise(pore, 9, variant) * room))
|
|
pixels.put(spot.x, spot.y, _c(kind + "_top_dark"))
|
|
var at := Vector2i(4 + int(_noise(7, 8, variant) * 7.0), 3 + int(_noise(7, 9, variant) * room))
|
|
match kind:
|
|
"soft":
|
|
if variant % 2 == 0:
|
|
pixels.put(at.x, at.y, _c("soft_top_dark"))
|
|
pixels.put(at.x + 1, at.y, _c("soft_top_dark"))
|
|
pixels.put(at.x + 1, at.y - 1, _c("soft_top_light"))
|
|
pixels.put(at.x + 2, at.y + 1, _c("soft_top_dark"))
|
|
"hard":
|
|
if variant % 2 == 1:
|
|
pixels.put(at.x, at.y, _c("hard_glint"))
|
|
pixels.put(at.x + 1, at.y, _c("hard_top_light"))
|
|
pixels.put(at.x, at.y + 1, _c("hard_top_light"))
|
|
"bed":
|
|
for row in range(4, top_rows - 1, 5):
|
|
for column in range(3, TILE - 3):
|
|
if _noise(column, row, variant) < 0.5:
|
|
pixels.put(column, row, _c("bed_top_dark"))
|
|
|
|
|
|
func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
|
var hits := clampi(state.get("hits", 1), 1, 3)
|
|
var variant: int = state.get("variant", 0) % 4
|
|
_put(canvas, _image("cracks/%d/%d" % [hits, variant], func() -> Pixels: return _crack_image(hits, variant)), rect.position)
|
|
|
|
|
|
func _crack_image(hits: int, variant: int) -> Pixels:
|
|
var pixels := Pixels.new(TILE, TILE)
|
|
var path: Array[Vector2i] = []
|
|
var at := Vector2i(5 + variant, 3)
|
|
for step in 2 + hits * 3:
|
|
path.append(at)
|
|
var turn := _noise(step, hits, variant)
|
|
at += Vector2i(-1 if turn < 0.3 else (1 if turn > 0.65 else 0), 1)
|
|
if hits >= 2:
|
|
var branch: Vector2i = path[path.size() >> 1]
|
|
for step in 3:
|
|
branch += Vector2i(1, 1 if step % 2 == 0 else 0)
|
|
path.append(branch)
|
|
for point in path:
|
|
pixels.put(point.x + 1, point.y, _c("soft_top_light"))
|
|
for point in path:
|
|
pixels.put(point.x, point.y, _c("outline"))
|
|
return pixels
|
|
|
|
|
|
func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
|
var frame := clampi(int(float(state.get("age", 0.0)) * HIT_FRAMES), 0, HIT_FRAMES - 1)
|
|
var variant: int = state.get("variant", 0) % 4
|
|
_put(canvas, _image("hit/%d/%d" % [frame, variant], func() -> Pixels: return _hit_image(frame, variant)), rect.position - Vector2(8, 8))
|
|
|
|
|
|
## Chips that fly up and fall, and a puff of dust, in a square of two tiles
|
|
## around the tile.
|
|
func _hit_image(frame: int, variant: int) -> Pixels:
|
|
var pixels := Pixels.new(TILE * 2, TILE * 2)
|
|
var t := (frame + 1.0) / HIT_FRAMES
|
|
for chip in 5:
|
|
var angle := -PI / 2.0 + (chip - 2) * 0.6 + (variant - 1.5) * 0.15
|
|
var distance := 3.0 + t * 10.0
|
|
var x := roundi(TILE + cos(angle) * distance)
|
|
var y := roundi(TILE - 1 + sin(angle) * distance + t * t * 10.0)
|
|
pixels.put(x, y, _c("soft_top_light"))
|
|
if frame < 3 and chip % 2 == 0:
|
|
pixels.put(x + 1, y, _c("soft_top"))
|
|
pixels.put(x, y + 1, _c("soft_face"))
|
|
pixels.put(x + 1, y + 1, _c("soft_face_dark"))
|
|
if frame < 3:
|
|
pixels.outline(_c("outline"), tinted_outlines)
|
|
var radius := 3.0 + frame * 2.0
|
|
for puff in 8:
|
|
var angle := TAU * puff / 8.0 + variant
|
|
var dust := _c("dust")
|
|
dust.a = 0.75 - frame * 0.2
|
|
pixels.put(roundi(TILE + cos(angle) * radius), roundi(TILE - 1 + sin(angle) * radius * 0.6), dust)
|
|
return pixels
|
|
|
|
|
|
func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
|
var reachable: bool = state.get("reachable", false)
|
|
_put(canvas, _image("marker/%s" % reachable, func() -> Pixels: return _marker_image(reachable)), rect.position - Vector2(1, 1))
|
|
|
|
|
|
## Four corners around the tile, with a dark rim that shows on any ground.
|
|
func _marker_image(reachable: bool) -> Pixels:
|
|
var pixels := Pixels.new(TILE + 2, TILE + 2)
|
|
var color := _c("marker_near") if reachable else _c("marker_far")
|
|
var last := TILE
|
|
for corner: Vector2i in [Vector2i(1, 1), Vector2i(last, 1), Vector2i(1, last), Vector2i(last, last)]:
|
|
var dx := 1 if corner.x == 1 else -1
|
|
var dy := 1 if corner.y == 1 else -1
|
|
for i in 4:
|
|
pixels.put(corner.x + dx * i, corner.y, color)
|
|
pixels.put(corner.x, corner.y + dy * i, color)
|
|
pixels.outline(_c("outline"), false)
|
|
return pixels
|
|
|
|
|
|
# Figures.
|
|
|
|
|
|
func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
|
var frame := 0
|
|
if state.get("moving", false):
|
|
frame = 1 + int(fposmod(float(state.get("step", 0.0)), TAU) / TAU * 4.0) % 4
|
|
var colour := maxi(0, state.get("player", -1)) % players.size()
|
|
var left: bool = float(state.get("facing", 1.0)) < 0.0
|
|
var foot := Vector2(rect.get_center().x, rect.end.y)
|
|
_put(canvas, _image("shadow/12", func() -> Pixels: return _shadow_image(12)), foot - Vector2(6, 2))
|
|
var key := "figure/%d/%d/%s" % [colour, frame, left]
|
|
_put(canvas, _image(key, func() -> Pixels: return _figure_image(colour, frame, left)), foot - Vector2(FIGURE_SIZE.x / 2.0, FIGURE_SIZE.y))
|
|
|
|
|
|
## A soft shadow on the ground, `width` pixels wide.
|
|
func _shadow_image(width: int) -> Pixels:
|
|
var pixels := Pixels.new(width, 3)
|
|
pixels.ellipse(width / 2.0, 1.5, width / 2.0, 1.5, Color(0, 0, 0, 0.28))
|
|
return pixels
|
|
|
|
|
|
## A figure in walking frame `frame` (0 standing, 1 to 4 walking), facing
|
|
## right or, with `left`, left. The shirt has the colour of the player.
|
|
func _figure_image(colour: int, frame: int, left: bool) -> Pixels:
|
|
var pixels := Pixels.new(FIGURE_SIZE.x, FIGURE_SIZE.y)
|
|
var shirt := players[colour]
|
|
# The body sinks a pixel while the legs are apart, so the head always keeps
|
|
# a free row above it for its outline.
|
|
var bob := 1 if frame == 1 or frame == 3 else 0
|
|
# The far leg and the near leg, by frame.
|
|
var legs: Array[Vector2i] = [Vector2i(6, 9), Vector2i(5, 10), Vector2i(7, 8), Vector2i(10, 5), Vector2i(8, 7)]
|
|
for leg in 2:
|
|
var x := legs[frame].x if leg == 0 else legs[frame].y
|
|
var lifted := 1 if frame > 0 and frame % 2 == 1 and leg == 0 else 0
|
|
pixels.rect(x, 18, 2, 4 - lifted, _c("pants_dark") if leg == 0 else _c("pants"))
|
|
pixels.rect(x, 22 - lifted, 3, 2, _c("boots"))
|
|
pixels.rect(5, 11 + bob, 6, 8, shirt)
|
|
pixels.rect(5, 11 + bob, 1, 8, shirt.darkened(0.2))
|
|
pixels.rect(5, 18 + bob, 6, 1, shirt.darkened(0.35))
|
|
var swing: int = [0, 1, 0, -1, 0][frame]
|
|
pixels.rect(8 + swing, 12 + bob, 2, 5, shirt.darkened(0.15))
|
|
pixels.rect(8 + swing, 17 + bob, 2, 1, _c("skin"))
|
|
# The head: hair behind, the face in front, bangs above the eye.
|
|
pixels.ellipse(7.8, 5.8 + bob, 5.2, 5.0, _c("hair"))
|
|
pixels.ellipse(9.6, 7.4 + bob, 3.7, 3.9, _c("skin"))
|
|
pixels.rect(9, 3 + bob, 4, 2, _c("hair"))
|
|
pixels.put(6, 2 + bob, _c("hair_light"))
|
|
pixels.put(7, 2 + bob, _c("hair_light"))
|
|
pixels.put(8, 1 + bob, _c("hair_light"))
|
|
pixels.put(11, 6 + bob, _c("eye"))
|
|
pixels.put(11, 7 + bob, _c("eye"))
|
|
pixels.put(8, 8 + bob, _c("skin_shade"))
|
|
pixels.put(12, 9 + bob, _c("skin_shade"))
|
|
pixels.outline(_ink(shirt), tinted_outlines)
|
|
if left:
|
|
pixels.image.flip_x()
|
|
return pixels
|