Spinning wheel
A 48×48 floor prop: an old Saxony-style wooden spinning wheel with a big eight-spoked wheel on a splayed three-legged bench, a flyer with a fat bobbin of yarn, a long iron spindle with a needle-sharp tip, a distaff of fleece tied with a red ribbon, and a treadle. All three variants are 12-frame loops: “idle” keeps it still while a glint runs along the spindle; “spin” turns the wheel half a turn per loop (eight spokes, so the loop closes) while the treadle pumps; “cursed” spins it by itself with violet yarn, violet sparks and a glowing spindle tip.
6×
Get the animated sprites: Witch's Hut on itch.ioVol.5 · 12 Halloween props · 42 animations
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// Spinning wheel — 48x48 floor prop, top-down 3/4, key light top-left.// An old Saxony-style wooden spinning wheel: a big eight-spoked wheel on a splayed three-legged bench, the// mother-of-all with two finialed maidens holding the flyer and a fat bobbin of yarn, a long iron spindle with a// needle-sharp tip, a distaff of fleece tied with a red ribbon, and a treadle linked to the crank by a footman.// Variants (12f loops @ 10 fps):// idle still; a glint runs along the spindle and flashes on its tip// spin the wheel turns (half a turn per loop, 8 spokes, so the loop closes), the flyer whirls, the yarn runs// from the fleece to the spindle, the treadle pumps// cursed spinning by itself: violet yarn, violet sparks, the spindle tip glowsThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(SpinningWheel, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: 24, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "spin", args: ["spin"] }, { name: "cursed", args: ["cursed"] }, ],});Show the whole program (332 lines)
// Spinning wheel — 48x48 floor prop, top-down 3/4, key light top-left.// An old Saxony-style wooden spinning wheel: a big eight-spoked wheel on a splayed three-legged bench, the// mother-of-all with two finialed maidens holding the flyer and a fat bobbin of yarn, a long iron spindle with a// needle-sharp tip, a distaff of fleece tied with a red ribbon, and a treadle linked to the crank by a footman.// Variants (12f loops @ 10 fps):// idle still; a glint runs along the spindle and flashes on its tip// spin the wheel turns (half a turn per loop, 8 spokes, so the loop closes), the flyer whirls, the yarn runs// from the fleece to the spindle, the treadle pumps// cursed spinning by itself: violet yarn, violet sparks, the spindle tip glowsconst W = 48, H = 48, GROUND = 44;const WC = { x: 14, y: 20 }, R = 11;const L = [-0.74, -0.67];const WOOD = ["w0", "w1", "w2", "w3", "w4"];const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, d) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < d;// a wooden limb swept along a path, shaded as a cylinder lit from the top-leftfunction limb(g, path, w0, w1, opt = {}) { const a = path[0], b = path[path.length - 1]; const dx = b[0] - a[0], dy = b[1] - a[1], len = Math.hypot(dx, dy) || 1; const nl = (-dy / len) * L[0] + (dx / len) * L[1]; g.sweep(path, { w0, w1, wobble: opt.wobble ?? 0, wobblePhase: opt.ph ?? 0.5 }, (u, v) => { const l = v * nl * 1.3 + 0.1 - (opt.dark || 0); return WOOD[l > 0.85 ? 4 : l > 0.15 ? 3 : l > -0.4 ? 2 : l > -0.85 ? 1 : 0]; });}// ---- the wheel ----------------------------------------------------------------------------------------------function rim(g, ang) { g.part("wheelRim", () => g.field({ x: [WC.x - R - 1, WC.x + R + 1], y: [WC.y - R - 1, WC.y + R + 1] }, (x, y) => { const dx = x - WC.x, dy = y - WC.y, r = Math.hypot(dx, dy); if (r > R + 0.45 || r < R - 2.5) return null; const nx = dx / r, ny = dy / r, lit = L[0] * nx + L[1] * ny; // felloe joints, between the spokes, turn with the wheel const a = Math.atan2(dy, dx) - ang - TAU / 16; const s = a / (TAU / 8), d = (s - Math.round(s)) * (TAU / 8); if (r < R - 0.5 && Math.abs(Math.sin(d)) * r < 0.6) return lit > 0.2 ? "w1" : "w0"; if (r > R - 0.55) return lit > 0.55 ? "w4" : lit > 0 ? "w3" : lit > -0.55 ? "w2" : "w1"; if (r > R - 1.55) return lit > 0.3 ? "w3" : lit > -0.45 ? "w2" : "w1"; return -lit > 0.3 ? "w3" : -lit > -0.4 ? "w2" : "w1"; // inner face: lit at the bottom of the ring }));}function spokes(g, ang) { g.part("spokes", () => { for (let k = 0; k < 8; k++) { const a = ang + (k * TAU) / 8, c = Math.cos(a), s = Math.sin(a); const lit = L[0] * c + L[1] * s; const p0 = [Math.round(WC.x + 2.4 * c), Math.round(WC.y + 2.4 * s)]; const pm = [Math.round(WC.x + 5.2 * c), Math.round(WC.y + 5.2 * s)]; const p1 = [Math.round(WC.x + (R - 2.3) * c), Math.round(WC.y + (R - 2.3) * s)]; g.line(p0, p1, lit > -0.2 ? "w3" : "w2"); g.line(p0, pm, lit > -0.2 ? "w4" : "w3"); // turned bead near the hub } });}function hub(g) { g.part("hub", () => { g.field({ x: [WC.x - 3, WC.x + 3], y: [WC.y - 3, WC.y + 3] }, (x, y) => { const i = litIndex(x, y, WC.x + 0.5, WC.y + 0.5, 3, 3, 4, 0.05); return i < 0 ? null : WOOD[i + 1]; }); });}// ---- bench, legs, treadle ---------------------------------------------------------------------------------function bench(g) { g.part("legs", () => { limb(g, [[23, 35], [24, 39], [24, 41]], 1.0, 0.9, { dark: 0.5 }); // far leg, in shadow limb(g, [[6, 35], [5, 40], [3, 44]], 1.3, 1.1); limb(g, [[40, 35], [42, 40], [43, 44]], 1.3, 1.1, { dark: 0.2 }); }); g.part("bench", () => { // lit top face, then the front face; the left end shows its end grain g.rect({ x: [2, 44], y: [31, 32] }, "w3", { fill: "w3" }); g.line([3, 31], [20, 31], "w4"); g.rect({ x: [2, 44], y: [33, 35] }, "w2", { fill: "w2" }); g.line([2, 35], [44, 35], "w1"); g.line([3, 33], [44, 33], "w1"); // shadow under the lip g.rect({ x: [2, 3], y: [33, 35] }, "w3", { fill: "w3" }); // end grain g.pixel(2, 34, "w2"); // grain and a couple of old nicks g.line([8, 34], [12, 34], "w1"); g.line([27, 34], [31, 34], "w1"); g.pixel(37, 32, "w2"); g.pixel(24, 32, "w2"); g.pixel(17, 34, "w3"); g.pixel(34, 34, "w3"); });}function treadle(g, yT) { g.part("treadle", () => { // a broad plank hinged at the back-left; its free end rides up and down with the crank g.fillPoly([[8, 40], [27, yT - 2], [27, yT], [8, 42]], "w3"); g.line([8, 42], [27, yT], "w2"); g.line([8, 43], [27, yT + 1], "w1"); g.line([8, 40], [16, 40 + Math.round(((yT - 42) * 8) / 19)], "w4"); g.pixel(8, 41, "n3"); g.pixel(8, 42, "n1"); // iron hinge pin g.pixel(20, 41 + Math.round(((yT - 42) * 12) / 19), "w2"); // a worn foot mark });}// ---- mother-of-all, flyer, spindle --------------------------------------------------------------------------function motherOfAll(g) { g.part("tension", () => { g.rect({ x: [32, 34], y: [29, 30] }, "w2", { fill: "w2" }); g.pixel(32, 29, "w3"); g.pixel(34, 30, "w1"); }); g.part("mother", () => { g.rect({ x: [26, 40], y: [26, 26] }, "w3", { fill: "w3" }); g.rect({ x: [26, 40], y: [27, 28] }, "w2", { fill: "w2" }); g.line([26, 28], [40, 28], "w1"); g.line([26, 26], [31, 26], "w4"); g.pixel(26, 27, "w3"); }); for (const [x, nm] of [[28, "maidenL"], [37, "maidenR"]]) { g.part(nm, () => { g.rect({ x: [x, x + 1], y: [18, 25] }, "w2", { fill: "w2" }); g.line([x, 18], [x, 25], "w3"); g.pixel(x + 1, 21, "w1"); g.pixel(x + 1, 23, "w1"); g.rect({ x: [x - 1, x + 2], y: [21, 23] }, "w0", { fill: "w0" }); // leather bearing g.pixel(x - 1, 21, "w1"); // ball finial g.ellipse({ x: [x - 1, x + 2], y: [14, 17] }, "w2", { fill: "w2" }); g.pixel(x - 1, 15, "w3"); g.pixel(x, 14, "w4"); g.pixel(x, 15, "w3"); g.pixel(x + 2, 16, "w1"); g.pixel(x + 1, 17, "w1"); }); }}function flyer(g, phi) { g.part("whorl", () => { g.ellipse({ x: [25, 27], y: [19, 25] }, "w2", { fill: "w2" }); g.line([25, 20], [25, 23], "w3"); g.pixel(26, 19, "w3"); g.line([27, 21], [27, 24], "w1"); g.pixel(26, 22, "w0"); // the groove the drive band rides in }); g.part("spindle", () => { g.line([24, 22], [38, 22], "n2"); // the long needle-sharp tip past the right maiden g.line([39, 21], [41, 21], "n3"); g.pixel(39, 21, "n4"); g.line([39, 22], [41, 22], "n2"); g.line([42, 22], [44, 22], "n3"); g.pixel(42, 22, "n4"); g.pixel(45, 22, "n5"); }); g.part("bobbin", () => { g.rect({ x: [31, 31], y: [18, 26] }, "w3", { fill: "w3" }); g.rect({ x: [35, 35], y: [18, 26] }, "w2", { fill: "w2" }); g.pixel(31, 18, "w4"); g.pixel(35, 26, "w1"); // the yarn wound on it: a soft fat roll, lit top-left, with a few wraps marked g.field({ x: [32, 34], y: [19, 25] }, (x, y) => { const i = litIndex(x, y, 33.5, 22.5, 2.2, 3.8, 3, 0.15); return ["w2", "w4", "n5"][Math.max(0, i)]; }); g.pixel(33, 20, "w4"); g.pixel(34, 22, "w3"); g.pixel(33, 24, "w3"); }); // flyer arms whirl round the spindle: seen side-on they swing above and below it const off = Math.round(4 * Math.cos(phi)); g.part("flyer", () => { for (const s of [1, -1]) { const y = 22 + s * off; g.line([30, y], [36, y], s * off <= 0 ? "w4" : "w2"); g.pixel(33, y, "g2"); g.pixel(35, y, "g1"); // brass hooks } g.line([30, 22 - Math.abs(off)], [30, 22 + Math.abs(off)], "w1"); });}// ---- distaff and fleece -------------------------------------------------------------------------------------function distaff(g) { g.part("distaff", () => { g.line([33, 0], [33, 25], "w2"); g.pixel(33, 0, "w3"); g.pixel(33, 16, "w3"); g.pixel(33, 18, "w3"); }); g.part("fleece", () => { g.field({ x: [25, 42], y: [2, 15] }, (x, y) => { // a soft cone of carded wool wound round the distaff: narrow at the top, full at the ribbon, rounded below, // its edge scalloped where the locks bulge const yc = y + 0.5; let hw = yc < 10.5 ? 1.2 + 5.4 * Math.pow((yc - 2) / 8.5, 0.75) : 6.6 * Math.sqrt(Math.max(0, 1 - Math.pow((yc - 10.5) / 4.6, 2))); const dx = x + 0.5 - 33.5; hw += 0.8 * Math.abs(Math.sin(y * (dx < 0 ? 1.15 : 0.95) + (dx < 0 ? 0.4 : 1.9))); const d = dx / Math.max(0.5, hw); if (Math.abs(d) > 1 || yc < 2) return null; const yy = (yc - 9) / 7; // wrapped locks of wool: soft diagonal bands, lit from the top-left const lit = -0.75 * d - 0.45 * yy + 0.22 * Math.sin(y * 1.4 - d * 2.4) + 0.05; return lit > 0.5 ? "n5" : lit > -0.05 ? "n4" : lit > -0.55 ? "n3" : "n2"; }); g.pixel(26, 9, "n5"); g.pixel(41, 7, "n3"); g.pixel(30, 4, "n4"); // loose tufts g.pixel(36, 13, "w4"); g.pixel(31, 12, "w4"); // a warmer lock or two }); g.part("ribbon", () => { // a red ribbon binds the fleece g.line([28, 9], [38, 10], "r1"); g.line([28, 9], [31, 9], "r2"); g.pixel(32, 8, "r2"); g.pixel(33, 8, "r1"); // the bow g.pixel(39, 11, "r1"); g.pixel(39, 12, "r0"); g.pixel(40, 13, "r1"); g.pixel(40, 14, "r0"); });}// ---- yarn, drive band, sparks, glow ------------------------------------------------------------------------const YARN = [[37, 13], [38, 15], [39, 17], [40, 19], [41, 21]];function yarnPath() { const pts = []; for (let i = 0; i < YARN.length - 1; i++) { const [x0, y0] = YARN[i], [x1, y1] = YARN[i + 1]; const n = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0)); for (let s = 0; s < n; s++) pts.push([Math.round(x0 + ((x1 - x0) * s) / n), Math.round(y0 + ((y1 - y0) * s) / n)]); } return pts;}function yarn(g, t, mode) { const pts = yarnPath(); const step = Math.floor(t * 12 + 1e-6); g.detached("yarn", () => { pts.forEach(([x, y], i) => { if (mode === "idle") { g.pixel(x, y, i % 3 === 0 ? "n3" : "n4"); return; } const m = (((i - step) % 4) + 4) % 4; // twists run down the yarn toward the spindle if (mode === "cursed") g.pixel(x, y, m === 0 ? "n5" : m === 1 ? "v3" : "v2"); else g.pixel(x, y, m === 0 ? "n5" : m === 1 ? "n3" : "n4"); }); });}function driveBand(g, mode) { const k = mode === "cursed" ? "v2" : "n4"; g.detached("driveBand", () => { g.line([17, 9], [25, 19], k); g.line([22, 28], [25, 25], k); });}function glint(g, t) { // idle: a glint slides out along the spindle and winks on the needle tip const p = t; g.detached("glint", () => { if (p < 0.5) { const x = 38 + Math.round((p / 0.5) * 6); g.pixel(x, 22, "n5"); if (x <= 40) g.pixel(x, 21, "n5"); } else if (p < 0.7) { g.pixel(44, 22, "n5"); g.pixel(45, 22, "n5"); g.pixel(44, 21, "n5"); g.pixel(44, 23, "n5"); if (p < 0.6) { g.pixel(46, 22, "n4"); g.pixel(44, 20, "n4"); g.pixel(44, 24, "n4"); } } });}function curse(g, t) { const pulse = 0.5 + 0.5 * Math.sin(TAU * 2 * t); g.detached("tipGlow", () => { g.field({ x: [38, 47], y: [16, 28] }, (x, y) => { const r = Math.hypot(x - 44, (y - 22) * 1.1); const R0 = 4.3 + pulse; if (r > R0 || (y === 22 && x >= 39 && x <= 44)) return null; if (!dith(x, y, 0.75 * (1 - r / R0) + 0.1)) return null; return r < 2 ? "v2" : "v1"; }); }); g.detached("tip", () => { g.line([42, 22], [43, 22], "v3"); g.pixel(44, 22, "n5"); if (pulse > 0.5) { g.pixel(45, 22, "v3"); g.pixel(44, 21, "v3"); g.pixel(44, 23, "v3"); } }); g.detached("sparks", () => { const src = [[44, 21], [24, 12], [41, 18], [6, 14], [36, 13], [44, 23]]; for (let i = 0; i < 6; i++) { const p = (t * (i % 2 ? 1 : 2) + i / 6) % 1; const [sx, sy] = src[i]; const x = sx + Math.round(Math.sin(TAU * (p + i * 0.37)) * 2), y = sy - Math.round(p * 9); if (y < 0) continue; if (p < 0.3) { g.pixel(x, y, "n5"); g.pixel(x, y + 1, "v3"); } else if (p < 0.65) g.pixel(x, y, "v3"); else if (chk(x, y)) g.pixel(x, y, "v2"); } });}const SpinningWheel = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; const turning = mode !== "idle"; const ang = turning ? Math.PI * t : 0; // half a turn per loop; 8 spokes repeat every 45 degrees const crank = turning ? TAU * t : 0; const phi = turning ? TAU * t : 0.6; const pin = [Math.round(WC.x + 3 * Math.cos(crank + 1.2)), Math.round(WC.y + 3 * Math.sin(crank + 1.2))]; const yT = 41 + Math.round(1.4 * Math.sin(crank + 1.2)); contactShadow(g, 23, GROUND, 21, 2); g.outlined("ink", () => distaff(g)); g.outlined("ink", () => rim(g, ang)); spokes(g, ang); driveBand(g, mode); g.outlined("ink", () => { treadle(g, yT); bench(g); g.part("post", () => { // the front upright holding the axle g.rect({ x: [13, 15], y: [21, 30] }, "w2", { fill: "w2" }); g.line([13, 21], [13, 30], "w3"); g.line([15, 22], [15, 30], "w1"); g.pixel(13, 25, "w4"); g.line([13, 26], [15, 26], "w1"); // a turned ring }); hub(g); motherOfAll(g); flyer(g, phi); }); g.part("footman", () => { // the rod from the crank down to the treadle, in front of the bench const foot = [25, yT - 2]; g.line([pin[0] - 1, pin[1]], [foot[0] - 1, foot[1]], "ink"); g.line([pin[0] + 1, pin[1]], [foot[0] + 1, foot[1]], "ink"); g.line(pin, foot, "w4"); g.pixel(foot[0], foot[1] + 1, "ink"); }); g.part("crank", () => { g.line([WC.x, WC.y], pin, "n2"); g.pixel(WC.x, WC.y, "n3"); g.pixel(pin[0], pin[1], "n4"); }); yarn(g, t, mode); if (mode === "idle") glint(g, t); if (mode === "cursed") curse(g, t);});submit(SpinningWheel, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: 24, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "spin", args: ["spin"] }, { name: "cursed", args: ["cursed"] }, ],});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.