SOURCE / PINNED RELEASE
Made of little things.
Stick Runner
- Release
- 068f2c4fd180…
- Author-recorded commit
- 2f0b589e2e75…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-938fb0aa7b7
Archive hash verified: a0399c408781bf50…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
export type Pose = 'stand' | 'run' | 'jump' | 'fall' | 'cheer' | 'roll' | 'slide';
export interface Point {
x: number;
y: number;
}
/** One straight piece of Rog's body (a bone), and whether it's on the near or far side. */
export interface Bone {
a: Point;
b: Point;
side: 'near' | 'far' | 'middle';
}
/** Rog's whole body for one moment, measured from his feet, with +x pointing the way he faces and +y down. */
export interface Shape {
bones: Bone[];
head: Point;
headRadius: number;
/** Is he moving (so the bandana tails stream out behind)? */
moving: boolean;
}
/**
* Work out where Rog's arms, legs, body and head are.
* `cycle` moves the legs and arms while running, or is how far through a roll he is (0 to 1).
*/
export function stickmanShape(pose: Pose, cycle: number, vy: number): Shape {
if (pose === 'roll') return rollShape(cycle);
const hip = { x: 0, y: -16 };
let lean = 0;
let lift = 0;
let legs: [number, number][] = [];
let arms: [number, number][] = [];
if (pose === 'run') {
const swing = Math.sin(cycle);
lean = 3;
legs = [limb(hip, 0.2 + swing * 0.8, 16), limb(hip, 0.2 - swing * 0.8, 16)];
arms = [[-swing * 7, 7], [swing * 7, 7]];
} else if (pose === 'jump') {
const rising = vy < 0;
lean = 2;
legs = rising ? [[7, -6], [-5, -2]] : [[5, -1], [-4, 0]];
arms = rising ? [[6, -8], [-6, -6]] : [[8, -2], [-8, -3]];
} else if (pose === 'slide') {
// Surfing on the ice: feet apart, leaning back, arms out for balance.
const wobble = Math.sin(cycle * 3) * 2;
lean = -3;
legs = [[9, 0], [-7, 0]];
arms = [[10, -4 + wobble], [-10, -6 - wobble]];
} else if (pose === 'fall') {
legs = [[-7, -4], [7, -2]];
arms = [[-9, -12], [9, -12]];
} else if (pose === 'cheer') {
lift = Math.abs(Math.sin(cycle)) * 3;
legs = [[-5, 0], [5, 0]];
arms = [[-7, -14], [7, -14]];
} else {
legs = [[-4, 0], [4, 0]];
arms = [[-4, 9], [4, 9]];
}
const shoulder = { x: lean, y: -29 };
const bones: Bone[] = [{ a: hip, b: shoulder, side: 'middle' }];
// Legs (knee halfway, pushed forward a little).
legs.forEach(([fx, fy], i) => {
const knee = { x: (hip.x + fx) / 2 + 3, y: (hip.y + fy) / 2 };
const side = i === 0 ? 'near' : 'far';
bones.push({ a: hip, b: knee, side }, { a: knee, b: { x: fx, y: fy }, side });
});
// Arms.
arms.forEach(([hx, hy], i) => {
const top = { x: shoulder.x, y: shoulder.y + 1 };
const elbow = { x: shoulder.x + hx * 0.5 + (pose === 'run' ? -2 : 0), y: shoulder.y + hy * 0.5 + 2 };
const side = i === 0 ? 'near' : 'far';
bones.push({ a: top, b: elbow, side }, { a: elbow, b: { x: shoulder.x + hx, y: shoulder.y + hy }, side });
});
const shape: Shape = { bones, head: { x: shoulder.x + lean * 0.4, y: shoulder.y - 6 }, headRadius: 5, moving: pose !== 'stand' };
return lift ? moveShape(shape, (p) => ({ x: p.x, y: p.y - lift })) : shape;
}
/** Curled up in a ball, spinning forward once. */
function rollShape(progress: number): Shape {
// Squash down at the start and stand back up at the end.
const tuck = Math.sin(progress * Math.PI);
const angle = progress * Math.PI * 2;
const centreY = -10 - (1 - tuck) * 4;
const place = (p: Point): Point => ({
x: p.x * Math.cos(angle) - p.y * Math.sin(angle),
y: centreY + p.x * Math.sin(angle) + p.y * Math.cos(angle),
});
const bones: Bone[] = [];
// Curved back.
const steps = 5;
for (let i = 0; i < steps; i++) {
const t0 = Math.PI * (0.75 + (1.35 * i) / steps);
const t1 = Math.PI * (0.75 + (1.35 * (i + 1)) / steps);
bones.push({ a: { x: Math.cos(t0) * 8, y: Math.sin(t0) * 8 }, b: { x: Math.cos(t1) * 8, y: Math.sin(t1) * 8 }, side: 'middle' });
}
// Knees pulled up, arms hugging them.
const chains: [number[], Bone['side']][] = [
[[-6, 5, 2, -1, 6, 6], 'near'],
[[-5, 6, 4, 3, 7, 8], 'far'],
[[3, -4, 4, 2, 8, 1], 'near'],
];
for (const [points, side] of chains) {
for (let i = 0; i + 3 < points.length; i += 2) {
bones.push({ a: { x: points[i], y: points[i + 1] }, b: { x: points[i + 2], y: points[i + 3] }, side });
}
}
return moveShape({ bones, head: { x: 6, y: -5 }, headRadius: 4.5, moving: true }, place);
}
function moveShape(shape: Shape, move: (p: Point) => Point): Shape {
return {
...shape,
bones: shape.bones.map((bone) => ({ a: move(bone.a), b: move(bone.b), side: bone.side })),
head: move(shape.head),
};
}
function limb(from: Point, angle: number, length: number): [number, number] {
return [from.x + Math.sin(angle) * length, Math.min(0, from.y + Math.cos(angle) * length)];
}
