Sprite sheet
inspired by Lexaloffle, PICO-8 sprite editor1
A PICO-8 sprite editor with a campfire enlarged on its grid, the only lit sprite on a half-filled sheet.
Made with Claude Opus 5.5
- Technique
- pixel art, instrument displays
- Shape
- Any screen
- Added
- 27 September 2026
Colours
- #1C1B1Abackground
- #DAD8CEforeground
- #CF6A4Caccent
Export
FormatThis browser can’t make WebP files.
Shapecropped from 16:9
Crop
SizeThis browser can’t draw a file that large.
Notes
Past the hand-drawn tiles and items, the gaps fill with random mirrored creatures that thin out towards the lower right, like a sheet still being worked on. The eight squares in the bar under it are the sprite’s flags, as in PICO-8’s own editor.
Sources
- Lexaloffle, PICO-8 sprite editor, 2015. ↑
Source code
wallpapers/sprite-sheet/design.py, 212 lines
"""A PICO-8-style sprite editor: a campfire sprite zoomed on the edit grid beside the half-filled sheet it was picked from, in pixel runs and bitmap labels."""
from walldye import (
ACCENT,
ACCENT_1,
ACCENT_3,
ACCENT_5,
BG_ALT,
BG_DEEP,
UI,
UI_ALT,
UI_HI,
Canvas,
P,
Rng,
design,
)
from walldye.pixel import Pixels, glyphs
COLS, ROWS = 16, 7
SP = 5 # sheet pixel; each cell is 9 pixels: a 1-pixel gutter plus the 8x8 sprite
CELL = 9 * SP
SW, SH = COLS * CELL + SP, ROWS * CELL + SP
EP = 40 # editor zoom; 8 * EP == SH so both panels share top and bottom edges
GAP = 80 # between the panels: side by side on landscape screens, stacked on portrait ones
BAR = 34 # palette strip and status bar height
LABEL = 36 # label row above each panel
SEL = (5, 2)
# '1' UI, '2' UI_ALT, '3' UI_HI (see calm()); flame: 'a' dark, 'b' body, 'c' heart, 'd' core.
ART = {
(0, 0): "........|........|.1....1.|.1.1..1.|11.1.1.1|2222222|1111111|.1..1..1", # grass
(1, 0): "11111111|1....1..|1....1..|11111111|..1....1|..1....1|11111111|1....1..", # brick
(2, 0): "........|..2222..|.2331121|.2311111|.2111111|..11111.|........|........", # boulder
(3, 0): "........|..1..1..|.1.11.1.|........|........|.1..1...|1.11.11.|........", # water
(4, 0): "1......1|12222221|1......1|1......1|12222221|1......1|1......1|12222221", # ladder
(5, 0): ".222222.|2......2|2......2|2......2|2....3.2|2......2|2......2|2......2", # door
(6, 0): "..222...|.22222..|2223222.|2222222.|.22222..|...1....|...1....|..111...", # tree
(7, 0): "22222222|21....12|2.1..1.2|2..11..2|2..11..2|2.1..1.2|21....12|22222222", # crate
(0, 1): "........|.33.....|3..3....|3..3333.|.33..3.3|.....3..|........|........", # key
(1, 1): "........|.33.33..|3332333.|3222223.|.32223..|..323...|...3....|........", # heart
(2, 1): "........|..222...|.23332..|.23.32..|.23.32..|.23332..|..222...|........", # coin
(3, 1): "...22...|...11...|..2..2..|.2....2.|.233332.|.233332.|..2222..|........", # potion
(4, 1): "......3.|.....3..|....3...|.1.3....|..1.....|.2.1....|2.......|........", # sword
(5, 1): ".222222.|.233332.|.232232.|.233332.|.232232.|..2332..|...22...|........", # shield
(6, 1): "........|.222222.|21111112|22222222|21133112|21111112|22222222|........", # chest
(7, 1): "...3....|...3....|..333...|3333333.|.33333..|.3...3..|3.....3.|........", # star
(6, 2): "...3....|..3.3...|..3.3...|...3....|..222...|...2....|...2....|...2....", # torch
(7, 2): "........|..222...|.2...2..|..222...|...2....|.......|........|........", # ring
(0, 3): "..222...|.23332..|.3.3.3..|.33333..|..333...|..2.2...|........|........", # skull
(1, 3): "..222...|.22222..|.2.2.2..|.22222..|.22222..|.22222..|.2.2.2..|........", # ghost
(2, 3): "........|1..2..1.|12222221|12.22.21|.2.22.2.|...22...|........|........", # bat
(3, 3): "........|........|..222...|.22222..|.2.2.22.|2222222.|2222222.|........", # slime
(4, 3): "..222...|.23232..|2322232.|.......|...2....|...2....|..222...|........", # mushroom
(0, 4): "...3....|..333...|.3.3.3..|...3....|...3....|...3....|........|........", # up
(1, 4): "...3....|...3....|...3....|.3.3.3..|..333...|...3....|........|........", # down
(2, 4): "........|..3.....|.3......|3333333.|.3......|..3.....|........|........", # left
(3, 4): "........|....3...|.....3..|3333333.|.....3..|....3...|........|........", # right
}
# Walk cycle, frames 0..3 on row 2: body fixed, legs vary.
BODY = "..222...|..222...|..222...|.22222..|2.222.2.|..222..."
LEGS = ["..2.2...|.2...2..", "..2.2...|..2.2...", "..2.2...|.2...2..", "...22...|...22..."]
FIRE = """
....a...
...ab...
..abb.a.
..bcbab.
.abcdbb.
.bcddba.
..3223..
.32..23.
"""
# Chunky 2-pixel-stroke letters, like a font drawn into the sheet by hand.
LETTERS = [
".111111.|.111111.|.11..11.|.111111.|.111111.|.11.....|.11.....|........",
".111111.|.111111.|...11...|...11...|...11...|.111111.|.111111.|........",
"..11111.|.111111.|.11.....|.11.....|.11.....|.111111.|..11111.|........",
"..1111..|.111111.|.11..11.|.11..11.|.11..11.|.111111.|..1111..|........",
"..1111..|.11..11.|.111111.|..1111..|.11..11.|.111111.|..1111..|........",
]
# The sheet at full strength; the flame's heart and core share the accent itself.
SHEET = (None, UI, UI_ALT, UI_HI, ACCENT_3, ACCENT_1, ACCENT)
SHEET_KEY = {"1": 1, "2": 2, "3": 3, "a": 4, "b": 5, "c": 6, "d": 6}
# The editor one step dimmer than the sheet, so the selection frame stays the event.
ZOOM = (UI, UI_ALT, ACCENT_5, ACCENT_3, ACCENT_1, ACCENT)
ZOOM_KEY = {"1": 0, "2": 0, "3": 1, "a": 2, "b": 3, "c": 4, "d": 5}
SWATCHES = (BG_DEEP, BG_ALT, UI, UI_ALT, UI_HI, ACCENT) # the last one is selected
FLAGS = (0, 1, 0, 0, 1, 0, 0, 0) # sprite flags, as the eight toggles in PICO-8's editor
def calm(art: str) -> str:
"""Keep the sheet even and quiet: at most two UI_HI highlight pixels per sprite."""
if art.count("3") <= 2:
return art
if "2" in art:
return art.replace("2", "1").replace("3", "2")
return art.replace("3", "2")
def invader(r: Rng) -> str:
"""Mirrored random 5-wide body padded to 8x8."""
h = r.randint(4, 6)
half: list[list[bool]] = []
while sum(map(sum, half)) < 8: # reject sparse fragments
half = [[r.random() < 0.6 for _ in range(3)] for _ in range(h)]
out = ["........"] * 8
top = (8 - h) // 2
for j, row in enumerate(half):
bits = row + row[1::-1]
tone = "2" if r.random() < 0.8 else "1"
out[top + j] = "." + "".join(tone if b else "." for b in bits) + ".."
return "|".join(out)
def rows(art: str) -> list[str]:
"""The pixel rows of `art`, written one per line or separated by '|'."""
return art.strip("\n").replace("\n", "|").split("|")
@design(aspects="any")
def draw(s: Canvas) -> None:
# Panel origins: editor (ex, ey) and sheet (sx, sy). Landscape puts them side by side on
# one baseline; portrait stacks the editor over the sheet, as PICO-8 itself does.
if s.landscape:
ex = (s.w - 8 * EP - GAP - SW) // 2
sx = ex + 8 * EP + GAP
ey = sy = (s.h - SH - 24 - BAR) // 2 + 12
else:
ex, sx = (s.w - 8 * EP) // 2, (s.w - SW) // 2
drop = 8 * EP + 24 + BAR + GAP + LABEL # editor top to sheet top
span = LABEL + drop + SH + 24 + BAR
ey = round(s.h * 0.46 - span / 2) + LABEL
sy = ey + drop
r = s.rng(8)
art = {k: calm(a) for k, a in ART.items()}
for f, legs in enumerate(LEGS):
art[(f, 2)] = BODY + "|" + legs
for k, a in enumerate(LETTERS):
art[(4 + k, 4)] = a
art[SEL] = FIRE
# The sheet fills from the top left and trails off, like work in progress.
for j in range(ROWS):
for i in range(COLS):
if (i, j) not in art and r.random() < 1.4 - i / 14 - j / 9:
art[(i, j)] = invader(r)
# Sheet: one index grid at sheet-pixel resolution, a 1-pixel gutter before each sprite.
sheet = Pixels(COLS * 9 + 1, ROWS * 9 + 1, SHEET)
for (i, j), a in art.items():
sheet.stamp(rows(a), i * 9 + 1, j * 9 + 1, SHEET_KEY)
s.fill(P().rect(sx, sy, SW, SH), BG_DEEP)
sheet.draw(s, SP, (sx, sy))
seps = P()
for i in range(COLS + 1):
seps.M(sx + i * CELL + SP / 2, sy).V(sy + SH)
for j in range(ROWS + 1):
seps.M(sx, sy + j * CELL + SP / 2).H(sx + SW)
s.stroke(seps, BG_ALT, 1.5)
ci, cj = SEL
s.stroke(P().rect(sx + ci * CELL + SP / 2, sy + cj * CELL + SP / 2, CELL, CELL), ACCENT, 3)
# Labels: sprite number over the editor, bank tabs over the sheet's right edge.
glyphs(s, [f"{cj * COLS + ci:03d}"], UI_ALT, at=(ex - 6, ey - LABEL), font="5x8", px=3)
glyphs(
s,
["0 1 2 3"],
lambda _c, _r, ch: UI_HI if ch == "0" else UI_ALT,
at=(sx + SW + 3, sy - LABEL),
font="5x8",
px=3,
anchor="end",
)
# Editor: the selected sprite at zoom.
s.fill(P().rect(ex, ey, 8 * EP, 8 * EP), BG_DEEP)
lines = P()
for k in range(1, 8):
lines.M(ex + k * EP, ey).V(ey + 8 * EP).M(ex, ey + k * EP).H(ex + 8 * EP)
s.stroke(lines, BG_ALT, 1)
with s.buckets(ZOOM, "fill") as b:
for y, row in enumerate(rows(FIRE)):
for x, ch in enumerate(row):
if ch in ZOOM_KEY:
b[ZOOM_KEY[ch]].rect(ex + x * EP + 3, ey + y * EP + 3, EP - 6, EP - 6)
s.stroke(P().rect(ex - 6, ey - 6, 8 * EP + 12, 8 * EP + 12), UI_ALT, 2)
# Palette strip under the editor, spanning its frame; the first swatch matches the
# editor ground, so it gets an outline.
by = ey + 8 * EP + 24
step = (8 * EP + 16) // 6
for k, c in enumerate(SWATCHES):
box = P().rect(ex - 6 + k * step, by, step - 4, BAR)
if k == 0:
s.path(box, fill=c, stroke=UI, stroke_width=1)
else:
s.fill(box, c)
s.stroke(P().rect(ex - 6 + 5 * step - 5, by - 5, step + 6, BAR + 10), UI_HI, 2)
# Status bar under the sheet: position, size and the flag toggles.
ty = sy + SH + 24
s.path(P().rect(sx, ty, SW, BAR), fill=BG_DEEP, stroke=BG_ALT, stroke_width=1.5)
glyphs(s, [f"X{ci} Y{cj} 8X8"], UI_ALT, at=(sx + 16, ty + 9), font="5x8", px=2)
on, off = P(), P()
for k, flag in enumerate(FLAGS):
(on if flag else off).rect(sx + SW - 16 - (8 - k) * 22 + 6, ty + 10, 14, 14)
s.path(on, fill=UI_ALT, stroke=UI, stroke_width=1.5)
s.stroke(off, UI, 1.5)"""A PICO-8-style sprite editor: a campfire sprite zoomed on the edit grid beside the half-filled sheet it was picked from, in pixel runs and bitmap labels."""
from walldye import (
ACCENT,
ACCENT_1,
ACCENT_3,
ACCENT_5,
BG_ALT,
BG_DEEP,
UI,
UI_ALT,
UI_HI,
Canvas,
P,
Rng,
design,
)
from walldye.pixel import Pixels, glyphs
COLS, ROWS = 16, 7
SP = 5 # sheet pixel; each cell is 9 pixels: a 1-pixel gutter plus the 8x8 sprite
CELL = 9 * SP
SW, SH = COLS * CELL + SP, ROWS * CELL + SP
EP = 40 # editor zoom; 8 * EP == SH so both panels share top and bottom edges
GAP = 80 # between the panels: side by side on landscape screens, stacked on portrait ones
BAR = 34 # palette strip and status bar height
LABEL = 36 # label row above each panel
SEL = (5, 2)
# '1' UI, '2' UI_ALT, '3' UI_HI (see calm()); flame: 'a' dark, 'b' body, 'c' heart, 'd' core.
ART = {
(0, 0): "........|........|.1....1.|.1.1..1.|11.1.1.1|2222222|1111111|.1..1..1", # grass
(1, 0): "11111111|1....1..|1....1..|11111111|..1....1|..1....1|11111111|1....1..", # brick
(2, 0): "........|..2222..|.2331121|.2311111|.2111111|..11111.|........|........", # boulder
(3, 0): "........|..1..1..|.1.11.1.|........|........|.1..1...|1.11.11.|........", # water
(4, 0): "1......1|12222221|1......1|1......1|12222221|1......1|1......1|12222221", # ladder
(5, 0): ".222222.|2......2|2......2|2......2|2....3.2|2......2|2......2|2......2", # door
(6, 0): "..222...|.22222..|2223222.|2222222.|.22222..|...1....|...1....|..111...", # tree
(7, 0): "22222222|21....12|2.1..1.2|2..11..2|2..11..2|2.1..1.2|21....12|22222222", # crate
(0, 1): "........|.33.....|3..3....|3..3333.|.33..3.3|.....3..|........|........", # key
(1, 1): "........|.33.33..|3332333.|3222223.|.32223..|..323...|...3....|........", # heart
(2, 1): "........|..222...|.23332..|.23.32..|.23.32..|.23332..|..222...|........", # coin
(3, 1): "...22...|...11...|..2..2..|.2....2.|.233332.|.233332.|..2222..|........", # potion
(4, 1): "......3.|.....3..|....3...|.1.3....|..1.....|.2.1....|2.......|........", # sword
(5, 1): ".222222.|.233332.|.232232.|.233332.|.232232.|..2332..|...22...|........", # shield
(6, 1): "........|.222222.|21111112|22222222|21133112|21111112|22222222|........", # chest
(7, 1): "...3....|...3....|..333...|3333333.|.33333..|.3...3..|3.....3.|........", # star
(6, 2): "...3....|..3.3...|..3.3...|...3....|..222...|...2....|...2....|...2....", # torch
(7, 2): "........|..222...|.2...2..|..222...|...2....|.......|........|........", # ring
(0, 3): "..222...|.23332..|.3.3.3..|.33333..|..333...|..2.2...|........|........", # skull
(1, 3): "..222...|.22222..|.2.2.2..|.22222..|.22222..|.22222..|.2.2.2..|........", # ghost
(2, 3): "........|1..2..1.|12222221|12.22.21|.2.22.2.|...22...|........|........", # bat
(3, 3): "........|........|..222...|.22222..|.2.2.22.|2222222.|2222222.|........", # slime
(4, 3): "..222...|.23232..|2322232.|.......|...2....|...2....|..222...|........", # mushroom
(0, 4): "...3....|..333...|.3.3.3..|...3....|...3....|...3....|........|........", # up
(1, 4): "...3....|...3....|...3....|.3.3.3..|..333...|...3....|........|........", # down
(2, 4): "........|..3.....|.3......|3333333.|.3......|..3.....|........|........", # left
(3, 4): "........|....3...|.....3..|3333333.|.....3..|....3...|........|........", # right
}
# Walk cycle, frames 0..3 on row 2: body fixed, legs vary.
BODY = "..222...|..222...|..222...|.22222..|2.222.2.|..222..."
LEGS = ["..2.2...|.2...2..", "..2.2...|..2.2...", "..2.2...|.2...2..", "...22...|...22..."]
FIRE = """
....a...
...ab...
..abb.a.
..bcbab.
.abcdbb.
.bcddba.
..3223..
.32..23.
"""
# Chunky 2-pixel-stroke letters, like a font drawn into the sheet by hand.
LETTERS = [
".111111.|.111111.|.11..11.|.111111.|.111111.|.11.....|.11.....|........",
".111111.|.111111.|...11...|...11...|...11...|.111111.|.111111.|........",
"..11111.|.111111.|.11.....|.11.....|.11.....|.111111.|..11111.|........",
"..1111..|.111111.|.11..11.|.11..11.|.11..11.|.111111.|..1111..|........",
"..1111..|.11..11.|.111111.|..1111..|.11..11.|.111111.|..1111..|........",
]
# The sheet at full strength; the flame's heart and core share the accent itself.
SHEET = (None, UI, UI_ALT, UI_HI, ACCENT_3, ACCENT_1, ACCENT)
SHEET_KEY = {"1": 1, "2": 2, "3": 3, "a": 4, "b": 5, "c": 6, "d": 6}
# The editor one step dimmer than the sheet, so the selection frame stays the event.
ZOOM = (UI, UI_ALT, ACCENT_5, ACCENT_3, ACCENT_1, ACCENT)
ZOOM_KEY = {"1": 0, "2": 0, "3": 1, "a": 2, "b": 3, "c": 4, "d": 5}
SWATCHES = (BG_DEEP, BG_ALT, UI, UI_ALT, UI_HI, ACCENT) # the last one is selected
FLAGS = (0, 1, 0, 0, 1, 0, 0, 0) # sprite flags, as the eight toggles in PICO-8's editor
def calm(art: str) -> str:
"""Keep the sheet even and quiet: at most two UI_HI highlight pixels per sprite."""
if art.count("3") <= 2:
return art
if "2" in art:
return art.replace("2", "1").replace("3", "2")
return art.replace("3", "2")
def invader(r: Rng) -> str:
"""Mirrored random 5-wide body padded to 8x8."""
h = r.randint(4, 6)
half: list[list[bool]] = []
while sum(map(sum, half)) < 8: # reject sparse fragments
half = [[r.random() < 0.6 for _ in range(3)] for _ in range(h)]
out = ["........"] * 8
top = (8 - h) // 2
for j, row in enumerate(half):
bits = row + row[1::-1]
tone = "2" if r.random() < 0.8 else "1"
out[top + j] = "." + "".join(tone if b else "." for b in bits) + ".."
return "|".join(out)
def rows(art: str) -> list[str]:
"""The pixel rows of `art`, written one per line or separated by '|'."""
return art.strip("\n").replace("\n", "|").split("|")
@design(aspects="any")
def draw(s: Canvas) -> None:
# Panel origins: editor (ex, ey) and sheet (sx, sy). Landscape puts them side by side on
# one baseline; portrait stacks the editor over the sheet, as PICO-8 itself does.
if s.landscape:
ex = (s.w - 8 * EP - GAP - SW) // 2
sx = ex + 8 * EP + GAP
ey = sy = (s.h - SH - 24 - BAR) // 2 + 12
else:
ex, sx = (s.w - 8 * EP) // 2, (s.w - SW) // 2
drop = 8 * EP + 24 + BAR + GAP + LABEL # editor top to sheet top
span = LABEL + drop + SH + 24 + BAR
ey = round(s.h * 0.46 - span / 2) + LABEL
sy = ey + drop
r = s.rng(8)
art = {k: calm(a) for k, a in ART.items()}
for f, legs in enumerate(LEGS):
art[(f, 2)] = BODY + "|" + legs
for k, a in enumerate(LETTERS):
art[(4 + k, 4)] = a
art[SEL] = FIRE
# The sheet fills from the top left and trails off, like work in progress.
for j in range(ROWS):
for i in range(COLS):
if (i, j) not in art and r.random() < 1.4 - i / 14 - j / 9:
art[(i, j)] = invader(r)
# Sheet: one index grid at sheet-pixel resolution, a 1-pixel gutter before each sprite.
sheet = Pixels(COLS * 9 + 1, ROWS * 9 + 1, SHEET)
for (i, j), a in art.items():
sheet.stamp(rows(a), i * 9 + 1, j * 9 + 1, SHEET_KEY)
s.fill(P().rect(sx, sy, SW, SH), BG_DEEP)
sheet.draw(s, SP, (sx, sy))
seps = P()
for i in range(COLS + 1):
seps.M(sx + i * CELL + SP / 2, sy).V(sy + SH)
for j in range(ROWS + 1):
seps.M(sx, sy + j * CELL + SP / 2).H(sx + SW)
s.stroke(seps, BG_ALT, 1.5)
ci, cj = SEL
s.stroke(P().rect(sx + ci * CELL + SP / 2, sy + cj * CELL + SP / 2, CELL, CELL), ACCENT, 3)
# Labels: sprite number over the editor, bank tabs over the sheet's right edge.
glyphs(s, [f"{cj * COLS + ci:03d}"], UI_ALT, at=(ex - 6, ey - LABEL), font="5x8", px=3)
glyphs(
s,
["0 1 2 3"],
lambda _c, _r, ch: UI_HI if ch == "0" else UI_ALT,
at=(sx + SW + 3, sy - LABEL),
font="5x8",
px=3,
anchor="end",
)
# Editor: the selected sprite at zoom.
s.fill(P().rect(ex, ey, 8 * EP, 8 * EP), BG_DEEP)
lines = P()
for k in range(1, 8):
lines.M(ex + k * EP, ey).V(ey + 8 * EP).M(ex, ey + k * EP).H(ex + 8 * EP)
s.stroke(lines, BG_ALT, 1)
with s.buckets(ZOOM, "fill") as b:
for y, row in enumerate(rows(FIRE)):
for x, ch in enumerate(row):
if ch in ZOOM_KEY:
b[ZOOM_KEY[ch]].rect(ex + x * EP + 3, ey + y * EP + 3, EP - 6, EP - 6)
s.stroke(P().rect(ex - 6, ey - 6, 8 * EP + 12, 8 * EP + 12), UI_ALT, 2)
# Palette strip under the editor, spanning its frame; the first swatch matches the
# editor ground, so it gets an outline.
by = ey + 8 * EP + 24
step = (8 * EP + 16) // 6
for k, c in enumerate(SWATCHES):
box = P().rect(ex - 6 + k * step, by, step - 4, BAR)
if k == 0:
s.path(box, fill=c, stroke=UI, stroke_width=1)
else:
s.fill(box, c)
s.stroke(P().rect(ex - 6 + 5 * step - 5, by - 5, step + 6, BAR + 10), UI_HI, 2)
# Status bar under the sheet: position, size and the flag toggles.
ty = sy + SH + 24
s.path(P().rect(sx, ty, SW, BAR), fill=BG_DEEP, stroke=BG_ALT, stroke_width=1.5)
glyphs(s, [f"X{ci} Y{cj} 8X8"], UI_ALT, at=(sx + 16, ty + 9), font="5x8", px=2)
on, off = P(), P()
for k, flag in enumerate(FLAGS):
(on if flag else off).rect(sx + SW - 16 - (8 - k) * 22 + 6, ty + 10, 14, 14)
s.path(on, fill=UI_ALT, stroke=UI, stroke_width=1.5)
s.stroke(off, UI, 1.5)
Run it yourself
$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render sprite-sheet --theme fireproof -o sprite-sheet-fireproof-16x9.svg