Festival lanterns
This 64×48 floor prop features two wooden posts holding a sagging rope with four coloured paper lanterns and five cloth bunting flags. In the 16-frame idle loop, the candles flicker with dithered halos as the lanterns sway and flags flutter. The 12-frame windy loop whips the tassels and rope in a breeze, and the 16 frames unlit variant is daytime, with the paper lanterns dark and gently swaying.
6×
Get the animated sprites: Harvest Festival on itch.ioVol.6 · 12 harvest-fair props · 37 animations
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// Festival lanterns — 64x48 floor prop, top-down 3/4, key light top-left.// Two wooden posts (pointed tops, a sprig of wheat tucked into each rope knot, a little mound of earth and grass at// the foot) carry a sagging rope strung with four round paper lanterns — orange, red, violet and gold, each with a// dark cap, ribbed paper, a bottom ring and a tassel — and five small cloth bunting flags (green, cream, blue)// hung between them. Lantern glows are detached dithered halos; posts have contact shadows.// Variants:// idle 16f loop: the candles flicker (each lantern on its own phase), the halos breathe, the lanterns sway on// their cords with the tassels trailing, the rope bobs, the flags flutter// windy 12f loop: a steady breeze from the left — the lanterns swing hard to the right and back, the tassels// whip, the rope bellies, the flags snap out sideways// unlit 16f loop: daytime — the paper is unlit (lit from the top-left like any other surface), gentle sway,// flags flutterThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Lanterns, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 32, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "windy", args: ["windy"], animate: { fps: 10, duration: N_WIND / 10, loop: true } }, { name: "unlit", args: ["unlit"] }, ],});Show the whole program (197 lines)
// Festival lanterns — 64x48 floor prop, top-down 3/4, key light top-left.// Two wooden posts (pointed tops, a sprig of wheat tucked into each rope knot, a little mound of earth and grass at// the foot) carry a sagging rope strung with four round paper lanterns — orange, red, violet and gold, each with a// dark cap, ribbed paper, a bottom ring and a tassel — and five small cloth bunting flags (green, cream, blue)// hung between them. Lantern glows are detached dithered halos; posts have contact shadows.// Variants:// idle 16f loop: the candles flicker (each lantern on its own phase), the halos breathe, the lanterns sway on// their cords with the tassels trailing, the rope bobs, the flags flutter// windy 12f loop: a steady breeze from the left — the lanterns swing hard to the right and back, the tassels// whip, the rope bellies, the flags snap out sideways// unlit 16f loop: daytime — the paper is unlit (lit from the top-left like any other surface), gentle sway,// flags flutterconst W = 64, H = 48, GROUND = 45;const N_IDLE = 16, N_WIND = 12;const POSTS = [5, 58];const ROPE = { x0: 7, x1: 56, y: 9 };const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, dens) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < dens;// [x on the rope, cord length, colour ramps, sway phase, flicker phase]// lit: [edge, mid, bright, core] unlit: [shadow, base, light, highlight]const LANTERNS = [ { x: 13, cord: 3, lit: ["r1", "r2", "r3", "g3"], day: ["r0", "r1", "r2", "r3"], ph: 0.0, fph: 0.13 }, { x: 25, cord: 4, lit: ["r0", "r1", "r2", "r3"], day: ["r0", "r0", "r1", "r2"], ph: 0.27, fph: 0.61 }, { x: 38, cord: 4, lit: ["v1", "v2", "v3", "n5"], day: ["v0", "v1", "v2", "v3"], ph: 0.55, fph: 0.37 }, { x: 49, cord: 3, lit: ["g1", "g2", "g3", "n5"], day: ["g0", "g1", "g2", "g3"], ph: 0.8, fph: 0.85 },];const FLAGS = [ { x: 9, k: ["e2", "e1"], ph: 0.1 }, { x: 19, k: ["n5", "n4"], ph: 0.42 }, { x: 31, k: ["b2", "b1"], ph: 0.74 }, { x: 44, k: ["n5", "n4"], ph: 0.2 }, { x: 54, k: ["e2", "e1"], ph: 0.57 },];// ---- rope ------------------------------------------------------------------------------------------------------function ropeY(x, sag) { const u = (x - (ROPE.x0 + ROPE.x1) / 2) / ((ROPE.x1 - ROPE.x0) / 2); return ROPE.y + sag * (1 - u * u);}function rope(g, sag) { g.part("rope", () => { for (let x = ROPE.x0; x <= ROPE.x1; x++) { const y = Math.round(ropeY(x, sag)); g.pixel(x, y, x % 3 === 0 ? "w2" : "w3"); // twisted strands const y2 = Math.round(ropeY(x + 1, sag)); if (x < ROPE.x1 && Math.abs(y2 - y) > 1) g.line([x, y], [x, y2 + (y2 > y ? -1 : 1)], "w2"); // keep it unbroken } });}// ---- posts -----------------------------------------------------------------------------------------------------function post(g, cx, lit) { g.outlined("ink", () => g.part(lit ? "postL" : "postR", () => { for (let y = 6; y <= 44; y++) { g.pixel(cx - 1, y, "w2"); g.pixel(cx, y, "w1"); g.pixel(cx + 1, y, "w0"); } g.pixel(cx, 4, "w2"); g.pixel(cx - 1, 5, "w3"); g.pixel(cx, 5, "w2"); g.pixel(cx + 1, 5, "w1"); // pointed top for (const y of [13, 14, 27, 28, 29, 38]) g.pixel(cx - 1, y, "w3"); // lit streaks g.pixel(cx, 21, "w0"); g.pixel(cx, 33, "w0"); g.pixel(cx + 1, 17, "ink"); // knots, a split }));}function knot(g, cx, side) { g.part(side < 0 ? "knotL" : "knotR", () => { g.line([cx - 1, 8], [cx + 1, 8], "w4"); g.line([cx - 1, 10], [cx + 1, 10], "w3"); // rope wraps g.pixel(cx + 1, 9, "w3"); g.pixel(cx - 1, 9, "w2"); g.line([cx - side, 11], [cx - side, 13], "w2"); // loose end }); g.part(side < 0 ? "wheatL" : "wheatR", () => { // a sprig of wheat tucked into the knot const d = -side; g.line([cx, 9], [cx + d * 3, 4], "g0"); g.pixel(cx + d * 3, 4, "g2"); g.pixel(cx + d * 3, 3, "g3"); g.pixel(cx + d * 2, 5, "g1"); g.pixel(cx + d * 4, 5, "g2"); g.pixel(cx + d * 4, 4, "g1"); });}function footing(g, cx, lit) { g.outlined("ink", () => g.part(lit ? "moundL" : "moundR", () => g.field({ x: [cx - 4, cx + 4], y: [42, 45] }, (x, y) => { const nx = (x + 0.5 - (cx + 0.5)) / 4.2, ny = (y + 0.5 - 45) / 2.6; if (nx * nx + ny * ny > 1 || y > 45) return null; return -0.8 * nx - 0.6 * ny > 0.45 ? "w2" : nx < 0.3 ? "w1" : "w0"; }))); g.part(lit ? "grassL" : "grassR", () => { for (const [dx, h, k] of [[-3, 3, "e2"], [-2, 4, "e1"], [2, 3, "e1"], [3, 2, "e2"]]) { g.line([cx + dx, 44], [cx + dx + (dx < 0 ? -1 : 1), 44 - h], k); g.pixel(cx + dx + (dx < 0 ? -1 : 1), 44 - h, k === "e2" ? "e3" : "e2"); } });}// ---- flags -----------------------------------------------------------------------------------------------------// a cloth triangle hung from the rope: top edge follows the rope, the tip flutters (and snaps out in the wind)function flag(g, f, sag, t, wind) { const a = Math.sin(TAU * (2 * t + f.ph)), b = Math.sin(TAU * (3 * t + f.ph * 1.7)); const tipDx = wind ? Math.round(2.2 + 1.3 * a) : Math.round(0.9 * a); const tipH = wind ? 5 - Math.round(1 + 0.8 * (0.5 + 0.5 * b)) : 5; const yl = ropeY(f.x - 2, sag), yr = ropeY(f.x + 2, sag), yc = ropeY(f.x, sag); const P = [[f.x - 2.5, yl + 0.5], [f.x + 2.5, yr + 0.5], [f.x + tipDx, yc + tipH + 0.5]]; const side = (ax, ay, bx, by, px, py) => (bx - ax) * (py - ay) - (by - ay) * (px - ax); g.outlined("ink", () => g.part("flag", () => g.field({ x: [f.x - 4, f.x + 5], y: [Math.floor(Math.min(yl, yr)), Math.ceil(yc + tipH + 1)] }, (x, y) => { const px = x + 0.5, py = y + 0.5; const s1 = side(...P[0], ...P[1], px, py), s2 = side(...P[1], ...P[2], px, py), s3 = side(...P[2], ...P[0], px, py); if (!((s1 >= 0 && s2 >= 0 && s3 >= 0) || (s1 <= 0 && s2 <= 0 && s3 <= 0))) return null; const mid = f.x + (tipDx * (py - yc) / tipH) * 0.6; // the fold runs from the top centre to the tip return px < mid + 0.2 ? f.k[0] : f.k[1]; })));}// ---- lanterns --------------------------------------------------------------------------------------------------const BODY_RX = 4.2, BODY_RY = 4.1;function lanternGeom(L, sag, swing) { const ax = L.x, ay = Math.round(ropeY(L.x, sag)); const len = L.cord + 5; const off = Math.round(len * Math.sin(swing)); const top = ay + L.cord; // cap row return { ax, ay, cx: L.x + off, top, cy: top + 6, off };}function halo(g, L, G, amt) { g.detached("halo", () => g.field({ x: [G.cx - 9, G.cx + 9], y: [G.cy - 9, G.cy + 9] }, (x, y) => { const r = Math.hypot(x + 0.5 - (G.cx + 0.5), (y + 0.5 - G.cy) * 1.1); const R = 7.5 * amt; if (r > R || r < 4.6) return null; return dith(x, y, 0.34 * (1 - (r - 4.6) / (R - 4.6))) ? L.lit[1] : null; }));}function lantern(g, i, L, G, lit, flick, tassel) { const { cx, cy, top } = G; g.part("cord" + i, () => g.line([G.ax, G.ay + 1], [cx, top - 1], "n1")); g.outlined("ink", () => g.part("lantern" + i, () => { g.line([cx - 1, top], [cx + 1, top], "n1"); g.pixel(cx - 1, top, "n3"); // cap g.line([cx - 2, top + 1], [cx + 2, top + 1], "n0"); g.pixel(cx - 2, top + 1, "n2"); g.field({ x: [cx - 5, cx + 5], y: [top + 2, top + 9] }, (x, y) => { const nx = (x + 0.5 - (cx + 0.5)) / BODY_RX, ny = (y + 0.5 - cy) / BODY_RY; if (nx * nx + ny * ny > 1) return null; const rib = y === cy - 2 || y === cy + 1; if (lit) { // light from the candle near the bottom: brightest low in the middle, falling off to the rim const d = Math.hypot(nx, (ny - 0.25) * 1.1) / flick; let k = d > 0.85 ? 0 : d > 0.6 ? 1 : d > 0.3 ? 2 : 3; if (rib && k < 3) k = Math.max(0, k - 1); // ribs show against the paper, not the flame return L.lit[k]; } let k = litIndex(x, y, cx + 0.5, cy, BODY_RX, BODY_RY, 4, 0.1); if (k === 3 && !(nx < -0.2 && ny < -0.15)) k = 2; // keep the highlight a small top-left patch if (rib) k = Math.max(0, k - 1); return L.day[k]; }); g.line([cx - 2, top + 10], [cx + 2, top + 10], "n0"); g.pixel(cx - 2, top + 10, "n2"); // bottom ring })); g.part("tassel" + i, () => { const tb = Math.round(tassel); g.pixel(cx, top + 11, "n1"); g.line([cx, top + 12], [cx + tb, top + 14], lit ? L.lit[1] : L.day[1]); g.pixel(cx + tb, top + 14, lit ? L.lit[2] : L.day[2]); });}// ---- scene -----------------------------------------------------------------------------------------------------function scene(g, t, mode) { const wind = mode === "windy", lit = mode !== "unlit"; const sag = wind ? 11.6 + 1.3 * Math.sin(TAU * t) : 11 + 0.6 * Math.sin(TAU * t); for (const cx of POSTS) contactShadow(g, cx, GROUND, 5, 1); const G = LANTERNS.map((L) => { const swing = wind ? (0.22 + 0.16 * Math.sin(TAU * (t + L.ph * 0.5))) + 0.06 * Math.sin(TAU * (2 * t + L.ph)) : 0.13 * Math.sin(TAU * (t + L.ph)); return lanternGeom(L, sag, swing); }); const flicks = LANTERNS.map((L) => 1 + 0.07 * Math.sin(TAU * (2 * t + L.fph)) + 0.05 * Math.sin(TAU * (3 * t + 2 * L.fph))); if (lit) LANTERNS.forEach((L, i) => halo(g, L, G[i], 0.95 + 0.6 * (flicks[i] - 1))); rope(g, sag); for (const f of FLAGS) flag(g, f, sag, t, wind); LANTERNS.forEach((L, i) => { const lag = wind ? Math.sin(TAU * (t + L.ph * 0.5 - 0.15)) : Math.sin(TAU * (t + L.ph - 0.12)); const tassel = wind ? 1.6 + 1.2 * lag : 0.9 * lag; lantern(g, i, L, G[i], lit, flicks[i], tassel); }); post(g, POSTS[0], true); post(g, POSTS[1], false); knot(g, POSTS[0], -1); knot(g, POSTS[1], 1); footing(g, POSTS[0], true); footing(g, POSTS[1], false);}const Lanterns = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; scene(g, t, mode);});submit(Lanterns, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 32, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "windy", args: ["windy"], animate: { fps: 10, duration: N_WIND / 10, loop: true } }, { name: "unlit", args: ["unlit"] }, ],});These excerpts omit the boilerplate prepended by the asset pack build script: a single-line runtime header, the shared palette, and common helper utilities (contactShadow, litIndex, flame, hash, chk, TAU). The displayed line numbers match the original source files. Each program was authored by an AI coding agent and adjusted after visual inspection of the rendered frames.
Have your agent write programs like this.
Tessarune gives it the drawing API reference, 25 production manuals and a renderer to check its work against.