v0.4 — Sprint 4a (today): Beauty module shipped. 5 retouching sliders inside face-oval-clipped pipeline: skin smooth (face-mesh Gaussian blur via offscreen buffer), skin glow (warm radial highlight), teeth whiten (inner-lip screen blend), eye brighten (warm lighten), eye whiten (sclera screen blend). All within face oval polygon for clean boundary. Beauty layer renders before makeup as base coat.
v0.4.1 — Precision cap (today): Beauty sliders hard-capped at 20 with 0.1 step precision after side-by-side test vs Banuba SDK. Above 20 the underlying coefficients cause unrealistic results (skin smudge, sclera bleach, denture teeth). Each slider now ships with a paired numeric input (accepts 16.5 or 16,5) for fine entry. Makeup & eyewear sliders unchanged (color/intensity domain, 0–100 still natural). Teeth-whiten algorithm preserved — outperforms reference SDK at equivalent values.
v0.5 — Sprint 4b iter 1 (today): Face Deform card scaffolded with 8 aesthetic sliders matching Banuba reference (Face slim, V-shape, Chin slim, Nose slim, Nose short, Eye enlarge, Lip plump, Smile). Same 0–20 cap + 0.1 step + paired numeric input UX as Beauty. Eye enlarge functional via Canvas2D region warp: radial scale around eye centroid with radial-gradient alpha feather edge for soft blend back into surrounding skin. Slider 0→20 maps to scale factor 1.0→1.22 (max +22% — beyond appears alien). Other 7 sliders UI-wired but warp TODO (next iterations). Render order: Face Deform runs BEFORE Beauty so retouch sees warped pixels.
v0.5 — Sprint 4b iter 2 attempt & rollback (today): Face Slim implemented via Canvas2D bilateral X-compression around face midline, exposed a fundamental limitation: compression maps pixels from outside the face oval (hair, background) into the face region, layering a translucent ghost-face over the original — visible after slider value ~2. Diagnosis: Canvas2D region warp suits expansion (Eye Enlarge works because pixels stay interior) but not compression (4 of 8 sliders: Face slim, V-shape, Chin slim, Nose slim). Implementation reverted to placeholder. Proper fix scheduled as v0.6 Phase 2 — WebGL mesh warp using 478pt Delaunay triangulation + vertex shader (Banuba's approach), single refactor solves all 4 compression sliders cleanly.
v0.5 — Sprint 4b iter 3 (today): Lip Plump implemented via Canvas2D radial expansion around lip centroid using LIPS_OUTER 20-point polygon. Same proven pattern as Eye Enlarge: lazy buffer, translate-scale-translate transform, radial alpha feather. Tighter buffer radii than Eye Enlarge (refRadius × 1.30 instead of × 2.4) because lip is much wider than tall — prevents chin/cheek pixels from being pulled into the expansion zone. Initial coefficient 0.75 (max +15%) tested as too subtle vs Banuba reference at 100%; tuned to 1.5 (max +30%) for parallel intensity. Slider 0→20 maps to scale factor 1.0→1.30.
v0.5 — Sprint 4b iter 4 (today): Nose Short implemented via Canvas2D pure Y-axis translation. Uses landmarks 168 (bridge top) and 1 (tip) for vertical extent, 49 and 279 (nostril wings) for horizontal extent. Region pixels shift upward; the area below the nose tip in the video is revealed where the tip was, making the nose appear shorter / tip upturned. Vertical-priority elliptical mask via scale-transformed radial gradient (squashed X so circle becomes ellipse in device space). Pure translation has no compression artifact risk (no exterior pixels mapped into face area).
v0.5 — Sprint 4b iter 5 (today): Smile implemented via Canvas2D diagonal translation, two independent regions for left and right mouth corners (landmarks 61 and 291). Each corner shifts up-and-out: vertical lift dominant (coefficient 0.5, max 10% mouth width up) plus secondary horizontal spread (coefficient 0.2, max 4% mouth width outward, negative for left corner / positive for right). Mask centered at the midpoint between each corner's old and new position so both endpoints sit in the full-alpha zone — the original corner is cleanly masked over by pixels from the cheek/chin side, and the new corner becomes visible at its lifted position. Localized region radius (mouthWidth × 0.20) prevents bleed into surrounding cheek or chin geometry.
v0.5 — Sprint 4b iter 5.2 intensity tune (today): Banuba 0% vs 100% comparison measured ~18% horizontal stretch; iter 5.1 was 10% max plus a fade-zone problem (stretched corner landed in the mask's partial-alpha region, washing out the effect). Two coupled fixes: coefficient bumped 0.5 → 1.0 so slider 20 produces +20% mouth width (Banuba parallel); mask widened from rx × 0.75–1.05 to rx × 0.85–1.10 and the region itself enlarged from mouthWidth × 0.65 to × 0.75 so the new corner position (0.6 × mouthWidth at full stretch) sits cleanly inside the full-alpha zone (now 0.6375 × mouthWidth) with a 22.5px smooth fade beyond.
v0.5 — Sprint 4b iter 5.3 upper-lip lift (today): Iter 5.2 matched Banuba's horizontal stretch level but missed a second component — Banuba's smile also subtly lifts the upper lip, which is what makes the smile read as a smile rather than just a wider mouth. Added Y-scale around a lower-lip anchor (landmark 17, lower lip outer bottom center): scaleY = 1 + smile × 0.5 (max +10% Y expansion at slider 20). Lower lip outer edge stays at original Y while everything above rises proportionally to its distance from the pivot — upper lip top moves up ~3px on a 30px mouth height, mouth opening widens slightly, lower lip body thins subtly. Combined pivot (mouthCx, lowerLipY) lets X-stretch and Y-expansion share a single transform — X-component depends on mouthCx, Y-component depends on pivotY, no interaction. Pure expansion in both axes = Canvas2D-safe (no compression artifact).
🎯 CANVAS2D PHASE COMPLETE: 4 of 8 Face Deform sliders fully functional in Canvas2D — Eye Enlarge (radial expansion), Lip Plump (radial expansion), Nose Short (Y-translation), Smile (X-axis stretch). All four use translation/expansion operations that map interior pixels outward or shift face features, avoiding the Canvas2D compression artifact diagnosed in iter 2. Remaining four sliders (Face slim, V-shape, Chin slim, Nose slim) all require Y- or X-compression toward an axis, which Canvas2D region warp cannot do cleanly — these are deferred to v0.6 Phase 2 WebGL milestone: Three.js + 478pt Delaunay triangulation + custom vertex shader for mesh deformation (Banuba's approach). Single refactor will deliver all 4 compression sliders plus migrate the 4 Canvas2D sliders to unified WebGL quality.
v0.5 — Sprint 4b iter 5.4 Banuba calibration (today): MediaPipe-landmark measurement on Banuba smile reference (123 frames @ 4fps, mouth_w/face_w normalized): Banuba reaches +%38.5 mouth-width + +%215 corner-lift at full slider. Our iter 5.3 measured +%9.56 / +%29.6 — 4x mouth-width deficit, 7x corner-lift deficit. scaleX coef 1.0→2.0 (max +40% transform), scaleY coef 0.5→0.25, mask rx scaled with stretched mouth width — but the Canvas2D pipeline still capped at %24 efficiency (40% transform produced only ~10% pixel-measured stretch). Root cause: ctx.scale(1.4, 1.05) + drawImage() resample softens stretched edges; MediaPipe corner detection on softened pixels drifts back toward pre-stretch position. Coefficient bumps cannot fix this — it's a structural ceiling, not tuning.
🚀 v0.6 — Sprint 5 WebGL migration (today): Architecture pivot — Three.js WebGL renderer driving a 478-vertex face BufferGeometry, MediaPipe landmarks update position.xy per frame in NDC space, Delaunator triangulates the mesh once at first lock (~900 triangles, stays constant). Single ShaderMaterial with 8 slider uniforms — all face deforms run in one vertex shader pass on the GPU, no drawImage resample loss, no anti-alias drift. Pipeline alpha-composites onto main ctx atop the raw video frame; Beauty / Makeup / Eyewear layers downstream see warped pixels and run unchanged.
v0.6.0 — Skeleton (today): WebGL pipeline live with identity vertex shader. 478-vertex mesh covers face exactly at landmark positions, fragment shader samples video texture at each vertex's image-space UV. All 8 slider uniforms wired (uSmile, uEyeEnlarge, uLipPlump, uNoseShort, uFaceSlim, uVshape, uChinSlim, uNoseSlim) but vertex shader does not yet read them — this iter verifies the WebGL pipeline matches raw video pixel-for-pixel before any warp logic is added. Acceptance test: toggle Face Deform on/off → output should be visually identical. If matched, pipeline is sound and iter 6.1+ can add per-slider displacement with confidence.
v0.6.1 — Smile vertex shader (next): X-stretch around mouth center + corner-biased Y-lift, Banuba parity target (+%38.5 mw / +%215 lift in same landmark-normalized measurement). Vertex shader: distance-from-mouth-center falloff for X-stretch, distance-from-corner falloff for Y-lift. GPU-side, no resample, no drift.
v0.6.2–6.4 — Compression sliders / migrate iter 1-4 / Banuba parity verification: Face slim, V-shape, Chin slim, Nose slim (compression, finally tractable now that we displace vertices not pixels) → migrate Eye Enlarge, Lip Plump, Nose Short, Smile to WebGL → measurement gate: pixel delta ≥ %90 of Banuba's for each slider at full value.
v0.6.31 — Sprint B Eyeliner (today): Upper-lid stroke implemented via single Canvas2D path tracing LEFT_EYE_UPPER / RIGHT_EYE_UPPER landmark sequences (same arcs Eyeshadow uses, so the line stays glued to the lid). round line caps + joins give soft termination at inner canthus and clean rounding through the lash bend. Four sliders: Intensity (0–100% alpha), Thickness (1–6 px stroke width), Cat-eye flick (0–15 px outward+upward extension prepended to the path at the outer corner — sign of x-component flips left vs right so both eyes flick away from face centre). Six-tone swatch (black, dark brown, navy, oxblood, plum, charcoal) covers Banuba's siyah/kahve parity plus four extra liquid-liner shades. Render order: between Eyeshadow and Brow — eyeshadow lays pigment first, eyeliner crisp lash line on top, brow stays untouched above. Pure source-over blend so dark stroke reads on any skin/lid base, no multiply washout.
v0.6.32 — Sprint C Contour & Highlight (today): Two paired sculpting layers shipped — Contour (multiply blend, 8 shadow zones: 2× temple / 2× jaw angle / 2× nose side / 2× cheek hollow) and Highlight (screen blend, 7 lifted zones: forehead centre / 3-point nose bridge for elongated glide / 2× cheekbone top / chin tip). Shared drawSculptZone helper drives both: per zone a radial gradient from full-alpha centre through 45 % at mid-stop to 0 at the edge, so colour fades into skin with no flat patch. Zone radius is computed as faceWidth × zone.scale × spread where faceWidth is the eye-corner distance — keeps the effect proportional whether the face fills the frame or sits small in it. Each layer has Color + Intensity + Spread sliders; Contour ships cool-brown shadow palette (6 swatch), Highlight ships champagne / gold / pearl / pink palette (6 swatch). Render order: Beauty → Face Deform → Contour → Highlight → Eyeshadow → Eyeliner → Brow → Blush → Lipstick → Shimmer → Eyewear — sculpt happens on the bare retouched skin before any colour makeup, mirroring real-world primer→contour→makeup workflow.
v0.6.32d — Highlight Natural / Dewy modes (today): Highlight rendering iterated through screen → soft-light → dual-pass during live tuning. Final architecture: two named style modes sharing the same zone geometry. Natural = single soft-light pass at full scale — glow adapts to skin tone, no saturated peak, "good skin" daily finish. Dewy = Natural halo + second screen pass at 0.40× scale & 0.45× alpha — small bright peak at each zone centre, "wet skin" gloss reminiscent of Banuba's Glow / Instagram dewy filter. Mode toggle as 2-button pill UI in the Highlight card; state held in state.highlightMode. Contour stays single-pass multiply (no mode split needed — shadow rendering is unambiguous).
v0.6.33 — Sprint D Mascara (today): Procedural fibre render shipped. For each of the 9 upper-lash landmarks per eye, a small bunch of volume quadratic Bezier curves is stroked outward from the lid line. Direction = unit vector from eye centre to landmark, with the y-component pushed toward -1.5 — gives the radial fan a real upward lash sweep instead of pointing flat sideways. Fibres in the bunch are angle-jittered ±15° around the base direction so a 3-fibre bunch spreads naturally; per-fibre length has 0.85–1.15 variance so the bunch isn't a uniform fan. Curl slider lifts the Bezier control point perpendicular-up from the fibre midpoint: 0 = straight, 100 = strong C-bend at the tip (lash-curler look). Five controls: Color (6 swatch — soft black, true black, brown, navy, dark green, plum), Intensity, Length (8–30 px), Volume (1–5 fibres per landmark), Curl (0–100 %). Pure source-over blend so dark fibres read on any skin/lid base. Total fibre count = 9 landmarks × volume × 2 eyes — at volume 3 that's 54 quadratic Beziers per frame, FPS impact < 1 ms at 480p. Render order: … Eyeshadow → Eyeliner → Mascara → Brow … — eyeshadow + eyeliner lay base on the lid, mascara draws fibres on top.
v0.6.34 — Sprint E Eye Lens (today): Coloured contact lens render unlocked by MediaPipe FaceLandmarker's 478-pt model — landmarks 468–472 (left iris: centre + 4 cardinal rim) & 473–477 (right iris) come for free, no extra inference cost. Per eye: iris centre = landmark 468/473 in pixel space; iris radius = mean Euclidean distance from centre to the four rim points (handles head tilt & eye squint naturally as the rim moves with the eyelid). Render = radial gradient disk, alpha 0.20 near pupil (0–20 % radius) so user's natural pupil black shows through, full alpha across iris body (30–85 % radius), fade to 0 at outer edge so limbal border blends cleanly into sclera. Optional Limbal Ring slider strokes a dark contour at 96 % radius — the "definition ring" effect coloured contacts use to make light eyes pop. Three controls: Color (8 swatch — brown, hazel, green, teal, deep blue, sky blue, grey, gold/amber), Intensity, Limbal Ring. Render order: … Highlight → Eye Lens → Eyeshadow → Eyeliner → Mascara → Brow … — lens recolours the iris first, then makeup layers go on the lid above. Blend mode chosen: source-over with alpha (multiply was tried, but it can only darken — cannot lift dark brown eyes toward blue/green, the most common lens use case).
v0.6.35 — Sprint F Background (today): Selfie segmentation pipeline wired alongside FaceLandmarker — MediaPipe Tasks Vision ImageSegmenter running the selfie_segmenter.tflite float16 model on GPU, outputConfidenceMasks: true so we get a smooth Float32 person-probability map instead of binary regions (no morphology pass needed). Two modes: Blur — Canvas2D ctx.filter = blur(Npx) applied to the raw frame on an offscreen bgCanvas, slider 5–40 px. Color — solid fill on bgCanvas, hex picker. Composite is three Canvas2D ops in the existing main ctx: draw raw video full-size → destination-in with the upsampled mask (Canvas bilinear handles the 256×256→frame upscale, soft edges) → destination-over with bgCanvas. FaceDeform / Beauty / makeup all run downstream on the composited frame, completely unchanged. Inference cost: ~3 ms per frame on GPU (FPS impact < 5 % at 480p). Bundled UX: Hide HUD toggle inside the Background card defaults ON — when Background is enabled, the top status / fps / face / infer / tilt / layers pills disappear from the live preview so the stage reads clean for video-call screenshots or content capture. Render order: Background → Face Deform → Beauty → … → Eyewear — composite first, everything else paints on top.
v0.6.36 — Sprint G Hair Color (today): Came almost free off the back of the v0.6.35c multiclass segmenter switch — that model already emits a dedicated confidenceMasks[1] = P(hair) channel separate from face-skin, body-skin, clothes, and accessories. New layer reads that mask, applies the same smoothstep edge cleanup (lo 0.30, hi 0.70), then composites with Canvas2D's color blend mode — that operator takes hue+saturation from the source (chosen colour) and luminance from the destination (real hair texture), so highlights / shadows / strand detail are preserved while only the base hue changes. Same technique professional hair-dye preview apps use. Two controls: Color (8 swatch — black, dark brown, brown, auburn, blonde, platinum, red, purple) + Intensity. 2 px composite blur softens the mask edge upsample.
v0.6.37 — Sprint D iter 2 mascara iris-referenced (today): Sprint D's v0.6.33 mascara used the upper-lash landmark centroid as the direction reference plus a hard dy -= 1.5 upward bias to push the fan up. On squinted or partly-closed eyes — common selfie pose — MediaPipe FaceLandmarker's pose-adaptive mesh pulls the upper-lash landmarks down toward the iris row, the centroid lands near eye-centre, centroid-relative dy collapses to ~0, and the bias-only fan visually sprouts from INSIDE the eye opening — "spider-lash" downward read instead of upward sweep from the lid edge. Fix: switch direction reference to the iris-centre landmark (468 left / 473 right — already tracked at no extra cost by the 478-pt model used for Sprint E Eye Lens). Direction vector = upper-lash − iris-centre; upper-lash arc geometrically sits above the iris row in every eye pose, so the vector is structurally upward+outward, no dy -= 1.5 bias needed. Side benefit: corner landmarks (33, 133, 263, 362) get a more horizontal vector, mid-lid landmarks (159, 386) get a near-vertical vector — natural anatomical fan dispersion without per-landmark special-casing. Two further tweaks: (a) fibre root pushed 2 px outward along the base direction so origin sits on the lid skin edge rather than inside the eye orifice; (b) Curl coefficient pulled back 0.35 → 0.25 — max curl on a 30 px fibre now lifts the control point 7.5 px instead of 10.5 px, natural lash-curler bend rather than over-lifted C.
v0.6.37b — Sprint D iter 3 mascara density boost (today): iter 2 fixed direction but live test read thin — 9 lash landmarks × volume 3 = 27 theoretical fibres per eye yet visually only ~10. Two causes diagnosed: (a) fan span ±15° was so narrow that multi-fibre bunches stacked on top of each other instead of spreading, so volume slider added overlap not density; (b) the 9 base landmarks leave ~4–5 px gaps along the lid arc, no fibre origins in between. Three coordinated tweaks: (1) origin list interpolated 9 → 17 — each consecutive lash-landmark pair gets an in-between midpoint, base-point density doubles along the lid without adding any new model inputs; (2) fan span widened ±15° → ±22°, so the volume slider now spreads fibres outward rather than stacking them; (3) line width 1.4 → 1.6 px, marginal but adds visible presence per fibre at low intensity. Direction logic unchanged — still origin − iris-centre. Total fibre count at volume 3 climbs 54 → 102 quadratic Beziers per frame, still well inside the < 5 ms budget at 480p (live infer holds ~10 ms total).
v0.6.37c — Sprint D iter 4 mascara inner crop (today): iter 3 density boost surfaced a second issue at high intensity — fibre bunches rooted at the inner-canthus landmarks (173 + 133 left, 398 + 362 right) fanned outward into the caruncula and onto the nose bridge, anatomically wrong since real upper-lash roots stop at the lash line, not the tear duct. Fix: introduce mascara-specific cropped arcs LEFT_LASH_MASCARA / RIGHT_LASH_MASCARA = upper-lid landmarks minus inner 2 per eye (kept: 33, 246, 161, 160, 159, 158, 157 left and 263, 466, 388, 387, 386, 385, 384 right). After interpolation origin count goes 17 → 13, fibre count at volume 3 = 78 (was 102), density drops ~25 % but inner bleed onto the nose is fully gone. Eyeliner / Eyeshadow continue to use the full LEFT_EYE_UPPER / RIGHT_EYE_UPPER arcs — they trace a smooth path with no radial fan emission, so inner corner remains correct for those layers.
v0.6.37d — Shimmer Lipstick silver-glint final iter (today): Shimmer Lipstick subsystem evolved through v0.6.23 → v0.6.27 (5 iterations: density 480 → 220 → 350 → 105 → 31, size 0.7–1.8 px → 0.9–1.3 px, colour pearl → pure white). Each round Ömer flagged the result as overshooting or off-tone — last iter dropped to 31 grains at slider 100 %, still read as patchy gold/sand rather than silver. Root cause finally diagnosed: source-over blend mode was overwriting the underlying multiply-applied lipstick base with pure white grains, but at 0.9–1.3 px radius the anti-aliased grain edges blended back into the dark lipstick base colour through the partial-alpha pixel ring around each dot, contaminating the white emission and shifting perceived hue toward gold/sand on red, brown, plum bases. Three orthogonal fixes shipped together: (1) BLEND source-over → lighter (additive) — pure-white emission is added on top of the base instead of overwriting it, so anti-aliased grain edges cannot drag base colour through; grain renders as true silver glint on every lipstick hue. (2) SIZE locked to fixed 0.6 px radius — no per-particle variation, all grains identical (Ömer brief: 'eşit ölçü, mikron'); ~1.2 px anti-aliased display footprint. (3) DENSITY shimmer * 31 → shimmer * 400 (12.9× boost) — with additive blend the higher count no longer overpowers, it now reads as homogeneous coated pigment instead of scattered specks. Alpha sabit at 0.85, no slider modulation — slider intensity governs grain count only. Distribution still stratified-grid jittered (homogeneous, frame-stable).
v0.6.37e — Shimmer Lipstick mouth-cavity clip (today): v0.6.37d shipped the silver-glint fix, but the shimmer grain pass was still clipping to LIPS_OUTER only — Step 2 base colour fill already used LIPS_OUTER + reversed LIPS_INNER with evenodd to carve the mouth opening out, but Step 3 grain clip was a single outer path. With the mouth closed the inner cavity is near-zero area so the bug was invisible; opening the mouth exposed grains landing on teeth / tongue / inner cavity. Fix: extend the grain clip path to match Step 2 — outer outline forward, inner outline reversed, ctx.clip('evenodd'). Anatomically correct (real lipstick sits on the lid skin, never inside the mouth); the stratified grid still distributes 400 grains across the OUTER bbox, but any grain landing in the now-carved inner hole is clipped out so the effective on-lip count shrinks gracefully as the mouth opens — same behaviour the base colour layer already had.
v0.6.37f — Eye Lens lid clip + Wayfarer card scope (today): Two unrelated production bugs paired into one deploy. Eye Lens: v0.6.34 drew the iris disk at r×1.05 and limbal ring at r×0.96 with no clip path — fine on fully open eyes but the disk's top arc and the dark limbal stroke overshot onto the upper-lid skin as soon as the user squinted, and Intensity above 20 % made the spill obvious. Fix: introduce LEFT_EYE_LOWER / RIGHT_EYE_LOWER constants (MediaPipe canonical 478-pt mesh: 33-7-163-144-145-153-154-155-133 left, 263-249-390-373-374-380-381-382-362 right) and per-eye ctx.clip() to the closed polygon traced by upper-lid forward + lower-lid reversed. Iris disk + limbal ring now physically cannot cross the lid boundary; squinting trims the disk along the lid contour for natural occlusion. Two secondary tweaks: limbal radius 0.96 → 0.92 (sits inside iris body, more headroom before lid clip kicks in), limbal width factor 0.10 → 0.08 (thinner ring, no chunky edge at high Limbal Ring values). Wayfarer card: the Eyewear card markup hard-codes class="frame-thumb active" on the Wayfarer thumbnail to indicate it's the default frame selection, and the matching CSS .frame-thumb.active applies border-color: var(--accent) + a 3 px red glow + accent-coloured name. When the Eyewear toggle is off the card receives .off for 0.42 opacity, but the active red border still bleeds through. Fix: scope both .frame-thumb.active CSS rules under .card:not(.off) so the red highlight only appears while Eyewear is enabled — toggle off, the frame thumbnails read as neutral selectable cards exactly like the other layer panels.
v0.6.37g — Eye Lens limbal ring anatomical render (today): Ömer captured a 10-step Limbal Ring slider sweep (0 → 100 % at 10 % increments) on his own eye plus 16 reference photographs spanning sky-blue, green, hazel, light brown, dark brown, and clinical anatomy diagrams (Image 11 documents "thick limbal seen in children" → "middle aged no limbal" → "arcus senilis over 60s"). Three live observations against the anatomical reference: (a) the v0.6.37f ring renders as a sharp opaque stroke even at slider 10 %, not the soft semi-transparent transition tissue actually present at the iris-sclera boundary; (b) ring colour reads as pure black, but real melanin-pigment edges sit closer to a warm dark grey; (c) ring opacity stays constant on every swatch, but real anatomy shows the ring prominent on light blue / green / grey eyes and effectively invisible on dark brown irises where iris darkness already absorbs the limbal edge. v0.6.37g rewrites the ring as a radial-gradient kuşak instead of an arc stroke — inner edge fades transparent into the iris body, peak opacity sits at r×0.95 (the anatomical ring centre), outer edge fades transparent into the sclera. Colour pulled from #0a0a0a pure black to rgba(20,15,10) warm dark grey matching melanin tone. Per-eye iris luminance scaling: Rec. 601 luminance of the swatch hex maps to a lumScale factor in [0.40, 1.00] — sky-blue / pearl swatches render at full ring opacity, gold-amber / hazel mid-band, brown / dark-brown swatches reduce ring opacity by ~50 % so the ring naturally recedes into the iris exactly like the reference photographs. The eyelid clip from v0.6.37f still bounds the entire pass — squinting trims the gradient ring along the lid contour without re-introducing the v0.6.34 spill. Limbal slider at 0 still produces zero ring (anatomical "no ring" option Ömer explicitly requested for users who don't want the effect).
v0.6.37h — Eye Lens lid clip outer-arc compensation (today): Ömer's 10-step Limbal Ring slider sweep on his own eye revealed a systematic asymmetry — the ring rendered cleanly on the inner-canthus side of each eye but vanished on the outer-canthus side at every slider value 25 → 100 %. Centring was correct (iris centre landmark 468/473 sat correctly on the pupil); the asymmetry came from the lid clip path itself. MediaPipe canonical face mesh places landmark 33 (left outer corner) and 263 (right outer corner) directly on the iris outer rim — anatomically the lateral canthus sits flush against the iris — while inner-corner landmarks 133 / 362 sit further out in the caruncula region. The v0.6.37f raw lid contour clip therefore passes through the iris on the outer side, and the limbal ring outer fade at r × 1.04 falls outside the clip on that half. Fix: per-landmark radial expansion. Each lid landmark is pushed outward from the iris centre to guarantee a minimum clearance of r × 1.06, just beyond the ring's outer fade. Landmarks already further from the iris (mid-lid arc points like 159, 386) stay untouched, so the natural lid silhouette is preserved everywhere except the points that would have pinched the iris. Net effect: the limbal ring now reads continuously 360° around the iris on every swatch and at every Limbal Ring slider value, on both eyes; the lens disk colour fade still ends at r × 1.02, well inside the new clip, so there's no skin bleed from the disk either.
v0.6.37i — Eye Lens two-pass clip (today): v0.6.37h's single-pass expanded clip fixed the limbal ring's outer-canthus drop-off but introduced a second bug — on squinted poses (Ömer's natural selfie position), MediaPipe pulls the upper-lid mid-arc landmarks (159 left, 386 right) down close to the iris row, and the v0.6.37h pushOut logic pushed those landmarks radially outward to r × 1.06 just like the lateral-canthus points. The clip therefore opened above the actual eyelid skin, and the lens disk colour fade (which ends at r × 1.02) bled onto the upper lid as a coloured haze. Classic case of solving one symptom and breaking another. Correct architecture: render the lens disk and the limbal ring as two independent clipped passes. Pass 1 (lens disk): raw lid contour clip — disk is anatomically incapable of crossing the visible eye aperture, so squint trims it along the real lid edge with no skin spill, exactly the v0.6.37f behaviour Ömer originally signed off on. Pass 2 (limbal ring): expanded clip with r × 1.06 minimum clearance — same per-landmark pushOut as v0.6.37h, but now isolated to the ring pass so disk skin-bleed is impossible. The ring itself is a semi-transparent radial gradient (peak alpha 0.55 × lumScale × slider value, outer fade at r × 1.04), so any portion that lands on lid skin beyond the actual lid edge appears only as a soft fade — not a hard disk colour. Net result: lens disk anatomically clean, ring 360° visible around the iris on every swatch / slider value, both problems addressed by separation of concerns rather than a single compromise clip.
v0.6.37j — Embed mode via URL parameter (today): Prepares Spring Mirror for iframe embed inside the springengine.ai Next.js app (planned /tools/spring-mirror route per AK#3). v0.6.35 Sprint F shipped a "Hide HUD" toggle, but it only activates when the Background layer is enabled and lives inside the Background card — useless for an embedded preview surface where the right-hand control panel might also be hidden by the host page CSS, and the user can't reach the toggle. New mechanism: URL parameter ?embed=1 (also ?hideHud=1 as alias) read on init via URLSearchParams, adds embed-mode class to document.body. CSS rule body.embed-mode .hud { display: none !important; } hides the entire pill container (status / fps / face / infer / tilt / layers) regardless of which layers are active or which toggles the dev has flipped. Direct page load at springengine.ai/spring-mirror-v06.html stays unchanged — HUD visible for live tuning, FPS / inference timing observable. Iframe load at springengine.ai/spring-mirror-v06.html?embed=1 renders a clean stage with no dev overlay, ready for production embed. No JS state, no toggle UI, no runtime cost beyond a single class check at init.
v0.6.37k — Face Deform mesh extension shoulder-drag fix (today): Live V-shape sweep on Ömer's webcam preview at slider values 5 / 11 / 20 % revealed that the jacket / shoulder silhouette directly below the jaw (left + right shoulder caps adjacent to the chin) tracks inward together with the chin as the slider rises — meant to be a strictly facial deform, but bleeding into garment / body pixels. Root cause traced to the v0.6.19 mesh extension architecture: to give Face Slim ear-proximity pixel coverage (beyond MediaPipe's lm234/454 mesh boundary), 6 synthetic vertices were appended to the face BufferGeometry at {forehead-L, cheek-mid-L, jaw-L, forehead-R, cheek-mid-R, jaw-R} with X offsets at ±0.30/0.35 of face width beyond the canonical landmarks. The 2 "jaw" extension vertices (lm172 / lm397 Y, ear-proximity X) sit squarely inside V-shape's jaw band falloff (anchor Y = mix(faceCenter, chinTip, 0.42), radius mw × 0.65), so the X-compression formula pos.x = faceCenter.x + (pos.x - faceCenter.x) × (1 − vshape × 0.40 × jawBand) pulls them inward by ~6–8 % at slider 20. The triangles they anchor span the beard outer edge, neck start, and the visible upper-shoulder pixels — texture sampling follows the deformed vertices, so all those pixels drag inward with the jaw. Fix: tag the 6 extension vertices with a per-vertex attribute aIsExtension = 1.0, real MediaPipe landmarks stay at 0.0; vertex shader checks the flag right after reading position.xy and early-returns with identity output for extension vertices. Net effect: extensions stay anchored at their reference-landmark + offset position regardless of slider state, only the 478 real landmarks deform. Face Slim ear-proximity coverage that motivated the extensions is preserved (extensions still widen mesh boundary for expansion-style deforms — Eye Enlarge / Lip Plump / Smile / Nose Short don't care about the flag because they don't compress at the boundary anyway). Shoulder / neck / jacket silhouette stays put across the full 0 → 20 V-shape slider range. Same fix simultaneously closes the matching latent bug on Face Slim, Chin Slim, Nose Slim — all four compression sliders.
🎨 v0.6.38 — Sprint H Eyeshadow Soft Edge (today): Live test in the embedded /tools/spring-mirror page revealed that the v0.6.34 eyeshadow implementation rendered as a flat "wall paint" patch — uniform multiply blend over the full LEFT_EYE_UPPER / RIGHT_EYE_UPPER polygon lifted by eyeH × liftMul, no edge diffusion. Real makeup (Ömer captured 3 anatomical reference frames spanning brown/copper/purple eyeshadow looks plus a clinical 3-tone tutorial photo) shows pigment density peaks mid-lid and fades smoothly into inner canthus, outer canthus, and upward into the crease — never a hard polygon boundary, especially noticeable on the top edge where the rendered lift meets clean brow-bone skin. Direct request: port the mask edge softening pattern from Hair Color v0.6.36 + Brow modules to Eyeshadow with a user-controllable slider. Architecture identical to Hair Color: (1) single reusable offscreen esOffscreen canvas (lazy-init, viewport-aware re-alloc), (2) draw eyeshadow shape on offscreen at full opacity / source-over with no blend, (3) transfer to main ctx with ctx.filter = blur(softness px) applied during drawImage — Canvas2D filter runs the Gaussian blur on the source bitmap in pixel domain, diffusing every polygon edge (inner canthus, outer canthus, lifted upper boundary, lash-line lower edge) uniformly before the multiply composite. New slider Soft Edge 0–30 px, default 12 — 0 reproduces the v0.6.37k sharp polygon look exactly (backward-compat), 12 produces the natural-makeup parity that matched the reference photos in live test, 30 ships full smoky-eye diffusion. Multiply blend preserved so lid texture (eyelid creases, skin tone) shows through the pigment rather than being painted over. Performance: single offscreen reuse + browser-accelerated Gaussian blur path (Chromium hardware-blur on supported GPUs); 480p frame measured <1 ms — well inside the per-frame budget. Render order unchanged: … Eye Lens → Eyeshadow → Eyeliner → Mascara → Brow … — eyeshadow lays the diffuse pigment base, eyeliner crisp lash line stays unaffected on top, no mutual interference.
🎨 v0.6.39 — Eyeliner Tapered & Winged Polygon (today): v0.6.31 Sprint B implementation kullanıcı testinde tek tip uniform stroke + round lineCap nedeniyle "yapay" görünüyordu. Ömer 5 anatomik referans fotoğraf paylaştı (klasik doğal lash line / 6-stil winged grid / 6-renk winged grid / dramatic smoky / doğal genç-kız look) — ortak pattern net: real eyeliner inner canthus'tan outer corner'a doğru kalınlaşır, wing tip ise sivri bir nokta (liquid-liner brush'unun fırça-ucu izi). Mevcut ctx.stroke() mimarisi yapısal olarak ikisini de veremiyor — uniform lineWidth taper imkânsız, lineCap: 'round' wing'i yuvarlak top yapar (sivri uç değil). Refactor: stroke → filled polygon. Top edge MediaPipe canonical LEFT_EYE_UPPER / RIGHT_EYE_UPPER arc'ını izler (outer→inner, lash line). Bottom edge her top vertex'in iris center'a (lm 468 sol / lm 473 sağ) doğru radial offset'i — magnitude per-vertex thickness, hesaplandığı için kafa eğikse bile lash line yönü anatomik doğru kalır (radial-from-iris geometry inertial frame'den bağımsız). Thickness per-vertex linear: thickAt(t) = thickMax × (1 − t) + thickMin × t, thickMin = thickMax × (1 − taper × 0.85). Taper=0 uniform (v0.6.31 backward-compat eşdeğeri), taper=60 doğal anatomic gradient (default), taper=100 inner'da neredeyse görünmez ince (Image 7 doğal genç look). Wing entegrasyonu polygon path'in dışsal extension'ı: flick > 0 ise path moveTo(wing_tip) ile başlar, lineTo(outer_top) wing'in üst kenarını çizer, sonra normal lid trace, en sonda closePath() outer_bottom'dan wing_tip'e düz hat çekerek wing'in alt kenarını kapatır — tip noktasında üst+alt kenar birleşir = anatomik sivri uç, hiç round lineCap artefakti yok. Slider güncellemeleri: Thickness 1–6 → 1–10 px (default 3 → 4, referans paritesi için), Flick 0–15 → 0–30 px (Image 6 dramatic range için), Taper YENİ slider 0–100% default 60. Color & Intensity slider'ları değişmedi. Render order korundu: … Eyeshadow → Eyeliner → Mascara … — eyeshadow soft-edge difüzyonu altta, eyeliner crisp polygon üstte, mascara fibres en üstte.
👓 v0.6.39h/i/j — Round-Gold pipeline final iter (today): 39g rembg u2netp pipeline saydam plastik nose-pad anatomik kusur yarattı (yüzde "krem kanat"). 39h color-distance refactor + 39i HSV-based nose-pad ROI silme + 39j geometric circle-fit lens protection. Final pipeline: HSV teal blob detection (top-2 connected components) → her lens için bbox'tan daire parametreleri (center + radius = (W+H)/4) → protect = daire-içi + 20 px halka + bridge dikdörtgen + 50 px hinge band → KILL = alpha>0 AND NOT protect. Saydam plastik nose-pad + lens-arası + lens-altı tüm transparent gri pikseller tek mask ile sıfırlandı (31735 px). Final 480×200 / 88 KB. Pipeline öğrenimi: round/elips gözlükler için circle-fit region protection, HSV/threshold-based mask'ten anatomik olarak daha doğru sınır verir.
👶 v0.6.40 — Kids Mode Sprint (today): Spring Mirror artık çift mod sistemi. Yeni state.eyewearMode = 'adult' | 'kids' ile aktif galeri seçilir. Adult: mevcut 10 frame (gerçek ürün fotoğrafı baz). Kids: 17 yeni cartoon/illustration sticker (Heart, Cat-Eye Green/Red, Aviator Blue/Pink, Round Blue/Olive/Pink, Square Orange, Rect Red, Wayfarer, Shield Blue, Half-Rim Purple/Orange, Oval Yellow, Oversized Pink, Shield Pink). Pipeline: 2 görsel kaynak (Set A 12 glossy + Set B 15 flat), color-distance threshold ile checkered BG kaldırma (BG iki ton: dark + light corner sample → minimum distance), open+close morphology + hole-fill, cell merkezine yakın connected components seçimi, color decontamination (inner_mask = dist>50 OR V<0.18 — siyah outline zorla iç-maskeli; iç bölge RGB gaussian blur σ=3 ile yayılıp edge zone'da BG-renkli kenar piksellerinin yerine geçer → yeşilimsi halo elimine), 480×variable LANCZOS resize. 27 aday → kalite + stil çeşitliliği filtresi ile 17 unique seçildi (clubmaster A_01 üst çerçeve washout, A_12 yarım, B_02 BG kalıntı, B_12 çerçevesiz vb. elendi). Toplam 634.8 KB inline base64 embed. UI değişikliği: Eyewear card title row'una Adult/Kids 2-button pill toggle eklendi (Mascara Natural/Dewy pattern bazlı), frame count badge dynamic (10/17). Her mode kendi state.frameAdult / state.frameKids seçimini hatırlar; toggle anında geri yükleme. JS yapısı: framesAdult + framesKids ayrı objeler, legacy frames global aktif mode'a alias yapılır (drawEyewear render lojigi backward-compat). Thumb click handler scope'lu: el.closest('.frame-gallery').dataset.mode ile galeri belirlenir, sadece o galerinin active class'ı yönetilir. Scale slider min 140 → 100 genişletildi: 1.40× → 1.00× alt sınır (kullanıcı kafa pozisyonuna göre çocuk gözlüklerini de küçültebilir, Ömer talebi).
👓 v0.6.41 — Adult+Kids genişletme partisi 2 (today): Üçüncü gözlük partisi işlendi. 5 unique kaynak görsel + 3 silhouette set duplicate. Seçenek 3 stratejisi (renksiz silhouette set'leri ATLA, sadece anlamlı renkli/gerçek olanları al) uygulandı. Adult'a 3 ekleme: Optic Metal (round ince metal optik, rembg u2netp + geometric circle-fit + nose-pad temizliği), Optic Black (round siyah optik, rembg + sıkı opening/closing morphology + center-keep), Oval Pink (oval pembe sunglasses, V7 pipeline). Kids'e 11 ekleme: Wayfarer Yellow (cartoon glossy, simple hard threshold), 10 renkli outline (12'li grid kaynak, global connected components + dilation 8px ile yakın lens'ler birleştirilip 12 sticker auto-detect, sonra bbox-cropped cell üzerinde V7 pipeline): Aviator Red, Cat-Eye Yellow, Round Green, Oversized Orange, Oversized Blue, Wayfarer Blue, Round Purple, Cat-Eye Pink, Rect Clear, Heart Red (Wayfarer Teal + Rect Dark yarım çıkış nedeniyle elendi). Pipeline öğrenimleri: (1) Optik gözlükler için ML segmentation (rembg) zorunlu — color-distance threshold ince siyah metal çerçeveyi yutuyor; (2) Grid'de bitişik sticker'ları doğru ayrıştırmak için global mask + 8 px dilation gerekli (grid kesimi tek başına yetersiz, sticker'lar cell sınırına sıkışınca yarım kalıyor); (3) cartoon glossy'lerde decontamination kapatılmalı (siyah outline rengini yana yayıyor); (4) gerçek ürün fotoğraflarında nose-pad temizliği geometric circle-fit ile ayrı sprint olarak yapılmalı. Sticker isimleri AK#10 dışı (Spring brand değil, fal'dan da değil — generic outline/illustration). Toplam UI: Adult 10 → 13 frame (Optic Metal/Black/Round + Oval Pink), Kids 17 → 28 frame (Wayfarer Yellow + 10 outline). Frame count badge dynamic. Scale slider 1.00× - 2.60× korundu (v0.6.40'tan). 14 yeni inline base64 PNG embed.
👓 v0.6.42 — Outline pipeline iyileştirme + 2 yarım sticker eleme (today): v0.6.41 deploy SS'te 3 sticker etrafında belirgin checkered BG kalıntıları görüldü (Wayfarer Blue, Cat-Eye Yellow, Oversized Blue). Pipeline V10 yeniden tasarım: (1) Local cell BG-distance threshold 30 → 60 (checkered BG'nin maksimum renk-uzayı uzaklığı ~57, eski threshold checker pattern'ı geçiriyordu); (2) Inner mask threshold 50 → 90 (decontamination etkisi pekişti); (3) Global processing yerine MANUEL bbox koordinatları (4×3 grid, her sticker için sabit cell sınırı — komşu sticker pixel sızıntısı önlendi). 8 grid sticker temiz çıktı (Cat-Eye Yellow, Round Green, Oversized Orange, Oversized Blue, Wayfarer Blue, Round Purple, Cat-Eye Pink, Heart Red), 4 sticker (Aviator Red + Wayfarer Teal + Rect Dark + Rect Clear) ince outline + yarım çıkış nedeniyle elendi. Pipeline öğrenimi: checkered BG'li transparent grid'lerde threshold sticker piksellerinin BG'den minimum uzaklığına göre kalibre edilmeli — renkli sticker'lar için dist>60 yeterli, ince siyah/koyu outline sticker'lar için ML segmentation gerekli. Toplam UI: Adult 13 frame (değişmedi), Kids 28 → 26 frame (2 yarım sticker eleme).
👓 v0.6.43 — Optic Metal/Black hold-back, Oval Pink V11 fix (today): v0.6.42 deploy SS'te 3 yeni adult sticker'da (Optic Metal, Optic Black, Oval Pink) PNG'nin etrafında BÜYÜK opaque checkered BG alanı yüze render edildi — pipeline kaynak görselin checkered transparent indicator desenlerini alpha=0'a kesemediği için PNG dosyasında dikdörtgen "BG göstergesi" alanları opak olarak embed edilmişti. V11 pipeline iter: çok sıkı threshold (dist_outer 30→60-70, dist_inner 50→90-110). Oval Pink: ✓ tam temiz çıktı (pembe lens + altın metal çerçeve, BG kalıntı yok). Optic Metal + Optic Black: ÇOK PARÇALI sonuç — ince siyah/gümüş metal çerçeve color-distance pipeline'a karşı düşük kontrast (BG ~31-58 gri, metal çerçeve ~40-80 koyu gri, distance < 50 → threshold > 60 çerçeveyi yutuyor). Bu iki sticker bu sprintten geri çekildi; daha yüksek kontrastlı kaynak görsel gerekli (beyaz/açık BG + koyu çerçeve, veya HDR studio shot — current Gemini-generated source telefon screenshot kompozit + checkered transparent indicator BG, kontrast yetersiz). Toplam UI: Adult 13 → 11 frame (Oval Pink kalır), Kids 26 (değişmez). Toplam 37 frame.
🎨 v0.6.44 — Canonical color catalog sync (today): The 9 makeup swatch arrays (Lipstick, Shimmer, Eyeshadow, Eyeliner, Mascara, Brow, Contour, Highlight, Blush) had their hex values synced to the shared SpringEngine makeup color catalog — the same CIELAB-referenced canonical palette used by Spring Beauty (standards: CIE L*a*b*, Pantone SkinTone framework, ITA, Color Index). Pure value swap: array names, lengths, indices, swatch UI, render pipeline and every slider are byte-identical to v0.6.43 — only the colour string values changed. Both modules now draw from one consistent color system. Eyeshadow palette additionally rebalanced from all-warm-brown to the full family spread (warm/cool neutral, smoky, champagne, rose, bronze, plum, navy, forest).
🔧 v0.6.45 — Swatch double-render fix (today): Diagnosed dead / non-responsive colour swatches reported across every makeup category. Root cause: the HTML source carried a set of static swatch <div>s left behind from an earlier browser "save page" snapshot (serialised as style="background: rgb(...)" instead of hex). makeSwatches() also builds a fresh, fully-wired swatch set on every load and appends it into the same containers — so each panel held two swatch sets, the stale saved set having no click listeners. Invisible until v0.6.44 because both sets shared the same colours; the canonical colour sync made the stale set visually distinct. Fix: the 11 swatch containers (lip / shimmer / eyeshadow / eye-lens / hair / eyeliner / mascara / brow / contour / highlight / blush) are emptied in the HTML so makeSwatches() populates each one cleanly — every category now shows exactly its palette, all swatches live and correctly coloured, no duplicates. makeSwatches, the render pipeline, every slider and all state are byte-identical to v0.6.44.
🧭 v0.6.46 — Back-to-SpringLab nav (today): A "← Spring Lab" link added to the top-left corner of the header (inside .brand, before the brand mark), linking to /tools with target="_top" to break out of the iframe embed. SpringEngine standard: every module carries a top-left lab-back link so users always have a one-tap route back to the Spring Lab hub.
Inference cache: Background and Hair Color both consume the same segmentation result, so we run imageSegmenter.segmentForVideo once per frame in the render loop (gated on either layer being active) and read from a global segmentationResult. Toggling both ON costs the same inference time as toggling one — no double-pay.
Later: Lower-lash mascara pass, Background image upload (replace solid colour with photo), AR masks via 3D pipeline, foundation tint via full face mesh tessellation, mobile optimization.