Repository navigation
Layer Cutter v2 — Smart Cut + 5-step UI + branding - #9
Merged
Merged
Conversation
…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>
… '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
…mized organic thunder burn
…nic tile burn mark
…ayer alpha & organic burn on tile
…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>
…sets & OGGameCard primitive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
bf15727)Test plan
🤖 Generated with Claude Code