From 288a6ce31d2b5897d10716ef464589956bba933b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B0=D0=B5=D1=80=20=D0=9C=D0=B8=D1=85=D0=B0=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D1=87?= Date: Wed, 26 Aug 2026 23:29:43 +0300 Subject: [PATCH] feat: add frame 172 avatar styles --- src/app/styles.css | 16 +++ src/assets/frame-172-colored.svg | 13 +++ src/assets/frame-172-stroke.svg | 6 + .../live-expression-preview-test.tsx | 41 ++++++- src/features/avatar/avatars.ts | 79 ++++++++++--- .../avatar/components/ExpressionWorkspace.tsx | 108 ++++++++++++++---- .../__tests__/rendered-scene-test.ts | 36 +++++- src/features/rendering/avatarAccessories.ts | 56 +++++++++ src/features/rendering/avatarAppearance.ts | 7 ++ .../rendering/components/AvatarCanvas.tsx | 17 ++- .../studio/__tests__/studio-document-test.ts | 31 ++++- .../studio/components/AvatarDrawer.tsx | 56 ++++++--- .../studio/components/StudioStage.tsx | 10 +- 13 files changed, 403 insertions(+), 73 deletions(-) create mode 100644 src/assets/frame-172-colored.svg create mode 100644 src/assets/frame-172-stroke.svg diff --git a/src/app/styles.css b/src/app/styles.css index f906c3f..32911c8 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -1224,6 +1224,22 @@ input { opacity: 1; stroke: none !important; } +.is-stroke-vector .avatar-head, +.is-stroke-vector .avatar-eye { + fill: none; + stroke: var(--avatar-body-color, #ff9d45); + stroke-width: var(--avatar-outline-width, 12); +} +.is-stroke-vector .avatar-headset { + fill: none !important; + stroke: var(--avatar-body-color, #ff9d45) !important; + stroke-width: var(--avatar-outline-width, 12) !important; + vector-effect: non-scaling-stroke; +} +.is-source-artwork .avatar-head, +.is-source-artwork .avatar-eye { + vector-effect: non-scaling-stroke; +} .avatar-eye, .preview-eye { fill: var(--avatar-eye-color, #ffffff); diff --git a/src/assets/frame-172-colored.svg b/src/assets/frame-172-colored.svg new file mode 100644 index 0000000..b021782 --- /dev/null +++ b/src/assets/frame-172-colored.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/frame-172-stroke.svg b/src/assets/frame-172-stroke.svg new file mode 100644 index 0000000..76fdf4d --- /dev/null +++ b/src/assets/frame-172-stroke.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/features/avatar/__tests__/live-expression-preview-test.tsx b/src/features/avatar/__tests__/live-expression-preview-test.tsx index b25cf56..9a5d738 100644 --- a/src/features/avatar/__tests__/live-expression-preview-test.tsx +++ b/src/features/avatar/__tests__/live-expression-preview-test.tsx @@ -5,10 +5,7 @@ import { render } from '@testing-library/react' import { LiveExpressionPreview } from '@/features/avatar/components/ExpressionWorkspace' import { poseFromExpression } from '@/features/avatar/geometry' import { renderAvatarByStyle } from '@/features/rendering/avatarAccessories' -import { - createRenderedColors, - createRenderedScene, -} from '@/features/rendering/renderedScene' +import { createRenderedColors, createRenderedScene } from '@/features/rendering/renderedScene' import { loadStudioDocument } from '@/features/studio/studioDocument' it('keeps the headset visible and transformed in a live animation preview', () => { @@ -40,3 +37,39 @@ it('keeps the headset visible and transformed in a live animation preview', () = expect(headset?.getAttribute('d')).toBe(geometry.frontPaths.at(-1)) expect(headset?.getAttribute('transform')).toBe(geometry.pathTransforms?.front.at(-1)) }) + +it('keeps the stroked Frame 172 skin unfilled in a live animation preview', () => { + const studio = loadStudioDocument({ getItem: () => null }) + const avatar = studio.library.avatars.find(item => item.id === 'primitive-ghost-headset-stroke')! + const expression = studio.expressions.find(item => item.semanticKey === 'front-facing')! + const geometry = renderAvatarByStyle( + poseFromExpression(expression), + avatar.body.primary, + 1, + avatar.renderStyle, + { bodyNodes: avatar.body.nodes } + ) + const scene = createRenderedScene(geometry) + + const view = render( + + ) + + const bodyPaths = view.container.querySelectorAll('.preview-head') + const body = bodyPaths.item(0) + const eyes = view.container.querySelectorAll('.preview-eye') + const headset = Array.from(bodyPaths).find( + path => path.getAttribute('d') === geometry.frontPaths.at(-1) + )! + expect(body.style.fill).toBe('none') + expect(Array.from(eyes).every(eye => eye.style.fill === 'none')).toBe(true) + expect(headset.style.fill).toBe('none') + expect(headset?.getAttribute('transform')).toBe(geometry.pathTransforms?.front.at(-1)) +}) diff --git a/src/features/avatar/avatars.ts b/src/features/avatar/avatars.ts index 9835b80..5762513 100644 --- a/src/features/avatar/avatars.ts +++ b/src/features/avatar/avatars.ts @@ -35,7 +35,9 @@ export type VectorAccessory = 'headset' export type VectorRenderStyle = { type: 'vector' filled?: boolean + strokeOnly?: boolean accessory?: VectorAccessory + artwork?: 'frame-172-colored' | 'frame-172-stroke' } export type AvatarRenderStyle = VectorRenderStyle | PixelRenderStyle export type AvatarEyeDefaults = Pick< @@ -78,6 +80,18 @@ export const headsetAvatarRenderStyle: VectorRenderStyle = { filled: true, accessory: 'headset', } +export const frame172ColoredAvatarRenderStyle: VectorRenderStyle = { + type: 'vector', + filled: true, + artwork: 'frame-172-colored', +} +export const frame172StrokeAvatarRenderStyle: VectorRenderStyle = { + type: 'vector', + strokeOnly: true, + artwork: 'frame-172-stroke', +} +export const isStrokeOnlyRenderStyle = (style: AvatarRenderStyle) => + style.type === 'vector' && style.strokeOnly === true const filledAvatarEyeColor = '#111316' export const defaultPixelRenderStyle: PixelRenderStyle = { type: 'pixel', @@ -165,6 +179,7 @@ const bundledAvatarColorMigrations: Record = { + 'primitive-ghost-headset-stroke': 'Memento · Colored', 'oneworks-cat': 'OneWorks Cat', 'oneworks-dog': 'OneWorks Dog', 'oneworks-bear': 'OneWorks Bear', @@ -237,6 +253,7 @@ const primitiveBundledAvatarSpecs = [ ['primitive-apple-bite', 'Apple Bite', 'apple-bite', deslopAccentColors.red, 0.9], ['primitive-ghost', 'Memento', 'ghost', deslopAccentColors.orange, 0.9], ['primitive-ghost-headset', 'Memento · Headset', 'ghost', deslopAccentColors.orange, 0.9], + ['primitive-ghost-headset-stroke', 'Memento · Stroke', 'ghost', deslopAccentColors.orange, 0.9], ['primitive-lock', 'Lock', 'lock', deslopAccentColors.blue, 0.9], ] as const @@ -830,9 +847,13 @@ const primitiveBundledAvatars: StudioAvatar[] = [ colors: { body: color, eyes: - surface === 'ghost' || surface === 'apple-bite' || surface === 'lock' - ? filledAvatarEyeColor - : color, + id === 'primitive-ghost-headset' + ? '#222222' + : id === 'primitive-ghost-headset-stroke' + ? deslopAccentColors.orange + : surface === 'ghost' || surface === 'apple-bite' || surface === 'lock' + ? filledAvatarEyeColor + : color, }, eyes: surface === 'ghost' @@ -856,10 +877,12 @@ const primitiveBundledAvatars: StudioAvatar[] = [ : { ...defaultAvatarEyes }, renderStyle: id === 'primitive-ghost-headset' - ? { ...headsetAvatarRenderStyle } - : surface === 'ghost' || surface === 'apple-bite' || surface === 'lock' - ? { ...filledAvatarRenderStyle } - : { ...defaultAvatarRenderStyle }, + ? { ...frame172ColoredAvatarRenderStyle } + : id === 'primitive-ghost-headset-stroke' + ? { ...frame172StrokeAvatarRenderStyle } + : surface === 'ghost' || surface === 'apple-bite' || surface === 'lock' + ? { ...filledAvatarRenderStyle } + : { ...defaultAvatarRenderStyle }, ...(surface === 'ghost' ? { behavior: mementoBehavior() } : surface === 'apple-bite' @@ -952,7 +975,11 @@ const filledBundledAvatarIds = new Set([ const oneWorksOriginalAnimalIds = new Set(Object.keys(oneWorksBundledAnimalColors)) const filledPrimitiveAvatarColors: Record = { 'primitive-ghost': { body: deslopAccentColors.orange, eyes: filledAvatarEyeColor }, - 'primitive-ghost-headset': { body: deslopAccentColors.orange, eyes: filledAvatarEyeColor }, + 'primitive-ghost-headset': { body: deslopAccentColors.orange, eyes: '#222222' }, + 'primitive-ghost-headset-stroke': { + body: deslopAccentColors.orange, + eyes: deslopAccentColors.orange, + }, 'primitive-apple-bite': { body: deslopAccentColors.red, eyes: filledAvatarEyeColor }, 'primitive-lock': { body: deslopAccentColors.blue, eyes: filledAvatarEyeColor }, } @@ -991,7 +1018,11 @@ export const ensurePrimitiveBundledAvatars = (library: AvatarLibrary): AvatarLib ) if (!visibleLibraryAvatars.some(avatar => originalSimpleBundledAvatarIds.has(avatar.id))) { const avatarsWithCurrentColorBehavior = visibleLibraryAvatars.map(avatar => { - if (['primitive-ghost', 'primitive-ghost-headset'].includes(avatar.id)) { + if ( + ['primitive-ghost', 'primitive-ghost-headset', 'primitive-ghost-headset-stroke'].includes( + avatar.id + ) + ) { return { ...avatar, behavior: mementoBehavior(avatar.behavior) } } if (filledBundledAvatarIds.has(avatar.id)) { @@ -1122,7 +1153,9 @@ export const ensurePrimitiveBundledAvatars = (library: AvatarLibrary): AvatarLib ? { ...avatarWithCurrentNodes, name: bundled.name } : avatarWithCurrentNodes const avatarWithCurrentEyes = - ['primitive-ghost', 'primitive-ghost-headset'].includes(avatarWithCurrentName.id) && + ['primitive-ghost', 'primitive-ghost-headset', 'primitive-ghost-headset-stroke'].includes( + avatarWithCurrentName.id + ) && [defaultAvatarEyes.positionYLeft, defaultAvatarEyes.positionYLeft - 12].includes( avatarWithCurrentName.eyes.positionYLeft ) && @@ -1137,9 +1170,11 @@ export const ensurePrimitiveBundledAvatars = (library: AvatarLibrary): AvatarLib avatarWithCurrentEyes.eyes.positionXRight === defaultAvatarEyes.positionXRight ? { ...avatarWithCurrentEyes, eyes: { ...bundled.eyes } } : avatarWithCurrentEyes - const avatarWithFixedColorBehavior = ['primitive-ghost', 'primitive-ghost-headset'].includes( - avatarWithAppleBiteEyes.id - ) + const avatarWithFixedColorBehavior = [ + 'primitive-ghost', + 'primitive-ghost-headset', + 'primitive-ghost-headset-stroke', + ].includes(avatarWithAppleBiteEyes.id) ? { ...avatarWithAppleBiteEyes, behavior: mementoBehavior(avatarWithAppleBiteEyes.behavior), @@ -1152,10 +1187,12 @@ export const ensurePrimitiveBundledAvatars = (library: AvatarLibrary): AvatarLib : avatarWithAppleBiteEyes const avatarWithCurrentRenderStyle = avatarWithFixedColorBehavior.id === 'primitive-ghost-headset' - ? { ...avatarWithFixedColorBehavior, renderStyle: { ...headsetAvatarRenderStyle } } - : filledBundledAvatarIds.has(avatarWithFixedColorBehavior.id) - ? { ...avatarWithFixedColorBehavior, renderStyle: { ...filledAvatarRenderStyle } } - : avatarWithFixedColorBehavior + ? { ...avatarWithFixedColorBehavior, renderStyle: { ...frame172ColoredAvatarRenderStyle } } + : avatarWithFixedColorBehavior.id === 'primitive-ghost-headset-stroke' + ? { ...avatarWithFixedColorBehavior, renderStyle: { ...frame172StrokeAvatarRenderStyle } } + : filledBundledAvatarIds.has(avatarWithFixedColorBehavior.id) + ? { ...avatarWithFixedColorBehavior, renderStyle: { ...filledAvatarRenderStyle } } + : avatarWithFixedColorBehavior const bundledFilledColors = mementoAnimalColors[avatarWithCurrentRenderStyle.id] ?? oneWorksBundledAnimalColors[avatarWithCurrentRenderStyle.id] ?? @@ -1238,14 +1275,22 @@ export const parseAvatarRenderStyle = (value: unknown): AvatarRenderStyle => { const candidate = value as { type?: unknown filled?: unknown + strokeOnly?: unknown accessory?: unknown + artwork?: unknown resolution?: unknown } | null if (candidate?.type === 'vector') { return { type: 'vector', ...(candidate.filled === true ? { filled: true } : {}), + ...(candidate.strokeOnly === true ? { strokeOnly: true } : {}), ...(candidate.accessory === 'headset' ? { accessory: 'headset' as const } : {}), + ...(candidate.artwork === 'frame-172-colored' + ? { artwork: 'frame-172-colored' as const } + : candidate.artwork === 'frame-172-stroke' + ? { artwork: 'frame-172-stroke' as const } + : {}), } } if (!PIXEL_RENDERING_ENABLED || candidate?.type !== 'pixel') { diff --git a/src/features/avatar/components/ExpressionWorkspace.tsx b/src/features/avatar/components/ExpressionWorkspace.tsx index f519441..d81e6c4 100644 --- a/src/features/avatar/components/ExpressionWorkspace.tsx +++ b/src/features/avatar/components/ExpressionWorkspace.tsx @@ -22,6 +22,7 @@ import { type AvatarColors, type AvatarEyeDefaults, type AvatarRenderStyle, + isStrokeOnlyRenderStyle, } from '@/features/avatar/avatars' import { type BodyNode } from '@/features/avatar/body' import { scaleEye, updateEyeDimension } from '@/features/avatar/expressionEditing' @@ -30,6 +31,7 @@ import { defaultExpression } from '@/features/avatar/presets' import { type SurfaceConfig } from '@/features/avatar/surfaces' import { avatarBodyOutlineWidth, + avatarOutlineWidth, resolveAvatarNodeColor, useAvatarBodyColorOverride, useRenderedSceneNodeOrder, @@ -84,11 +86,17 @@ export function ExpressionPreview({ id: string }) { const geometry = getPreviewGeometry(expression, surface, bodyNodes, avatarEyes, renderStyle) - const resolvedColors = resolveColors(expression, colors) - const outlineWidth = avatarBodyOutlineWidth() + const outlineWidth = avatarOutlineWidth(renderStyle) const filled = renderStyle.type === 'vector' && renderStyle.filled === true - const bodyFill = filled ? resolvedColors.body : 'var(--avatar-interior-color, #ffffff)' - const eyeFill = resolvedColors.eyes + const strokeOnly = isStrokeOnlyRenderStyle(renderStyle) + const sourceArtwork = renderStyle.type === 'vector' && Boolean(renderStyle.artwork) + const resolvedColors = strokeOnly ? colors : resolveColors(expression, colors) + const bodyFill = strokeOnly + ? 'none' + : filled + ? resolvedColors.body + : 'var(--avatar-interior-color, #ffffff)' + const eyeFill = strokeOnly ? 'none' : resolvedColors.eyes const nodeColor = (id: string | null | undefined) => resolveAvatarNodeColor({ nodeColor: bodyNodes.find(node => node.id === id)?.color, @@ -121,7 +129,7 @@ export function ExpressionPreview({
{playback && ( - + @@ -852,9 +856,10 @@ export function AvatarCanvas({ onPointerDown={event => selectBodyPath(event, backNodeIds.current[index])} /> ))} - { onBodyNodeSelect('primary') startDrag(event) @@ -865,15 +870,17 @@ export function AvatarCanvas({ wirePaths.map((pathValue, index) => ( ))} - selectEye(-1, event)} /> - selectEye(1, event)} /> diff --git a/src/features/studio/__tests__/studio-document-test.ts b/src/features/studio/__tests__/studio-document-test.ts index 2ad3783..b1b5fa0 100644 --- a/src/features/studio/__tests__/studio-document-test.ts +++ b/src/features/studio/__tests__/studio-document-test.ts @@ -31,6 +31,7 @@ describe('Studio document', () => { const bundledAvatarNames = [ 'Memento', 'Memento · Headset', + 'Memento · Stroke', 'Apple Bite', 'Lock', 'Siamese', @@ -80,7 +81,8 @@ describe('Studio document', () => { .filter(avatar => avatar.renderStyle.type !== 'vector' || !avatar.renderStyle.filled) .every(avatar => avatar.colors.body === avatar.colors.eyes) ).toBe(true) - expect(document.library.avatars.slice(0, 14).map(avatar => avatar.body.primary.type)).toEqual([ + expect(document.library.avatars.slice(0, 15).map(avatar => avatar.body.primary.type)).toEqual([ + 'ghost', 'ghost', 'ghost', 'apple-bite', @@ -121,6 +123,13 @@ describe('Studio document', () => { type: 'vector', filled: true, }) + expect( + document.library.avatars.find(avatar => avatar.id === 'primitive-ghost-headset')?.renderStyle + ).toEqual({ type: 'vector', filled: true, artwork: 'frame-172-colored' }) + expect( + document.library.avatars.find(avatar => avatar.id === 'primitive-ghost-headset-stroke') + ?.renderStyle + ).toEqual({ type: 'vector', strokeOnly: true, artwork: 'frame-172-stroke' }) expect( document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.renderStyle ).toEqual({ type: 'vector', filled: true }) @@ -203,7 +212,18 @@ describe('Studio document', () => { ?.body.nodes.map(node => node.color) ).toEqual(['#a95f47', '#a95f47']) const memento = document.library.avatars.find(avatar => avatar.id === 'primitive-ghost') + const headsetMemento = document.library.avatars.find( + avatar => avatar.id === 'primitive-ghost-headset' + ) + const strokeMemento = document.library.avatars.find( + avatar => avatar.id === 'primitive-ghost-headset-stroke' + ) expect(memento?.colors).toEqual({ body: '#ff9d45', eyes: '#111316' }) + expect(headsetMemento?.colors).toEqual({ body: '#ff9d45', eyes: '#222222' }) + expect(strokeMemento?.colors).toEqual({ body: '#ff9d45', eyes: '#ff9d45' }) + expect( + strokeMemento?.behavior?.sequences.some(sequence => sequence.semanticKey === 'onboarding') + ).toBe(true) expect( document.library.avatars.find(avatar => avatar.id === 'primitive-apple-bite')?.colors ).toEqual({ body: '#ff5558', eyes: '#111316' }) @@ -226,7 +246,14 @@ describe('Studio document', () => { ]) expect( document.library.avatars - .filter(avatar => !['primitive-ghost', 'primitive-ghost-headset'].includes(avatar.id)) + .filter( + avatar => + ![ + 'primitive-ghost', + 'primitive-ghost-headset', + 'primitive-ghost-headset-stroke', + ].includes(avatar.id) + ) .every(avatar => avatar.colors.body !== '#ff9d45') ).toBe(true) expect(memento?.eyes.positionYLeft).toBe(defaultAvatarEyes.positionYLeft - 20) diff --git a/src/features/studio/components/AvatarDrawer.tsx b/src/features/studio/components/AvatarDrawer.tsx index 7f143f8..5c0f110 100644 --- a/src/features/studio/components/AvatarDrawer.tsx +++ b/src/features/studio/components/AvatarDrawer.tsx @@ -9,11 +9,11 @@ import { ContextMenuSeparator, ContextMenuTrigger, } from '@/components/ui/context-menu' -import { defaultAvatarEyes } from '@/features/avatar/avatars' +import { defaultAvatarEyes, isStrokeOnlyRenderStyle } from '@/features/avatar/avatars' import { ExpressionPreview } from '@/features/avatar/components/ExpressionWorkspace' import { defaultExpression } from '@/features/avatar/presets' import { - avatarBodyOutlineWidth, + avatarOutlineWidth, useRenderedSceneNodeOrder, } from '@/features/rendering/avatarAppearance' import { LivePixelAvatarCanvas } from '@/features/rendering/components/PixelAvatarCanvas' @@ -33,16 +33,26 @@ function LiveAvatarPreview({ expressionBodyColor?: string }) { useRenderedSceneNodeOrder(scene) - const outlineWidth = avatarBodyOutlineWidth() + const outlineWidth = avatarOutlineWidth(avatar.renderStyle) const filled = avatar.renderStyle.type === 'vector' && avatar.renderStyle.filled === true - const bodyColor = expressionBodyColor ? colors.body : avatar.colors.body - const eyeColor = expressionBodyColor ? colors.eyes : avatar.colors.eyes + const strokeOnly = isStrokeOnlyRenderStyle(avatar.renderStyle) + const sourceArtwork = avatar.renderStyle.type === 'vector' && Boolean(avatar.renderStyle.artwork) + const bodyColor = strokeOnly + ? avatar.colors.body + : expressionBodyColor + ? colors.body + : avatar.colors.body + const eyeColor = strokeOnly + ? avatar.colors.eyes + : expressionBodyColor + ? colors.eyes + : avatar.colors.eyes const inheritBodyColor = Boolean(expressionBodyColor) const nodeColor = (id: string | null | undefined) => inheritBodyColor ? bodyColor : (avatar.body.nodes.find(node => node.id === id)?.color ?? bodyColor) - const bodyFill = filled ? bodyColor : '#ffffff' + const bodyFill = strokeOnly ? 'none' : filled ? bodyColor : '#ffffff' const clipId = `live-avatar-${avatar.id}` if (avatar.renderStyle.type === 'pixel') { @@ -64,7 +74,7 @@ function LiveAvatarPreview({ > - + @@ -80,38 +90,56 @@ function LiveAvatarPreview({ /> ) })} - - - {scene.frontPaths.map((pathValue, index) => { const color = nodeColor(scene.frontNodeIds.current[index]) + const headset = scene.headsetFrontIndex.current === index return ( ) })} diff --git a/src/features/studio/components/StudioStage.tsx b/src/features/studio/components/StudioStage.tsx index 54d9a2c..8890f77 100644 --- a/src/features/studio/components/StudioStage.tsx +++ b/src/features/studio/components/StudioStage.tsx @@ -6,7 +6,8 @@ import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { defaultExpression } from '@/features/avatar/presets' -import { avatarBodyOutlineWidth } from '@/features/rendering/avatarAppearance' +import { isStrokeOnlyRenderStyle } from '@/features/avatar/avatars' +import { avatarOutlineWidth } from '@/features/rendering/avatarAppearance' import { AvatarCanvas } from '@/features/rendering/components/AvatarCanvas' import { PhotoStageFrame } from '@/features/studio/components/PhotoStageFrame' import { StudioIdentity } from '@/features/studio/components/StudioIdentity' @@ -61,6 +62,7 @@ export function StudioStage({ controller }: { controller: StudioController }) { updateImmediate, } = controller const photoMode = mode === 'photo' + const fixedStrokeColor = isStrokeOnlyRenderStyle(activeAvatar.renderStyle) const activatePhotoTool = (tool: typeof photoTool) => { setPhotoTool(tool) setPhotoPanelSections(current => (current.includes(tool) ? current : [...current, tool])) @@ -116,9 +118,9 @@ export function StudioStage({ controller }: { controller: StudioController }) { className={`stage-column${photoMode ? ' photo-mode-active' : ''}`} style={ { - '--avatar-body-color': renderedColors.body, - '--avatar-eye-color': renderedColors.eyes, - '--avatar-outline-width': avatarBodyOutlineWidth(), + '--avatar-body-color': fixedStrokeColor ? activeAvatar.colors.body : renderedColors.body, + '--avatar-eye-color': fixedStrokeColor ? activeAvatar.colors.eyes : renderedColors.eyes, + '--avatar-outline-width': avatarOutlineWidth(activeAvatar.renderStyle), } as CSSProperties } >