Skip to content

Layer Cutter v2 — Smart Cut + 5-step UI + branding - #9

Merged
ozgesolidkey merged 87 commits into
mainfrom
tools/layer-cutter
Nov 7, 2026
Merged

ozgesolidkey merged 87 commits into
mainfrom
tools/layer-cutter

Conversation

@ozgesolidkey

Copy link
Copy Markdown
Contributor

Summary

  • ✂️ Smart Cut: one-tap k-means color segmentation for automatic background removal, with optional hint-box to guide subject/background split
  • 🪜 5-step guided UI: Load → Cut → Layers → Motion → Export stepper (committed bf15727)
  • 🏷️ Branding footer: KMPMedia hex mark + "v2.0 · by SolidKey" in sidebar
  • Also includes lib updates: Gerstner waves, KH flame, aurora/firefly FX, OGControl press effects, OGGlass fixes

Test plan

  • Load an image → Smart Cut fires, removes background, ↩︎ undoes it
  • Draw hint box → Smart Cut respects the box boundary
  • Branding footer visible at bottom of sidebar on all steps
  • Existing tools (Wand, Brush, Lasso, Knock white) still work

🤖 Generated with Claude Code

7storm7 and others added 30 commits October 23, 2026 10:01
…transparent part-layers

Single-file HTML (no deps). Load image → white-knockout / magic-wand / brush /
eraser / lasso to mask each part → grow/shrink/feather to tidy soft pencil edges →
set a pivot per part → export a .zip of tight-cropped transparent PNGs (base with
mover-holes punched) + rig.json in SuperXRigScreen v2 format (x,y,size,rot,pivX,pivY).
Touch-first (pinch-zoom/pan); verified in-browser end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Real undo/redo stacks (was single-level undo); ⌘Z / ⇧⌘Z + header ↩︎/↪︎ buttons.
- Wand ✚adds / ✖removes toggle so a wrong flood is reverted directly, not only via history.
- New action forks history (clears redo). Verified in-browser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…) to app host

Adds '📡 Send to app (nexum)' → builds an og-rig-payload {rig, images:{name→dataURL}}
and delivers it to an attached host: Android WebView bridge (window.NexumHost.receivePayload),
ReactNative, or parent postMessage; falls back to saving rig_payload.json when standalone.
Exposed window.__lc.buildNexumPayload for the host. Verified in-browser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ity + cutter payload

Lib (com.solidkey.painpoints.stage):
- OGCutoutRig / OGCutoutLayer + OGCutoutRigs codec: a base image + mover part-images,
  each with a joint pivot (fraction of canvas). The data a cut-out puppet needs.
- OGOfflineCutoutRigAuthor: deterministic rig → looping OGStage (base still; movers swing
  about their joints, desynced, tappable). The honest on-device floor.
- OGCutoutRigCapability (id="cutoutRig"): first-class OGAiCapability for the manifest next
  to illustrate; agent authors the OGStage, floor bakes when no AI. + bakeCutoutRig() helper.
- Joint rotation with NO lib render change: mover frames are pivot-centred, so the stage's
  centre-rotation swings about the joint; x/y offset by (pivot-0.5) seats it.
- 7 JVM tests green (:library:testDebugUnitTest).

Cutter: 'Send to app' now emits an og-rig-payload in OGCutoutRig shape — full-canvas base
(mover holes punched) + full-canvas pivot-centred mover frames. Verified in-browser: the
joint maps exactly to canvas centre, pivot fractions + shape match the lib.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e puppet move

Adds ▶ Preview motion: composites base (still) + movers (swing about their pinned
pivot) on a dark stage via requestAnimationFrame, so you verify joints + see the WHOLE
figure assemble before sending. Editing gated during preview (pan/zoom only); un-pinned
movers flagged. Guidance hint updated: cut movers OFF the body, pin each joint. Verified
in-browser (motion advances, cuts built, pivot respected, no console errors).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…auto-resume)

Full session persistence so a cut can be stopped and continued:
- 💾 Save: serialize image + every layer (mask PNG, role, punch, visibility,
  colour, pinned joint, swingDeg) to a .ogcut.json you can reopen.
- 📂 Open: restore the whole project and keep cutting.
- Best-effort autosave to localStorage (debounced, quota-safe) + a
  '↩︎ Resume last cut' button on the empty screen after an accidental reload.
Verified in-browser: serialize→restore round-trip is pixel-exact (mask px,
pivot, roles), reload→resume works, no console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tuck-to-body

Three requested rig controls:
- Front/back ordering: ▲▼ buttons on each layer row (ends disabled).
- Per-mover motion: Angle (swingDeg 0–40°) + Speed (0.2–3×) sliders in a
  new 'Motion (selected mover)' panel; both drive the live ▶ Preview.
- Tuck to body: ◀▲▼▶ nudge (tuckX/tuckY px) pulls a part toward the torso
  so the empty cut area (e.g. behind the right hand) stays hidden.
Fields flow into Preview, the nexum rig payload (swingDeg/speed/offsetX/
offsetY per mover), and the save/resume project file. Verified in-browser:
reorder, motion store+emit, serialize→restore round-trip, no console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…om cut features

Özge ("build it in the cutter, full"): a second purpose for the Layer Cutter beyond the
puppet rig. New "What are you making?" toggle: 🧍 Puppet rig (the existing cut+joints+
swing) vs 😀 Face expressions.

In Face mode you cut a face's features once (brows/eyes/mouth as mover layers) and then
pose named expressions — Neutral / Happy / Sad / Angry / Shocked — each a per-feature
{dx,dy,rot,scale} relative to neutral. Pose by dragging a feature on the canvas (tap to
select) or via nudge/Rotate/Scale controls. The assembled face composites live; ▶ Preview
expressions morphs through the whole set (demonstrates blending). Poses persist in Save/Open
and export into rig.json / the nexum payload as rig.expressions{expr:[{id,dx,dy,rot,scale}]}.

Reuses the existing cut layers + buildCut; additive (puppet-rig flow unchanged). Mouth
smile↔frown via drawn shapes is the next step (parametric mouth tool). Verified in-browser:
mode toggle swaps panels, poses collect + export correctly, morph preview runs, no console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…les + non-uniform scale

Özge: "play with the face by stretching, zooming, changing the size of parts (eyes, eyebrows)
and save the transforms as animation input." Upgrades the Face expressions mode:

- Pose model gains non-uniform scale: `scale` → `sx` (width×) + `sy` (height×), so a part can be
  stretched wide/tall independently, not just uniformly zoomed (legacy `scale` auto-migrates).
- On-canvas TRANSFORM BOX on the selected feature: 8 handles — corners = uniform zoom, side mids =
  stretch width, top/bottom = stretch height; drag the middle = move. Rotate stays a slider.
  (faceDown hit-tests handles first, then selects; faceMove applies per grabbed handle about the
  feature centre.) Panel now has Width / Height / Rotate sliders.
- Each posed state still saves + exports as animation input: collectExpressions → rig.expressions
  {expr:[{id,dx,dy,rot,sx,sy}]} in rig.json and the nexum payload. Persisted in Save/Open.

Verified in-browser: handle grab → stretch (sx/sy update non-uniformly), export carries sx/sy,
morph preview + mode toggle intact, no console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Face mode used to only transform the WHOLE selected part (affine move/
rotate/scale about its centre). Add a "🫳 Warp a point" sub-mode: a
5×5 control grid over the selected feature; drag any grid point to
stretch just that spot. A "Warp radius" slider sets how wide the pull
spreads (small = one point; big = scale a sub-area like the eye), with
smooth falloff to neighbours. Rendered via per-triangle texture mapping
so it composes with the affine box; zero-warp renders identically to the
old affine path. Warp persists in the project save and is exported
(normalized per-node) into rig.expressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion tools

Replace the fixed 5×5 grid warp with localized OPERATORS on a feature
(per expression), composed over the affine box pose and rendered via a
fine mesh + summed displacement field:
  • ⭕ Region — drag a circle over an area (the eye), then Zoom / Rotate
    just that area with a feathered (soft) edge. Drag its centre to move,
    its ring to resize. This is the "scale the eye area" ask.
  • 📍 Point — tap to drop a control point anywhere and drag to pull it
    (addable points, with an adjustable reach). Falloff is smooth.
Each operator is selectable with its own side panel (zoom/rotate/feather
or reach + delete) and a per-feature "clear".

Unify history into one timeline covering BOTH mask edits and expression
poses: every box move/stretch/rotate and every warp (add/drag/slider/
nudge/reset/clear) now snapshots the pose (lazily, one step per gesture)
so ↩︎/⌘Z + redo work across all expression tools.

Legacy grid-warps auto-migrate to point operators on load. Warp is saved
in the project and exported (normalized operators) into rig.expressions.

Browser-verified: region zoom + point pull render correctly (feathered,
no seams), real on-canvas gesture creates/undoes/redoes a region, pose+
warp undo/redo restores both, migration converts, UI wiring + panel OK,
console clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… history

The per-slider pose snapshot was tied to the slider's `pointerdown`, which
doesn't fire on every device/touch path — so scaling/rotating via the
sliders left no undo step. Replace it with histSlider(): snapshot on the
FIRST `input` of an interaction, re-armed on `change`/`pointerdown`. One
slider-drag = one undo step, robust to touch/mouse/keyboard. Applied to the
box Width/Height/Rotate and the warp region Zoom/Rotate/Soft-edge + point
Reach sliders. (On-canvas handle/warp drags already snapshot via faceMove/
warpMove.) Browser-verified: input-without-pointerdown now records + undoes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Moving Least Squares (Schaefer 2006) rigid deformation as the warp
engine, driven by a FACS action-unit preset table. One tap '✨ FACS
auto-pose' poses the whole face (happy/sad/angry/shocked) from features
named brow/eye/mouth: brows rotate/shift (inner-up=sad, inner-down=angry,
mirrored L/R), eyes squint/widen, mouth curls via two MLS handles at the
corners. Additive & backward-compatible: identity when no mls handles, so
the existing region/point field warp and affine box are untouched; mls
composes on top. Serialized in poses, exported normalized into
rig.expressions, cross-fades in morph preview, covered by undo/redo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r list (names always visible)

- Delete now pushes a layerdel entry to the shared undo stack (full layer
  object incl. mask canvas + poses preserved); undo restores it at its
  original index, redo re-deletes. Toast points to the undo button.
- New lightweight setActive(i): tap anywhere on a row (or its name/swatch)
  to select — no full re-render, so a focused name field survives and it
  feels instant.
- Layer rows are now two lines: name gets a full-width box on its own row
  (always readable, even in the narrow side panel on an unfolded Fold);
  role/punch/reorder sit on the second line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (eye → head)

A mover can now ride another layer's motion and add its own on top, so face
parts (eyes/brows/mouth) layered on the head travel WITH the head instead of
pivoting about a fixed point and drifting off the face.

- New per-mover 'Swings with ▾' selector in the Motion panel. Options list
  every other layer (self + descendants excluded, so no cycles); '— on its
  own —' clears it. Hint nudges toward eyes/brows/mouth → head.
- Render composes the full ancestor chain: each child applies every ancestor's
  tuck+swing first, then its own (cycle-guarded via a seen-set).
- parent persists in Save/Open (numeric id) and is emitted in both exports:
  rig_payload (og-rig-payload, parent = parent's sanitized name) and the .zip
  rig.json. Consumers compose parent motion first, then the child's.
- Set a child's Angle to 0 to make it ONLY follow its parent (no own swing).
- tools/rig_preview.html honors parent too (dev/verify tool).

Verified in-browser on the real SUPER X rig: wiring eyes/brows/mouth → head
keeps the face intact through the whole swing (previously a 37deg eye flung off
the face); cutter Motion panel shows the selector, cycle guard holds, preview
renders with no console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st the base

A punched child (eye) is now removed from the layer it Swings with (head) across
preview + both exports — so face parts lift off the head and animate on their own
with no baked-in ghost. Unparented movers still cut out of the base (unchanged).
Renamed the 'punch' pill to 'cut out'; hints updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…brush area

Reopening a saved project already restores the source image + every layer's mask
(so cuts ARE re-editable), but if you'd saved in the Face view the cut tools were
hidden with no obvious way back. New ✏️ on each layer row leaves Face mode, selects
that layer, and scrolls to the Wand/Brush/Erase tools — re-editing any cut is now
one tap from any state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A part wired to the head (Swings with) rides the head's joint, so it needs no
joint of its own. The preview warning now flags only movers that are both un-pinned
AND unparented (truly won't move) and names them + points to 'Swings with' as the
alternative — so it stops nagging about wired eyes/brows and just points at the head
until its neck joint is pinned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ble-shift

Diagnosed from a real rig: eyes/brows were wired to Head but ALSO carried their own
Tuck [0,16] on top of Head's [0,16], so they sat ~16px below their sockets every
frame. A wired child inherits the parent's position (incl. tuck); its own tuck is
only an extra nudge. Now: wiring a part to a parent (first time) auto-clears its own
tuck with a toast, and the Motion hint says the child already rides the parent's tuck.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… brow raise

Happy mouth was mls=corners(+1.0) = 0.45*H upward pull on both corners
(nearly half the mouth height) → pinched, tiny, over-curled smile; sx=1.06
widened it too. Now sx=1.03 + corners(+0.5). Brow raise -0.25*H read as
surprised / too prominent; now -0.12*H.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-tap revert

Lock (row toggle): a locked layer can't be grabbed/moved/warped/transformed
by touch on the canvas (face mode) nor pivot/paint/erase-edited (rig mode) —
still selectable via the Layers list to unlock. Persisted in save/open.

Reset (row ⟲): Face mode clears that layer's pose for the current expression
(undoable); Rig mode zeroes its tuck.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otion

Source grins hard, so prior subtle corner-pulls left sad≈angry (only a tiny
brow tilt differed — and that flips if L/R brow layers are named mirror-wise).
Now the MOUTH + EYES are the primary signal so the two can't look alike:
• sad  = wide strongly down-turned frown (corners -1.15) + droopy lids + inner-brow-up
• angry = small tight flat mouth (sx .84 sy .82) + squinted glare + low furrowed brows
Happy left as-is (she said it's good).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New Face-mode edit sub-tool '👄 Lip line': on Neutral, tap 3–6 points along
the mouth left→right to trace the actual lip curve (stored per mouth layer as
l.lips, rest-space). facsPose now bends THAT curve for happy/sad/angry — ends
curl up/down weighted by distance from centre, middle holds — instead of
guessing corners at fixed box fractions. Falls back to the box guess if no
line traced. Gold overlay tracks the pose so you see it bend per expression.
⌫ point / ⟲ clear to edit. Persisted in save/open. No AI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ce → any expression

Was: lip trace bent the mouth RELATIVE to its drawn shape, so a grinning
source still fought Neutral/Sad. Now facsPose measures the traced lip's own
vertical centre and reshapes it to an ABSOLUTE target per expression:
corners sit T·H above(+)/below(−) centre, middle at centre. So regardless of
what the art was drawn as (grin, frown, flat): Happy=smile(+0.22), Neutral=FLAT
(0), Sad=frown(−0.28), Angry=tight(−0.07). Neutral now flattens a drawn smile
into a true rest face — ✨ auto-pose is allowed on Neutral when a lip line
exists. Untraced mouths keep the old relative box-corner guess.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s skin, not a gap

When a part is cut out of its parent (mouth/eye out of the head, limb off the
body), the parent used to get a transparent hole. As soon as the part moved for
an expression, that gap showed through. Now the hole is inpainted with the
parent's own surrounding colour (wave-dilation from the hole border, 8-neighbour
average, mean-skin fallback) so the head/body stays whole and a moved part
reveals skin underneath. Applied in preview (buildCut), .zip export and the
nexum payload. Global toggle '🩹 fill cut-out holes with skin' (default on,
persisted in the .ogcut project).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… video behind

Adds a per-layer see-through option (shown on any cut-out part). Default is the
new skin-fill; tick see-through to leave that part's hole in its parent as a
genuine transparent hole, so a video or background behind the image shows
through it. Holes are now partitioned per parent into skin-filled vs see-through
(splitHoles): skin holes keep full silhouette alpha + inpainted colour,
see-through holes punch the alpha. Applied in preview, .zip export and nexum
payload. Persisted (seeThrough) in the .ogcut project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…angry

Sad's mouth was a big deep frown (~0.28·H corner depress) while angry's was
shrunk to a tiny tight dot — a deep frown reads as a scowl, so the intensities
were effectively swapped. Soften sad (gentle downturn + clearly raised-inner
worried brows + droopy lids) and strengthen angry (hard low furrowed brows +
tight glare + a firm present mouth). The brow furrow now carries the separation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the dominant cue)

Özge: the sad expression still looked really angry — the brows dominate. A raised
brow reads worried/sad and can never read angry; a lowered brow reads angry. So
make the vertical contrast loud: sad brow dy -0.10->-0.30*H (lifted high on the
forehead), angry stays -/+0.55*H (crushed low). The high-vs-low separation is
unmistakable regardless of the oblique rotation sign.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vs sharp-angled

Özge's model: SAD = low droopy brows + small/closed eyes + mouth down (everything
sags, soft, NO sharp angle). ANGRY = furrowed ANGLED brows + ANGLED/slanted eyes
(sharp, tense, diagonal — the angle is the anger cue). Prior version had sad's
brows RAISED (backwards) and never slanted the eyes. Now: sad eye sy 0.55 straight
droop (no rot); angry eye sy 0.72 + rot 12°*m slant; sad brow gentle rot -7; angry
brow sharp rot +22 + knit inward. Soft-sag vs sharp-angle is the separation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (angle was inverted)

Özge: angry button generated a sad face & vice-versa. Cause: the furrow/slant
direction used m from the l/r layer NAME, but her Brow_l/Eye_l are named
character-relative (= viewer's right), so every directional tilt pointed the
wrong way — angry brows splayed inner-UP (read sad), sad brows dipped inner-down
(read angry). Now m is computed by comparing each feature's centre-x to the
midline of all brow/eye features, so viewer-left/right is always correct and the
angle can never flip from naming again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7storm7 and others added 29 commits October 26, 2026 20:59
… 'dark colour')

draw() painted revealBase.mc (the base mask filled with the base layer colour,
e.g. #8b7bff purple) at 30% alpha over the ENTIRE figure whenever the Patch
tool was active. It cast a persistent purple/dark tint over everything —
unrelated to Infuse or the fill — so patches always looked darker/off-colour
and the true artwork colour was hidden. Removed it: Patch mode now shows the
real source pixels + magenta holes + backfill, true colours, no veil.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Erase tool only ever cut area out of a layer's mask (→ transparent). Added
a target choice shown when Erase is active:
• ⌫ Transparent (default) — unchanged: destination-out on the layer mask, a
  real see-through gap.
• ▢ White — paints opaque white onto the layer's non-destructive backfill, so
  the colour is wiped to white while the shape stays; shows in the edit view
  and bakes into the export. Undoes via the backfill history (erase→white pushes
  a backfill snapshot; brush + transparent-erase still push a mask snapshot), so
  each stroke is a single clean undo entry.

Verified in-browser on the elf: panel toggles with the tool, White paints an
opaque white dab that renders over the source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…primitive

OGSnowflake: add a 'depth' param (default 2) — each arm twig now recursively
sprouts smaller twigs (a seeded dendrite grown as the right half + mirrored),
so crystals read as real snowflakes instead of plain prongs. depth=0 keeps the
old look. Non-breaking (new optional param).

OGFlame: new fire sibling of OGFog/EMBERS in com.solidkey.painpoints.fx — a
body of flame as tapered, flickering, waving tongues with a hot core→cool tip
gradient; pure+deterministic flameTongues(spec,t), spec/codec/presets
(CANDLE/CAMPFIRE/TORCH/INFERNO) + OGFlameView. A 'wind' field bends the whole
flame downwind (quadratic in height, base stays rooted), drivable live so a
fire reacts to a scene's breeze. No shader, 60fps, frame-identical cross-platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tint)

New com.solidkey.painpoints.glass pack: a round draggable lens over any content.
OGGlassSpec (zoom >1 magnify / <1 minify, radius, tint+strength, rim, highlight)
+ OGGlass codec/presets (MAGNIFIER/READER/MINIFIER/AMBER/ROSE) + OGGlassView —
records the content into a GraphicsLayer (same trick as ogGlow) and redraws it
scaled about the lens centre, clipped to a circle, with tint + rim + sheen. No
shader, 60fps, Android + iOS. An accessibility win: a movable magnifier for
older eyes, a minifier to fit more, or a colour-filter glass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OGGlass body is now pointer-transparent: the lens stays parked over an area and
taps/drags fall through to the content beneath (place a word with the glass
fixed). Only a grip handle on the rim moves it; a new onRemove callback drives a
✕ button. Handle/✕ glyphs drawn with Canvas (no material dep). Android + iOS green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + gusty particle wind

OGStageContent gains two layer kinds behind the existing interface:
• Cloth — a wind-blown scarf/cape/banner/long-hair strip wrapping the motion
  family's 1-D Verlet OGClothSpec (gravity + prevailing wind + gusts), pinned in
  the box and re-simulated 0→local-time so live and exported frames match.
• Image — a raster cutout (bundled PNG/WebP) with optional frame-swap sprite
  (blink/mouth/wave) on the layer-local clock; fits widthFrac×heightFrac, aspect
  preserved. App-bundled resources (not blind-model-authorable).

Particles gain a living breeze: OGParticleSpec.wind + windAmplitude/windFrequency/
windSeed — prevailing wind plus a per-particle fractal-noise gust field, mirroring
OGClothStrip's wind model so cloth and particles blow as one. Off by default →
byte-identical to old output when wind == 0.

OGStages.cloth/image/imageStill builders; OGStageView + OGStageExport render both
kinds (export bakes them); OGStage.isLiveAt treats Cloth as live and Image as live
when animated. Full JVM suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…icle wind

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… hole

OGGlass drew the content twice: the full base pass (drawLayer) AND the magnified
copy clipped to the circle on top. Where the content is transparent (e.g. beside a
sprite like WordStorm's board watcher) the un-magnified base showed through next to
the magnified copy — a "doubled" ghost under the lens.

Now the base pass is clipped to EXCLUDE the lens disc (ClipOp.Difference), so inside
the circle only the magnified copy is visible. Shared the resolved disc (center/
radius/path) between the hole-punch and the lens draw via a LensCircle holder so they
line up exactly. No API change; lib + WordStorm build green, JVM suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lens grip drew a plain "+" (two crossed lines), which read as a zoom/plus
control rather than "grab here to move" — and clashed with app zoom "+" glyphs.
Replaced it with the universal 2×3 drag-handle dots, inked a fixed readable
charcoal (GripInk) so they contrast on the light knob regardless of lens tint
(a pale tint previously washed the glyph out). The ✕ remove handle is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dle grip)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… particle wind, fractal snow

New docs: docs/GLASS.md (OGGlass lens), docs/FLAME.md (OGFlame fire), docs/PUPPET.md
(OGPuppet cut-out puppet). Updated: STAGE.md (Cloth + Image layer kinds + builders),
PARTICLES.md (wind/windAmplitude/windFrequency/windSeed), PARAMETRIC_SHAPES.md
(recursive-fractal OGSnowflake depth), MOTION.md (particles now share OGClothStrip's
wind model), README.md (3 comparison-table rows + a v1.48.0 tour bullet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e → pill → rect)

OGGlass was circle-only, so near the edges it clipped content on its curve. Added
OGGlassSpec.aspect (width÷height) and cornerRadius (end rounding): aspect 1 + corner 1
= the old circle; aspect > 1 + corner 1 = a horizontal pill/stadium that spans a full
row with flat top/bottom; corner < 1 = a rounded rectangle. The view now builds a
rounded-rect path (shared by the hole-punch + lens draw), fills tint/sheen/rim along it,
and anchors the grip/✕ to half-width/half-height. No behaviour change at defaults.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ✕ and grip were positioned from the raw (unclamped) centre, while the
lens path is clamped by computeLens to keep the whole lens on screen. Pushed
against an edge, the handles detached from the rim and drifted toward the
drag point. Position both handles off the same clamped LensShape the Canvas
draws so they always hug the rim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g letter)

The lens centre was clamped to [halfW, w-halfW] to keep the whole lens on
screen. A wide pill lens (halfW ≈ half the view) could therefore never move
onto the first column — magnification pushed its leading content off-screen,
so "UYRUK" read as "YRUK". Clamp only the centre on screen and let the rim
spill past an edge, so the loupe can be dragged right onto edge content. Keep
each handle's anchor clamped on-screen so ✕/grip stay tappable when a corner
of the rim is off-screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… edges

The grip sat on the lens's right side (c.x + halfW*0.72). Panning right, it
hit the right wall and clamped while the centre could still travel further —
so the rightmost content was unreachable (the left panned fine because the
grip was on the far side). Move the grip to the BOTTOM-CENTRE (tracks the
clamped centre.x, clear of both side walls) and the ✕ to the TOP-CENTRE, so
the loupe reaches either edge equally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A width-less (shrink-wrapped) OGControl measured to the bare label with zero
side margin, so the roughened sketch edge clipped the first/last glyph (the
WordStorm lobby Find/Invite "sliver" bug worked around it with hard widths).

Add a `labelPadding: Dp = 14.dp` param (applied as horizontal padding around
the label) so a control gets natural breathing room by default. Callers that
drive width explicitly still get the inset inside that width; pass 0.dp for an
edge-to-edge label. Non-breaking (defaulted). Unpublished — rides the next cut.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three root algorithmic upgrades for visual quality:

1. GERSTNER WAVES (OGWaveSpec/OGWaveView)
   - Replaces sine+fbm base with trochoidal Gerstner wave trains — closed-form
     solution to the irrotational free-surface gravity-wave equation
   - 4 trains per band: seed-hashed frequencies, ±35° direction spread,
     deep-water dispersion ω=√(g·k), non-self-intersecting steepness capped at 0.85
   - Visual: sharp peaked crests + broad troughs — unmistakable water silhouette
   - New spec field: steepness (0..0.95, default 0.65)

2. KELVIN-HELMHOLTZ FLAME EDGES (OGFlameView)
   - khEdgeNoise() replaces plain fbm: 3 spatial KH modes (subharmonic +
     fundamental + first-harmonic), each with exponential height envelope
     exp(3.5·s) modelling instability growth to nonlinear saturation
   - Drift rates scaled by swaySpeed; fine fbm detail at 30% amplitude
   - bellTaper belly peak moved 30%→38% (buoyant-mixing maximum)
   - Visual: coherent vortex roll-ups scrolling upward, structured licking

3. CURL-NOISE PARTICLE TURBULENCE (OGParticleSpec/OGParticles)
   - Bridson 2007 curl noise: v = (∂P/∂y, −∂P/∂x) of fbm potential field
   - Divergence-free → no artificial clumping; gated so strength=0 is
     byte-identical to old integrator
   - New spec field: turbulenceStrength (default 0f)
   - EMBERS=0.20, LEAVES=0.45, DROPLETS=0.15 activated

Also adds OGShockwave, OGCrack, OGWave, OGBubble primitives with full
codec + presets + tests (see individual PRs for details).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…+ kira-kira sparkle

OGAuroraSpec/OGAurora/OGAuroraView:
  Multi-harmonic vertical curtain columns, altitude-stratified gradient
  (green 557nm → cyan → violet 630nm), simplex fBm shear, BlendMode.Screen.
  Presets: NORTHERN, POLAR, DREAMY.

OGFireflySpec/OGFirefly/OGFireflyView + OGFireflyState:
  Kuramoto coupled-oscillator model (mean-field O(N), Strogatz 2000).
  Luminescence = max(0,sin θ)^blinkPower. Toroidal Brownian drift.
  Presets: MEADOW (synchronisation waves), FOREST (tight flash), GHIBLI (loose glimmers).

OGRippleSpec/OGRipple/OGRippleView + OGRippleState:
  Worthington (1908) + Fournier & Reeves (1986) capillary dispersion.
  r(t) = speed·t^0.65·minDim, exp(-λt) amplitude decay, N secondary rings,
  Worthington crown droplet arcs at t < 150ms, perspective squash.
  Modifier.ogRippleInput() wires tap-to-spawn. Presets: RAIN, POND, PUDDLE.

OGParticleShape.SPARKLE + OGParticles.SPARKLES:
  Kira-kira 4-pointed diamond shape (Spencer 1995 astroid polygon).
  Fast-spinning burst preset for glass/gem/magical highlights.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…apsed hints

Replace the flat always-visible sidebar with a 5-step stepper:
  1 Load → 2 Cut → 3 Layers → 4 Motion → 5 Export
Each step panel shows only its relevant controls; long hints collapse
under ▸ toggles so the first-time view is clean. Next → buttons guide
the user forward; all step chips are always clickable for power users.
applyMode() no longer hides tools in face mode (the step system owns
that now). loadImage + restoreProject auto-advance to step 2 on load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…od vortex timing

Thunder: add OGFlameView (campfire preset) during burn phase — real fire tongues
instead of star sparks that read as bubbles. Flame sized to tile, fades in with
burning state.

Earthquake: remove all lava/fire (orange ovals, glow halo, OGFlameView, lavaSparks).
Abyss now geological gray/black depth: cliff charcoal gradient, strata shelves
3A→26→16→05 (slate→charcoal→near-black→void), floor cold dark shimmer, rock
rubble gray, shockwave gray, rim highlight cool gray not warm amber.

Flood: vortex opens only when the flood wave front actually reaches the target tile
(tileHitSweep = (cx−tileW/2+0.15)/1.50), not at a fixed p=0.14.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QUAKE — complete redesign of the hole/void:
- Old: circular/elliptical 3D cave entrance (jagged polygon + gradient cliff face)
- New: vertical tectonic rift — the board literally tears open along a jagged
  fissure exactly tile-width wide and 2.4× tile-height tall. Pure black void
  interior; no complex 3D geometry. Jagged rock TEETH on both walls pointing
  inward give the spiky crack texture. Tile drops straight DOWN through the
  opening with existing cubic-gravity physics (unchanged — it was already good).
- Random board cracks scattered across the board are kept as-is.
- Rock walls: thick dark outer stroke + illuminated bevel highlight + white glint.

THUNDER — bigger flame centered on tile:
- flameW: 3.6× → 4.8× tile width
- flameH: 4.8× → 5.5× tile height
- y offset shifted so flame base is 35% of tile-height BELOW tile center,
  making the flame engulf and cover the tile rather than rising from above it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…organic thunder flame

- Earthquake: re-orchestrated timing so ground shakes first (p: 0..0.20), cracks fracture directly on the board (p: 0.20..0.40), primary fault strikes tile and tears open a bigger crack hole (1.45x tile width), rock debris/dust erupt only at rupture, and tile plunges cinematically into the abyss
- Earthquake: removed internal overlay camera shake so cracks stay 100% anchored to the shaking ground
- Thunder: sized fire to 1.45x tile width and 2.8x height centered on the struck tile
- Fire dynamics: parabolic curved bowl root distribution, smooth teardrop root taper, and soft alpha base fade to eliminate straight horizontal cutoff line
- Added unit test for multi-tongue parabolic flame root elevation
…al hint box

One-tap ✂️ Smart Cut in Step 2:
- Samples border pixels → background color clusters (k=5)
- Samples centre pixels → foreground clusters (k=3)
- Classifies every pixel by nearest cluster, 2-pass 3×3 majority-vote smoothing
- Works at reduced resolution (max 400px) for speed, then upscales mask

Optional "⬛ Draw subject hint" mode: drag a loose box around the subject.
Outside the box → treated as background, inside → foreground.
Hint box shown as a purple dashed overlay; ✕ Clear hint removes it.

Normal manual tools (Wand, Brush, Lasso, Erase) remain below for refinement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ozgesolidkey
ozgesolidkey merged commit 36529b7 into main Nov 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants