diff --git a/src/features/avatar/__tests__/ambient-motion-test.ts b/src/features/avatar/__tests__/ambient-motion-test.ts index bcce0ef..28a89a9 100644 --- a/src/features/avatar/__tests__/ambient-motion-test.ts +++ b/src/features/avatar/__tests__/ambient-motion-test.ts @@ -1,9 +1,13 @@ import { ambientBodyOffset, + ambientEyeOffset, + applyAmbientBodyMotion, applyAmbientMotion, hasAmbientMotion, } from '@/features/avatar/ambientMotion' +import { poseFromExpression, renderAvatar } from '@/features/avatar/geometry' import { defaultExpression } from '@/features/avatar/presets' +import { surfacePresets } from '@/features/avatar/surfaces' describe('perpetual expression motion', () => { it('leaves a motionless expression unchanged', () => { @@ -27,6 +31,38 @@ describe('perpetual expression motion', () => { expect(expression.positionXLeft).toBe(defaultExpression.positionXLeft) }) + it('fades into the first micro-adjustment immediately, then holds the new target', () => { + const expression = { ...defaultExpression, eyeMotion: 'microSaccades' as const } + const halfway = ambientEyeOffset(expression, 70) + const target = ambientEyeOffset(expression, 200) + + expect(ambientEyeOffset(expression, 0)).toEqual({ x: 0, y: 0 }) + expect(halfway).not.toEqual({ x: 0, y: 0 }) + expect(halfway).not.toEqual(target) + expect(target).toEqual(ambientEyeOffset(expression, 1000)) + }) + + it('keeps eye micro-adjustments separate from the transitioning expression', () => { + const expression = { ...defaultExpression, eyeMotion: 'microSaccades' as const } + const transitioning = { ...expression, headX: 42, headY: -18, headZ: 71 } + + expect(applyAmbientBodyMotion(expression, 500)).toEqual(expression) + expect(ambientEyeOffset(expression, 500)).not.toEqual({ x: 0, y: 0 }) + expect(ambientEyeOffset(transitioning, 500)).toEqual(ambientEyeOffset(expression, 500)) + }) + + it('applies the eye offset as a render layer without moving the head', () => { + const pose = poseFromExpression(defaultExpression) + const base = renderAvatar(pose, surfacePresets.sphere, 1) + const adjusted = renderAvatar(pose, surfacePresets.sphere, 1, { + eyeOffset: { x: 1.5, y: -0.75 }, + }) + + expect(adjusted.headPath).toBe(base.headPath) + expect(adjusted.leftPath).not.toBe(base.leftPath) + expect(adjusted.rightPath).not.toBe(base.rightPath) + }) + it('adds body motion without changing eye placement', () => { const expression = { ...defaultExpression, bodyMotion: 'shake' as const } const animated = applyAmbientMotion(expression, 500) @@ -42,6 +78,27 @@ describe('perpetual expression motion', () => { expect(ambientBodyOffset(expression, 500)).not.toEqual({ x: 0, y: 0 }) }) + it('ramps perpetual motion with transition strength', () => { + const expression = { + ...defaultExpression, + eyeMotion: 'shake' as const, + bodyMotion: 'shake' as const, + } + const fullBodyOffset = ambientBodyOffset(expression, 500) + const halfBodyOffset = ambientBodyOffset(expression, 500, 0.5) + const fullEyeOffset = ambientEyeOffset(expression, 500) + const halfEyeOffset = ambientEyeOffset(expression, 500, 0.5) + + expect(ambientBodyOffset(expression, 500, 0).x).toBeCloseTo(0) + expect(ambientBodyOffset(expression, 500, 0).y).toBeCloseTo(0) + expect(ambientEyeOffset(expression, 500, 0).x).toBeCloseTo(0) + expect(ambientEyeOffset(expression, 500, 0).y).toBeCloseTo(0) + expect(halfBodyOffset.x).toBeCloseTo(fullBodyOffset.x / 2) + expect(halfBodyOffset.y).toBeCloseTo(fullBodyOffset.y / 2) + expect(halfEyeOffset.x).toBeCloseTo(fullEyeOffset.x / 2) + expect(halfEyeOffset.y).toBeCloseTo(fullEyeOffset.y / 2) + }) + it('visibly offsets a symmetric body during slow drift', () => { const expression = { ...defaultExpression, bodyMotion: 'slowDrift' as const } diff --git a/src/features/avatar/ambientMotion.ts b/src/features/avatar/ambientMotion.ts index 2e392a0..2df3a01 100644 --- a/src/features/avatar/ambientMotion.ts +++ b/src/features/avatar/ambientMotion.ts @@ -17,6 +17,7 @@ const hash = (value: number) => { const expressionSeed = (expression: Expression) => expression.headX * 0.71 + expression.headY * 1.13 + expression.headZ * 1.37 +const EYE_MOTION_SEED = 17.29 const smoothNoise = (elapsedMs: number, axis: number, seed: number, interval: number) => { const progress = elapsedMs / interval @@ -29,68 +30,86 @@ const smoothNoise = (elapsedMs: number, axis: number, seed: number, interval: nu const saccade = (elapsedMs: number, axis: number, seed: number) => { const interval = 1100 - const warpedTime = elapsedMs + Math.sin(elapsedMs / 1700 + seed) * 280 - const step = Math.floor(warpedTime / interval) - const progress = (((warpedTime % interval) + interval) % interval) / interval - const blend = smoothstep(Math.min(progress / 0.09, 1)) - const previous = hash((step - 1) * 2 + axis + seed) + const duration = 140 + if (elapsedMs <= 0) return 0 + const step = Math.floor(elapsedMs / interval) + const progress = (elapsedMs - step * interval) / duration + const blend = smoothstep(Math.min(progress, 1)) + const previous = step === 0 ? 0 : hash((step - 1) * 2 + axis + seed) const next = hash(step * 2 + axis + seed) - const fadeIn = Math.min(elapsedMs / 240, 1) - return (previous + (next - previous) * blend) * fadeIn + return previous + (next - previous) * blend } export const hasAmbientMotion = (expression: Expression) => expression.eyeMotion !== 'none' || expression.bodyMotion !== 'none' -export const ambientBodyOffset = (expression: Expression, elapsedMs: number) => { +export const ambientBodyOffset = (expression: Expression, elapsedMs: number, strength = 1) => { const seed = expressionSeed(expression) if (expression.bodyMotion === 'slowDrift') { return { - x: smoothNoise(elapsedMs, 3, seed, 2900) * 1.45, - y: smoothNoise(elapsedMs, 4, seed, 3700) * 1.1, + x: smoothNoise(elapsedMs, 3, seed, 2900) * 1.45 * strength, + y: smoothNoise(elapsedMs, 4, seed, 3700) * 1.1 * strength, } } if (expression.bodyMotion === 'shake') { const time = elapsedMs / 1000 return { - x: (Math.sin(time * 31) + Math.sin(time * 53) * 0.45) * 1.35, - y: (Math.sin(time * 37) + Math.sin(time * 61) * 0.4) * 1.1, + x: (Math.sin(time * 31) + Math.sin(time * 53) * 0.45) * 1.35 * strength, + y: (Math.sin(time * 37) + Math.sin(time * 61) * 0.4) * 1.1 * strength, } } return { x: 0, y: 0 } } -export const applyAmbientMotion = (expression: Expression, elapsedMs: number): Expression => { +export const ambientEyeOffset = (expression: Expression, elapsedMs: number, strength = 1) => { + if (expression.eyeMotion === 'microSaccades') { + return { + x: saccade(elapsedMs, 0, EYE_MOTION_SEED) * 1.5 * strength, + y: saccade(elapsedMs, 1, EYE_MOTION_SEED) * 0.9 * strength, + } + } + if (expression.eyeMotion === 'shake') { + const time = elapsedMs / 1000 + return { + x: (Math.sin(time * 47) + Math.sin(time * 71) * 0.45) * 1.2 * strength, + y: (Math.sin(time * 59) + Math.sin(time * 83) * 0.4) * 0.8 * strength, + } + } + return { x: 0, y: 0 } +} + +export const applyAmbientBodyMotion = ( + expression: Expression, + elapsedMs: number, + strength = 1 +): Expression => { const next = { ...expression } const seed = expressionSeed(expression) - if (expression.eyeMotion === 'microSaccades') { - const x = saccade(elapsedMs, 0, seed) * 1.5 - const y = saccade(elapsedMs, 1, seed) * 0.9 - next.positionXLeft += x - next.positionXRight += x - next.positionYLeft += y - next.positionYRight += y - } else if (expression.eyeMotion === 'shake') { - const time = elapsedMs / 1000 - const x = (Math.sin(time * 47) + Math.sin(time * 71) * 0.45) * 1.2 - const y = (Math.sin(time * 59) + Math.sin(time * 83) * 0.4) * 0.8 - next.positionXLeft += x - next.positionXRight += x - next.positionYLeft += y - next.positionYRight += y - } - if (expression.bodyMotion === 'slowDrift') { - next.headX += smoothNoise(elapsedMs, 0, seed, 2600) * 0.8 - next.headY += smoothNoise(elapsedMs, 1, seed, 3300) * 1.15 - next.headZ += smoothNoise(elapsedMs, 2, seed, 4100) * 0.45 + next.headX += smoothNoise(elapsedMs, 0, seed, 2600) * 0.8 * strength + next.headY += smoothNoise(elapsedMs, 1, seed, 3300) * 1.15 * strength + next.headZ += smoothNoise(elapsedMs, 2, seed, 4100) * 0.45 * strength } else if (expression.bodyMotion === 'shake') { const time = elapsedMs / 1000 - next.headX += (Math.sin(time * 31) + Math.sin(time * 53) * 0.45) * 1.15 - next.headY += (Math.sin(time * 37) + Math.sin(time * 61) * 0.4) * 1.35 - next.headZ += Math.sin(time * 43) * 0.7 + next.headX += (Math.sin(time * 31) + Math.sin(time * 53) * 0.45) * 1.15 * strength + next.headY += (Math.sin(time * 37) + Math.sin(time * 61) * 0.4) * 1.35 * strength + next.headZ += Math.sin(time * 43) * 0.7 * strength } return next } + +export const applyAmbientMotion = ( + expression: Expression, + elapsedMs: number, + strength = 1 +): Expression => { + const next = applyAmbientBodyMotion(expression, elapsedMs, strength) + const eyeOffset = ambientEyeOffset(expression, elapsedMs, strength) + next.positionXLeft += eyeOffset.x + next.positionXRight += eyeOffset.x + next.positionYLeft += eyeOffset.y + next.positionYRight += eyeOffset.y + return next +} diff --git a/src/features/avatar/geometry.ts b/src/features/avatar/geometry.ts index f2cf98c..cc03995 100644 --- a/src/features/avatar/geometry.ts +++ b/src/features/avatar/geometry.ts @@ -61,6 +61,7 @@ export type AvatarGeometry = { export type RenderAvatarOptions = { includeWire?: boolean bodyNodes?: BodyNode[] + eyeOffset?: Readonly<{ x: number; y: number }> } export type EyeEditorGeometry = { @@ -552,15 +553,16 @@ const eyePoints = ( pose: AvatarPose, surface: SurfaceConfig, side: -1 | 1, - blink: number + blink: number, + offset: Readonly<{ x: number; y: number }> = { x: 0, y: 0 } ): ProjectedSurfacePoint[] => { const expression = pose.expression const suffix = side < 0 ? 'Left' : 'Right' const width = expression[`width${suffix}`] const restingHeight = expression[`height${suffix}`] const height = 5 + (restingHeight - 5) * blink - const centerX = (side * expression.spacing) / 2 + expression[`positionX${suffix}`] - const centerY = expression[`positionY${suffix}`] + const centerX = (side * expression.spacing) / 2 + expression[`positionX${suffix}`] + offset.x + const centerY = expression[`positionY${suffix}`] + offset.y const angle = radians(side < 0 ? expression.leftAngle : expression.rightAngle) return roundedRectangle(width, height).map(([localX, localY]) => { const rotatedX = localX * Math.cos(angle) - localY * Math.sin(angle) @@ -1230,8 +1232,8 @@ export const renderAvatar = ( blink = 1, options: RenderAvatarOptions = {} ): AvatarGeometry => { - const leftSamples = eyePoints(pose, surface, -1, blink) - const rightSamples = eyePoints(pose, surface, 1, blink) + const leftSamples = eyePoints(pose, surface, -1, blink, options.eyeOffset) + const rightSamples = eyePoints(pose, surface, 1, blink, options.eyeOffset) const left = leftSamples.map(sample => sample.point) const right = rightSamples.map(sample => sample.point) const accessories = accessoryLayers(pose, options.bodyNodes ?? []) diff --git a/src/features/export/proceduralBrowserRuntime.ts b/src/features/export/proceduralBrowserRuntime.ts index 2d88c4d..75b4e82 100644 --- a/src/features/export/proceduralBrowserRuntime.ts +++ b/src/features/export/proceduralBrowserRuntime.ts @@ -105,28 +105,38 @@ function mountAvatar(target, options = {}) { let pausedBlink = null; let pausedBlinkDelay = 0; let stepDueAt = null; - let ambientStartedAt = performance.now(); - let ambientSignature = initialExpression.eyeMotion + ':' + initialExpression.bodyMotion; + let eyeAmbientStartedAt = performance.now(); + let bodyAmbientStartedAt = performance.now(); + let eyeAmbientSignature = initialExpression.eyeMotion; + let bodyAmbientSignature = initialExpression.bodyMotion; + let ambientStrength = 1; let lastAmbientFrame = 0; const applyMotion = expression => { - const signature = expression.eyeMotion + ':' + expression.bodyMotion; - if (signature !== ambientSignature) { - ambientSignature = signature; - ambientStartedAt = performance.now(); + const now = performance.now(); + if (expression.eyeMotion !== eyeAmbientSignature) { + eyeAmbientSignature = expression.eyeMotion; + eyeAmbientStartedAt = now; + } + if (expression.bodyMotion !== bodyAmbientSignature) { + bodyAmbientSignature = expression.bodyMotion; + bodyAmbientStartedAt = now; } }; const render = (time = performance.now()) => { - const elapsed = time - ambientStartedAt; - const expression = AvatarProceduralEngine.hasAmbientMotion(currentPose.expression) - ? AvatarProceduralEngine.applyAmbientMotion(currentPose.expression, elapsed) + const eyeElapsed = time - eyeAmbientStartedAt; + const bodyElapsed = time - bodyAmbientStartedAt; + const expression = currentPose.expression.bodyMotion !== 'none' + ? AvatarProceduralEngine.applyAmbientBodyMotion(currentPose.expression, bodyElapsed, ambientStrength) : currentPose.expression; + const eyeOffset = AvatarProceduralEngine.ambientEyeOffset(currentPose.expression, eyeElapsed, ambientStrength); const renderedPose = AvatarProceduralEngine.poseFromExpression(expression); const geometry = AvatarProceduralEngine.renderAvatar(renderedPose, DATA.avatar.surface, blinkAmount, { includeWire: false, bodyNodes: DATA.avatar.bodyNodes, + eyeOffset, }); - const offset = AvatarProceduralEngine.ambientBodyOffset(currentPose.expression, elapsed); + const offset = AvatarProceduralEngine.ambientBodyOffset(currentPose.expression, bodyElapsed, ambientStrength); motionLayer.setAttribute('transform', 'translate(' + offset.x + ' ' + offset.y + ')'); ensurePaths(backLayer, geometry.backPaths, currentColors.body); ensurePaths(frontLayer, geometry.frontPaths, currentColors.body); @@ -145,6 +155,7 @@ function mountAvatar(target, options = {}) { if (transitionState) { const linear = clamp01((time - transitionState.startedAt) / transitionState.durationMs); const eased = easeProgress(linear, transitionState.transition); + ambientStrength = clamp01(eased); const expression = { ...transitionState.fromPose.expression }; AvatarProceduralEngine.expressionFields.forEach(field => { expression[field] = transitionState.fromPose.expression[field] + @@ -161,6 +172,7 @@ function mountAvatar(target, options = {}) { currentPose = transitionState.toPose; currentColors = transitionState.toColors; transitionState = null; + ambientStrength = 1; } } if (blinkState) { @@ -195,6 +207,7 @@ function mountAvatar(target, options = {}) { const targetPose = AvatarProceduralEngine.poseFromExpression(resolved); const targetColors = resolveColors(target); if (durationMs <= 0) { + ambientStrength = 1; transitionState = null; currentPose = targetPose; currentColors = targetColors; @@ -212,6 +225,7 @@ function mountAvatar(target, options = {}) { transition, expressionId, }; + ambientStrength = 0; requestTick(); }; const clearSchedule = () => { diff --git a/src/features/export/standaloneEngine.entry.ts b/src/features/export/standaloneEngine.entry.ts index 18f8ac6..adea6e8 100644 --- a/src/features/export/standaloneEngine.entry.ts +++ b/src/features/export/standaloneEngine.entry.ts @@ -1,2 +1,8 @@ export { expressionFields, poseFromExpression, renderAvatar } from '../avatar/geometry' -export { ambientBodyOffset, applyAmbientMotion, hasAmbientMotion } from '../avatar/ambientMotion' +export { + ambientBodyOffset, + ambientEyeOffset, + applyAmbientBodyMotion, + applyAmbientMotion, + hasAmbientMotion, +} from '../avatar/ambientMotion' diff --git a/src/features/export/standaloneEngine.generated.ts b/src/features/export/standaloneEngine.generated.ts index e7f91ec..b1eac8c 100644 --- a/src/features/export/standaloneEngine.generated.ts +++ b/src/features/export/standaloneEngine.generated.ts @@ -1,2 +1,2 @@ // Generated by scripts/generate-standalone-engine.mjs. Do not edit. -export const standaloneEngineSource = "var AvatarProceduralEngine=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=(e,t)=>Math.sign(e)*Math.abs(e)**t,n=(e,n,r,i,a,o,s)=>{let c=t(Math.cos(n),o);return[r/2*c*t(Math.sin(e),s),i/2*t(Math.sin(n),o),a/2*c*t(Math.cos(e),s)]},r=(e,t,n)=>{let r=e.width/2,i=e.depth/2,a=Math.min(r,e.height/2),o=Math.max(0,(e.height-a*2)/2),s=o*2+Math.PI*a,c=(n+Math.PI/2)/Math.PI*s,l=r,u=0;if(cMath.max(0,Math.min(2,e??0)),a=e=>1+i(e.roundness)/2,o=.04,s=e=>e.roundness<=0?1/0:2/(o+i(e.roundness)/2*.96),c=(e,t,n,r)=>{let i=Math.cos(n)*Math.sin(t),a=Math.sin(n),o=Math.cos(n)*Math.cos(t),s=Number.isFinite(r)?(Math.abs(i)**r+Math.abs(a)**r+Math.abs(o)**r)**(1/r)||1:Math.max(Math.abs(i),Math.abs(a),Math.abs(o))||1;return[e.width/2*(i/s),e.height/2*(a/s),e.depth/2*(o/s)]},l=(e,t,n)=>c(e,t,n,a(e)),u=(e,t,n)=>c(e,t,n,s(e)),d=.24,f=.2,p=.22,m=e=>i(e.morphRoundness)/2,h=(e,t,n)=>{let r=m(e),i=Math.max(0,Math.min(1,t)),a=Math.sin(i*Math.PI),o=(1-Math.cos(i*Math.PI))/2;return{radiusScale:n.radiusScale+(a-n.radiusScale)*r,verticalProgress:n.verticalProgress+(o-n.verticalProgress)*r}},g=(e,t,n,r,i)=>{let a=1-i;return a**3*e+3*a*a*i*t+3*a*i*i*n+i**3*r},_=e=>({tipFraction:(e.tipRoundness??0)*d,baseFraction:(e.baseRoundness??0)*f}),v=(e,t)=>{let n=Math.max(0,Math.min(1,t)),r=e.roundness*p;if(r<=0)return{radiusScale:1,verticalProgress:(Math.sin((n-.5)*Math.PI)+1)/2};if(n1-r){let e=(n-(1-r))/r*(Math.PI/2);return{radiusScale:1-r+r*Math.cos(e),verticalProgress:1-r/2+r*Math.sin(e)/2}}let i=(n-r)/(1-r*2);return{radiusScale:1,verticalProgress:r/2+i*(1-r)}},y=(e,t)=>h(e,t,v(e,t)),b=(e,t,n)=>{let r=Math.max(0,Math.min(1,t)),i=0,a=1;for(let t=0;t<14;t+=1){let t=(i+a)/2;n(e,t).verticalProgress{let n=Math.max(0,Math.min(1,t)),{tipFraction:r,baseFraction:i}=_(e);if(i>0&&n0&&n>1-r){let e=(n-(1-r))/r;return{radiusScale:g(r,r/2,r/4,0,e),verticalProgress:g(1-r,1-r/2,1,1,e)}}return{radiusScale:1-n,verticalProgress:n}},S=(e,t)=>h(e,t,x(e,t)),C=e=>{let t=e.height*.36,n=e.height-t;return{coneApexY:-e.height/2,coneBaseY:-e.height/2+t,bodyHeight:n,bodyCenterY:e.height/2-n/2,bodyWidth:e.width*.54,bodyDepth:e.depth*.62}},w=(e,t,i)=>{let{width:a,height:o,depth:s}=e;switch(e.type){case`sphere`:case`mickey`:return n(t,i,a,o,s,1,1);case`cube`:return u(e,t,i);case`cylinder`:{let n=y(e,(i+Math.PI/2)/Math.PI);return[a/2*n.radiusScale*Math.sin(t),-o/2+o*n.verticalProgress,s/2*n.radiusScale*Math.cos(t)]}case`cursor`:{let n=C(e),r=(i+Math.PI/2)/Math.PI,a=v({...e,width:n.bodyWidth,height:n.bodyHeight,depth:n.bodyDepth},r);return[n.bodyWidth/2*a.radiusScale*Math.sin(t),n.bodyCenterY-n.bodyHeight/2+n.bodyHeight*a.verticalProgress,n.bodyDepth/2*a.radiusScale*Math.cos(t)]}case`diamond`:return l(e,t,i);case`capsule`:return r(e,t,i);case`cone`:{let n=S(e,(i+Math.PI/2)/Math.PI);return[a/2*n.radiusScale*Math.sin(t),o/2-o*n.verticalProgress,s/2*n.radiusScale*Math.cos(t)]}}},ee=(e,t)=>[e[0]-t[0],e[1]-t[1],e[2]-t[2]],T=([e,t,n])=>{let r=Math.hypot(e,t,n)||1;return[e/r,t/r,n/r]},te=(e,t,n)=>{let r=e.type===`cone`?-1:1;return T([r*(t[1]*n[2]-t[2]*n[1]),r*(t[2]*n[0]-t[0]*n[2]),r*(t[0]*n[1]-t[1]*n[0])])},ne=(e,t,n)=>{let r=5e-4;if(e.type===`cone`&&n>=Math.PI/2-r)return[0,-1,0];let i=w(e,t-r,n),a=w(e,t+r,n),o=w(e,t,Math.max(-Math.PI/2,n-r)),s=w(e,t,Math.min(Math.PI/2,n+r));return te(e,ee(a,i),ee(s,o))},E=(e,t)=>Math.sign(e)*Math.abs(e)**t,re=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;return T([E(t[0]/r,n-1)/r,E(t[1]/i,n-1)/i,E(t[2]/a,n-1)/a])},ie=(e,t)=>re(e,t,a(e)),ae=(e,t)=>{let n=s(e);if(Number.isFinite(n))return re(e,t,n);let r=[t[0]/(e.width/2||1),t[1]/(e.height/2||1),t[2]/(e.depth/2||1)],i=r.reduce((e,t,n)=>Math.abs(t)>Math.abs(r[e])?n:e,0);return[i===0?Math.sign(r[0]):0,i===1?Math.sign(r[1]):0,i===2?Math.sign(r[2]):0]},oe=(e,t,n,r,i)=>{let a=e.width/2||1,o=e.height/2||1,s=e.depth/2||1;if(!Number.isFinite(r)){let r=[Math.max(-a,Math.min(a,t)),Math.max(-o,Math.min(o,n)),s];return{point:r,normal:i(e,r)}}let c=Math.max(-1,Math.min(1,n/o)),l=Math.max(0,1-Math.abs(c)**r)**(1/r),u=Math.max(-a*l,Math.min(a*l,t)),d=u/a,f=Math.max(0,1-Math.abs(d)**r-Math.abs(c)**r)**(1/r),p=[u,c*o,s*f];return{point:p,normal:i(e,p)}},se=(e,t,n,r,i,a=0)=>{let o=t-a,s=Math.max(0,1-(e/(n||1))**2-(o/(r||1))**2),c=i*Math.sqrt(s);return{point:[e,t,c],normal:T([e/(n*n||1),o/(r*r||1),c/(i*i||1)])}},D=(e,t,n,r,i)=>{let a=e.width/2||1,o=e.depth/2||1,s=Math.max(0,Math.min(1,.5+i*(n/e.height))),c=b(e,s,r),l=a*c,u=o*c,d=Math.max(-l,Math.min(l,t)),f=l>0?Math.max(0,1-(d/l)**2):0,p=u*Math.sqrt(f),m=1e-4,h=Math.max(0,s-m),g=Math.min(1,s+m),_=b(e,h,r),v=(b(e,g,r)-_)/(g-h||1),y=Math.max(Math.sqrt(f),1e-4),x=-(o/a)*d/(l*y||1),S=i*o*v/(e.height*y||1);return{point:[d,n,p],normal:T([-x,-S,1])}},ce=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,o=e.depth/2||1;switch(e.type){case`sphere`:case`mickey`:return se(t,n,r,i,o);case`cube`:return oe(e,t,n,s(e),ae);case`capsule`:{let e=Math.min(r,i),a=Math.max(0,i-e);return se(t,n,r,e,o,n<-a?-a:n>a?a:n)}case`cylinder`:return D(e,t,n,y,1);case`cursor`:{let r=C(e),i=D({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},t,n-r.bodyCenterY,v,1);return{point:[i.point[0],i.point[1]+r.bodyCenterY,i.point[2]],normal:i.normal}}case`cone`:return D(e,t,n,S,-1);case`diamond`:return oe(e,t,n,a(e),ie)}},le=(e,t,n)=>{let r=w(e,t,n);if(e.type===`sphere`||e.type===`mickey`){let t=e.width/2||1,n=e.height/2||1,i=e.depth/2||1;return{point:r,normal:T([r[0]/(t*t),r[1]/(n*n),r[2]/(i*i)])}}return e.type===`cylinder`&&e.roundness<=0&&(e.morphRoundness??0)<=0?{point:r,normal:T([Math.sin(t)/(e.width/2||1),0,Math.cos(t)/(e.depth/2||1)])}:e.type===`diamond`?{point:r,normal:ie(e,r)}:e.type===`cube`?{point:r,normal:ae(e,r)}:{point:r,normal:ne(e,t,n)}},O=620,ue=14,de=[`headX`,`headY`,`headZ`,`widthLeft`,`widthRight`,`heightLeft`,`heightRight`,`spacing`,`positionXLeft`,`positionXRight`,`positionYLeft`,`positionYRight`,`leftAngle`,`rightAngle`,`perspective`],k=e=>e*Math.PI/180,fe=([e,t,n,r])=>{let i=Math.hypot(e,t,n,r)||1;return[e/i,t/i,n/i,r/i]},pe=([e,t,n,r],[i,a,o,s])=>fe([e*i-t*a-n*o-r*s,e*a+t*i+n*s-r*o,e*o-t*s+n*i+r*a,e*s+t*o-n*a+r*i]),A=([e,t,n],r)=>{let i=r/2,a=Math.sin(i);return fe([Math.cos(i),e*a,t*a,n*a])},j=(e,t,n)=>{let r=A([1,0,0],e),i=A([0,1,0],t);return pe(pe(A([0,0,1],n),r),i)},M=([e,t,n,r],[i,a,o])=>{let s=2*(n*o-r*a),c=2*(r*i-t*o),l=2*(t*a-n*i);return[i+e*s+(n*l-r*c),a+e*c+(r*s-t*l),o+e*l+(t*c-n*s)]},me=(e,t)=>{let n=e/2,r=t/2,i=Math.min(r,n),a=[],o=(e,t)=>{let n=Math.max(2,Math.ceil(Math.hypot(t[0]-e[0],t[1]-e[1])/1.5));for(let r=0;r{for(let r=0;r{let n=O-e[2]*t,r=Math.abs(n)<1e-4?O/1e-4:O/n;return[e[0]*r,e[1]*r,e[2]]},P=(e,t=!0)=>e.length?`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.slice(1).map(e=>`L${e[0].toFixed(2)} ${e[1].toFixed(2)}`).join(``)}${t?`Z`:``}`:``,he=e=>({expression:e,orientation:j(k(e.headX),k(e.headY),k(e.headZ))}),ge=24,_e=25,ve=73,F=144,ye=33,be=73,I=new Map,L=new Map,R=new Map,z=e=>[e.type,e.width,e.height,e.depth,e.roundness,e.morphRoundness,e.tipRoundness,e.baseRoundness].map(e=>typeof e==`number`?e.toFixed(4):e).join(`:`),B=(e,t,n)=>(e.size>=ge&&e.delete(e.keys().next().value),e.set(t,n),n),V=(e,t,n)=>le(e,t,n),xe=(e,t)=>({point:N(M(e.orientation,t.point),e.expression.perspective),normal:M(e.orientation,t.normal)}),Se=(e,t)=>{let n=e/120,r=t/120;return[120*Math.cos(r)*Math.sin(n),120*Math.sin(r)]},Ce=(e,t,n,r)=>{let[i,a]=Se(n,r);return xe(e,ce(t,i,a))},we=(e,t,n,r)=>{let i=e.expression,a=n<0?`Left`:`Right`,o=i[`width${a}`],s=5+(i[`height${a}`]-5)*r,c=n*i.spacing/2+i[`positionX${a}`],l=i[`positionY${a}`],u=k(n<0?i.leftAngle:i.rightAngle);return me(o,s).map(([n,r])=>{let i=n*Math.cos(u)-r*Math.sin(u),a=n*Math.sin(u)+r*Math.cos(u);return Ce(e,t,c+i,l+a)})},Te=e=>{let t=[],n=[];return e.forEach(({point:e,normal:r})=>{r[2]>0?n.push(e):n.length&&(t.push(n),n=[])}),n.length&&t.push(n),t.filter(e=>e.length>1).map(e=>P(e,!1)).join(``)},Ee=(e,t)=>{let n=z(t),r=R.get(n);if(!r){let e=[-60,-30,0,30,60].map(e=>Array.from({length:73},(n,r)=>V(t,k(-180+r*5),k(e)))),i=Array.from({length:12},(e,t)=>-150+t*30).map(e=>Array.from({length:37},(n,r)=>V(t,k(e),k(-90+r*5))));r=B(R,n,[...e,...i])}return r.map(t=>Te(t.map(t=>xe(e,t))))},H=e=>{let t=[...e].sort((e,t)=>e[0]-t[0]||e[1]-t[1]),n=(e,t,n)=>(t[0]-e[0])*(n[1]-e[1])-(t[1]-e[1])*(n[0]-e[0]),r=e=>{let t=[];return e.forEach(e=>{for(;t.length>=2&&n(t.at(-2),t.at(-1),e)<=0;)t.pop();t.push(e)}),t};return[...r(t).slice(0,-1),...r(t.reverse()).slice(0,-1)]},U=e=>{if(e.length<3)return P(e);let t=t=>e[(t+e.length)%e.length];return`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.map((e,n)=>{let r=t(n-1),i=t(n+1),a=t(n+2),o=[e[0]+(i[0]-r[0])/6,e[1]+(i[1]-r[1])/6,e[2]],s=[i[0]-(a[0]-e[0])/6,i[1]-(a[1]-e[1])/6,i[2]];return`C${o[0].toFixed(2)} ${o[1].toFixed(2)} ${s[0].toFixed(2)} ${s[1].toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``)}Z`},W=(e,t=7)=>e.flatMap((n,r)=>{let i=e[(r+1)%e.length],a=Math.max(1,Math.ceil(Math.hypot(i[0]-n[0],i[1]-n[1])/t));return Array.from({length:a},(e,t)=>{let r=t/a;return[n[0]+(i[0]-n[0])*r,n[1]+(i[1]-n[1])*r,n[2]+(i[2]-n[2])*r]})}),De=e=>e.length?e.length===1?`${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}`:e.slice(0,-1).map((t,n)=>{let r=e[Math.max(0,n-1)],i=e[n+1],a=e[Math.min(e.length-1,n+2)],o=t[0]+(i[0]-r[0])/6,s=t[1]+(i[1]-r[1])/6,c=i[0]-(a[0]-t[0])/6,l=i[1]-(a[1]-t[1])/6;return`C${o.toFixed(2)} ${s.toFixed(2)} ${c.toFixed(2)} ${l.toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``):``,G=(e,t)=>N(M(e.orientation,t),e.expression.perspective),K=(e,t,n)=>Array.from({length:145},(r,i)=>{let a=i/F*Math.PI*2;return[e/2*Math.sin(a),n,t/2*Math.cos(a)]}),q=(e,t)=>{let n=z(t),r=I.get(n);return r||(r=Array.from({length:ye},(e,n)=>{let r=-Math.PI/2+n/32*Math.PI;return Array.from({length:be},(e,n)=>w(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),B(I,n,r)),U(W(H(r.map(t=>G(e,t)))))},Oe=(e,t)=>{if(t.roundness>0||(t.morphRoundness??0)>0)return q(e,t);let n=t.height/2;return U(W(H([...K(t.width,t.depth,-n),...K(t.width,t.depth,n)].map(t=>G(e,t)))))},ke=(e,t)=>{let n=C(t),r=n.bodyHeight/2;return U(W(H([...K(n.bodyWidth,n.bodyDepth,n.bodyCenterY-r),...K(n.bodyWidth,n.bodyDepth,n.bodyCenterY+r)].map(t=>G(e,t)))))},Ae=(e,t)=>{let n=C(t),r=G(e,[0,n.coneApexY,0]);return U(W(H([...K(t.width,t.depth,n.coneBaseY).map(t=>G(e,t)),r])))},je=(e,t)=>{if((t.morphRoundness??0)>0||(t.tipRoundness??0)>0||(t.baseRoundness??0)>0)return q(e,t);let n=G(e,[0,-t.height/2,0]),r=H([...K(t.width,t.depth,t.height/2).map(t=>G(e,t)),n]),i=r.findIndex(e=>Math.hypot(e[0]-n[0],e[1]-n[1])<.01);if(i<0)return U(r);let a=[...r.slice(i),...r.slice(0,i)].slice(1);return a.length<2?P(r):`M${n[0].toFixed(2)} ${n[1].toFixed(2)}L${a[0][0].toFixed(2)} ${a[0][1].toFixed(2)}${De(a)}L${n[0].toFixed(2)} ${n[1].toFixed(2)}Z`},Me=(e,t)=>{if(t.roundness>0)return q(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return P(H([-1,1].flatMap(e=>[-1,1].flatMap(t=>[-1,1].map(a=>[e*n,t*r,a*i]))).map(t=>G(e,t))))},Ne=(e,t)=>{if(t.roundness>0)return q(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return P(H([[-n,0,0],[n,0,0],[0,-r,0],[0,r,0],[0,0,-i],[0,0,i]].map(t=>G(e,t))))},Pe=(e,t,n,r,i)=>{let a=n+i,o=Math.hypot(n-i,r*2),s=(a+o)/2,c=(a-o)/2;return s<=0||c<=0?null:{centerX:e,centerY:t,majorRadius:Math.sqrt(s),minorRadius:Math.sqrt(c),rotation:Math.atan2(r*2,n-i)/2}},J=({centerX:e,centerY:t,majorRadius:n,minorRadius:r,rotation:i})=>{let a=i*180/Math.PI,o=Math.cos(i)*n,s=Math.sin(i)*n,c=e+o,l=t+s,u=e-o,d=t-s;return`M${c.toFixed(2)} ${l.toFixed(2)}A${n.toFixed(2)} ${r.toFixed(2)} ${a.toFixed(2)} 0 1 ${u.toFixed(2)} ${d.toFixed(2)}A${n.toFixed(2)} ${r.toFixed(2)} ${a.toFixed(2)} 0 1 ${c.toFixed(2)} ${l.toFixed(2)}Z`},Y=(e,t,n=[0,0,0])=>{let r=[M(e.orientation,[1,0,0]),M(e.orientation,[0,1,0]),M(e.orientation,[0,0,1])],i=M(e.orientation,n);if(Math.abs(e.expression.perspective)<1e-4){let e=r.reduce((e,n,r)=>e+n[0]*n[0]*t[r]*t[r],0),n=r.reduce((e,n,r)=>e+n[0]*n[1]*t[r]*t[r],0),a=r.reduce((e,n,r)=>e+n[1]*n[1]*t[r]*t[r],0);return Pe(i[0],i[1],e,n,a)}let a=t.map(e=>1/(e*e)),o=Array.from({length:3},(e,t)=>Array.from({length:3},(e,n)=>r.reduce((e,r,i)=>e+r[t]*a[i]*r[n],0))),s=O/e.expression.perspective,c=[-i[0],-i[1],s-i[2]],l=[o[0][0]*c[0]+o[0][1]*c[1]+o[0][2]*c[2],o[1][0]*c[0]+o[1][1]*c[1]+o[1][2]*c[2],o[2][0]*c[0]+o[2][1]*c[1]+o[2][2]*c[2]],u=c[0]*l[0]+c[1]*l[1]+c[2]*l[2]-1,d=[l[0],l[1],-s*l[2]],f=[[o[0][0],o[0][1],-s*o[0][2]],[o[1][0],o[1][1],-s*o[1][2]],[-s*o[2][0],-s*o[2][1],s*s*o[2][2]]],p=Array.from({length:3},(e,t)=>Array.from({length:3},(e,n)=>d[t]*d[n]-u*f[t][n])),m=p[0][0]*p[1][1]-p[0][1]*p[0][1];if(Math.abs(m)<1e-12)return null;let h=-(p[1][1]*p[0][2]-p[0][1]*p[1][2])/m,g=(p[0][1]*p[0][2]-p[0][0]*p[1][2])/m,_=-(p[2][2]+p[0][2]*h+p[1][2]*g);if(Math.abs(_)<1e-12)return null;let v=p[0][0]/_,y=p[0][1]/_,b=p[1][1]/_,x=v*b-y*y;return x<=0?null:Pe(h,g,b/x,-y/x,v/x)},Fe=(e,t)=>{let n=Y(e,[t.width/2,t.height/2,t.depth/2]),r=t.width===t.height&&t.height===t.depth;if(n&&r){let e=(n.majorRadius+n.minorRadius)/2;return J({centerX:0,centerY:0,majorRadius:e,minorRadius:e,rotation:0})}return n?J(n):null},Ie=(e,t)=>{if(t.type!==`mickey`)return[];let n=Math.min(t.width,t.height)*.23,r=Math.min(n,t.depth*.29),i=t.width*.37,a=-t.height*.39,o=-t.depth*.12,s=[n,n,r];return[-1,1].map(t=>Y(e,s,[t*i,a,o])).filter(e=>e!==null).map(J)},Le=(e,t)=>t.type===`mickey`?Ie(e,t):t.type===`cursor`?[Ae(e,t)]:[],X=e=>Array.from({length:F},(t,n)=>{let r=n/F*Math.PI*2,i=Math.cos(r)*e.majorRadius,a=Math.sin(r)*e.minorRadius;return[e.centerX+i*Math.cos(e.rotation)-a*Math.sin(e.rotation),e.centerY+i*Math.sin(e.rotation)+a*Math.cos(e.rotation),0]}),Re=e=>{if(e.length<3)return P(e);let t=e.map((t,n)=>{let r=e[(n+1)%e.length];return Math.hypot(r[0]-t[0],r[1]-t[1])}),n=[...t].sort((e,t)=>e-t),r=n[Math.floor(n.length/2)]||1,i=Math.max(8,r*3.5),a=t.map(e=>e>i);return`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.map((t,n)=>{let r=(n+1)%e.length,i=e[r];if(a[n])return`L${i[0].toFixed(2)} ${i[1].toFixed(2)}`;let o=a[(n-1+e.length)%e.length]?t:e[(n-1+e.length)%e.length],s=a[r]?i:e[(n+2)%e.length],c=t[0]+(i[0]-o[0])/6,l=t[1]+(i[1]-o[1])/6,u=i[0]-(s[0]-t[0])/6,d=i[1]-(s[1]-t[1])/6;return`C${c.toFixed(2)} ${l.toFixed(2)} ${u.toFixed(2)} ${d.toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``)}Z`},ze=(e,t)=>{let n=t.width/2,r=Math.min(n,t.height/2),i=t.depth/2,a=Math.max(0,(t.height-r*2)/2),o=[n,r,i],s=Y(e,o,[0,a,0]),c=Y(e,o,[0,-a,0]);return!s||!c?null:Re(H([...X(s),...X(c)]))},Be=(e,t)=>{if(t.type===`sphere`||t.type===`mickey`){let n=Fe(e,t);if(n)return n}if(t.type===`capsule`){let n=ze(e,t);if(n)return n}if(t.type===`cylinder`)return Oe(e,t);if(t.type===`cursor`)return ke(e,t);if(t.type===`cone`)return je(e,t);if(t.type===`cube`)return Me(e,t);if(t.type===`diamond`)return Ne(e,t);let n=z(t),r=I.get(n);return r||(r=Array.from({length:_e},(e,n)=>{let r=-Math.PI/2+n/24*Math.PI;return Array.from({length:ve},(e,n)=>w(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),B(I,n,r)),P(H(r.map(t=>N(M(e.orientation,t),e.expression.perspective))))},Ve=(e,t)=>{let n=z(t.surface),r=L.get(n);r||(r=Array.from({length:17},(e,n)=>{let r=-Math.PI/2+n/16*Math.PI;return Array.from({length:49},(e,n)=>{let i=-Math.PI+n/48*Math.PI*2;return w(t.surface,i,r)})}).flat(),B(L,n,r));let i=j(k(t.rotation[0]),k(t.rotation[1]),k(t.rotation[2])),a=H(r.map(n=>{let r=M(i,n),a=[r[0]+t.position[0],r[1]+t.position[1],r[2]+t.position[2]];return N(M(e.orientation,a),e.expression.perspective)}));return(t.surface.type===`cube`||t.surface.type===`diamond`)&&t.surface.roundness<=0?P(a):U(W(a))},He=.1,Ue=(e,t)=>{let n=j(k(t.rotation[0]),k(t.rotation[1]),k(t.rotation[2])),r=[[1,0,0],[0,1,0],[0,0,1]].map(t=>M(e.orientation,M(n,t))[2]);return Math.hypot(r[0]*(t.surface.width/2),r[1]*(t.surface.height/2),r[2]*(t.surface.depth/2))},We=(e,t)=>{let n=t.map(t=>{let n=M(e.orientation,t.position)[2];return{id:t.id,path:Ve(e,t),depth:n,front:n>Ue(e,t)*He}}).sort((e,t)=>e.depth-t.depth);return{backPaths:n.filter(e=>!e.front).map(e=>e.path),frontPaths:n.filter(e=>e.front).map(e=>e.path),backNodeIds:n.filter(e=>!e.front).map(e=>e.id),frontNodeIds:n.filter(e=>e.front).map(e=>e.id)}},Ge=(e,t,n=1,r={})=>{let i=we(e,t,-1,n),a=we(e,t,1,n),o=i.map(e=>e.point),s=a.map(e=>e.point),c=We(e,r.bodyNodes??[]),l=Le(e,t);return{backPaths:[...l,...c.backPaths],frontPaths:c.frontPaths,backNodeIds:[...l.map(()=>null),...c.backNodeIds],frontNodeIds:c.frontNodeIds,headPath:Be(e,t),leftPath:P(o),rightPath:P(s),leftVisible:i.reduce((e,t)=>e+t.normal[2],0)>0,rightVisible:a.reduce((e,t)=>e+t.normal[2],0)>0,wirePaths:r.includeWire===!1?[]:Ee(e,t)}},Ke=e=>e*e*(3-2*e),Z=e=>{let t=Math.sin(e*127.1+311.7)*43758.5453;return(t-Math.floor(t))*2-1},qe=e=>e.headX*.71+e.headY*1.13+e.headZ*1.37,Q=(e,t,n,r)=>{let i=e/r,a=Math.floor(i),o=Ke(i-a),s=Z(a*3+t+n);return s+(Z((a+1)*3+t+n)-s)*o},$=(e,t,n)=>{let r=1100,i=e+Math.sin(e/1700+n)*280,a=Math.floor(i/r),o=(i%r+r)%r/r,s=Ke(Math.min(o/.09,1)),c=Z((a-1)*2+t+n),l=Z(a*2+t+n),u=Math.min(e/240,1);return(c+(l-c)*s)*u};return e.ambientBodyOffset=(e,t)=>{let n=qe(e);if(e.bodyMotion===`slowDrift`)return{x:Q(t,3,n,2900)*1.45,y:Q(t,4,n,3700)*1.1};if(e.bodyMotion===`shake`){let e=t/1e3;return{x:(Math.sin(e*31)+Math.sin(e*53)*.45)*1.35,y:(Math.sin(e*37)+Math.sin(e*61)*.4)*1.1}}return{x:0,y:0}},e.applyAmbientMotion=(e,t)=>{let n={...e},r=qe(e);if(e.eyeMotion===`microSaccades`){let e=$(t,0,r)*1.5,i=$(t,1,r)*.9;n.positionXLeft+=e,n.positionXRight+=e,n.positionYLeft+=i,n.positionYRight+=i}else if(e.eyeMotion===`shake`){let e=t/1e3,r=(Math.sin(e*47)+Math.sin(e*71)*.45)*1.2,i=(Math.sin(e*59)+Math.sin(e*83)*.4)*.8;n.positionXLeft+=r,n.positionXRight+=r,n.positionYLeft+=i,n.positionYRight+=i}if(e.bodyMotion===`slowDrift`)n.headX+=Q(t,0,r,2600)*.8,n.headY+=Q(t,1,r,3300)*1.15,n.headZ+=Q(t,2,r,4100)*.45;else if(e.bodyMotion===`shake`){let e=t/1e3;n.headX+=(Math.sin(e*31)+Math.sin(e*53)*.45)*1.15,n.headY+=(Math.sin(e*37)+Math.sin(e*61)*.4)*1.35,n.headZ+=Math.sin(e*43)*.7}return n},e.expressionFields=de,e.hasAmbientMotion=e=>e.eyeMotion!==`none`||e.bodyMotion!==`none`,e.poseFromExpression=he,e.renderAvatar=Ge,e})({});" +export const standaloneEngineSource = "var AvatarProceduralEngine=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=(e,t)=>Math.sign(e)*Math.abs(e)**t,n=(e,n,r,i,a,o,s)=>{let c=t(Math.cos(n),o);return[r/2*c*t(Math.sin(e),s),i/2*t(Math.sin(n),o),a/2*c*t(Math.cos(e),s)]},r=(e,t,n)=>{let r=e.width/2,i=e.depth/2,a=Math.min(r,e.height/2),o=Math.max(0,(e.height-a*2)/2),s=o*2+Math.PI*a,c=(n+Math.PI/2)/Math.PI*s,l=r,u=0;if(cMath.max(0,Math.min(2,e??0)),a=e=>1+i(e.roundness)/2,o=.04,s=e=>e.roundness<=0?1/0:2/(o+i(e.roundness)/2*.96),c=(e,t,n,r)=>{let i=Math.cos(n)*Math.sin(t),a=Math.sin(n),o=Math.cos(n)*Math.cos(t),s=Number.isFinite(r)?(Math.abs(i)**r+Math.abs(a)**r+Math.abs(o)**r)**(1/r)||1:Math.max(Math.abs(i),Math.abs(a),Math.abs(o))||1;return[e.width/2*(i/s),e.height/2*(a/s),e.depth/2*(o/s)]},l=(e,t,n)=>c(e,t,n,a(e)),u=(e,t,n)=>c(e,t,n,s(e)),d=.24,f=.2,p=.22,m=e=>i(e.morphRoundness)/2,h=(e,t,n)=>{let r=m(e),i=Math.max(0,Math.min(1,t)),a=Math.sin(i*Math.PI),o=(1-Math.cos(i*Math.PI))/2;return{radiusScale:n.radiusScale+(a-n.radiusScale)*r,verticalProgress:n.verticalProgress+(o-n.verticalProgress)*r}},g=(e,t,n,r,i)=>{let a=1-i;return a**3*e+3*a*a*i*t+3*a*i*i*n+i**3*r},_=e=>({tipFraction:(e.tipRoundness??0)*d,baseFraction:(e.baseRoundness??0)*f}),v=(e,t)=>{let n=Math.max(0,Math.min(1,t)),r=e.roundness*p;if(r<=0)return{radiusScale:1,verticalProgress:(Math.sin((n-.5)*Math.PI)+1)/2};if(n1-r){let e=(n-(1-r))/r*(Math.PI/2);return{radiusScale:1-r+r*Math.cos(e),verticalProgress:1-r/2+r*Math.sin(e)/2}}let i=(n-r)/(1-r*2);return{radiusScale:1,verticalProgress:r/2+i*(1-r)}},y=(e,t)=>h(e,t,v(e,t)),b=(e,t,n)=>{let r=Math.max(0,Math.min(1,t)),i=0,a=1;for(let t=0;t<14;t+=1){let t=(i+a)/2;n(e,t).verticalProgress{let n=Math.max(0,Math.min(1,t)),{tipFraction:r,baseFraction:i}=_(e);if(i>0&&n0&&n>1-r){let e=(n-(1-r))/r;return{radiusScale:g(r,r/2,r/4,0,e),verticalProgress:g(1-r,1-r/2,1,1,e)}}return{radiusScale:1-n,verticalProgress:n}},S=(e,t)=>h(e,t,x(e,t)),C=e=>{let t=e.height*.36,n=e.height-t;return{coneApexY:-e.height/2,coneBaseY:-e.height/2+t,bodyHeight:n,bodyCenterY:e.height/2-n/2,bodyWidth:e.width*.54,bodyDepth:e.depth*.62}},w=(e,t,i)=>{let{width:a,height:o,depth:s}=e;switch(e.type){case`sphere`:case`mickey`:return n(t,i,a,o,s,1,1);case`cube`:return u(e,t,i);case`cylinder`:{let n=y(e,(i+Math.PI/2)/Math.PI);return[a/2*n.radiusScale*Math.sin(t),-o/2+o*n.verticalProgress,s/2*n.radiusScale*Math.cos(t)]}case`cursor`:{let n=C(e),r=(i+Math.PI/2)/Math.PI,a=v({...e,width:n.bodyWidth,height:n.bodyHeight,depth:n.bodyDepth},r);return[n.bodyWidth/2*a.radiusScale*Math.sin(t),n.bodyCenterY-n.bodyHeight/2+n.bodyHeight*a.verticalProgress,n.bodyDepth/2*a.radiusScale*Math.cos(t)]}case`diamond`:return l(e,t,i);case`capsule`:return r(e,t,i);case`cone`:{let n=S(e,(i+Math.PI/2)/Math.PI);return[a/2*n.radiusScale*Math.sin(t),o/2-o*n.verticalProgress,s/2*n.radiusScale*Math.cos(t)]}}},ee=(e,t)=>[e[0]-t[0],e[1]-t[1],e[2]-t[2]],T=([e,t,n])=>{let r=Math.hypot(e,t,n)||1;return[e/r,t/r,n/r]},te=(e,t,n)=>{let r=e.type===`cone`?-1:1;return T([r*(t[1]*n[2]-t[2]*n[1]),r*(t[2]*n[0]-t[0]*n[2]),r*(t[0]*n[1]-t[1]*n[0])])},ne=(e,t,n)=>{let r=5e-4;if(e.type===`cone`&&n>=Math.PI/2-r)return[0,-1,0];let i=w(e,t-r,n),a=w(e,t+r,n),o=w(e,t,Math.max(-Math.PI/2,n-r)),s=w(e,t,Math.min(Math.PI/2,n+r));return te(e,ee(a,i),ee(s,o))},E=(e,t)=>Math.sign(e)*Math.abs(e)**t,D=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;return T([E(t[0]/r,n-1)/r,E(t[1]/i,n-1)/i,E(t[2]/a,n-1)/a])},re=(e,t)=>D(e,t,a(e)),ie=(e,t)=>{let n=s(e);if(Number.isFinite(n))return D(e,t,n);let r=[t[0]/(e.width/2||1),t[1]/(e.height/2||1),t[2]/(e.depth/2||1)],i=r.reduce((e,t,n)=>Math.abs(t)>Math.abs(r[e])?n:e,0);return[i===0?Math.sign(r[0]):0,i===1?Math.sign(r[1]):0,i===2?Math.sign(r[2]):0]},ae=(e,t,n,r,i)=>{let a=e.width/2||1,o=e.height/2||1,s=e.depth/2||1;if(!Number.isFinite(r)){let r=[Math.max(-a,Math.min(a,t)),Math.max(-o,Math.min(o,n)),s];return{point:r,normal:i(e,r)}}let c=Math.max(-1,Math.min(1,n/o)),l=Math.max(0,1-Math.abs(c)**r)**(1/r),u=Math.max(-a*l,Math.min(a*l,t)),d=u/a,f=Math.max(0,1-Math.abs(d)**r-Math.abs(c)**r)**(1/r),p=[u,c*o,s*f];return{point:p,normal:i(e,p)}},oe=(e,t,n,r,i,a=0)=>{let o=t-a,s=Math.max(0,1-(e/(n||1))**2-(o/(r||1))**2),c=i*Math.sqrt(s);return{point:[e,t,c],normal:T([e/(n*n||1),o/(r*r||1),c/(i*i||1)])}},O=(e,t,n,r,i)=>{let a=e.width/2||1,o=e.depth/2||1,s=Math.max(0,Math.min(1,.5+i*(n/e.height))),c=b(e,s,r),l=a*c,u=o*c,d=Math.max(-l,Math.min(l,t)),f=l>0?Math.max(0,1-(d/l)**2):0,p=u*Math.sqrt(f),m=1e-4,h=Math.max(0,s-m),g=Math.min(1,s+m),_=b(e,h,r),v=(b(e,g,r)-_)/(g-h||1),y=Math.max(Math.sqrt(f),1e-4),x=-(o/a)*d/(l*y||1),S=i*o*v/(e.height*y||1);return{point:[d,n,p],normal:T([-x,-S,1])}},se=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,o=e.depth/2||1;switch(e.type){case`sphere`:case`mickey`:return oe(t,n,r,i,o);case`cube`:return ae(e,t,n,s(e),ie);case`capsule`:{let e=Math.min(r,i),a=Math.max(0,i-e);return oe(t,n,r,e,o,n<-a?-a:n>a?a:n)}case`cylinder`:return O(e,t,n,y,1);case`cursor`:{let r=C(e),i=O({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},t,n-r.bodyCenterY,v,1);return{point:[i.point[0],i.point[1]+r.bodyCenterY,i.point[2]],normal:i.normal}}case`cone`:return O(e,t,n,S,-1);case`diamond`:return ae(e,t,n,a(e),re)}},ce=(e,t,n)=>{let r=w(e,t,n);if(e.type===`sphere`||e.type===`mickey`){let t=e.width/2||1,n=e.height/2||1,i=e.depth/2||1;return{point:r,normal:T([r[0]/(t*t),r[1]/(n*n),r[2]/(i*i)])}}return e.type===`cylinder`&&e.roundness<=0&&(e.morphRoundness??0)<=0?{point:r,normal:T([Math.sin(t)/(e.width/2||1),0,Math.cos(t)/(e.depth/2||1)])}:e.type===`diamond`?{point:r,normal:re(e,r)}:e.type===`cube`?{point:r,normal:ie(e,r)}:{point:r,normal:ne(e,t,n)}},k=620,A=14,le=[`headX`,`headY`,`headZ`,`widthLeft`,`widthRight`,`heightLeft`,`heightRight`,`spacing`,`positionXLeft`,`positionXRight`,`positionYLeft`,`positionYRight`,`leftAngle`,`rightAngle`,`perspective`],j=e=>e*Math.PI/180,M=([e,t,n,r])=>{let i=Math.hypot(e,t,n,r)||1;return[e/i,t/i,n/i,r/i]},ue=([e,t,n,r],[i,a,o,s])=>M([e*i-t*a-n*o-r*s,e*a+t*i+n*s-r*o,e*o-t*s+n*i+r*a,e*s+t*o-n*a+r*i]),N=([e,t,n],r)=>{let i=r/2,a=Math.sin(i);return M([Math.cos(i),e*a,t*a,n*a])},P=(e,t,n)=>{let r=N([1,0,0],e),i=N([0,1,0],t);return ue(ue(N([0,0,1],n),r),i)},F=([e,t,n,r],[i,a,o])=>{let s=2*(n*o-r*a),c=2*(r*i-t*o),l=2*(t*a-n*i);return[i+e*s+(n*l-r*c),a+e*c+(r*s-t*l),o+e*l+(t*c-n*s)]},de=(e,t)=>{let n=e/2,r=t/2,i=Math.min(r,n),a=[],o=(e,t)=>{let n=Math.max(2,Math.ceil(Math.hypot(t[0]-e[0],t[1]-e[1])/1.5));for(let r=0;r{for(let r=0;r{let n=k-e[2]*t,r=Math.abs(n)<1e-4?k/1e-4:k/n;return[e[0]*r,e[1]*r,e[2]]},L=(e,t=!0)=>e.length?`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.slice(1).map(e=>`L${e[0].toFixed(2)} ${e[1].toFixed(2)}`).join(``)}${t?`Z`:``}`:``,fe=e=>({expression:e,orientation:P(j(e.headX),j(e.headY),j(e.headZ))}),pe=24,me=25,he=73,R=144,ge=33,_e=73,z=new Map,ve=new Map,ye=new Map,B=e=>[e.type,e.width,e.height,e.depth,e.roundness,e.morphRoundness,e.tipRoundness,e.baseRoundness].map(e=>typeof e==`number`?e.toFixed(4):e).join(`:`),V=(e,t,n)=>(e.size>=pe&&e.delete(e.keys().next().value),e.set(t,n),n),be=(e,t,n)=>ce(e,t,n),H=(e,t)=>({point:I(F(e.orientation,t.point),e.expression.perspective),normal:F(e.orientation,t.normal)}),xe=(e,t)=>{let n=e/120,r=t/120;return[120*Math.cos(r)*Math.sin(n),120*Math.sin(r)]},Se=(e,t,n,r)=>{let[i,a]=xe(n,r);return H(e,se(t,i,a))},U=(e,t,n,r,i={x:0,y:0})=>{let a=e.expression,o=n<0?`Left`:`Right`,s=a[`width${o}`],c=5+(a[`height${o}`]-5)*r,l=n*a.spacing/2+a[`positionX${o}`]+i.x,u=a[`positionY${o}`]+i.y,d=j(n<0?a.leftAngle:a.rightAngle);return de(s,c).map(([n,r])=>{let i=n*Math.cos(d)-r*Math.sin(d),a=n*Math.sin(d)+r*Math.cos(d);return Se(e,t,l+i,u+a)})},Ce=e=>{let t=[],n=[];return e.forEach(({point:e,normal:r})=>{r[2]>0?n.push(e):n.length&&(t.push(n),n=[])}),n.length&&t.push(n),t.filter(e=>e.length>1).map(e=>L(e,!1)).join(``)},we=(e,t)=>{let n=B(t),r=ye.get(n);if(!r){let e=[-60,-30,0,30,60].map(e=>Array.from({length:73},(n,r)=>be(t,j(-180+r*5),j(e)))),i=Array.from({length:12},(e,t)=>-150+t*30).map(e=>Array.from({length:37},(n,r)=>be(t,j(e),j(-90+r*5))));r=V(ye,n,[...e,...i])}return r.map(t=>Ce(t.map(t=>H(e,t))))},W=e=>{let t=[...e].sort((e,t)=>e[0]-t[0]||e[1]-t[1]),n=(e,t,n)=>(t[0]-e[0])*(n[1]-e[1])-(t[1]-e[1])*(n[0]-e[0]),r=e=>{let t=[];return e.forEach(e=>{for(;t.length>=2&&n(t.at(-2),t.at(-1),e)<=0;)t.pop();t.push(e)}),t};return[...r(t).slice(0,-1),...r(t.reverse()).slice(0,-1)]},G=e=>{if(e.length<3)return L(e);let t=t=>e[(t+e.length)%e.length];return`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.map((e,n)=>{let r=t(n-1),i=t(n+1),a=t(n+2),o=[e[0]+(i[0]-r[0])/6,e[1]+(i[1]-r[1])/6,e[2]],s=[i[0]-(a[0]-e[0])/6,i[1]-(a[1]-e[1])/6,i[2]];return`C${o[0].toFixed(2)} ${o[1].toFixed(2)} ${s[0].toFixed(2)} ${s[1].toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``)}Z`},K=(e,t=7)=>e.flatMap((n,r)=>{let i=e[(r+1)%e.length],a=Math.max(1,Math.ceil(Math.hypot(i[0]-n[0],i[1]-n[1])/t));return Array.from({length:a},(e,t)=>{let r=t/a;return[n[0]+(i[0]-n[0])*r,n[1]+(i[1]-n[1])*r,n[2]+(i[2]-n[2])*r]})}),Te=e=>e.length?e.length===1?`${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}`:e.slice(0,-1).map((t,n)=>{let r=e[Math.max(0,n-1)],i=e[n+1],a=e[Math.min(e.length-1,n+2)],o=t[0]+(i[0]-r[0])/6,s=t[1]+(i[1]-r[1])/6,c=i[0]-(a[0]-t[0])/6,l=i[1]-(a[1]-t[1])/6;return`C${o.toFixed(2)} ${s.toFixed(2)} ${c.toFixed(2)} ${l.toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``):``,q=(e,t)=>I(F(e.orientation,t),e.expression.perspective),J=(e,t,n)=>Array.from({length:145},(r,i)=>{let a=i/R*Math.PI*2;return[e/2*Math.sin(a),n,t/2*Math.cos(a)]}),Y=(e,t)=>{let n=B(t),r=z.get(n);return r||(r=Array.from({length:ge},(e,n)=>{let r=-Math.PI/2+n/32*Math.PI;return Array.from({length:_e},(e,n)=>w(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),V(z,n,r)),G(K(W(r.map(t=>q(e,t)))))},Ee=(e,t)=>{if(t.roundness>0||(t.morphRoundness??0)>0)return Y(e,t);let n=t.height/2;return G(K(W([...J(t.width,t.depth,-n),...J(t.width,t.depth,n)].map(t=>q(e,t)))))},De=(e,t)=>{let n=C(t),r=n.bodyHeight/2;return G(K(W([...J(n.bodyWidth,n.bodyDepth,n.bodyCenterY-r),...J(n.bodyWidth,n.bodyDepth,n.bodyCenterY+r)].map(t=>q(e,t)))))},Oe=(e,t)=>{let n=C(t),r=q(e,[0,n.coneApexY,0]);return G(K(W([...J(t.width,t.depth,n.coneBaseY).map(t=>q(e,t)),r])))},ke=(e,t)=>{if((t.morphRoundness??0)>0||(t.tipRoundness??0)>0||(t.baseRoundness??0)>0)return Y(e,t);let n=q(e,[0,-t.height/2,0]),r=W([...J(t.width,t.depth,t.height/2).map(t=>q(e,t)),n]),i=r.findIndex(e=>Math.hypot(e[0]-n[0],e[1]-n[1])<.01);if(i<0)return G(r);let a=[...r.slice(i),...r.slice(0,i)].slice(1);return a.length<2?L(r):`M${n[0].toFixed(2)} ${n[1].toFixed(2)}L${a[0][0].toFixed(2)} ${a[0][1].toFixed(2)}${Te(a)}L${n[0].toFixed(2)} ${n[1].toFixed(2)}Z`},Ae=(e,t)=>{if(t.roundness>0)return Y(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return L(W([-1,1].flatMap(e=>[-1,1].flatMap(t=>[-1,1].map(a=>[e*n,t*r,a*i]))).map(t=>q(e,t))))},je=(e,t)=>{if(t.roundness>0)return Y(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return L(W([[-n,0,0],[n,0,0],[0,-r,0],[0,r,0],[0,0,-i],[0,0,i]].map(t=>q(e,t))))},Me=(e,t,n,r,i)=>{let a=n+i,o=Math.hypot(n-i,r*2),s=(a+o)/2,c=(a-o)/2;return s<=0||c<=0?null:{centerX:e,centerY:t,majorRadius:Math.sqrt(s),minorRadius:Math.sqrt(c),rotation:Math.atan2(r*2,n-i)/2}},X=({centerX:e,centerY:t,majorRadius:n,minorRadius:r,rotation:i})=>{let a=i*180/Math.PI,o=Math.cos(i)*n,s=Math.sin(i)*n,c=e+o,l=t+s,u=e-o,d=t-s;return`M${c.toFixed(2)} ${l.toFixed(2)}A${n.toFixed(2)} ${r.toFixed(2)} ${a.toFixed(2)} 0 1 ${u.toFixed(2)} ${d.toFixed(2)}A${n.toFixed(2)} ${r.toFixed(2)} ${a.toFixed(2)} 0 1 ${c.toFixed(2)} ${l.toFixed(2)}Z`},Z=(e,t,n=[0,0,0])=>{let r=[F(e.orientation,[1,0,0]),F(e.orientation,[0,1,0]),F(e.orientation,[0,0,1])],i=F(e.orientation,n);if(Math.abs(e.expression.perspective)<1e-4){let e=r.reduce((e,n,r)=>e+n[0]*n[0]*t[r]*t[r],0),n=r.reduce((e,n,r)=>e+n[0]*n[1]*t[r]*t[r],0),a=r.reduce((e,n,r)=>e+n[1]*n[1]*t[r]*t[r],0);return Me(i[0],i[1],e,n,a)}let a=t.map(e=>1/(e*e)),o=Array.from({length:3},(e,t)=>Array.from({length:3},(e,n)=>r.reduce((e,r,i)=>e+r[t]*a[i]*r[n],0))),s=k/e.expression.perspective,c=[-i[0],-i[1],s-i[2]],l=[o[0][0]*c[0]+o[0][1]*c[1]+o[0][2]*c[2],o[1][0]*c[0]+o[1][1]*c[1]+o[1][2]*c[2],o[2][0]*c[0]+o[2][1]*c[1]+o[2][2]*c[2]],u=c[0]*l[0]+c[1]*l[1]+c[2]*l[2]-1,d=[l[0],l[1],-s*l[2]],f=[[o[0][0],o[0][1],-s*o[0][2]],[o[1][0],o[1][1],-s*o[1][2]],[-s*o[2][0],-s*o[2][1],s*s*o[2][2]]],p=Array.from({length:3},(e,t)=>Array.from({length:3},(e,n)=>d[t]*d[n]-u*f[t][n])),m=p[0][0]*p[1][1]-p[0][1]*p[0][1];if(Math.abs(m)<1e-12)return null;let h=-(p[1][1]*p[0][2]-p[0][1]*p[1][2])/m,g=(p[0][1]*p[0][2]-p[0][0]*p[1][2])/m,_=-(p[2][2]+p[0][2]*h+p[1][2]*g);if(Math.abs(_)<1e-12)return null;let v=p[0][0]/_,y=p[0][1]/_,b=p[1][1]/_,x=v*b-y*y;return x<=0?null:Me(h,g,b/x,-y/x,v/x)},Ne=(e,t)=>{let n=Z(e,[t.width/2,t.height/2,t.depth/2]),r=t.width===t.height&&t.height===t.depth;if(n&&r){let e=(n.majorRadius+n.minorRadius)/2;return X({centerX:0,centerY:0,majorRadius:e,minorRadius:e,rotation:0})}return n?X(n):null},Pe=(e,t)=>{if(t.type!==`mickey`)return[];let n=Math.min(t.width,t.height)*.23,r=Math.min(n,t.depth*.29),i=t.width*.37,a=-t.height*.39,o=-t.depth*.12,s=[n,n,r];return[-1,1].map(t=>Z(e,s,[t*i,a,o])).filter(e=>e!==null).map(X)},Fe=(e,t)=>t.type===`mickey`?Pe(e,t):t.type===`cursor`?[Oe(e,t)]:[],Ie=e=>Array.from({length:R},(t,n)=>{let r=n/R*Math.PI*2,i=Math.cos(r)*e.majorRadius,a=Math.sin(r)*e.minorRadius;return[e.centerX+i*Math.cos(e.rotation)-a*Math.sin(e.rotation),e.centerY+i*Math.sin(e.rotation)+a*Math.cos(e.rotation),0]}),Le=e=>{if(e.length<3)return L(e);let t=e.map((t,n)=>{let r=e[(n+1)%e.length];return Math.hypot(r[0]-t[0],r[1]-t[1])}),n=[...t].sort((e,t)=>e-t),r=n[Math.floor(n.length/2)]||1,i=Math.max(8,r*3.5),a=t.map(e=>e>i);return`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.map((t,n)=>{let r=(n+1)%e.length,i=e[r];if(a[n])return`L${i[0].toFixed(2)} ${i[1].toFixed(2)}`;let o=a[(n-1+e.length)%e.length]?t:e[(n-1+e.length)%e.length],s=a[r]?i:e[(n+2)%e.length],c=t[0]+(i[0]-o[0])/6,l=t[1]+(i[1]-o[1])/6,u=i[0]-(s[0]-t[0])/6,d=i[1]-(s[1]-t[1])/6;return`C${c.toFixed(2)} ${l.toFixed(2)} ${u.toFixed(2)} ${d.toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``)}Z`},Re=(e,t)=>{let n=t.width/2,r=Math.min(n,t.height/2),i=t.depth/2,a=Math.max(0,(t.height-r*2)/2),o=[n,r,i],s=Z(e,o,[0,a,0]),c=Z(e,o,[0,-a,0]);return!s||!c?null:Le(W([...Ie(s),...Ie(c)]))},ze=(e,t)=>{if(t.type===`sphere`||t.type===`mickey`){let n=Ne(e,t);if(n)return n}if(t.type===`capsule`){let n=Re(e,t);if(n)return n}if(t.type===`cylinder`)return Ee(e,t);if(t.type===`cursor`)return De(e,t);if(t.type===`cone`)return ke(e,t);if(t.type===`cube`)return Ae(e,t);if(t.type===`diamond`)return je(e,t);let n=B(t),r=z.get(n);return r||(r=Array.from({length:me},(e,n)=>{let r=-Math.PI/2+n/24*Math.PI;return Array.from({length:he},(e,n)=>w(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),V(z,n,r)),L(W(r.map(t=>I(F(e.orientation,t),e.expression.perspective))))},Be=(e,t)=>{let n=B(t.surface),r=ve.get(n);r||(r=Array.from({length:17},(e,n)=>{let r=-Math.PI/2+n/16*Math.PI;return Array.from({length:49},(e,n)=>{let i=-Math.PI+n/48*Math.PI*2;return w(t.surface,i,r)})}).flat(),V(ve,n,r));let i=P(j(t.rotation[0]),j(t.rotation[1]),j(t.rotation[2])),a=W(r.map(n=>{let r=F(i,n),a=[r[0]+t.position[0],r[1]+t.position[1],r[2]+t.position[2]];return I(F(e.orientation,a),e.expression.perspective)}));return(t.surface.type===`cube`||t.surface.type===`diamond`)&&t.surface.roundness<=0?L(a):G(K(a))},Ve=.1,He=(e,t)=>{let n=P(j(t.rotation[0]),j(t.rotation[1]),j(t.rotation[2])),r=[[1,0,0],[0,1,0],[0,0,1]].map(t=>F(e.orientation,F(n,t))[2]);return Math.hypot(r[0]*(t.surface.width/2),r[1]*(t.surface.height/2),r[2]*(t.surface.depth/2))},Ue=(e,t)=>{let n=t.map(t=>{let n=F(e.orientation,t.position)[2];return{id:t.id,path:Be(e,t),depth:n,front:n>He(e,t)*Ve}}).sort((e,t)=>e.depth-t.depth);return{backPaths:n.filter(e=>!e.front).map(e=>e.path),frontPaths:n.filter(e=>e.front).map(e=>e.path),backNodeIds:n.filter(e=>!e.front).map(e=>e.id),frontNodeIds:n.filter(e=>e.front).map(e=>e.id)}},We=(e,t,n=1,r={})=>{let i=U(e,t,-1,n,r.eyeOffset),a=U(e,t,1,n,r.eyeOffset),o=i.map(e=>e.point),s=a.map(e=>e.point),c=Ue(e,r.bodyNodes??[]),l=Fe(e,t);return{backPaths:[...l,...c.backPaths],frontPaths:c.frontPaths,backNodeIds:[...l.map(()=>null),...c.backNodeIds],frontNodeIds:c.frontNodeIds,headPath:ze(e,t),leftPath:L(o),rightPath:L(s),leftVisible:i.reduce((e,t)=>e+t.normal[2],0)>0,rightVisible:a.reduce((e,t)=>e+t.normal[2],0)>0,wirePaths:r.includeWire===!1?[]:we(e,t)}},Ge=e=>e*e*(3-2*e),Q=e=>{let t=Math.sin(e*127.1+311.7)*43758.5453;return(t-Math.floor(t))*2-1},Ke=e=>e.headX*.71+e.headY*1.13+e.headZ*1.37,qe=17.29,$=(e,t,n,r)=>{let i=e/r,a=Math.floor(i),o=Ge(i-a),s=Q(a*3+t+n);return s+(Q((a+1)*3+t+n)-s)*o},Je=(e,t,n)=>{let r=1100;if(e<=0)return 0;let i=Math.floor(e/r),a=(e-i*r)/140,o=Ge(Math.min(a,1)),s=i===0?0:Q((i-1)*2+t+n);return s+(Q(i*2+t+n)-s)*o},Ye=e=>e.eyeMotion!==`none`||e.bodyMotion!==`none`,Xe=(e,t,n=1)=>{let r=Ke(e);if(e.bodyMotion===`slowDrift`)return{x:$(t,3,r,2900)*1.45*n,y:$(t,4,r,3700)*1.1*n};if(e.bodyMotion===`shake`){let e=t/1e3;return{x:(Math.sin(e*31)+Math.sin(e*53)*.45)*1.35*n,y:(Math.sin(e*37)+Math.sin(e*61)*.4)*1.1*n}}return{x:0,y:0}},Ze=(e,t,n=1)=>{if(e.eyeMotion===`microSaccades`)return{x:Je(t,0,qe)*1.5*n,y:Je(t,1,qe)*.9*n};if(e.eyeMotion===`shake`){let e=t/1e3;return{x:(Math.sin(e*47)+Math.sin(e*71)*.45)*1.2*n,y:(Math.sin(e*59)+Math.sin(e*83)*.4)*.8*n}}return{x:0,y:0}},Qe=(e,t,n=1)=>{let r={...e},i=Ke(e);if(e.bodyMotion===`slowDrift`)r.headX+=$(t,0,i,2600)*.8*n,r.headY+=$(t,1,i,3300)*1.15*n,r.headZ+=$(t,2,i,4100)*.45*n;else if(e.bodyMotion===`shake`){let e=t/1e3;r.headX+=(Math.sin(e*31)+Math.sin(e*53)*.45)*1.15*n,r.headY+=(Math.sin(e*37)+Math.sin(e*61)*.4)*1.35*n,r.headZ+=Math.sin(e*43)*.7*n}return r};return e.ambientBodyOffset=Xe,e.ambientEyeOffset=Ze,e.applyAmbientBodyMotion=Qe,e.applyAmbientMotion=(e,t,n=1)=>{let r=Qe(e,t,n),i=Ze(e,t,n);return r.positionXLeft+=i.x,r.positionXRight+=i.x,r.positionYLeft+=i.y,r.positionYRight+=i.y,r},e.expressionFields=le,e.hasAmbientMotion=Ye,e.poseFromExpression=fe,e.renderAvatar=We,e})({});" diff --git a/src/features/studio/useStudioController.ts b/src/features/studio/useStudioController.ts index 21551b2..0499c18 100644 --- a/src/features/studio/useStudioController.ts +++ b/src/features/studio/useStudioController.ts @@ -41,7 +41,8 @@ import { } from '@/features/animation/sequences' import { ambientBodyOffset, - applyAmbientMotion, + ambientEyeOffset, + applyAmbientBodyMotion, hasAmbientMotion, } from '@/features/avatar/ambientMotion' import { @@ -282,10 +283,14 @@ export function useStudioController() { const displayedPose = useRef(initialPose) const transitionFrame = useRef(null) const ambientFrame = useRef(null) - const ambientStartedAt = useRef(0) - const lastAmbientElapsed = useRef(0) + const eyeAmbientStartedAt = useRef(-1) + const bodyAmbientStartedAt = useRef(-1) + const lastEyeAmbientElapsed = useRef(0) + const lastBodyAmbientElapsed = useRef(0) const lastAmbientFrame = useRef(0) - const ambientSignature = useRef('none:none') + const eyeAmbientSignature = useRef('none') + const bodyAmbientSignature = useRef('none') + const lastAmbientStrength = useRef(1) const transitionTarget = useRef({ ...initialExpression }) const canonicalTarget = useRef({ ...initialExpression }) const retargetFrom = useRef(null) @@ -320,35 +325,68 @@ export function useStudioController() { const bodyColorAnimation = useRef | null>(null) const eyeColorAnimation = useRef | null>(null) - const paintPose = (pose: AvatarPose, blink?: number, frameTimeMs?: number) => { + const paintPose = ( + pose: AvatarPose, + blink?: number, + frameTimeMs?: number, + ambientStrength?: number + ) => { displayedPose.current = pose + const resolvedAmbientStrength = + ambientStrength ?? (transitionFrame.current === null ? 1 : lastAmbientStrength.current) + lastAmbientStrength.current = resolvedAmbientStrength paintRenderedRotationGizmo(renderedRotationGizmo, pose.expression) const avatar = avatarsRef.current.find(item => item.id === activeAvatarIdRef.current) - const signature = `${pose.expression.eyeMotion}:${pose.expression.bodyMotion}` - if (signature !== ambientSignature.current) { - ambientSignature.current = signature - ambientStartedAt.current = -1 - lastAmbientElapsed.current = 0 + const eyeAmbientEnabled = !reduceMotion && pose.expression.eyeMotion !== 'none' + const bodyAmbientEnabled = !reduceMotion && pose.expression.bodyMotion !== 'none' + const eyeSignature = eyeAmbientEnabled ? pose.expression.eyeMotion : 'none' + const bodySignature = bodyAmbientEnabled ? pose.expression.bodyMotion : 'none' + if (eyeSignature !== eyeAmbientSignature.current) { + eyeAmbientSignature.current = eyeSignature + eyeAmbientStartedAt.current = -1 + lastEyeAmbientElapsed.current = 0 } - if (frameTimeMs !== undefined) { - if (ambientStartedAt.current < 0) ambientStartedAt.current = frameTimeMs - lastAmbientElapsed.current = frameTimeMs - ambientStartedAt.current + if (bodySignature !== bodyAmbientSignature.current) { + bodyAmbientSignature.current = bodySignature + bodyAmbientStartedAt.current = -1 + lastBodyAmbientElapsed.current = 0 } - const renderedExpression = - !reduceMotion && hasAmbientMotion(pose.expression) - ? applyAmbientMotion(pose.expression, lastAmbientElapsed.current) - : pose.expression + if (eyeAmbientEnabled && frameTimeMs !== undefined) { + if (eyeAmbientStartedAt.current < 0) eyeAmbientStartedAt.current = frameTimeMs + lastEyeAmbientElapsed.current = frameTimeMs - eyeAmbientStartedAt.current + } + if (bodyAmbientEnabled && frameTimeMs !== undefined) { + if (bodyAmbientStartedAt.current < 0) bodyAmbientStartedAt.current = frameTimeMs + lastBodyAmbientElapsed.current = frameTimeMs - bodyAmbientStartedAt.current + } + const renderedExpression = bodyAmbientEnabled + ? applyAmbientBodyMotion( + pose.expression, + lastBodyAmbientElapsed.current, + resolvedAmbientStrength + ) + : pose.expression + const eyeOffset = eyeAmbientEnabled + ? ambientEyeOffset(pose.expression, lastEyeAmbientElapsed.current, resolvedAmbientStrength) + : { x: 0, y: 0 } const renderPose = avatar ? poseWithAvatarEyes(renderedExpression, avatar.eyes ?? defaultAvatarEyes) : poseFromExpression(renderedExpression) const geometry = renderAvatar(renderPose, surfaceRef.current, blink ?? blinkValue.get(), { includeWire: showWireRef.current || highlightRef.current === 'head', bodyNodes: bodyNodesRef.current, + eyeOffset, }) paintRenderedScene(renderedScene, geometry) paintRenderedOffset( renderedScene, - ambientBodyOffset(pose.expression, lastAmbientElapsed.current) + bodyAmbientEnabled + ? ambientBodyOffset( + pose.expression, + lastBodyAmbientElapsed.current, + resolvedAmbientStrength + ) + : { x: 0, y: 0 } ) } @@ -474,6 +512,7 @@ export function useStudioController() { stopTransition(true) stopColorTransitions() const durationMs = transitionSettings.transitionMs + lastAmbientStrength.current = 0 const from = { ...current } const fromColors = { body: renderedColors.body.get(), @@ -511,7 +550,7 @@ export function useStudioController() { body: interpolateHexColor(fromColors.body, targetColors.body, bounded(eased, 0, 1)), eyes: interpolateHexColor(fromColors.eyes, targetColors.eyes, bounded(eased, 0, 1)), }) - paintPose(poseFromExpression(animated), undefined, time) + paintPose(poseFromExpression(animated), undefined, time, bounded(eased, 0, 1)) if ( modeRef.current === 'manual' && time - lastInspectorFrame.current >= INSPECTOR_FRAME_MS @@ -554,6 +593,12 @@ export function useStudioController() { return } transitionTarget.current = resolvedTarget + lastAmbientStrength.current = 0 + const initialTransitionDistance = expressionFields.reduce( + (total, field) => total + Math.abs(resolvedTarget[field] - current[field]), + 0 + ) + let transitionProgress = 0 const tick = (time: number) => { const previousTime = lastTransitionTime.current ?? time const deltaTime = Math.min(Math.max((time - previousTime) / 1000, 1 / 240), 1 / 30) @@ -585,6 +630,15 @@ export function useStudioController() { } } const target = transitionTarget.current + const remainingTransitionDistance = expressionFields.reduce( + (total, field) => total + Math.abs(target[field] - currentExpression[field]), + 0 + ) + const geometricProgress = + initialTransitionDistance <= 0 + ? 1 + : 1 - remainingTransitionDistance / initialTransitionDistance + transitionProgress = Math.max(transitionProgress, bounded(geometricProgress, 0, 1)) let settled = true const animated = { ...currentExpression } animated.eyeMotion = target.eyeMotion @@ -609,7 +663,7 @@ export function useStudioController() { return } - paintPose(poseFromExpression(animated), undefined, time) + paintPose(poseFromExpression(animated), undefined, time, transitionProgress) if (modeRef.current === 'manual' && time - lastInspectorFrame.current >= INSPECTOR_FRAME_MS) { lastInspectorFrame.current = time setExpression(animated)