diff --git a/features/video-studio/frontend-live/src/renderers/AnonymousRenderer.ts b/features/video-studio/frontend-live/src/renderers/AnonymousRenderer.ts new file mode 100644 index 000000000..8ce34543c --- /dev/null +++ b/features/video-studio/frontend-live/src/renderers/AnonymousRenderer.ts @@ -0,0 +1,140 @@ +import type { NormalizedLandmark } from '@mediapipe/tasks-vision'; +import type { RendererScratch } from './RendererPool'; + +// Fixed palette — colors ARE the identity of this mask, not customizable +const FACE_WHITE = '#F5F2EC'; +const CHEEK_RED = '#CC2B1D'; +const FEATURE_INK = '#1A1A1A'; +const BROW_INK = '#111111'; + +export function drawAnonymousMask( + ctx: CanvasRenderingContext2D, + scratch: RendererScratch, + landmarks: NormalizedLandmark[], + w: number, + h: number, +): void { + function pt(idx: number): [number, number] | null { + const lm = landmarks[idx]; + if (!lm) return null; + return [lm.x * w, lm.y * h]; + } + + const oc = scratch.ctx; + oc.clearRect(0, 0, w, h); + + const leftIris = pt(468); + const rightIris = pt(473); + const chin = pt(152); + const forehead = pt(10); + const noseTip = pt(4); + const lipLeft = pt(61); + const lipRight = pt(291); + const leftTemple = pt(234); + const rightTemple = pt(454); + + if (!leftIris || !rightIris || !chin || !forehead) return; + + const interEye = Math.hypot(rightIris[0] - leftIris[0], rightIris[1] - leftIris[1]); + const cx = (leftIris[0] + rightIris[0]) / 2; + const cy = forehead[1] + (chin[1] - forehead[1]) * 0.48; + const headH = (chin[1] - forehead[1]) * 0.92; + const headW = headH * 0.62; + + // ── 1. White oval face ──────────────────────────────────────────────────── + oc.fillStyle = FACE_WHITE; + oc.beginPath(); + oc.ellipse(cx, cy, headW / 2, headH / 2, 0, 0, Math.PI * 2); + oc.fill(); + + // ── 2. Painted red cheeks ───────────────────────────────────────────────── + oc.globalAlpha = 0.85; + for (const templePt of [leftTemple ?? [cx - interEye * 0.9, cy], rightTemple ?? [cx + interEye * 0.9, cy]] as [number, number][]) { + const grad = oc.createRadialGradient(templePt[0], templePt[1], 0, templePt[0], templePt[1], interEye * 0.3); + grad.addColorStop(0, CHEEK_RED); + grad.addColorStop(1, 'rgba(204,43,29,0)'); + oc.fillStyle = grad; + oc.beginPath(); + oc.ellipse(templePt[0], templePt[1], interEye * 0.3, interEye * 0.22, 0, 0, Math.PI * 2); + oc.fill(); + } + oc.globalAlpha = 1; + + // ── 3. Dramatically arched brows ───────────────────────────────────────── + // Drawn well above actual brow landmarks — iconic exaggerated arch + oc.strokeStyle = BROW_INK; + oc.lineWidth = Math.max(2, interEye * 0.045); + oc.lineCap = 'round'; + + const browLift = interEye * 0.18; // dramatic lift above natural brow + const browY = leftIris[1] - interEye * 0.52 - browLift; + + // Left brow — arches from ~nose bridge outward + oc.beginPath(); + oc.moveTo(cx - interEye * 0.08, browY + interEye * 0.08); + oc.quadraticCurveTo( + cx - interEye * 0.38, browY - interEye * 0.05, + cx - interEye * 0.62, browY + interEye * 0.12, + ); + oc.stroke(); + + // Right brow + oc.beginPath(); + oc.moveTo(cx + interEye * 0.08, browY + interEye * 0.08); + oc.quadraticCurveTo( + cx + interEye * 0.38, browY - interEye * 0.05, + cx + interEye * 0.62, browY + interEye * 0.12, + ); + oc.stroke(); + + // ── 4. Pencil mustache with signature Guy Fawkes upward curl ───────────── + // Anchored to fixed nose/lip positions — does NOT track jaw + const mustacheY = noseTip ? noseTip[1] + interEye * 0.28 : cy + interEye * 0.18; + const lipMidX = lipLeft && lipRight ? (lipLeft[0] + lipRight[0]) / 2 : cx; + const mustacheW = interEye * 0.55; + + oc.strokeStyle = FEATURE_INK; + oc.lineWidth = Math.max(1.5, interEye * 0.028); + + // Left half — curves up and outward + oc.beginPath(); + oc.moveTo(lipMidX, mustacheY); + oc.bezierCurveTo( + lipMidX - mustacheW * 0.3, mustacheY, + lipMidX - mustacheW * 0.7, mustacheY - interEye * 0.04, + lipMidX - mustacheW, mustacheY - interEye * 0.12, + ); + oc.stroke(); + + // Right half — mirror + oc.beginPath(); + oc.moveTo(lipMidX, mustacheY); + oc.bezierCurveTo( + lipMidX + mustacheW * 0.3, mustacheY, + lipMidX + mustacheW * 0.7, mustacheY - interEye * 0.04, + lipMidX + mustacheW, mustacheY - interEye * 0.12, + ); + oc.stroke(); + + // ── 5. Teardrop chin beard ──────────────────────────────────────────────── + const beardBaseY = chin[1] - interEye * 0.08; + const beardTipY = chin[1] + interEye * 0.18; + const beardHalfW = interEye * 0.095; + + oc.fillStyle = FEATURE_INK; + oc.beginPath(); + oc.moveTo(lipMidX, beardBaseY); + oc.bezierCurveTo( + lipMidX + beardHalfW, beardBaseY + interEye * 0.06, + lipMidX + beardHalfW * 0.6, beardTipY - interEye * 0.04, + lipMidX, beardTipY, + ); + oc.bezierCurveTo( + lipMidX - beardHalfW * 0.6, beardTipY - interEye * 0.04, + lipMidX - beardHalfW, beardBaseY + interEye * 0.06, + lipMidX, beardBaseY, + ); + oc.fill(); + + ctx.drawImage(scratch.canvas, 0, 0); +}