diff --git a/packages/avatar-core/src/avatarDefinition.schema.json b/packages/avatar-core/src/avatarDefinition.schema.json index b14d50b..a02b1f4 100644 --- a/packages/avatar-core/src/avatarDefinition.schema.json +++ b/packages/avatar-core/src/avatarDefinition.schema.json @@ -66,7 +66,17 @@ "required": ["type", "width", "height", "depth", "roundness"], "properties": { "type": { - "enum": ["sphere", "mickey", "cursor", "cube", "capsule", "cylinder", "cone", "diamond"] + "enum": [ + "sphere", + "mickey", + "cursor", + "cube", + "capsule", + "cylinder", + "cone", + "diamond", + "ghost" + ] }, "width": { "$ref": "#/$defs/dimension" }, "height": { "$ref": "#/$defs/dimension" }, @@ -122,6 +132,7 @@ "additionalProperties": false, "required": ["surface", "position", "rotation"], "properties": { + "layer": { "enum": ["auto", "front", "back"] }, "surface": { "$ref": "#/$defs/nodeSurface" }, "position": { "$ref": "#/$defs/position" }, "rotation": { "$ref": "#/$defs/rotation" } diff --git a/packages/avatar-core/src/avatarDefinition.ts b/packages/avatar-core/src/avatarDefinition.ts index 8713c0e..7c9e83a 100644 --- a/packages/avatar-core/src/avatarDefinition.ts +++ b/packages/avatar-core/src/avatarDefinition.ts @@ -43,11 +43,12 @@ export type SurfaceDefinition = { baseRoundness?: number } -export type BodyNodeSurfaceType = Exclude +export type BodyNodeSurfaceType = Exclude export type PrimarySurfaceDefinition = SurfaceDefinition export type BodyNodeSurfaceDefinition = SurfaceDefinition export type AvatarBodyNodeDefinition = { + layer?: 'auto' | 'front' | 'back' surface: BodyNodeSurfaceDefinition position: [number, number, number] rotation: [number, number, number] diff --git a/packages/avatar-core/src/geometry.ts b/packages/avatar-core/src/geometry.ts index a96aa5e..db5ff1c 100644 --- a/packages/avatar-core/src/geometry.ts +++ b/packages/avatar-core/src/geometry.ts @@ -1117,6 +1117,87 @@ const projectedCapsulePath = (pose: AvatarPose, surface: SurfaceConfig) => { return smoothHullPath(convexHull([...ellipsePoints(top), ...ellipsePoints(bottom)])) } +const projectedGhostPath = (pose: AvatarPose, surface: SurfaceConfig) => { + const halfWidth = surface.width / 2 + const halfHeight = surface.height / 2 + const notchCenter = surface.width / 6 + const notchHalfWidth = surface.width * 0.018 + const sideStartY = -halfHeight * 0.08 + const lobeStartY = halfHeight * 0.5 + const notchTopY = halfHeight * 0.4 + const bottomY = halfHeight + + const point = (x: number, y: number) => projectLocalPoint(pose, [x, y, 0]) + const format = ([x, y]: Point3) => `${x.toFixed(2)} ${y.toFixed(2)}` + const move = (x: number, y: number) => `M${format(point(x, y))}` + const line = (x: number, y: number) => `L${format(point(x, y))}` + const cubicTo = ( + firstX: number, + firstY: number, + secondX: number, + secondY: number, + endX: number, + endY: number + ) => + `C${format(point(firstX, firstY))} ${format(point(secondX, secondY))} ${format(point(endX, endY))}` + + return [ + move(-halfWidth, sideStartY), + cubicTo(-halfWidth, -halfHeight * 0.62, -halfWidth * 0.56, -halfHeight, 0, -halfHeight), + cubicTo(halfWidth * 0.56, -halfHeight, halfWidth, -halfHeight * 0.62, halfWidth, sideStartY), + line(halfWidth, lobeStartY), + cubicTo(halfWidth, bottomY * 0.82, halfWidth * 0.82, bottomY, halfWidth * 0.66, bottomY), + cubicTo( + halfWidth * 0.49, + bottomY, + notchCenter + notchHalfWidth, + bottomY * 0.82, + notchCenter + notchHalfWidth, + lobeStartY + ), + line(notchCenter + notchHalfWidth, notchTopY), + cubicTo( + notchCenter + notchHalfWidth, + notchTopY - 6, + notchCenter - notchHalfWidth, + notchTopY - 6, + notchCenter - notchHalfWidth, + notchTopY + ), + line(notchCenter - notchHalfWidth, lobeStartY), + cubicTo(notchCenter - notchHalfWidth, bottomY * 0.82, halfWidth * 0.17, bottomY, 0, bottomY), + cubicTo( + -halfWidth * 0.17, + bottomY, + -notchCenter + notchHalfWidth, + bottomY * 0.82, + -notchCenter + notchHalfWidth, + lobeStartY + ), + line(-notchCenter + notchHalfWidth, notchTopY), + cubicTo( + -notchCenter + notchHalfWidth, + notchTopY - 6, + -notchCenter - notchHalfWidth, + notchTopY - 6, + -notchCenter - notchHalfWidth, + notchTopY + ), + line(-notchCenter - notchHalfWidth, lobeStartY), + cubicTo( + -notchCenter - notchHalfWidth, + bottomY * 0.82, + -halfWidth * 0.49, + bottomY, + -halfWidth * 0.66, + bottomY + ), + cubicTo(-halfWidth * 0.82, bottomY, -halfWidth, bottomY * 0.82, -halfWidth, lobeStartY), + line(-halfWidth, sideStartY), + 'Z', + ].join('') +} + const headPath = (pose: AvatarPose, surface: SurfaceConfig) => { if (surface.type === 'sphere' || surface.type === 'mickey') { const exactPath = projectedEllipsoidPath(pose, surface) @@ -1133,6 +1214,7 @@ const headPath = (pose: AvatarPose, surface: SurfaceConfig) => { if (surface.type === 'cone') return projectedConePath(pose, surface) if (surface.type === 'cube') return projectedCubePath(pose, surface) if (surface.type === 'diamond') return projectedDiamondPath(pose, surface) + if (surface.type === 'ghost') return projectedGhostPath(pose, surface) const key = surfaceCacheKey(surface) let localSamples = headSamplesCache.get(key) @@ -1222,7 +1304,10 @@ const accessoryLayers = (pose: AvatarPose, nodes: BodyNode[]) => { id: node.id, path: accessoryPath(pose, node), depth, - front: depth > accessoryCameraDepthRadius(pose, node) * ACCESSORY_FRONT_CROSSING_RATIO, + front: + node.layer === 'front' || + (node.layer !== 'back' && + depth > accessoryCameraDepthRadius(pose, node) * ACCESSORY_FRONT_CROSSING_RATIO), } }) .sort((left, right) => left.depth - right.depth) diff --git a/packages/avatar-core/src/surfaces.ts b/packages/avatar-core/src/surfaces.ts index 98295fd..62a7360 100644 --- a/packages/avatar-core/src/surfaces.ts +++ b/packages/avatar-core/src/surfaces.ts @@ -1,7 +1,7 @@ import type { Point3 } from './geometry' export type SurfaceType = - 'sphere' | 'mickey' | 'cursor' | 'cube' | 'capsule' | 'cylinder' | 'cone' | 'diamond' + 'sphere' | 'mickey' | 'cursor' | 'cube' | 'capsule' | 'cylinder' | 'cone' | 'diamond' | 'ghost' export type SurfaceConfig = { type: SurfaceType @@ -44,6 +44,7 @@ export const surfacePresets: Record = { baseRoundness: 0.45, }, diamond: { type: 'diamond', width: 235, height: 260, depth: 215, roundness: 0 }, + ghost: { type: 'ghost', width: 250, height: 245, depth: 190, roundness: 1 }, } export const surfaceLabels: Record = { @@ -55,6 +56,7 @@ export const surfaceLabels: Record = { cylinder: 'Cylindre', cone: 'Cône', diamond: 'Diamant', + ghost: 'Ghost', } const signedPower = (value: number, exponent: number) => @@ -336,6 +338,7 @@ export const surfacePointAt = ( case 'diamond': return diamond(config, longitude, latitude) case 'capsule': + case 'ghost': return capsule(config, longitude, latitude) case 'cone': { const progress = (latitude + Math.PI / 2) / Math.PI @@ -534,7 +537,8 @@ export const surfaceFrontSampleAt = ( case 'cube': return lpFrontSample(config, x, y, cubeExponent(config), cubeNormal) - case 'capsule': { + case 'capsule': + case 'ghost': { const capRadiusY = Math.min(radiusX, radiusY) const straightHalf = Math.max(0, radiusY - capRadiusY) const capCenterY = y < -straightHalf ? -straightHalf : y > straightHalf ? straightHalf : y diff --git a/src/features/avatar/avatars.ts b/src/features/avatar/avatars.ts index 5a9d491..f36e6cb 100644 --- a/src/features/avatar/avatars.ts +++ b/src/features/avatar/avatars.ts @@ -4,6 +4,7 @@ import { surfacePresets } from './surfaces' import type { Expression } from './geometry' import { isBodyMotion, isEyeMotion } from './ambientMotion' import { + createInitialSequences, normalizeSequencesForExpressions, parseSequences, type AvatarSequence, @@ -45,7 +46,25 @@ export type AvatarEyeDefaults = Pick< | 'leftAngle' | 'rightAngle' > -export const defaultAvatarColors: AvatarColors = { body: '#5b7fe5', eyes: '#111316' } +export const deslopAccentColors = { + red: '#ff5558', + orange: '#ff9d45', + yellow: '#ffda1a', + green: '#59e075', + mint: '#1adec9', + teal: '#1ad7e3', + cyan: '#50d7fe', + blue: '#1a9cff', + indigo: '#7c89ff', + purple: '#df48f3', + pink: '#ff4b6f', + brown: '#be9675', +} as const + +export const defaultAvatarColors: AvatarColors = { + body: deslopAccentColors.indigo, + eyes: deslopAccentColors.indigo, +} export const defaultAvatarRenderStyle: AvatarRenderStyle = { type: 'vector' } export const defaultPixelRenderStyle: PixelRenderStyle = { type: 'pixel', @@ -79,6 +98,336 @@ const parseColors = (value: unknown): AvatarColors => { } } +const bundledAvatarColorMigrations: Record = { + 'primitive-cube': { + from: { body: deslopAccentColors.orange, eyes: deslopAccentColors.orange }, + to: { body: deslopAccentColors.brown, eyes: deslopAccentColors.brown }, + }, + 'primitive-ghost': { + from: { body: deslopAccentColors.blue, eyes: deslopAccentColors.blue }, + to: { body: deslopAccentColors.orange, eyes: deslopAccentColors.orange }, + }, + strobi: { + from: { body: '#5b7fe5', eyes: '#111316' }, + to: { body: deslopAccentColors.indigo, eyes: deslopAccentColors.cyan }, + }, + 'avatar-4fe2d1bd-cf46-4e5e-a62d-d6b60be519ed': { + from: { body: '#e6855c', eyes: '#ffffff' }, + to: { body: deslopAccentColors.orange, eyes: deslopAccentColors.yellow }, + }, + 'avatar-295e74a7-5d70-4d61-83d4-7beebb22bdd8': { + from: { body: '#ffcf24', eyes: '#000000' }, + to: { body: deslopAccentColors.yellow, eyes: deslopAccentColors.orange }, + }, + 'avatar-1786600724626': { + from: { body: '#55b6c3', eyes: '#111316' }, + to: { body: deslopAccentColors.teal, eyes: deslopAccentColors.mint }, + }, + 'avatar-7874f78a-93ec-4536-a3b6-bb53ed744efd': { + from: { body: '#000000', eyes: '#ffffff' }, + to: { body: deslopAccentColors.purple, eyes: deslopAccentColors.pink }, + }, + 'avatar-1b2ee9c6-a6c5-4054-87e7-fec24f285269': { + from: { body: '#e69a5c', eyes: '#111316' }, + to: { body: deslopAccentColors.brown, eyes: deslopAccentColors.orange }, + }, + 'avatar-b6362e59-81a3-4334-a399-a721b23cf553': { + from: { body: '#ffc2e9', eyes: '#3e4e65' }, + to: { body: deslopAccentColors.pink, eyes: deslopAccentColors.purple }, + }, + 'avatar-fafdaf4d-2071-41d6-9d42-7d34670956f0': { + from: { body: '#c9cbcf', eyes: '#111316' }, + to: { body: deslopAccentColors.cyan, eyes: deslopAccentColors.blue }, + }, + 'avatar-2739f2c2-a5b4-45d9-8915-c9d6101d4d3b': { + from: { body: '#e65c5c', eyes: '#111316' }, + to: { body: deslopAccentColors.red, eyes: deslopAccentColors.orange }, + }, + 'avatar-4b9ea0c1-286f-4aa1-b053-61fcc416ba7e': { + from: { body: '#dbe2f5', eyes: '#111316' }, + to: { body: deslopAccentColors.blue, eyes: deslopAccentColors.indigo }, + }, +} + +export const simpleBundledAvatarIds = [ + 'avatar-4b9ea0c1-286f-4aa1-b053-61fcc416ba7e', + 'avatar-2739f2c2-a5b4-45d9-8915-c9d6101d4d3b', + 'avatar-1786600724626', + 'avatar-295e74a7-5d70-4d61-83d4-7beebb22bdd8', + 'primitive-sphere', + 'primitive-cube', + 'primitive-capsule', + 'primitive-cylinder', + 'primitive-cone', + 'primitive-diamond', + 'primitive-ghost', + 'primitive-willy', +] as const + +const simpleBundledAvatarOrder = new Map(simpleBundledAvatarIds.map((id, index) => [id, index])) + +const retiredBundledAvatarIds = new Set([ + 'strobi', + 'avatar-4fe2d1bd-cf46-4e5e-a62d-d6b60be519ed', + 'avatar-7874f78a-93ec-4536-a3b6-bb53ed744efd', + 'avatar-1b2ee9c6-a6c5-4054-87e7-fec24f285269', + 'avatar-b6362e59-81a3-4334-a399-a721b23cf553', + 'avatar-fafdaf4d-2071-41d6-9d42-7d34670956f0', +]) + +const primitiveBundledAvatarSpecs = [ + ['primitive-sphere', 'Sphere', 'sphere', deslopAccentColors.indigo, 1], + ['primitive-cube', 'Cube', 'cube', deslopAccentColors.brown, 0.78], + ['primitive-capsule', 'Capsule', 'capsule', deslopAccentColors.green, 1], + ['primitive-cylinder', 'Cylinder', 'cylinder', deslopAccentColors.mint, 0.82], + ['primitive-cone', 'Cone', 'cone', deslopAccentColors.purple, 0.82], + ['primitive-diamond', 'Diamond', 'diamond', deslopAccentColors.pink, 1], + ['primitive-ghost', 'Memento', 'ghost', deslopAccentColors.orange, 1], +] as const + +const mementoBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehaviorLibrary => ({ + expressions: (behavior?.expressions ?? initialExpressions).map(expression => { + const fixedColorExpression = { ...expression } + delete fixedColorExpression.bodyColor + delete fixedColorExpression.eyeColor + return fixedColorExpression + }), + sequences: (behavior?.sequences ?? createInitialSequences()).map(sequence => ({ + ...sequence, + steps: sequence.steps.map(step => ({ ...step })), + blink: { ...sequence.blink }, + })), +}) + +const primitiveBundledAvatars: StudioAvatar[] = [ + ...primitiveBundledAvatarSpecs.map(([id, name, surface, color, scale]) => ({ + id, + name, + body: { + primary: { + ...surfacePresets[surface], + width: surfacePresets[surface].width * scale, + height: surfacePresets[surface].height * scale, + depth: surfacePresets[surface].depth * scale, + }, + nodes: [], + }, + colors: { body: color, eyes: color }, + eyes: + surface === 'ghost' + ? { + ...defaultAvatarEyes, + positionYLeft: defaultAvatarEyes.positionYLeft - 20, + positionYRight: defaultAvatarEyes.positionYRight - 20, + } + : { ...defaultAvatarEyes }, + renderStyle: { ...defaultAvatarRenderStyle }, + ...(surface === 'ghost' ? { behavior: mementoBehavior() } : {}), + })), + { + id: 'primitive-willy', + name: 'Willy', + body: { + primary: { + ...surfacePresets.capsule, + width: 122, + height: 190, + depth: 115, + }, + nodes: [ + { + id: 'willy-left-sphere', + name: 'Left cap', + surface: { + ...surfacePresets.cone, + width: 112, + height: 58, + depth: 100, + morphRoundness: 1.2, + tipRoundness: 2, + baseRoundness: 2, + }, + position: [-47, 95, -22], + rotation: [0, 0, 0], + }, + { + id: 'willy-right-sphere', + name: 'Right cap', + surface: { + ...surfacePresets.cone, + width: 112, + height: 58, + depth: 100, + morphRoundness: 1.2, + tipRoundness: 2, + baseRoundness: 2, + }, + position: [47, 95, -22], + rotation: [0, 0, 0], + }, + { + id: 'willy-cap', + name: 'Rounded cap', + layer: 'front', + surface: { + ...surfacePresets.cone, + width: 178, + height: 86, + depth: 156, + morphRoundness: 1.2, + tipRoundness: 2, + baseRoundness: 2, + }, + position: [0, -94, 0], + rotation: [0, 0, 0], + }, + ], + }, + colors: { body: deslopAccentColors.red, eyes: deslopAccentColors.red }, + eyes: { ...defaultAvatarEyes }, + renderStyle: { ...defaultAvatarRenderStyle }, + }, +] + +const originalSimpleBundledAvatarIds = new Set(simpleBundledAvatarIds.slice(0, 4)) + +const clonePrimitiveBundledAvatar = (avatar: StudioAvatar): StudioAvatar => ({ + ...avatar, + body: { + primary: { ...avatar.body.primary }, + nodes: avatar.body.nodes.map(node => ({ + ...node, + surface: { ...node.surface }, + position: [...node.position], + rotation: [...node.rotation], + })), + }, + colors: { ...avatar.colors }, + eyes: { ...avatar.eyes }, + renderStyle: { ...avatar.renderStyle }, + ...(avatar.behavior ? { behavior: mementoBehavior(avatar.behavior) } : {}), +}) + +export const ensurePrimitiveBundledAvatars = (library: AvatarLibrary): AvatarLibrary => { + if (!library.avatars.some(avatar => originalSimpleBundledAvatarIds.has(avatar.id))) return library + const resizedAvatars = library.avatars.map(avatar => { + const bundled = primitiveBundledAvatars.find(candidate => candidate.id === avatar.id) + if (!bundled) return avatar + const migratedNodes = avatar.body.nodes.map(node => { + const bundledNode = bundled.body.nodes.find(candidate => candidate.id === node.id) + if (!bundledNode || avatar.id !== 'primitive-willy') return node + const isLegacyLowerSphere = + (node.id === 'willy-left-sphere' || node.id === 'willy-right-sphere') && + node.surface.type === 'sphere' && + node.surface.width === 108 && + node.surface.height === 96 && + node.surface.depth === 100 + const isLegacyTopCap = + node.id === 'willy-cap' && + ((node.surface.width === 150 && + node.surface.height === 72 && + node.surface.depth === 135 && + node.position[2] === 12) || + (node.surface.width === 166 && node.surface.height === 80 && node.surface.depth === 148)) + if (!isLegacyLowerSphere && !isLegacyTopCap) return node + return { + ...bundledNode, + surface: { ...bundledNode.surface }, + position: [...bundledNode.position] as typeof bundledNode.position, + rotation: [...bundledNode.rotation] as typeof bundledNode.rotation, + } + }) + const existingNodeIds = new Set(migratedNodes.map(node => node.id)) + const missingBundledNodes = bundled.body.nodes.filter(node => !existingNodeIds.has(node.id)) + const avatarWithCurrentNodes = missingBundledNodes.length + ? { + ...avatar, + body: { + ...avatar.body, + nodes: [ + ...migratedNodes, + ...missingBundledNodes.map(node => ({ + ...node, + surface: { ...node.surface }, + position: [...node.position] as typeof node.position, + rotation: [...node.rotation] as typeof node.rotation, + })), + ], + }, + } + : migratedNodes.some((node, index) => node !== avatar.body.nodes[index]) + ? { ...avatar, body: { ...avatar.body, nodes: migratedNodes } } + : avatar + const avatarWithCurrentName = + avatarWithCurrentNodes.id === 'primitive-ghost' && avatarWithCurrentNodes.name === 'Ghost' + ? { ...avatarWithCurrentNodes, name: bundled.name } + : avatarWithCurrentNodes + const avatarWithCurrentEyes = + avatarWithCurrentName.id === 'primitive-ghost' && + [defaultAvatarEyes.positionYLeft, defaultAvatarEyes.positionYLeft - 12].includes( + avatarWithCurrentName.eyes.positionYLeft + ) && + [defaultAvatarEyes.positionYRight, defaultAvatarEyes.positionYRight - 12].includes( + avatarWithCurrentName.eyes.positionYRight + ) + ? { ...avatarWithCurrentName, eyes: { ...bundled.eyes } } + : avatarWithCurrentName + const avatarWithFixedColorBehavior = + avatarWithCurrentEyes.id === 'primitive-ghost' + ? { ...avatarWithCurrentEyes, behavior: mementoBehavior(avatarWithCurrentEyes.behavior) } + : avatarWithCurrentEyes + const original = surfacePresets[bundled.body.primary.type] + const primary = avatarWithFixedColorBehavior.body.primary + const stillUsesOriginalSize = + primary.type === original.type && + primary.width === original.width && + primary.height === original.height && + primary.depth === original.depth + const stillUsesPreviousWillyBody = + avatar.id === 'primitive-willy' && + primary.type === 'capsule' && + primary.width === 122 && + primary.height === 210 && + primary.depth === 115 + if (!stillUsesOriginalSize && !stillUsesPreviousWillyBody) return avatarWithFixedColorBehavior + return { + ...avatarWithFixedColorBehavior, + body: { ...avatarWithFixedColorBehavior.body, primary: { ...bundled.body.primary } }, + } + }) + const existingIds = new Set(resizedAvatars.map(avatar => avatar.id)) + const avatars = [ + ...resizedAvatars, + ...primitiveBundledAvatars + .filter(avatar => !existingIds.has(avatar.id)) + .map(clonePrimitiveBundledAvatar), + ].sort((left, right) => { + const leftOrder = simpleBundledAvatarOrder.get( + left.id as (typeof simpleBundledAvatarIds)[number] + ) + const rightOrder = simpleBundledAvatarOrder.get( + right.id as (typeof simpleBundledAvatarIds)[number] + ) + if (leftOrder === undefined && rightOrder === undefined) return 0 + if (leftOrder === undefined) return 1 + if (rightOrder === undefined) return -1 + return leftOrder - rightOrder + }) + return { ...library, avatars } +} + +export const migrateBundledAvatarColors = (id: string, colors: AvatarColors): AvatarColors => { + const migration = bundledAvatarColorMigrations[id] + if ( + !migration || + colors.body.toLowerCase() !== migration.from.body.toLowerCase() || + colors.eyes.toLowerCase() !== migration.from.eyes.toLowerCase() + ) { + return colors + } + return { ...migration.to } +} + const finiteBounded = (value: unknown, fallback: number, min: number, max: number) => typeof value === 'number' && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) @@ -263,26 +612,45 @@ export const parseAvatarLibrary = ( const parsed = value as Partial | null if (!parsed || !Array.isArray(parsed.avatars) || !parsed.avatars.length) return fallback const seenIds = new Set() - const avatars = parsed.avatars + const parsedAvatars = parsed.avatars .filter(avatar => { if (!avatar || typeof avatar.id !== 'string' || typeof avatar.name !== 'string') return false + if (retiredBundledAvatarIds.has(avatar.id)) return false if (seenIds.has(avatar.id)) return false seenIds.add(avatar.id) return true }) .map(avatar => { const behavior = parseAvatarBehavior(avatar.behavior, baseBehavior) + const migratedColors = migrateBundledAvatarColors(avatar.id, parseColors(avatar.colors)) + const colors = simpleBundledAvatarOrder.has( + avatar.id as (typeof simpleBundledAvatarIds)[number] + ) + ? { body: migratedColors.body, eyes: migratedColors.body } + : migratedColors return { id: avatar.id, name: avatar.name, body: parseAvatarBody(avatar.body, surfacePresets.sphere), - colors: parseColors(avatar.colors), + colors, eyes: parseAvatarEyeDefaults(avatar.eyes), renderStyle: parseAvatarRenderStyle(avatar.renderStyle), ...(behavior ? { behavior } : {}), } }) + const avatars = parsedAvatars.sort((left, right) => { + const leftOrder = simpleBundledAvatarOrder.get( + left.id as (typeof simpleBundledAvatarIds)[number] + ) + const rightOrder = simpleBundledAvatarOrder.get( + right.id as (typeof simpleBundledAvatarIds)[number] + ) + if (leftOrder === undefined && rightOrder === undefined) return 0 + if (leftOrder === undefined) return 1 + if (rightOrder === undefined) return -1 + return leftOrder - rightOrder + }) if (!avatars.length) return fallback const activeAvatarId = avatars.some(avatar => avatar.id === parsed.activeAvatarId) ? parsed.activeAvatarId! diff --git a/src/features/export/standaloneEngine.generated.ts b/src/features/export/standaloneEngine.generated.ts index 38e0a7b..217f5c6 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=>e*e*(3-2*e),n=e=>{let t=Math.sin(e*127.1+311.7)*43758.5453;return(t-Math.floor(t))*2-1},r=e=>e.headX*.71+e.headY*1.13+e.headZ*1.37,i=17.29,a=(e,r,i,a)=>{let o=e/a,s=Math.floor(o),c=t(o-s),l=n(s*3+r+i);return l+(n((s+1)*3+r+i)-l)*c},o=(e,r,i)=>{let a=1100;if(e<=0)return 0;let o=Math.floor(e/a),s=(e-o*a)/140,c=t(Math.min(s,1)),l=o===0?0:n((o-1)*2+r+i);return l+(n(o*2+r+i)-l)*c},s=e=>e.eyeMotion!==`none`||e.bodyMotion!==`none`,c=(e,t,n=1)=>{let i=r(e);if(e.bodyMotion===`slowDrift`)return{x:a(t,3,i,2900)*1.45*n,y:a(t,4,i,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}},l=(e,t,n=1)=>{if(e.eyeMotion===`microSaccades`)return{x:o(t,0,i)*1.5*n,y:o(t,1,i)*.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}},u=(e,t,n=1)=>{let i={...e},o=r(e);if(e.bodyMotion===`slowDrift`)i.headX+=a(t,0,o,2600)*.8*n,i.headY+=a(t,1,o,3300)*1.15*n,i.headZ+=a(t,2,o,4100)*.45*n;else if(e.bodyMotion===`shake`){let e=t/1e3;i.headX+=(Math.sin(e*31)+Math.sin(e*53)*.45)*1.15*n,i.headY+=(Math.sin(e*37)+Math.sin(e*61)*.4)*1.35*n,i.headZ+=Math.sin(e*43)*.7*n}return i},d=(e,t,n=1)=>{let r=u(e,t,n),i=l(e,t,n);return r.positionXLeft+=i.x,r.positionXRight+=i.x,r.positionYLeft+=i.y,r.positionYRight+=i.y,r},f=(e,t)=>Math.sign(e)*Math.abs(e)**t,p=(e,t,n,r,i,a,o)=>{let s=f(Math.cos(t),a);return[n/2*s*f(Math.sin(e),o),r/2*f(Math.sin(t),a),i/2*s*f(Math.cos(e),o)]},m=(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)),g=e=>1+h(e.roundness)/2,_=.04,v=e=>e.roundness<=0?1/0:2/(_+h(e.roundness)/2*.96),y=(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)]},b=(e,t,n)=>y(e,t,n,g(e)),x=(e,t,n)=>y(e,t,n,v(e)),ee=.24,te=.2,ne=.22,re=e=>h(e.morphRoundness)/2,ie=(e,t,n)=>{let r=re(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}},S=(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},ae=e=>({tipFraction:(e.tipRoundness??0)*ee,baseFraction:(e.baseRoundness??0)*te}),C=(e,t)=>{let n=Math.max(0,Math.min(1,t)),r=e.roundness*ne;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)}},oe=(e,t)=>ie(e,t,C(e,t)),w=(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}=ae(e);if(i>0&&n0&&n>1-r){let e=(n-(1-r))/r;return{radiusScale:S(r,r/2,r/4,0,e),verticalProgress:S(1-r,1-r/2,1,1,e)}}return{radiusScale:1-n,verticalProgress:n}},T=(e,t)=>ie(e,t,se(e,t)),E=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}},D=(e,t,n)=>{let{width:r,height:i,depth:a}=e;switch(e.type){case`sphere`:case`mickey`:return p(t,n,r,i,a,1,1);case`cube`:return x(e,t,n);case`cylinder`:{let o=oe(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),-i/2+i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}case`cursor`:{let r=E(e),i=(n+Math.PI/2)/Math.PI,a=C({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},i);return[r.bodyWidth/2*a.radiusScale*Math.sin(t),r.bodyCenterY-r.bodyHeight/2+r.bodyHeight*a.verticalProgress,r.bodyDepth/2*a.radiusScale*Math.cos(t)]}case`diamond`:return b(e,t,n);case`capsule`:return m(e,t,n);case`cone`:{let o=T(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),i/2-i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}}},O=(e,t)=>[e[0]-t[0],e[1]-t[1],e[2]-t[2]],k=([e,t,n])=>{let r=Math.hypot(e,t,n)||1;return[e/r,t/r,n/r]},ce=(e,t,n)=>{let r=e.type===`cone`?-1:1;return k([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])])},le=(e,t,n)=>{let r=5e-4;if(e.type===`cone`&&n>=Math.PI/2-r)return[0,-1,0];let i=D(e,t-r,n),a=D(e,t+r,n),o=D(e,t,Math.max(-Math.PI/2,n-r)),s=D(e,t,Math.min(Math.PI/2,n+r));return ce(e,O(a,i),O(s,o))},A=(e,t)=>Math.sign(e)*Math.abs(e)**t,ue=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;return k([A(t[0]/r,n-1)/r,A(t[1]/i,n-1)/i,A(t[2]/a,n-1)/a])},de=(e,t)=>ue(e,t,g(e)),fe=(e,t)=>{let n=v(e);if(Number.isFinite(n))return ue(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]},pe=(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)}},me=(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:k([e/(n*n||1),o/(r*r||1),c/(i*i||1)])}},j=(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=w(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),_=w(e,h,r),v=(w(e,g,r)-_)/(g-h||1),y=Math.max(Math.sqrt(f),1e-4),b=-(o/a)*d/(l*y||1),x=i*o*v/(e.height*y||1);return{point:[d,n,p],normal:k([-b,-x,1])}},he=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;switch(e.type){case`sphere`:case`mickey`:return me(t,n,r,i,a);case`cube`:return pe(e,t,n,v(e),fe);case`capsule`:{let e=Math.min(r,i),o=Math.max(0,i-e);return me(t,n,r,e,a,n<-o?-o:n>o?o:n)}case`cylinder`:return j(e,t,n,oe,1);case`cursor`:{let r=E(e),i=j({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},t,n-r.bodyCenterY,C,1);return{point:[i.point[0],i.point[1]+r.bodyCenterY,i.point[2]],normal:i.normal}}case`cone`:return j(e,t,n,T,-1);case`diamond`:return pe(e,t,n,g(e),de)}},ge=(e,t,n)=>{let r=D(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:k([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:k([Math.sin(t)/(e.width/2||1),0,Math.cos(t)/(e.depth/2||1)])}:e.type===`diamond`?{point:r,normal:de(e,r)}:e.type===`cube`?{point:r,normal:fe(e,r)}:{point:r,normal:le(e,t,n)}},M=620,_e=14,ve=[`headX`,`headY`,`headZ`,`widthLeft`,`widthRight`,`heightLeft`,`heightRight`,`spacing`,`positionXLeft`,`positionXRight`,`positionYLeft`,`positionYRight`,`leftAngle`,`rightAngle`,`perspective`],N=e=>e*Math.PI/180,P=([e,t,n,r])=>{let i=Math.hypot(e,t,n,r)||1;return[e/i,t/i,n/i,r/i]},F=([e,t,n,r],[i,a,o,s])=>P([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]),I=([e,t,n],r)=>{let i=r/2,a=Math.sin(i);return P([Math.cos(i),e*a,t*a,n*a])},L=(e,t,n)=>{let r=I([1,0,0],e),i=I([0,1,0],t);return F(F(I([0,0,1],n),r),i)},R=([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)]},ye=(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<_e;r+=1){let o=n+r/_e*(Math.PI/2);a.push([e+Math.cos(o)*i,t+Math.sin(o)*i])}};return o([-n+i,-r],[n-i,-r]),s(n-i,-r+i,-Math.PI/2),o([n,-r+i],[n,r-i]),s(n-i,r-i,0),o([n-i,r],[-n+i,r]),s(-n+i,r-i,Math.PI/2),o([-n,r-i],[-n,-r+i]),s(-n+i,-r+i,Math.PI),a},z=(e,t)=>{let n=M-e[2]*t,r=Math.abs(n)<1e-4?M/1e-4:M/n;return[e[0]*r,e[1]*r,e[2]]},B=(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`:``}`:``,be=e=>({expression:e,orientation:L(N(e.headX),N(e.headY),N(e.headZ))}),xe=24,Se=25,Ce=73,V=144,we=33,Te=73,H=new Map,Ee=new Map,De=new Map,U=e=>JSON.stringify([e.type,e.width,e.height,e.depth,e.roundness,e.morphRoundness,e.tipRoundness,e.baseRoundness]),W=(e,t,n)=>(e.size>=xe&&e.delete(e.keys().next().value),e.set(t,n),n),Oe=(e,t,n)=>ge(e,t,n),ke=(e,t)=>({point:z(R(e.orientation,t.point),e.expression.perspective),normal:R(e.orientation,t.normal)}),Ae=(e,t)=>{let n=e/120,r=t/120;return[120*Math.cos(r)*Math.sin(n),120*Math.sin(r)]},je=(e,t,n,r)=>{let[i,a]=Ae(n,r);return ke(e,he(t,i,a))},Me=(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=N(n<0?a.leftAngle:a.rightAngle);return ye(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 je(e,t,l+i,u+a)})},Ne=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=>B(e,!1)).join(``)},Pe=(e,t)=>{let n=U(t),r=De.get(n);if(!r){let e=[-60,-30,0,30,60].map(e=>Array.from({length:73},(n,r)=>Oe(t,N(-180+r*5),N(e)))),i=Array.from({length:12},(e,t)=>-150+t*30).map(e=>Array.from({length:37},(n,r)=>Oe(t,N(e),N(-90+r*5))));r=W(De,n,[...e,...i])}return r.map(t=>Ne(t.map(t=>ke(e,t))))},G=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)]},K=e=>{if(e.length<3)return B(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`},q=(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]})}),Fe=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(``):``,J=(e,t)=>z(R(e.orientation,t),e.expression.perspective),Y=(e,t,n)=>Array.from({length:145},(r,i)=>{let a=i/V*Math.PI*2;return[e/2*Math.sin(a),n,t/2*Math.cos(a)]}),X=(e,t)=>{let n=U(t),r=H.get(n);return r||(r=Array.from({length:we},(e,n)=>{let r=-Math.PI/2+n/32*Math.PI;return Array.from({length:Te},(e,n)=>D(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),W(H,n,r)),K(q(G(r.map(t=>J(e,t)))))},Ie=(e,t)=>{if(t.roundness>0||(t.morphRoundness??0)>0)return X(e,t);let n=t.height/2;return K(q(G([...Y(t.width,t.depth,-n),...Y(t.width,t.depth,n)].map(t=>J(e,t)))))},Le=(e,t)=>{let n=E(t),r=n.bodyHeight/2;return K(q(G([...Y(n.bodyWidth,n.bodyDepth,n.bodyCenterY-r),...Y(n.bodyWidth,n.bodyDepth,n.bodyCenterY+r)].map(t=>J(e,t)))))},Re=(e,t)=>{let n=E(t),r=J(e,[0,n.coneApexY,0]);return K(q(G([...Y(t.width,t.depth,n.coneBaseY).map(t=>J(e,t)),r])))},ze=(e,t)=>{if((t.morphRoundness??0)>0||(t.tipRoundness??0)>0||(t.baseRoundness??0)>0)return X(e,t);let n=J(e,[0,-t.height/2,0]),r=G([...Y(t.width,t.depth,t.height/2).map(t=>J(e,t)),n]),i=r.findIndex(e=>Math.hypot(e[0]-n[0],e[1]-n[1])<.01);if(i<0)return K(r);let a=[...r.slice(i),...r.slice(0,i)].slice(1);return a.length<2?B(r):`M${n[0].toFixed(2)} ${n[1].toFixed(2)}L${a[0][0].toFixed(2)} ${a[0][1].toFixed(2)}${Fe(a)}L${n[0].toFixed(2)} ${n[1].toFixed(2)}Z`},Be=(e,t)=>{if(t.roundness>0)return X(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return B(G([-1,1].flatMap(e=>[-1,1].flatMap(t=>[-1,1].map(a=>[e*n,t*r,a*i]))).map(t=>J(e,t))))},Ve=(e,t)=>{if(t.roundness>0)return X(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return B(G([[-n,0,0],[n,0,0],[0,-r,0],[0,r,0],[0,0,-i],[0,0,i]].map(t=>J(e,t))))},He=(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}},Z=({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`},Q=(e,t,n=[0,0,0])=>{let r=[R(e.orientation,[1,0,0]),R(e.orientation,[0,1,0]),R(e.orientation,[0,0,1])],i=R(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 He(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=M/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:He(h,g,b/x,-y/x,v/x)},Ue=(e,t)=>{let n=Q(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 Z({centerX:0,centerY:0,majorRadius:e,minorRadius:e,rotation:0})}return n?Z(n):null},We=(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=>Q(e,s,[t*i,a,o])).filter(e=>e!==null).map(Z)},Ge=(e,t)=>t.type===`mickey`?We(e,t):t.type===`cursor`?[Re(e,t)]:[],$=e=>Array.from({length:V},(t,n)=>{let r=n/V*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]}),Ke=e=>{if(e.length<3)return B(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`},qe=(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=Q(e,o,[0,a,0]),c=Q(e,o,[0,-a,0]);return!s||!c?null:Ke(G([...$(s),...$(c)]))},Je=(e,t)=>{if(t.type===`sphere`||t.type===`mickey`){let n=Ue(e,t);if(n)return n}if(t.type===`capsule`){let n=qe(e,t);if(n)return n}if(t.type===`cylinder`)return Ie(e,t);if(t.type===`cursor`)return Le(e,t);if(t.type===`cone`)return ze(e,t);if(t.type===`cube`)return Be(e,t);if(t.type===`diamond`)return Ve(e,t);let n=U(t),r=H.get(n);return r||(r=Array.from({length:Se},(e,n)=>{let r=-Math.PI/2+n/24*Math.PI;return Array.from({length:Ce},(e,n)=>D(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),W(H,n,r)),B(G(r.map(t=>z(R(e.orientation,t),e.expression.perspective))))},Ye=(e,t)=>{let n=U(t.surface),r=Ee.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 D(t.surface,i,r)})}).flat(),W(Ee,n,r));let i=L(N(t.rotation[0]),N(t.rotation[1]),N(t.rotation[2])),a=G(r.map(n=>{let r=R(i,n),a=[r[0]+t.position[0],r[1]+t.position[1],r[2]+t.position[2]];return z(R(e.orientation,a),e.expression.perspective)}));return(t.surface.type===`cube`||t.surface.type===`diamond`)&&t.surface.roundness<=0?B(a):K(q(a))},Xe=.1,Ze=(e,t)=>{let n=L(N(t.rotation[0]),N(t.rotation[1]),N(t.rotation[2])),r=[[1,0,0],[0,1,0],[0,0,1]].map(t=>R(e.orientation,R(n,t))[2]);return Math.hypot(r[0]*(t.surface.width/2),r[1]*(t.surface.height/2),r[2]*(t.surface.depth/2))},Qe=(e,t)=>{let n=t.map(t=>{let n=R(e.orientation,t.position)[2];return{id:t.id,path:Ye(e,t),depth:n,front:n>Ze(e,t)*Xe}}).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)}};return e.ambientBodyOffset=c,e.ambientEyeOffset=l,e.applyAmbientBodyMotion=u,e.applyAmbientMotion=d,e.expressionFields=ve,e.hasAmbientMotion=s,e.poseFromExpression=be,e.renderAvatar=(e,t,n=1,r={})=>{let i=Me(e,t,-1,n,r.eyeOffset),a=Me(e,t,1,n,r.eyeOffset),o=i.map(e=>e.point),s=a.map(e=>e.point),c=Qe(e,r.bodyNodes??[]),l=Ge(e,t);return{backPaths:[...l,...c.backPaths],frontPaths:c.frontPaths,backNodeIds:[...l.map(()=>null),...c.backNodeIds],frontNodeIds:c.frontNodeIds,headPath:Je(e,t),leftPath:B(o),rightPath:B(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?[]:Pe(e,t)}},e})({});" +export const standaloneEngineSource = "var AvatarProceduralEngine=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=e=>e*e*(3-2*e),n=e=>{let t=Math.sin(e*127.1+311.7)*43758.5453;return(t-Math.floor(t))*2-1},r=e=>e.headX*.71+e.headY*1.13+e.headZ*1.37,i=17.29,a=(e,r,i,a)=>{let o=e/a,s=Math.floor(o),c=t(o-s),l=n(s*3+r+i);return l+(n((s+1)*3+r+i)-l)*c},o=(e,r,i)=>{let a=1100;if(e<=0)return 0;let o=Math.floor(e/a),s=(e-o*a)/140,c=t(Math.min(s,1)),l=o===0?0:n((o-1)*2+r+i);return l+(n(o*2+r+i)-l)*c},s=e=>e.eyeMotion!==`none`||e.bodyMotion!==`none`,c=(e,t,n=1)=>{let i=r(e);if(e.bodyMotion===`slowDrift`)return{x:a(t,3,i,2900)*1.45*n,y:a(t,4,i,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}},l=(e,t,n=1)=>{if(e.eyeMotion===`microSaccades`)return{x:o(t,0,i)*1.5*n,y:o(t,1,i)*.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}},u=(e,t,n=1)=>{let i={...e},o=r(e);if(e.bodyMotion===`slowDrift`)i.headX+=a(t,0,o,2600)*.8*n,i.headY+=a(t,1,o,3300)*1.15*n,i.headZ+=a(t,2,o,4100)*.45*n;else if(e.bodyMotion===`shake`){let e=t/1e3;i.headX+=(Math.sin(e*31)+Math.sin(e*53)*.45)*1.15*n,i.headY+=(Math.sin(e*37)+Math.sin(e*61)*.4)*1.35*n,i.headZ+=Math.sin(e*43)*.7*n}return i},d=(e,t,n=1)=>{let r=u(e,t,n),i=l(e,t,n);return r.positionXLeft+=i.x,r.positionXRight+=i.x,r.positionYLeft+=i.y,r.positionYRight+=i.y,r},f=(e,t)=>Math.sign(e)*Math.abs(e)**t,p=(e,t,n,r,i,a,o)=>{let s=f(Math.cos(t),a);return[n/2*s*f(Math.sin(e),o),r/2*f(Math.sin(t),a),i/2*s*f(Math.cos(e),o)]},m=(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)),g=e=>1+h(e.roundness)/2,_=.04,v=e=>e.roundness<=0?1/0:2/(_+h(e.roundness)/2*.96),y=(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)]},b=(e,t,n)=>y(e,t,n,g(e)),x=(e,t,n)=>y(e,t,n,v(e)),ee=.24,te=.2,ne=.22,re=e=>h(e.morphRoundness)/2,ie=(e,t,n)=>{let r=re(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}},S=(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},ae=e=>({tipFraction:(e.tipRoundness??0)*ee,baseFraction:(e.baseRoundness??0)*te}),C=(e,t)=>{let n=Math.max(0,Math.min(1,t)),r=e.roundness*ne;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)}},oe=(e,t)=>ie(e,t,C(e,t)),w=(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}=ae(e);if(i>0&&n0&&n>1-r){let e=(n-(1-r))/r;return{radiusScale:S(r,r/2,r/4,0,e),verticalProgress:S(1-r,1-r/2,1,1,e)}}return{radiusScale:1-n,verticalProgress:n}},T=(e,t)=>ie(e,t,se(e,t)),E=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}},D=(e,t,n)=>{let{width:r,height:i,depth:a}=e;switch(e.type){case`sphere`:case`mickey`:return p(t,n,r,i,a,1,1);case`cube`:return x(e,t,n);case`cylinder`:{let o=oe(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),-i/2+i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}case`cursor`:{let r=E(e),i=(n+Math.PI/2)/Math.PI,a=C({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},i);return[r.bodyWidth/2*a.radiusScale*Math.sin(t),r.bodyCenterY-r.bodyHeight/2+r.bodyHeight*a.verticalProgress,r.bodyDepth/2*a.radiusScale*Math.cos(t)]}case`diamond`:return b(e,t,n);case`capsule`:case`ghost`:return m(e,t,n);case`cone`:{let o=T(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),i/2-i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}}},O=(e,t)=>[e[0]-t[0],e[1]-t[1],e[2]-t[2]],k=([e,t,n])=>{let r=Math.hypot(e,t,n)||1;return[e/r,t/r,n/r]},ce=(e,t,n)=>{let r=e.type===`cone`?-1:1;return k([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])])},le=(e,t,n)=>{let r=5e-4;if(e.type===`cone`&&n>=Math.PI/2-r)return[0,-1,0];let i=D(e,t-r,n),a=D(e,t+r,n),o=D(e,t,Math.max(-Math.PI/2,n-r)),s=D(e,t,Math.min(Math.PI/2,n+r));return ce(e,O(a,i),O(s,o))},A=(e,t)=>Math.sign(e)*Math.abs(e)**t,ue=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;return k([A(t[0]/r,n-1)/r,A(t[1]/i,n-1)/i,A(t[2]/a,n-1)/a])},de=(e,t)=>ue(e,t,g(e)),fe=(e,t)=>{let n=v(e);if(Number.isFinite(n))return ue(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]},pe=(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)}},me=(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:k([e/(n*n||1),o/(r*r||1),c/(i*i||1)])}},j=(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=w(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),_=w(e,h,r),v=(w(e,g,r)-_)/(g-h||1),y=Math.max(Math.sqrt(f),1e-4),b=-(o/a)*d/(l*y||1),x=i*o*v/(e.height*y||1);return{point:[d,n,p],normal:k([-b,-x,1])}},he=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;switch(e.type){case`sphere`:case`mickey`:return me(t,n,r,i,a);case`cube`:return pe(e,t,n,v(e),fe);case`capsule`:case`ghost`:{let e=Math.min(r,i),o=Math.max(0,i-e);return me(t,n,r,e,a,n<-o?-o:n>o?o:n)}case`cylinder`:return j(e,t,n,oe,1);case`cursor`:{let r=E(e),i=j({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},t,n-r.bodyCenterY,C,1);return{point:[i.point[0],i.point[1]+r.bodyCenterY,i.point[2]],normal:i.normal}}case`cone`:return j(e,t,n,T,-1);case`diamond`:return pe(e,t,n,g(e),de)}},ge=(e,t,n)=>{let r=D(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:k([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:k([Math.sin(t)/(e.width/2||1),0,Math.cos(t)/(e.depth/2||1)])}:e.type===`diamond`?{point:r,normal:de(e,r)}:e.type===`cube`?{point:r,normal:fe(e,r)}:{point:r,normal:le(e,t,n)}},M=620,_e=14,ve=[`headX`,`headY`,`headZ`,`widthLeft`,`widthRight`,`heightLeft`,`heightRight`,`spacing`,`positionXLeft`,`positionXRight`,`positionYLeft`,`positionYRight`,`leftAngle`,`rightAngle`,`perspective`],N=e=>e*Math.PI/180,P=([e,t,n,r])=>{let i=Math.hypot(e,t,n,r)||1;return[e/i,t/i,n/i,r/i]},F=([e,t,n,r],[i,a,o,s])=>P([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]),I=([e,t,n],r)=>{let i=r/2,a=Math.sin(i);return P([Math.cos(i),e*a,t*a,n*a])},L=(e,t,n)=>{let r=I([1,0,0],e),i=I([0,1,0],t);return F(F(I([0,0,1],n),r),i)},R=([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)]},ye=(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<_e;r+=1){let o=n+r/_e*(Math.PI/2);a.push([e+Math.cos(o)*i,t+Math.sin(o)*i])}};return o([-n+i,-r],[n-i,-r]),s(n-i,-r+i,-Math.PI/2),o([n,-r+i],[n,r-i]),s(n-i,r-i,0),o([n-i,r],[-n+i,r]),s(-n+i,r-i,Math.PI/2),o([-n,r-i],[-n,-r+i]),s(-n+i,-r+i,Math.PI),a},z=(e,t)=>{let n=M-e[2]*t,r=Math.abs(n)<1e-4?M/1e-4:M/n;return[e[0]*r,e[1]*r,e[2]]},B=(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`:``}`:``,be=e=>({expression:e,orientation:L(N(e.headX),N(e.headY),N(e.headZ))}),xe=24,Se=25,Ce=73,V=144,we=33,Te=73,H=new Map,Ee=new Map,De=new Map,U=e=>JSON.stringify([e.type,e.width,e.height,e.depth,e.roundness,e.morphRoundness,e.tipRoundness,e.baseRoundness]),W=(e,t,n)=>(e.size>=xe&&e.delete(e.keys().next().value),e.set(t,n),n),Oe=(e,t,n)=>ge(e,t,n),ke=(e,t)=>({point:z(R(e.orientation,t.point),e.expression.perspective),normal:R(e.orientation,t.normal)}),Ae=(e,t)=>{let n=e/120,r=t/120;return[120*Math.cos(r)*Math.sin(n),120*Math.sin(r)]},je=(e,t,n,r)=>{let[i,a]=Ae(n,r);return ke(e,he(t,i,a))},Me=(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=N(n<0?a.leftAngle:a.rightAngle);return ye(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 je(e,t,l+i,u+a)})},Ne=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=>B(e,!1)).join(``)},Pe=(e,t)=>{let n=U(t),r=De.get(n);if(!r){let e=[-60,-30,0,30,60].map(e=>Array.from({length:73},(n,r)=>Oe(t,N(-180+r*5),N(e)))),i=Array.from({length:12},(e,t)=>-150+t*30).map(e=>Array.from({length:37},(n,r)=>Oe(t,N(e),N(-90+r*5))));r=W(De,n,[...e,...i])}return r.map(t=>Ne(t.map(t=>ke(e,t))))},G=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)]},K=e=>{if(e.length<3)return B(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`},q=(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]})}),Fe=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(``):``,J=(e,t)=>z(R(e.orientation,t),e.expression.perspective),Y=(e,t,n)=>Array.from({length:145},(r,i)=>{let a=i/V*Math.PI*2;return[e/2*Math.sin(a),n,t/2*Math.cos(a)]}),X=(e,t)=>{let n=U(t),r=H.get(n);return r||(r=Array.from({length:we},(e,n)=>{let r=-Math.PI/2+n/32*Math.PI;return Array.from({length:Te},(e,n)=>D(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),W(H,n,r)),K(q(G(r.map(t=>J(e,t)))))},Ie=(e,t)=>{if(t.roundness>0||(t.morphRoundness??0)>0)return X(e,t);let n=t.height/2;return K(q(G([...Y(t.width,t.depth,-n),...Y(t.width,t.depth,n)].map(t=>J(e,t)))))},Le=(e,t)=>{let n=E(t),r=n.bodyHeight/2;return K(q(G([...Y(n.bodyWidth,n.bodyDepth,n.bodyCenterY-r),...Y(n.bodyWidth,n.bodyDepth,n.bodyCenterY+r)].map(t=>J(e,t)))))},Re=(e,t)=>{let n=E(t),r=J(e,[0,n.coneApexY,0]);return K(q(G([...Y(t.width,t.depth,n.coneBaseY).map(t=>J(e,t)),r])))},ze=(e,t)=>{if((t.morphRoundness??0)>0||(t.tipRoundness??0)>0||(t.baseRoundness??0)>0)return X(e,t);let n=J(e,[0,-t.height/2,0]),r=G([...Y(t.width,t.depth,t.height/2).map(t=>J(e,t)),n]),i=r.findIndex(e=>Math.hypot(e[0]-n[0],e[1]-n[1])<.01);if(i<0)return K(r);let a=[...r.slice(i),...r.slice(0,i)].slice(1);return a.length<2?B(r):`M${n[0].toFixed(2)} ${n[1].toFixed(2)}L${a[0][0].toFixed(2)} ${a[0][1].toFixed(2)}${Fe(a)}L${n[0].toFixed(2)} ${n[1].toFixed(2)}Z`},Be=(e,t)=>{if(t.roundness>0)return X(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return B(G([-1,1].flatMap(e=>[-1,1].flatMap(t=>[-1,1].map(a=>[e*n,t*r,a*i]))).map(t=>J(e,t))))},Ve=(e,t)=>{if(t.roundness>0)return X(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return B(G([[-n,0,0],[n,0,0],[0,-r,0],[0,r,0],[0,0,-i],[0,0,i]].map(t=>J(e,t))))},He=(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}},Z=({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`},Q=(e,t,n=[0,0,0])=>{let r=[R(e.orientation,[1,0,0]),R(e.orientation,[0,1,0]),R(e.orientation,[0,0,1])],i=R(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 He(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=M/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:He(h,g,b/x,-y/x,v/x)},Ue=(e,t)=>{let n=Q(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 Z({centerX:0,centerY:0,majorRadius:e,minorRadius:e,rotation:0})}return n?Z(n):null},We=(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=>Q(e,s,[t*i,a,o])).filter(e=>e!==null).map(Z)},Ge=(e,t)=>t.type===`mickey`?We(e,t):t.type===`cursor`?[Re(e,t)]:[],$=e=>Array.from({length:V},(t,n)=>{let r=n/V*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]}),Ke=e=>{if(e.length<3)return B(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`},qe=(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=Q(e,o,[0,a,0]),c=Q(e,o,[0,-a,0]);return!s||!c?null:Ke(G([...$(s),...$(c)]))},Je=(e,t)=>{let n=t.width/2,r=t.height/2,i=t.width/6,a=t.width*.018,o=-r*.08,s=r*.5,c=r*.18,l=r,u=(t,n)=>J(e,[t,n,0]),d=([e,t])=>`${e.toFixed(2)} ${t.toFixed(2)}`,f=(e,t)=>`M${d(u(e,t))}`,p=(e,t)=>`L${d(u(e,t))}`,m=(e,t,n,r,i,a)=>`C${d(u(e,t))} ${d(u(n,r))} ${d(u(i,a))}`;return[f(-n,o),m(-n,-r*.62,-n*.56,-r,0,-r),m(n*.56,-r,n,-r*.62,n,o),p(n,s),m(n,l*.82,n*.82,l,n*.66,l),m(n*.49,l,i+a,l*.82,i+a,s),p(i+a,c),m(i+a,c-6,i-a,c-6,i-a,c),p(i-a,s),m(i-a,l*.82,n*.17,l,0,l),m(-n*.17,l,-i+a,l*.82,-i+a,s),p(-i+a,c),m(-i+a,c-6,-i-a,c-6,-i-a,c),p(-i-a,s),m(-i-a,l*.82,-n*.49,l,-n*.66,l),m(-n*.82,l,-n,l*.82,-n,s),p(-n,o),`Z`].join(``)},Ye=(e,t)=>{if(t.type===`sphere`||t.type===`mickey`){let n=Ue(e,t);if(n)return n}if(t.type===`capsule`){let n=qe(e,t);if(n)return n}if(t.type===`cylinder`)return Ie(e,t);if(t.type===`cursor`)return Le(e,t);if(t.type===`cone`)return ze(e,t);if(t.type===`cube`)return Be(e,t);if(t.type===`diamond`)return Ve(e,t);if(t.type===`ghost`)return Je(e,t);let n=U(t),r=H.get(n);return r||(r=Array.from({length:Se},(e,n)=>{let r=-Math.PI/2+n/24*Math.PI;return Array.from({length:Ce},(e,n)=>D(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),W(H,n,r)),B(G(r.map(t=>z(R(e.orientation,t),e.expression.perspective))))},Xe=(e,t)=>{let n=U(t.surface),r=Ee.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 D(t.surface,i,r)})}).flat(),W(Ee,n,r));let i=L(N(t.rotation[0]),N(t.rotation[1]),N(t.rotation[2])),a=G(r.map(n=>{let r=R(i,n),a=[r[0]+t.position[0],r[1]+t.position[1],r[2]+t.position[2]];return z(R(e.orientation,a),e.expression.perspective)}));return(t.surface.type===`cube`||t.surface.type===`diamond`)&&t.surface.roundness<=0?B(a):K(q(a))},Ze=.1,Qe=(e,t)=>{let n=L(N(t.rotation[0]),N(t.rotation[1]),N(t.rotation[2])),r=[[1,0,0],[0,1,0],[0,0,1]].map(t=>R(e.orientation,R(n,t))[2]);return Math.hypot(r[0]*(t.surface.width/2),r[1]*(t.surface.height/2),r[2]*(t.surface.depth/2))},$e=(e,t)=>{let n=t.map(t=>{let n=R(e.orientation,t.position)[2];return{id:t.id,path:Xe(e,t),depth:n,front:t.layer===`front`||t.layer!==`back`&&n>Qe(e,t)*Ze}}).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)}};return e.ambientBodyOffset=c,e.ambientEyeOffset=l,e.applyAmbientBodyMotion=u,e.applyAmbientMotion=d,e.expressionFields=ve,e.hasAmbientMotion=s,e.poseFromExpression=be,e.renderAvatar=(e,t,n=1,r={})=>{let i=Me(e,t,-1,n,r.eyeOffset),a=Me(e,t,1,n,r.eyeOffset),o=i.map(e=>e.point),s=a.map(e=>e.point),c=$e(e,r.bodyNodes??[]),l=Ge(e,t);return{backPaths:[...l,...c.backPaths],frontPaths:c.frontPaths,backNodeIds:[...l.map(()=>null),...c.backNodeIds],frontNodeIds:c.frontNodeIds,headPath:Ye(e,t),leftPath:B(o),rightPath:B(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?[]:Pe(e,t)}},e})({});" diff --git a/src/features/studio/__tests__/studio-document-test.ts b/src/features/studio/__tests__/studio-document-test.ts index 9dcbf94..74ad750 100644 --- a/src/features/studio/__tests__/studio-document-test.ts +++ b/src/features/studio/__tests__/studio-document-test.ts @@ -1,4 +1,4 @@ -import { createAvatar } from '@/features/avatar/avatars' +import { createAvatar, defaultAvatarEyes } from '@/features/avatar/avatars' import { createInitialSequences } from '@/features/animation/sequences' import { initialExpressions } from '@/features/avatar/presets' import { createAvatarDefinition } from '@/features/avatar/avatarDefinition' @@ -32,11 +32,64 @@ describe('Studio document', () => { it('loads the bundled Studio snapshot when no local project exists', () => { const document = loadStudioDocument(storage()) - expect(document.library.avatars).toHaveLength(10) + expect(document.library.avatars).toHaveLength(12) expect(document.library.activeAvatarId).toBe(document.library.avatars[0].id) - expect(document.library.avatars[0].name).toBe('Strobi') + expect(document.library.avatars.map(avatar => avatar.name)).toEqual([ + 'Onee', + 'Cubee', + 'Nova', + 'Citrus', + 'Sphere', + 'Cube', + 'Capsule', + 'Cylinder', + 'Cone', + 'Diamond', + 'Memento', + 'Willy', + ]) + expect( + document.library.avatars.every(avatar => avatar.colors.body === avatar.colors.eyes) + ).toBe(true) + expect(document.library.avatars.slice(4).map(avatar => avatar.body.primary.type)).toEqual([ + 'sphere', + 'cube', + 'capsule', + 'cylinder', + 'cone', + 'diamond', + 'ghost', + 'capsule', + ]) + expect(document.library.avatars.at(-1)?.body.nodes).toHaveLength(3) + const memento = document.library.avatars.find(avatar => avatar.id === 'primitive-ghost') + expect(memento?.colors).toEqual({ body: '#ff9d45', eyes: '#ff9d45' }) + expect( + document.library.avatars + .filter(avatar => avatar.id !== 'primitive-ghost') + .every(avatar => avatar.colors.body !== '#ff9d45') + ).toBe(true) + expect(memento?.eyes.positionYLeft).toBe(defaultAvatarEyes.positionYLeft - 20) + expect(memento?.eyes.positionYRight).toBe(defaultAvatarEyes.positionYRight - 20) + expect( + memento?.behavior?.expressions.every( + expression => !expression.bodyColor && !expression.eyeColor + ) + ).toBe(true) expect(document.expressions).toHaveLength(27) expect(document.sequences).toHaveLength(23) + expect(document.sequences.find(sequence => sequence.id === 'angry')?.steps).toHaveLength(2) + expect( + document.sequences + .find(sequence => sequence.id === 'angry') + ?.steps.map(step => step.expressionId) + ).toEqual(['expression-07', 'expression-3d2bed26-f97c-477d-922f-77600cb10e92']) + expect(document.sequences.find(sequence => sequence.id === 'scared')?.steps).toHaveLength(2) + expect( + document.sequences + .find(sequence => sequence.id === 'scared') + ?.steps.map(step => step.expressionId) + ).toEqual(['expression-03', 'expression-5220eaee-32fe-4bd8-ad31-432189534cc8']) expect(document.expressions.every(expression => expression.semanticKey)).toBe(true) expect(document.sequences.every(sequence => sequence.semanticKey)).toBe(true) expect(document.playback).toEqual({ stateId: 'proud', playing: true }) @@ -55,6 +108,81 @@ describe('Studio document', () => { expect(loadStudioDocument(storage(JSON.stringify(localDocument)))).toEqual(localDocument) }) + it('restores standalone built-in animations from the temporary idle pairs', () => { + const fallback = loadStudioDocument(storage()) + const pairedDocument: StudioDocument = { + ...fallback, + sequences: fallback.sequences.map(sequence => + sequence.id === 'idle' + ? sequence + : { + ...sequence, + id: `idle-${sequence.id}`, + semanticKey: `idle-${sequence.id}`, + name: `idle + ${sequence.name}`, + } + ), + playback: { stateId: 'idle-proud', playing: true }, + } + + const restored = loadStudioDocument(storage(JSON.stringify(pairedDocument))) + + expect(restored.sequences.map(sequence => sequence.id)).toEqual( + fallback.sequences.map(sequence => sequence.id) + ) + expect(restored.playback).toEqual({ stateId: 'proud', playing: true }) + }) + + it('removes retired bundled avatars from a locally saved project', () => { + const fallback = loadStudioDocument(storage()) + const retired = createAvatar('Freddy') + retired.id = 'avatar-4fe2d1bd-cf46-4e5e-a62d-d6b60be519ed' + const localDocument: StudioDocument = { + ...fallback, + library: { + activeAvatarId: retired.id, + avatars: [retired, ...fallback.library.avatars], + }, + } + + const document = loadStudioDocument(storage(JSON.stringify(localDocument))) + + expect(document.library.avatars.map(avatar => avatar.name)).toEqual([ + 'Onee', + 'Cubee', + 'Nova', + 'Citrus', + 'Sphere', + 'Cube', + 'Capsule', + 'Cylinder', + 'Cone', + 'Diamond', + 'Memento', + 'Willy', + ]) + expect(document.library.activeAvatarId).toBe(document.library.avatars[0].id) + }) + + it('renames the bundled ghost character in an existing local project', () => { + const fallback = loadStudioDocument(storage()) + const legacy = structuredClone(fallback) + const character = legacy.library.avatars.find(avatar => avatar.id === 'primitive-ghost') + if (!character) throw new Error('Bundled ghost character not found') + character.name = 'Ghost' + character.eyes.positionYLeft = defaultAvatarEyes.positionYLeft + character.eyes.positionYRight = defaultAvatarEyes.positionYRight + + const document = loadStudioDocument(storage(JSON.stringify(legacy))) + + expect(document.library.avatars.find(avatar => avatar.id === 'primitive-ghost')?.name).toBe( + 'Memento' + ) + expect( + document.library.avatars.find(avatar => avatar.id === 'primitive-ghost')?.eyes.positionYLeft + ).toBe(defaultAvatarEyes.positionYLeft - 20) + }) + it('restores bundled semantic keys in a legacy local project', () => { const fallback = loadStudioDocument(storage()) const legacy = structuredClone(fallback)