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>
109 lines
4.5 KiB
Plaintext
109 lines
4.5 KiB
Plaintext
FUTURE FUN: THREE FEATURES TO LAND
|
||
==================================
|
||
|
||
End-of-day backlog of ideas Claude pitched and the user liked. Each
|
||
sized for ~one patch bump. Wiggle goes first; the others can wait.
|
||
|
||
|
||
1. WIGGLE — procedural shake modifier (per layer)
|
||
-------------------------------------------------
|
||
|
||
Add a "Wiggle" property to Layer. When on, every sampled frame
|
||
adds deterministic noise to position and/or rotation, giving any
|
||
layer that classic animator wobble. Pairs thematically with Kaleido:
|
||
both are per-layer procedural modifiers stacked on top of the base
|
||
keyframe sample.
|
||
|
||
Data model (src/types/scene.ts):
|
||
wiggle?: {
|
||
amplitude?: number // pixels for x/y
|
||
rotation?: number // degrees for rotation
|
||
frequency?: number // Hz (cycles per second)
|
||
}
|
||
// Defaults when `wiggle` is set but a sub-field is missing:
|
||
// amplitude=8px, rotation=2°, frequency=2Hz. Toggle off ⇒ delete.
|
||
|
||
Math (src/lib/wiggle.ts) — shared truth:
|
||
function layerSeed(id: string): [number, number, number]
|
||
// 3 stable 32-bit ints derived from the layer id, used as
|
||
// independent seeds for x / y / rotation noise streams.
|
||
function wiggleNoise(seed: number, t: number): number
|
||
// Deterministic [-1, 1] output. Sum of three sin waves with
|
||
// different irrational phase offsets — cheap, smooth, no
|
||
// dependency on a noise lib.
|
||
return (
|
||
sin(seed * 12.9898 + t * 78.233) +
|
||
sin(seed * 4.31 + t * 11.71 ) +
|
||
sin(seed * 7.71 + t * 23.5 )
|
||
) / 3
|
||
function applyWiggle(props, layer, time): props'
|
||
// Adds amp * noise(seedX, t * freq) to props.x, etc.
|
||
|
||
Wire-in:
|
||
- src/lib/interpolate.ts sampleLayer: post-process the result of
|
||
the keyframe sample with applyWiggle. ALL renderers go through
|
||
sampleLayer (canvas2d, Konva, button-state, clip-internal), so a
|
||
single hook here lights up everywhere except the export engine.
|
||
- src/lib/exportPlayer.ts: mirror the math in JS inside the engine
|
||
source so exports look identical. RUNTIME_VERSION bump.
|
||
|
||
UI (src/components/PropertiesPanel.vue + i18n):
|
||
- New "Wiggle" collapsible section, between Mask and Kaleido feels
|
||
natural. Toggle (PhWaveSine icon), then three RangeSliders:
|
||
amplitude (0–64 px), rotation (0–45°), frequency (0–10 Hz).
|
||
Hidden for button-asset layers (consistent with Kaleido).
|
||
|
||
Store (src/stores/scene.ts):
|
||
- setLayerWiggle(id, patch) with same delete-on-default semantics
|
||
as Kaleido (rotation=0 ⇒ delete the field, etc.).
|
||
|
||
Tests:
|
||
- wiggleNoise determinism: noise(seed, t) === noise(seed, t).
|
||
- wiggleNoise smoothness: |n(t) - n(t+1/60)| stays bounded.
|
||
- sampleLayer with wiggle: x/y/rotation drift around base, return
|
||
to base when wiggle is off.
|
||
- Engine source carries wiggleNoise; same sample at the same time
|
||
matches between editor and engine.
|
||
- PropertiesPanel renders the new section + writes wiggle fields
|
||
back to the store.
|
||
|
||
|
||
2. MOTION-PATH OVERLAY (selected layer's position keyframes)
|
||
------------------------------------------------------------
|
||
|
||
Visualize a layer's keyframed path on the canvas as a faint dashed
|
||
line connecting consecutive position keyframes, with each keyframe
|
||
rendered as a draggable dot. The position-keyframe drag-handle
|
||
infrastructure (PathFrame, activeSegments in CanvasStage.vue ~2562)
|
||
is already 70% there — currently only used when a position-kf is
|
||
explicitly being dragged. Promote that overlay to "always on while
|
||
the layer is selected".
|
||
|
||
Scope:
|
||
- Toggle in the View menu (or a slash-key shortcut) called "Motion
|
||
paths" — global flag persisted in localStorage like
|
||
`useShowLocked`.
|
||
- For the selected layer (and only the selected layer), draw the
|
||
overlay even when nothing is being dragged. Reuse the existing
|
||
segment / curve-sampling helpers.
|
||
- Color follows layer.color so two layers' paths don't collide
|
||
visually.
|
||
|
||
Bigger lift than Wiggle but mostly leveraging existing code.
|
||
|
||
|
||
3. EYEDROPPER (color sampling from anywhere on the page)
|
||
--------------------------------------------------------
|
||
|
||
Tiny feature using the browser's EyeDropper API. Adds a small
|
||
pipette icon next to every color swatch in the PropertiesPanel
|
||
(fill, stroke, shadow color). Click → fires `new EyeDropper().open()`
|
||
→ user picks any pixel on screen → result sets the swatch.
|
||
|
||
Caveats:
|
||
- Chromium-only API. Hide the button when `window.EyeDropper` is
|
||
undefined (Safari + Firefox) — a clean progressive enhancement.
|
||
- Permission prompt is browser-handled, no extra plumbing.
|
||
|
||
Smallest of the three. Good "polish day" snack.
|