1.2.10: wiggle — deterministic procedural shake modifier per layer
Per-layer modifier that adds noise to the sampled position and
rotation each frame. Pairs thematically with kaleido: both stack
on top of the keyframe sample, both per-layer, both deterministic
so editor and exported runtime render identically.
Math (src/lib/wiggle.ts):
- hash32 + layerSeeds(layerId): three uncorrelated 32-bit seeds per
layer, derived from the id so the same layer always wobbles the
same way.
- wiggleNoise(seed, t): sum-of-three-sines in [-1, 1]. Smooth (C∞),
cheap, no noise-lib dependency. Phase constants are irrational
ratios picked so the streams don't collapse into a low-period
pattern.
- applyWiggle(props, layer, time): adds amp * noise(seed, t*freq)
to x / y / rotation. Defaults: 8px position, 2° rotation, 2Hz.
Wire-in:
- src/lib/interpolate.ts sampleLayer: post-processes the keyframe
sample with applyWiggle. Every renderer goes through sampleLayer
(canvas2d, Konva, button-state, clip-internal), so the modifier
lights up everywhere from a single hook.
- src/lib/exportPlayer.ts engine: byte-for-byte JS mirror of
hash32 / layerSeeds / wiggleNoise / applyWiggle, plus the same
post-process branch in the engine's sample(). RUNTIME_VERSION
31 → 32.
Data + store:
- Layer.wiggle?: { amplitude?, rotation?, frequency? }. All sub-
fields optional with the documented defaults.
- store.setLayerWiggle(id, patch | null): patch object updates
individual fields with magnitude clamping (≥ 0); null deletes
the whole wiggle field.
UI (PropertiesPanel + i18n):
- New "Wiggle" section between Mask and Kaleidoscope. Toggle
(PhWaveSine icon) seeds the pleasant defaults; three RangeSliders
for position amplitude (0-64 px), rotation amplitude (0-45°),
and frequency (0-10 Hz). Hidden for button-asset layers
(consistent with Kaleido's hit-area constraint).
Tests (8 new under "wiggle — procedural shake modifier"):
- wiggleNoise determinism + [-1, 1] bounds across 4 seeds × ~100
time points.
- layerSeeds stability + the three seeds are pairwise distinct;
different ids yield different triples.
- applyWiggle on a layer without `wiggle` is a no-op (returns the
input reference unchanged).
- sampleLayer with wiggle: drift visible across a 1s window and
always within the configured amplitude bands.
- sampleLayer with wiggle is deterministic — same time, same drift.
- setLayerWiggle roundtrip + magnitude clamp + null-delete.
- PropertiesPanel Wiggle toggle writes the wiggle field with the
documented defaults.
- Exported runtime carries wiggleNoise / applyWiggle / wiggleSeeds
and bumps RUNTIME_VERSION to '32'.
Plan for the next two backlog items (motion-path overlay + canvas
eyedropper) is in claude.txt for later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7d81a50e74
commit
9e25cf155a
11 files changed
+546
-32
No files matched your search
+53
-14
@@ -11,7 +11,7 @@ function isProject(p: ExportPayload): p is Project {
|
||||
return Array.isArray((p as Partial<Project>).scenes)
|
||||
}
|
||||
|
||||
const RUNTIME_VERSION = '31'
|
||||
const RUNTIME_VERSION = '32'
|
||||
|
||||
/** Materialize the engine source. The font map is rebuilt on every
|
||||
* call so user-uploaded custom fonts (which live in localStorage
|
||||
@@ -140,21 +140,60 @@ const ENGINE_TEMPLATE = `(function(){
|
||||
}
|
||||
return merge(base);
|
||||
}
|
||||
// Mirror of src/lib/wiggle.ts — keep in lockstep so editor and
|
||||
// exported runtime render an identical wobble at the same time.
|
||||
function wiggleHash32(s){
|
||||
var h=5381;
|
||||
for(var i=0;i<s.length;i++){ h=((h<<5)+h+s.charCodeAt(i))|0; }
|
||||
return h>>>0;
|
||||
}
|
||||
function wiggleSeeds(id){
|
||||
var h=wiggleHash32(id);
|
||||
return [(h^0x9e3779b9)>>>0, (h^0x85ebca6b)>>>0, (h^0xc2b2ae35)>>>0];
|
||||
}
|
||||
function wiggleNoise(seed, t){
|
||||
var a=Math.sin(seed*12.9898+t*78.233);
|
||||
var b=Math.sin(seed*4.31+t*11.71);
|
||||
var c=Math.sin(seed*7.71+t*23.5);
|
||||
return (a+b+c)/3;
|
||||
}
|
||||
function applyWiggle(props, layer, time){
|
||||
var w=layer.wiggle;
|
||||
if(!w) return props;
|
||||
var amp=(w.amplitude==null?8:w.amplitude);
|
||||
var rot=(w.rotation==null?2:w.rotation);
|
||||
var freq=(w.frequency==null?2:w.frequency);
|
||||
if(amp<=0 && rot<=0) return props;
|
||||
if(freq<=0) return props;
|
||||
var seeds=wiggleSeeds(layer.id);
|
||||
var tt=time*freq;
|
||||
var out={};
|
||||
for(var k in props) out[k]=props[k];
|
||||
out.x=props.x+amp*wiggleNoise(seeds[0],tt);
|
||||
out.y=props.y+amp*wiggleNoise(seeds[1],tt);
|
||||
out.rotation=props.rotation+rot*wiggleNoise(seeds[2],tt);
|
||||
return out;
|
||||
}
|
||||
|
||||
function sample(layer,time){
|
||||
var main=sampleKeyframeSet(layer.keyframes, layer.base, time);
|
||||
if(!layer.separated) return main;
|
||||
// Separated mode: position and rotation come from their own tracks
|
||||
// if present, merged back over the main-track values. This matches
|
||||
// the editor's sampleLayer() so animations on either side of the
|
||||
// Separate-toggle play identically.
|
||||
var pos = layer.positionKeyframes ? sampleKeyframeSet(layer.positionKeyframes, layer.base, time) : main;
|
||||
var rot = layer.rotationKeyframes ? sampleKeyframeSet(layer.rotationKeyframes, layer.base, time) : main;
|
||||
var outSep={};
|
||||
for(var k in main) outSep[k]=main[k];
|
||||
outSep.x=pos.x;
|
||||
outSep.y=pos.y;
|
||||
outSep.rotation=rot.rotation;
|
||||
return outSep;
|
||||
var result;
|
||||
if(!layer.separated){
|
||||
result=main;
|
||||
} else {
|
||||
// Separated mode: position and rotation come from their own
|
||||
// tracks if present, merged back over the main-track values.
|
||||
// Matches the editor's sampleLayer().
|
||||
var pos = layer.positionKeyframes ? sampleKeyframeSet(layer.positionKeyframes, layer.base, time) : main;
|
||||
var rot = layer.rotationKeyframes ? sampleKeyframeSet(layer.rotationKeyframes, layer.base, time) : main;
|
||||
result={};
|
||||
for(var k in main) result[k]=main[k];
|
||||
result.x=pos.x;
|
||||
result.y=pos.y;
|
||||
result.rotation=rot.rotation;
|
||||
}
|
||||
if(layer.wiggle) return applyWiggle(result, layer, time);
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadImages(assets){
|
||||
|
||||
Reference in new issue
Block a user