// Procedural shake modifier — adds deterministic noise to a layer's // sampled props so it visibly "wobbles" over time. The same time // always yields the same value (so the editor stage, render.ts // renderer and exported runtime stay in lockstep), and three // independent noise streams (x / y / rotation) keep the wobble from // looking like a single 1D oscillation. // // Mirrored as plain JS inside exportPlayer.ts's engine source — // keep the formulas in `wiggleNoise` and `applyWiggle` byte-for-byte // equivalent on both sides. import type { AnimatableProps, Layer } from '@/types/scene' const DEFAULT_AMPLITUDE = 8 const DEFAULT_ROTATION = 2 const DEFAULT_FREQUENCY = 2 /** Stable 32-bit hash of a string. djb2-ish — good enough for * picking three uncorrelated seeds per layer. */ function hash32(s: string): number { let h = 5381 for (let i = 0; i < s.length; i++) { h = ((h << 5) + h + s.charCodeAt(i)) | 0 } return h >>> 0 } /** Three independent seeds for the layer's x, y, rotation noise * streams. Derived deterministically from the layer id so the same * layer always wobbles the same way. */ export function layerSeeds(layerId: string): [number, number, number] { const h = hash32(layerId) return [ (h ^ 0x9e3779b9) >>> 0, (h ^ 0x85ebca6b) >>> 0, (h ^ 0xc2b2ae35) >>> 0, ] } /** Sum-of-sines noise in [-1, 1]. Smooth (each input maps to a * C∞ output) and cheap. The phase constants are irrational ratios * picked so the three components don't collapse into a low-period * pattern. */ export function wiggleNoise(seed: number, t: number): number { const a = Math.sin(seed * 12.9898 + t * 78.233) const b = Math.sin(seed * 4.31 + t * 11.71) const c = Math.sin(seed * 7.71 + t * 23.5) return (a + b + c) / 3 } /** Returns true when the layer has wiggle on AND at least one of * the magnitudes is non-zero. */ export function hasWiggle(layer: Layer): boolean { const w = layer.wiggle if (!w) return false const amp = w.amplitude ?? DEFAULT_AMPLITUDE const rot = w.rotation ?? DEFAULT_ROTATION return amp > 0 || rot > 0 } /** Adds wiggle to a layer's sampled props in place — caller passes * in a mutable copy. Does nothing when the layer has no wiggle. */ export function applyWiggle( props: AnimatableProps, layer: Layer, time: number, ): AnimatableProps { const w = layer.wiggle if (!w) return props const amp = w.amplitude ?? DEFAULT_AMPLITUDE const rot = w.rotation ?? DEFAULT_ROTATION const freq = w.frequency ?? DEFAULT_FREQUENCY if (amp <= 0 && rot <= 0) return props if (freq <= 0) return props const [sx, sy, sr] = layerSeeds(layer.id) const tt = time * freq return { ...props, x: props.x + amp * wiggleNoise(sx, tt), y: props.y + amp * wiggleNoise(sy, tt), rotation: props.rotation + rot * wiggleNoise(sr, tt), } }