Spaces:
Running
Running
File size: 9,180 Bytes
71eae78 d1e0284 71eae78 9f89772 3647d73 9f89772 3647d73 9f89772 71eae78 3647d73 9f89772 71eae78 9f89772 71eae78 9f89772 71eae78 3647d73 71eae78 9f89772 75ec82b 71eae78 75ec82b 71eae78 9f89772 71eae78 d1e0284 71eae78 9f89772 75ec82b 3647d73 9f89772 3647d73 9f89772 3647d73 75ec82b 3647d73 75ec82b 3647d73 71eae78 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | // The torch light. Shared by the demo and the level, because it is the one
// thing in the project that neither of them is allowed to have its own version
// of.
//
// Owns the raise ramp, the flicker, where the light sits relative to his hand,
// and the coverage test that decides whether something has been seen.
import { TORCH, REVEAL_T, TONE } from './tuning.js';
const smoothstep = (t) => t * t * (3 - 2 * t);
const lerp = (a, b, t) => a + (b - a) * t;
const RAD = Math.PI / 180;
/**
* Where the two bones of the torch arm are, at a given raise.
*
* ONE PLACE, because two would drift. The renderer draws the arm from this and
* the torch hangs the flame and the light off the same answer, so the flame is
* in the hand by construction rather than by a pair of numbers that agree
* until somebody edits one.
*
* Returns offsets from the clip pivot in game pixels, +x being the way he
* faces and -y up. `arm` is the manifest's torchArm block; null gives null and
* every caller falls back to the static handPivot.
*
* @returns {{shoulder:number[], elbow:number[], hand:number[]}|null}
*/
export function armPose(arm, eased) {
if (!arm) return null;
const a1 = lerp(arm.down[0], arm.up[0], eased) * RAD;
const a2 = lerp(arm.down[1], arm.up[1], eased) * RAD;
const [sx, sy] = arm.shoulder;
// The manifest's angles have y UP, the canvas has y down, so the sines flip.
const ex = sx + arm.upper * Math.cos(a1);
const ey = sy - arm.upper * Math.sin(a1);
const wx = ex + arm.fore * Math.cos(a2);
const wy = ey - arm.fore * Math.sin(a2);
// The fist sits on past the wrist, along the forearm.
const fx = wx + arm.wristToFist * Math.cos(a2);
const fy = wy - arm.wristToFist * Math.sin(a2);
// The shaft is NOT the forearm carried on: the wrist cocks back, and that
// angle is most of what stops a held torch reading as a stick on the end of
// an arm. Upright at his side, leaning forward when it is out.
const ta = lerp(arm.torchAngle[0], arm.torchAngle[1], eased) * RAD;
const ux = Math.cos(ta);
const uy = -Math.sin(ta); // one step up the shaft, in canvas axes
const bx = fx - arm.buttBelowFist * ux;
const by = fy - arm.buttBelowFist * uy;
const headUp = arm.shaftLen + arm.bindLen;
return {
shoulder: [sx, sy],
elbow: [ex, ey],
wrist: [wx, wy],
fist: [fx, fy],
// The butt of the shaft, back down the shaft from his fist, so the brand
// runs up THROUGH his hand instead of starting at it.
butt: [bx, by],
// The foot of the fire, on top of the bound head: the fire sprite's pivot.
head: [bx + headUp * ux, by + headUp * uy],
// And where the light comes from, which is up inside the fire. STRAIGHT UP
// from the head, not on up the shaft: fire rises, so the flame stands
// vertical however the brand is angled, and its middle is above the head
// rather than along the stick.
glow: [bx + headUp * ux, by + headUp * uy - arm.fireCentre],
up: [ux, uy],
torchDeg: ta / RAD,
};
}
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
// Smooth 1D value noise. A literal 12Hz square wave would read as a strobe;
// this keeps the stated rate but moves like a flame.
function hash(n) {
const s = Math.sin(n * 127.1) * 43758.5453123;
return s - Math.floor(s);
}
function valueNoise(t) {
const i = Math.floor(t);
const f = t - i;
const u = f * f * (3 - 2 * f);
return hash(i) * (1 - u) + hash(i + 1) * u;
}
export function createTorch() {
return {
raise: 0, prevRaise: 0,
torchDeg: 90, prevTorchDeg: 90,
// The eased raise, published so the renderer draws the arm at exactly the
// value the light was computed from rather than easing a second time.
eased: 0, prevEased: 0,
radius: 0, prevRadius: 0,
cx: 0, cy: 0, prevCx: 0, prevCy: 0,
handX: 0, handY: 0, prevHandX: 0, prevHandY: 0,
reset() {
this.raise = this.prevRaise = 0;
this.eased = this.prevEased = 0;
this.radius = this.prevRadius = 0;
},
/** Copy this frame's values into the previous ones, for render interpolation. */
snapshot() {
this.prevRaise = this.raise;
this.prevEased = this.eased;
this.prevRadius = this.radius;
this.prevCx = this.cx;
this.prevCy = this.cy;
this.prevHandX = this.handX;
this.prevHandY = this.handY;
this.prevTorchDeg = this.torchDeg;
},
/**
* @param {boolean} wantUp is the button held
* @param {number} clock seconds since boot, for the flicker
* @param {number} anchorX the prince's world x
* @param {number} groundY the ground line
* @param {number} facing +1 right, -1 left
* @param {number} handDX hand offset from the pivot, from the manifest
* @param {number} handDY
* @param {object} arm the manifest's torchArm block, or null
* @param {number} fireCentre how far up the fire its light sits, for a pose
* that holds the torch in the drawing; or null
* @param {number[][]} hands where the fire stands in each frame of that
* pose, as offsets from its pivot; or null
*/
update(dt, wantUp, clock, anchorX, groundY, facing, handDX, handDY, arm = null, fireCentre = null, hands = null) {
const rate = wantUp ? 1000 / TORCH.RAISE_MS : -1000 / TORCH.LOWER_MS;
this.raise = clamp01(this.raise + rate * dt);
const eased = smoothstep(this.raise);
this.eased = eased;
if (eased > 0) {
const t = clock * TORCH.FLICKER_HZ;
// Two octaves so it never settles into a visible rhythm.
const n = (valueNoise(t) * 2 - 1) * 0.7 + (valueNoise(t * 2.27 + 31.7) * 2 - 1) * 0.3;
// A SLOW WANDER ON TOP, and it is off until somebody judges it.
//
// 4% at 12Hz is a clean circle with a tremble in it: it buzzes rather
// than breathes, and at a glance it reads as a spotlight rather than
// as fire. A third of that frequency, at three times the depth, is the
// part of firelight you actually notice — the pool swelling and
// dropping over a second or two while the edge shivers.
const slow = TONE.TORCH_LIFE > 0
? (valueNoise(t * 0.28 + 7.3) * 2 - 1) * TORCH.FLICKER_AMOUNT * 3 * TONE.TORCH_LIFE
: 0;
this.radius = TORCH.RADIUS * eased * (1 + n * TORCH.FLICKER_AMOUNT + slow);
} else {
this.radius = 0;
}
// The hand rides the arm when there is one, so the flame goes up with it.
const pose = armPose(arm, eased);
// OR RIDES HER FRAMES. The raise is her arm coming up over four frames,
// picked by how far up it is exactly as the pose's frame is, so the fire
// stays on the torch in every one of them.
if (!pose && hands && hands.length) {
const k = Math.min(hands.length - 1, Math.floor(eased * (hands.length - 1) + 1e-6));
handDX = hands[k][0];
handDY = hands[k][1];
}
// handX/handY is where the FIRE sprite's pivot goes: the top of the
// bound head, which is where a flame sits.
this.handX = anchorX + facing * (pose ? pose.head[0] : handDX);
this.handY = groundY + (pose ? pose.head[1] : handDY);
this.torchDeg = pose ? pose.torchDeg : 90;
// THE POOL COMES OUT OF THE FIRE.
//
// For most of this project it hung off a fixed offset from him, which
// was invisible while his hand never moved -- and the moment the arm
// went out it was plainly a pool of light centred on nothing. It is on
// the middle of the flame now, measured off the sprite: the pack's
// centroid sits 40% up its box and wanders less than 6px over the whole
// loop.
//
// HER DRAWING HOLDS IT UP, and the fire sits where her flame was: the
// same deal as the arm, with no arm. The light comes out of the middle of
// that fire, straight above its foot.
//
// With neither there is nothing to hang it on, so it falls back.
if (pose) {
this.cx = anchorX + facing * pose.glow[0];
this.cy = groundY + pose.glow[1];
} else if (fireCentre != null) {
this.cx = this.handX;
this.cy = this.handY - fireCentre;
} else {
this.cx = anchorX + facing * TORCH.LIGHT_AHEAD;
this.cy = groundY - TORCH.LIGHT_LIFT;
}
return eased;
},
/**
* Has this point been properly lit? True inside the radius at which the
* mask is still at least 50% opaque.
*
* REVEAL_T is solved from the gradient's own stop table at boot, so it
* tracks the falloff instead of being a number somebody wrote down. See
* tuning.js.
*
* Testing the centre of a figure's box rather than integrating its area
* fires slightly early — 21px on the current falloff, about 0.38s of
* walking. REVEAL_MS of dwell absorbs that completely.
*/
covers(x, y) {
if (this.radius <= 0) return false;
const r = REVEAL_T * this.radius;
const dx = x - this.cx;
const dy = y - this.cy;
return dx * dx + dy * dy < r * r;
},
};
}
|