@dotcraft/avatar 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +46 -43
- package/dist/accessories.css +14 -14
- package/dist/avatar.css +62 -62
- package/dist/composer/ComposerMascot.d.ts +1 -1
- package/dist/composer/ComposerMascot.d.ts.map +1 -1
- package/dist/composer/ComposerMascot.js +2 -2
- package/dist/composer/ComposerMascot.js.map +1 -1
- package/dist/composer/styles/energy.css +99 -99
- package/dist/composer/styles/expressions.css +20 -20
- package/dist/composer/styles/feedback.css +188 -188
- package/dist/composer/styles/focus.css +72 -72
- package/dist/composer/styles/foundation.css +301 -287
- package/dist/composer/styles/idle.css +438 -438
- package/dist/composer/styles/overdrive.css +264 -264
- package/dist/composer/styles/props.css +21 -21
- package/dist/composer/types.d.ts +1 -0
- package/dist/composer/types.d.ts.map +1 -1
- package/dist/rig.css +265 -265
- package/dist/styles.css +13 -13
- package/package.json +56 -55
- package/src/AnimatedDecoration.tsx +43 -0
- package/src/AppearanceRig.tsx +15 -0
- package/src/Avatar.tsx +85 -0
- package/src/DecorationShapes.tsx +8 -0
- package/src/Decorations.tsx +31 -0
- package/src/Faces.tsx +37 -0
- package/src/HatDecorations.tsx +75 -0
- package/src/HeldDecorations.tsx +48 -0
- package/src/MascotRig.tsx +191 -0
- package/src/ObjectDecorations.tsx +55 -0
- package/src/accessories.css +14 -0
- package/src/appearanceModel.ts +65 -0
- package/src/avatar.css +62 -0
- package/src/characters.ts +5 -0
- package/src/composer/ComposerMascot.tsx +521 -0
- package/src/composer/ComposerMascotShadow.tsx +5 -0
- package/src/composer/constants.ts +68 -0
- package/src/composer/mascotHandoff.ts +19 -0
- package/src/composer/styles/energy.css +99 -0
- package/src/composer/styles/expressions.css +20 -0
- package/src/composer/styles/feedback.css +188 -0
- package/src/composer/styles/focus.css +72 -0
- package/src/composer/styles/foundation.css +301 -0
- package/src/composer/styles/idle.css +438 -0
- package/src/composer/styles/overdrive.css +264 -0
- package/src/composer/styles/props.css +21 -0
- package/src/composer/types.ts +32 -0
- package/src/composer/useComposerAvatarBehavior.ts +97 -0
- package/src/composer/useComposerMotion.ts +15 -0
- package/src/composer/useComposerProfile.ts +50 -0
- package/src/decorationCatalog.ts +45 -0
- package/src/decorationMotion.ts +68 -0
- package/src/environment.ts +46 -0
- package/src/index.ts +5 -0
- package/src/palette.ts +52 -0
- package/src/react.ts +6 -0
- package/src/rig.css +265 -0
- package/src/styles.css +13 -0
- package/src/useEventReplay.ts +13 -0
- package/src/useGesture.ts +51 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import type { PrimaryId } from './appearanceModel.js'
|
|
3
|
+
import type { AvatarPose } from './characters.js'
|
|
4
|
+
import { PrimaryDecoration } from './Decorations.js'
|
|
5
|
+
import { advanceDecoration, decorationMotionProfiles, frameTransform, requestDecorationEvent, restFrame, type DecorationClock, type DecorationFrame } from './decorationMotion.js'
|
|
6
|
+
|
|
7
|
+
export function AnimatedDecoration({ id, pose, sequence, enabled, paused }: {
|
|
8
|
+
id: Exclude<PrimaryId, 'none'>; pose: AvatarPose; sequence: number; enabled: boolean; paused: boolean
|
|
9
|
+
}) {
|
|
10
|
+
const art = useRef<SVGGElement>(null)
|
|
11
|
+
const shadow = useRef<SVGEllipseElement>(null)
|
|
12
|
+
const clock = useRef<DecorationClock | null>(null)
|
|
13
|
+
const profile = decorationMotionProfiles[id]
|
|
14
|
+
const previousId = useRef(id)
|
|
15
|
+
function paint(frame: DecorationFrame) {
|
|
16
|
+
art.current?.setAttribute('transform', frameTransform(profile, frame))
|
|
17
|
+
shadow.current?.setAttribute('opacity', String(.12 * frame.shadow))
|
|
18
|
+
shadow.current?.setAttribute('rx', String(profile.shadow * (.7 + .3 * frame.shadow)))
|
|
19
|
+
}
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (previousId.current !== id || !enabled) { clock.current = null; previousId.current = id }
|
|
22
|
+
clock.current = enabled ? requestDecorationEvent(clock.current, pose) : null
|
|
23
|
+
paint(clock.current?.frame ?? restFrame)
|
|
24
|
+
}, [id, pose, sequence, enabled])
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!enabled || paused) return
|
|
27
|
+
let handle = 0
|
|
28
|
+
let last: number | undefined
|
|
29
|
+
const tick = (now: number) => {
|
|
30
|
+
if (!clock.current) return
|
|
31
|
+
clock.current = advanceDecoration(clock.current, profile, last === undefined ? 0 : now - last)
|
|
32
|
+
last = now
|
|
33
|
+
paint(clock.current.frame)
|
|
34
|
+
if (clock.current.phase !== 'rest') handle = requestAnimationFrame(tick)
|
|
35
|
+
}
|
|
36
|
+
handle = requestAnimationFrame(tick)
|
|
37
|
+
return () => cancelAnimationFrame(handle)
|
|
38
|
+
}, [id, pose, sequence, enabled, paused])
|
|
39
|
+
return <g data-decoration-motion={profile.kind}>
|
|
40
|
+
{profile.shadow > 0 && <ellipse ref={shadow} cx={profile.x} cy="404" rx={profile.shadow} ry="7" fill="#243344" opacity=".12" />}
|
|
41
|
+
<g ref={art} transform={frameTransform(profile, restFrame)}><PrimaryDecoration id={id} /></g>
|
|
42
|
+
</g>
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import type { AvatarPose, AvatarExpression } from './characters.js'
|
|
3
|
+
import type { Appearance } from './appearanceModel.js'
|
|
4
|
+
import { MascotRig, type MascotExpression } from './MascotRig.js'
|
|
5
|
+
export function AppearanceRig({ appearance, pose, expression: override, top, accessory, held }: {
|
|
6
|
+
appearance: Appearance; pose: AvatarPose; expression?: AvatarExpression; top?: ReactNode; accessory?: ReactNode; held?: ReactNode
|
|
7
|
+
}) {
|
|
8
|
+
const derived: MascotExpression = pose === 'sleep' ? 'sleep' : ['done', 'greeting', 'acknowledge'].includes(pose) ? 'happy'
|
|
9
|
+
: ['working', 'waiting', 'blocked'].includes(pose) ? 'operator' : 'neutral'
|
|
10
|
+
const expression = override === 'base' ? 'neutral' : override ?? derived
|
|
11
|
+
return <MascotRig size={1024} expression={expression} top={top} accessory={accessory} held={held}
|
|
12
|
+
baseFace={appearance.baseFace}
|
|
13
|
+
light={pose === 'blocked' ? 'error' : pose === 'done' ? 'success' : 'default'}
|
|
14
|
+
avatar={appearance.palette === -1 ? undefined : { palette: appearance.palette }} />
|
|
15
|
+
}
|
package/src/Avatar.tsx
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type CSSProperties } from 'react'
|
|
2
|
+
import { AppearanceRig } from './AppearanceRig.js'
|
|
3
|
+
import type { AvatarPose, MotionMode, AvatarExpression, AvatarGesture } from './characters.js'
|
|
4
|
+
import { deriveAppearance, isHeld, type Appearance } from './appearanceModel.js'
|
|
5
|
+
|
|
6
|
+
import { SecondaryDecoration } from './Decorations.js'
|
|
7
|
+
import { AnimatedDecoration } from './AnimatedDecoration.js'
|
|
8
|
+
import { useMotionEnvironment, useOnscreen, useTransitionPause } from './environment.js'
|
|
9
|
+
import { useGesture } from './useGesture.js'
|
|
10
|
+
import { useEventReplay } from './useEventReplay.js'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export interface AppearanceAvatarProps {
|
|
14
|
+
appearance: Appearance
|
|
15
|
+
state?: AvatarPose
|
|
16
|
+
size?: number
|
|
17
|
+
motion?: MotionMode
|
|
18
|
+
className?: string
|
|
19
|
+
paused?: boolean
|
|
20
|
+
label?: string
|
|
21
|
+
eventSequence?: number
|
|
22
|
+
expression?: AvatarExpression
|
|
23
|
+
gesture?: AvatarGesture
|
|
24
|
+
gestureSequence?: number
|
|
25
|
+
onGestureComplete?: (sequence: number) => void
|
|
26
|
+
}
|
|
27
|
+
export function AppearanceAvatar({ appearance, state = 'idle', size = 44, motion = 'off', paused = false, label, eventSequence = 0, className, expression, gesture, gestureSequence = 0, onGestureComplete }: AppearanceAvatarProps) {
|
|
28
|
+
const ref = useRef<HTMLSpanElement>(null)
|
|
29
|
+
const visible = useOnscreen(ref, motion !== 'off' && size > 20)
|
|
30
|
+
const environment = useMotionEnvironment(motion, size > 20)
|
|
31
|
+
useTransitionPause(ref, paused || !visible)
|
|
32
|
+
const [displayed, setDisplayed] = useState(state)
|
|
33
|
+
const [displayedSequence, setDisplayedSequence] = useState(eventSequence)
|
|
34
|
+
const [exiting, setExiting] = useState(false)
|
|
35
|
+
const identity = JSON.stringify(appearance)
|
|
36
|
+
const previousIdentity = useRef(identity)
|
|
37
|
+
const compact = size <= 20
|
|
38
|
+
const animated = environment && !compact
|
|
39
|
+
const held = !compact && isHeld(appearance.secondary)
|
|
40
|
+
const [shownExpression, setShownExpression] = useState(expression)
|
|
41
|
+
useEffect(() => { if (!animated || (!paused && visible)) setShownExpression(expression) }, [expression, animated, paused, visible])
|
|
42
|
+
useGesture(ref, gesture, gestureSequence, animated, paused || !visible, onGestureComplete)
|
|
43
|
+
useEventReplay(ref, displayedSequence, animated, paused || !visible)
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (previousIdentity.current !== identity) {
|
|
47
|
+
previousIdentity.current = identity; setDisplayed(state); setDisplayedSequence(eventSequence); setExiting(false); return
|
|
48
|
+
}
|
|
49
|
+
if (!animated) { setDisplayed(state); setDisplayedSequence(eventSequence); setExiting(false); return }
|
|
50
|
+
if (state === displayed && eventSequence === displayedSequence) { setExiting(false); return }
|
|
51
|
+
if (paused || !visible) return
|
|
52
|
+
const changesWorkProp = displayed !== state && [displayed, state].some(pose => pose === 'working' || pose === 'waiting')
|
|
53
|
+
if (!changesWorkProp) { setDisplayed(state); setDisplayedSequence(eventSequence); setExiting(false); return }
|
|
54
|
+
// Retract props before changing the original paired arm pose; rapid changes cancel this timer.
|
|
55
|
+
setExiting(true)
|
|
56
|
+
const timer = window.setTimeout(() => { setDisplayed(state); setDisplayedSequence(eventSequence); setExiting(false) }, 160)
|
|
57
|
+
return () => window.clearTimeout(timer)
|
|
58
|
+
}, [state, displayed, animated, paused, visible, identity, eventSequence, displayedSequence])
|
|
59
|
+
|
|
60
|
+
const pose = animated ? displayed : state
|
|
61
|
+
const classes = [
|
|
62
|
+
'dca-rig',
|
|
63
|
+
!compact && pose === 'waiting' ? 'dca-action-hold-sign' : '',
|
|
64
|
+
!compact && pose === 'working' ? 'dca-action-prop-laptop' : '',
|
|
65
|
+
!compact && pose === 'done' ? 'dca-action-celebrate' : '',
|
|
66
|
+
!compact && pose === 'greeting' && !exiting ? 'dca-action-wave' : '',
|
|
67
|
+
].filter(Boolean).join(' ')
|
|
68
|
+
return <span ref={ref} className={`dca-robot${className ? ` ${className}` : ''}`} style={{ width: size, height: size, '--dca-loop': '2.6s' } as CSSProperties}
|
|
69
|
+
data-pose={pose} data-mode={motion} data-motion={animated ? 'on' : 'off'} data-paused={paused || !visible} data-exiting={exiting} data-compact={compact}
|
|
70
|
+
data-primary={appearance.primary} data-secondary={appearance.secondary} data-base-face={appearance.baseFace}
|
|
71
|
+
data-gesture={animated ? gesture : undefined} data-held-state={held ? (pose === 'working' || pose === 'waiting' ? 'stowed' : 'holding') : undefined}>
|
|
72
|
+
<svg className="dca-canvas" width={size} height={size} viewBox="0 0 1024 1024" fill="none" role={label ? 'img' : undefined} aria-hidden={label ? undefined : true}
|
|
73
|
+
aria-label={label}>
|
|
74
|
+
<g className="dca-body-motion"><g className={classes}>
|
|
75
|
+
<AppearanceRig appearance={appearance} pose={pose} expression={animated ? shownExpression : expression}
|
|
76
|
+
top={appearance.primary === 'none' ? undefined : <AnimatedDecoration id={appearance.primary} pose={state} sequence={eventSequence} enabled={animated} paused={paused || !visible} />}
|
|
77
|
+
held={!compact && held ? <g data-accessory={appearance.secondary} className="dca-part-held"><SecondaryDecoration id={appearance.secondary} /></g> : undefined}
|
|
78
|
+
accessory={!compact && !held ? <g data-accessory={appearance.secondary}><SecondaryDecoration id={appearance.secondary} /></g> : undefined} />
|
|
79
|
+
</g></g>
|
|
80
|
+
</svg>
|
|
81
|
+
</span>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AvatarProps extends Omit<AppearanceAvatarProps, 'appearance'> { name: string }
|
|
85
|
+
export function Avatar({ name, ...props }: AvatarProps) { return <AppearanceAvatar appearance={deriveAppearance(name)} {...props} /> }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export function Silhouette({ d, fill, children }: { d: string; fill: string; children?: ReactNode }) {
|
|
4
|
+
return <><path d={d} fill={fill} stroke="#fff" strokeWidth="18" strokeLinejoin="round" paintOrder="stroke fill" />{children}</>
|
|
5
|
+
}
|
|
6
|
+
export function Detail({ children }: { children: ReactNode }) {
|
|
7
|
+
return <g className="dca-decoration-detail">{children}</g>
|
|
8
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PrimaryId, SecondaryId, DecorationId } from './appearanceModel.js'
|
|
2
|
+
import { isHeld } from './appearanceModel.js'
|
|
3
|
+
import { HeldDecoration } from './HeldDecorations.js'
|
|
4
|
+
import { HatDecoration } from './HatDecorations.js'
|
|
5
|
+
import { ObjectDecoration } from './ObjectDecorations.js'
|
|
6
|
+
import { decorationOf } from './decorationCatalog.js'
|
|
7
|
+
|
|
8
|
+
export function SecondaryDecoration({ id }: { id: SecondaryId }) {
|
|
9
|
+
if (isHeld(id)) return <HeldDecoration id={id} />
|
|
10
|
+
switch (id) {
|
|
11
|
+
case 'forehead-goggles': return <g strokeLinejoin="round">
|
|
12
|
+
<path d="M374 419h109v46H374Zm167 0h109v46H541Z" fill="#a2c5d1" stroke="#8b7568" strokeWidth="14" />
|
|
13
|
+
<path d="M483 442q29 20 58 0" stroke="#8b7568" strokeWidth="11" fill="none" />
|
|
14
|
+
<path d="m389 429 18 21m149-21 18 21" stroke="#e0eff1" strokeWidth="8" />
|
|
15
|
+
</g>
|
|
16
|
+
default: return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function PrimaryDecoration({ id }: { id: PrimaryId }) {
|
|
20
|
+
return <g data-decoration={id}><HatDecoration id={id} /><ObjectDecoration id={id} /></g>
|
|
21
|
+
}
|
|
22
|
+
export function DecoratedTop({ id }: { id: Exclude<PrimaryId, 'none'> }) {
|
|
23
|
+
return <PrimaryDecoration id={id} />
|
|
24
|
+
}
|
|
25
|
+
export function DecorationSwatch({ id, size = 112 }: { id: DecorationId; size?: number }) {
|
|
26
|
+
const secondary = decorationOf(id).category === 'Accessories'
|
|
27
|
+
const forehead = id === 'forehead-goggles'
|
|
28
|
+
return <svg width={size} height={size} viewBox={secondary ? forehead ? '300 280 424 324' : '65 435 315 360' : '265 105 494 360'} fill="none" role="img" aria-label={`${decorationOf(id).name} specimen`}>
|
|
29
|
+
{secondary ? <SecondaryDecoration id={id as SecondaryId} /> : <PrimaryDecoration id={id as PrimaryId} />}
|
|
30
|
+
</svg>
|
|
31
|
+
}
|
package/src/Faces.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { JSX } from 'react'
|
|
2
|
+
|
|
3
|
+
function ProfileFace({ kind, mark, accent }: { kind: number; mark: string; accent: string }) {
|
|
4
|
+
return <g className={kind === 0 ? undefined : 'dca-part-eyes'} data-profile-face={kind} strokeLinecap="round" strokeLinejoin="round">
|
|
5
|
+
{kind === 0 && <><g className="dca-part-eyes"><path d="M387 568 477 634 387 700" stroke={mark} strokeWidth="38" /></g><path className="dca-part-caret" d="M531 696h116" stroke={accent} strokeWidth="27" /></>}
|
|
6
|
+
{kind === 1 && <><path d="M379 585 452 622 379 659M645 585 572 622 645 659" stroke={mark} strokeWidth="30" /><path d="M482 704c18 11 42 11 60 0" stroke={mark} strokeWidth="22" /></>}
|
|
7
|
+
{kind === 2 && <><path d="M389 612h58M577 612h58" stroke={mark} strokeWidth="30" /><path d="M493 700h42" stroke={mark} strokeWidth="20" /></>}
|
|
8
|
+
{kind === 3 && <><rect x="381" y="581" width="45" height="87" rx="16" fill={mark} /><rect x="598" y="581" width="45" height="87" rx="16" fill={mark} /><path d="M487 700h50" stroke={mark} strokeWidth="22" /></>}
|
|
9
|
+
{kind === 4 && <><path d="M366 586h88v46M570 586h88v46" stroke={mark} strokeWidth="28" /><path d="M481 700h62" stroke={mark} strokeWidth="22" /></>}
|
|
10
|
+
</g>
|
|
11
|
+
}
|
|
12
|
+
export function Faces({ mark, accent, baseFace = 0 }: { mark: string; accent: string; baseFace?: number }): JSX.Element {
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<g className="dca-part-face dca-part-face-neutral"><ProfileFace kind={baseFace} mark={mark} accent={accent} /></g>
|
|
16
|
+
<g className="dca-part-face dca-part-face-happy">
|
|
17
|
+
<g className="dca-part-eyes">
|
|
18
|
+
<path d="M379 585 452 622 379 659" stroke={mark} strokeWidth="30" strokeLinecap="round" strokeLinejoin="round" />
|
|
19
|
+
<path d="M645 585 572 622 645 659" stroke={mark} strokeWidth="30" strokeLinecap="round" strokeLinejoin="round" />
|
|
20
|
+
</g>
|
|
21
|
+
<path d="M470 702c20 18 64 18 84 0" stroke={accent} strokeWidth="24" strokeLinecap="round" fill="none" />
|
|
22
|
+
</g>
|
|
23
|
+
<g className="dca-part-face dca-part-face-operator">
|
|
24
|
+
<g className="dca-part-eyes">
|
|
25
|
+
<rect x="381" y="581" width="45" height="87" rx="16" fill={mark} />
|
|
26
|
+
<rect x="598" y="581" width="45" height="87" rx="16" fill={mark} />
|
|
27
|
+
</g>
|
|
28
|
+
<path d="M487 700h50" stroke={accent} strokeWidth="22" strokeLinecap="round" />
|
|
29
|
+
</g>
|
|
30
|
+
<g className="dca-part-face dca-part-face-sleep">
|
|
31
|
+
<path d="M373 618c26 20 56 20 82 0" stroke={mark} strokeWidth="26" strokeLinecap="round" fill="none" />
|
|
32
|
+
<path d="M569 618c26 20 56 20 82 0" stroke={mark} strokeWidth="26" strokeLinecap="round" fill="none" />
|
|
33
|
+
<path d="M488 704h48" stroke={accent} strokeWidth="20" strokeLinecap="round" />
|
|
34
|
+
</g>
|
|
35
|
+
</>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { PrimaryId } from './appearanceModel.js'
|
|
2
|
+
import { Detail, Silhouette as S } from './DecorationShapes.js'
|
|
3
|
+
|
|
4
|
+
export function HatDecoration({ id }: { id: PrimaryId }) {
|
|
5
|
+
switch (id) {
|
|
6
|
+
case 'baseball-cap': return <>
|
|
7
|
+
<S d="M337 368c0-94 44-163 139-163 91 0 142 65 142 155 36 0 71 8 88 24 9 9-2 23-24 23H363c-24 0-31-15-26-39Z" fill="#e87967" />
|
|
8
|
+
<path d="M513 214c63 18 91 72 91 146h-51c0-82-9-116-40-146Z" fill="#b94f50" />
|
|
9
|
+
<path d="M358 370c113-15 249-20 315 8 23 9 28 20 7 20H363c-19 0-25-15-5-28Z" fill="#b94f50" />
|
|
10
|
+
<Detail><path d="M385 322c8-48 32-75 61-83" stroke="#f5a18c" strokeWidth="14" strokeLinecap="round" /><ellipse cx="480" cy="205" rx="23" ry="10" fill="#b94f50" /></Detail>
|
|
11
|
+
</>
|
|
12
|
+
case 'bucket-hat': return <>
|
|
13
|
+
<S d="m386 233 243 0 25 111 56 47c8 8 1 19-12 19H320c-13 0-20-11-10-20l52-48Z" fill="#94bda6" />
|
|
14
|
+
<path d="m386 233 243 0-10 25H391Z" fill="#bed8c1" /><path d="m362 342 292 2 56 47c8 8 1 19-12 19H320c-13 0-20-11-10-20Z" fill="#5b887a" />
|
|
15
|
+
<Detail><path d="m380 323 261 0" stroke="#bed8c1" strokeWidth="11" strokeLinecap="round" /></Detail>
|
|
16
|
+
</>
|
|
17
|
+
case 'beret': return <>
|
|
18
|
+
<S d="M349 356c-54-6-57-73-4-112 40-30 95-48 151-43l5-26c2-13 23-11 22 3l-2 26c72 6 145 43 164 89 19 43-19 68-55 76l-16 32H370Z" fill="#b17498" />
|
|
19
|
+
<path d="M349 356c69 21 201 14 281-12l-16 57H370Z" fill="#784c71" />
|
|
20
|
+
<Detail><path d="M355 294c20-31 65-54 110-57" stroke="#cf9db5" strokeWidth="17" strokeLinecap="round" /></Detail>
|
|
21
|
+
</>
|
|
22
|
+
case 'beanie': return <>
|
|
23
|
+
<S d="M365 337c0-93 53-153 147-153s147 60 147 153l9 8v56H356v-56Z" fill="#e9a653" />
|
|
24
|
+
<path d="M540 187c82 14 119 76 119 150h-46c0-79-19-128-73-150Z" fill="#bc753d" />
|
|
25
|
+
<rect x="356" y="337" width="312" height="64" rx="19" fill="#f7c676" />
|
|
26
|
+
<Detail><path d="M383 371h53" stroke="#e9a653" strokeWidth="10" strokeLinecap="round" /><rect x="568" y="352" width="44" height="33" rx="7" fill="#bc753d" /></Detail>
|
|
27
|
+
</>
|
|
28
|
+
case 'top-hat': return <>
|
|
29
|
+
<S d="m382 180 260 0-24 187h67c20 0 20 36 0 36H339c-20 0-20-36 0-36h67Z" fill="#465069" />
|
|
30
|
+
<path d="m569 180 73 0-24 187h-66Z" fill="#293247" /><path d="M400 315h224l-6 52H406Z" fill="#c96e85" />
|
|
31
|
+
<path d="M339 384h346" stroke="#293247" strokeWidth="24" strokeLinecap="round" />
|
|
32
|
+
<Detail><path d="m411 207 8 68" stroke="#6a758e" strokeWidth="14" strokeLinecap="round" /></Detail>
|
|
33
|
+
</>
|
|
34
|
+
case 'wizard-hat': return <>
|
|
35
|
+
<S d="M340 367h34c47-70 55-144 113-186 24-18 53-13 79-5-45 18-30 43-9 70l82 121h44c22 0 23 37 0 37H340c-22 0-22-37 0-37Z" fill="#8773c1" />
|
|
36
|
+
<path d="M524 184c-8 29 13 52 34 83l70 100h-70l-42-113c-13-33-15-52 8-70Z" fill="#594b93" />
|
|
37
|
+
<path d="M340 385h343" stroke="#594b93" strokeWidth="26" strokeLinecap="round" />
|
|
38
|
+
<Detail><path d="m474 274 12 23 26 4-19 19 5 26-24-12-24 12 5-26-19-19 26-4Z" fill="#f4cc75" /></Detail>
|
|
39
|
+
</>
|
|
40
|
+
case 'chef-hat': return <>
|
|
41
|
+
<S d="M397 324c-70 4-93-82-39-116 27-18 51-9 67 1 10-72 147-77 170-7 74-32 125 64 77 101-14 11-29 16-45 18v79H397Z" fill="#f5eee3" />
|
|
42
|
+
<path d="M397 332h230v68H397Z" fill="#d7caba" /><path d="M397 332h194v53H397Z" fill="#fffaf1" />
|
|
43
|
+
<Detail><path d="M420 264v39m91-62v62m90-36v36" stroke="#d7caba" strokeWidth="13" strokeLinecap="round" /></Detail>
|
|
44
|
+
</>
|
|
45
|
+
case 'party-hat': return <>
|
|
46
|
+
<S d="m494 192-111 195c-6 12 5 16 17 16h224c14 0 24-4 17-18L531 193c21-18 10-50-17-50-29 0-41 31-20 49Z" fill="#ea9baf" />
|
|
47
|
+
<path d="m450 270 120 0 21 36H430Zm-49 86h223l17 29c7 14-3 18-17 18H400c-12 0-23-4-17-16Z" fill="#8075b7" />
|
|
48
|
+
<circle cx="513" cy="171" r="28" fill="#f5d07b" />
|
|
49
|
+
<Detail><path d="m526 212 75 132" stroke="#f6c4ce" strokeWidth="11" strokeLinecap="round" /></Detail>
|
|
50
|
+
</>
|
|
51
|
+
case 'crown': return <>
|
|
52
|
+
<S d="M363 282c-7-20 9-31 24-18l56 54 51-86c8-15 27-15 36 0l51 86 56-54c15-13 31-2 24 18l-24 117H387Z" fill="#efc65c" />
|
|
53
|
+
<path d="M387 365h250v34H387Z" fill="#c99139" />
|
|
54
|
+
<Detail><path d="m512 305 26 30-26 30-26-30Z" fill="#73b8cc" /><path d="m512 305 0 60-26-30Z" fill="#a3d8df" /><circle cx="410" cy="342" r="8" fill="#fff1af" /><circle cx="614" cy="342" r="8" fill="#fff1af" /></Detail>
|
|
55
|
+
</>
|
|
56
|
+
case 'hard-hat': return <>
|
|
57
|
+
<S d="M358 361c0-94 42-154 133-163v-11c0-15 42-15 42 0v11c91 9 133 69 133 163h22c23 0 23 39 0 39H336c-23 0-23-39 0-39Z" fill="#f5bd48" />
|
|
58
|
+
<path d="M533 198c91 9 133 69 133 163h-48c0-86-24-135-85-163Z" fill="#d68d32" /><path d="M494 192h36v169h-36Z" fill="#ffdc79" />
|
|
59
|
+
<path d="M336 382h352" stroke="#d68d32" strokeWidth="25" strokeLinecap="round" />
|
|
60
|
+
<Detail><path d="M390 326c2-41 21-70 43-87" stroke="#ffdc79" strokeWidth="14" strokeLinecap="round" /></Detail>
|
|
61
|
+
</>
|
|
62
|
+
case 'nightcap': return <>
|
|
63
|
+
<S d="M358 369c-1-99 40-172 141-172 109 0 161 48 157 110 40 5 46 54 12 68-31 13-55-19-40-43-15-17-36-26-51-30l49 67v32H358Z" fill="#789cc9" />
|
|
64
|
+
<path d="M499 197c83 0 161 48 157 110l-28 25c-20-29-53-40-81-43 19-16 39-24 63-20-25-39-57-57-111-72Z" fill="#4c709f" />
|
|
65
|
+
<rect x="355" y="363" width="276" height="39" rx="17" fill="#dce8f4" /><circle cx="653" cy="346" r="31" fill="#dce8f4" />
|
|
66
|
+
<Detail><circle cx="432" cy="296" r="10" fill="#dce8f4" /><circle cx="469" cy="245" r="9" fill="#dce8f4" /></Detail>
|
|
67
|
+
</>
|
|
68
|
+
case 'straw-hat': return <>
|
|
69
|
+
<S d="M319 352c19-7 47-11 78-15l13-83c3-17 200-17 204 0l13 83c31 4 59 8 78 15 32 12 17 45-20 48-115 12-231 12-346 0-37-3-52-36-20-48Z" fill="#e8c78a" />
|
|
70
|
+
<path d="m405 302 214 0 8 35c-72 11-150 11-230 0Z" fill="#c57568" /><ellipse cx="512" cy="258" rx="97" ry="16" fill="#f5dfb2" />
|
|
71
|
+
<Detail><path d="M334 379q177 26 356 0" stroke="#c49b5e" strokeWidth="10" strokeLinecap="round" /></Detail>
|
|
72
|
+
</>
|
|
73
|
+
default: return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { HeldId } from './appearanceModel.js'
|
|
2
|
+
|
|
3
|
+
export function HeldDecoration({ id }: { id: HeldId }) {
|
|
4
|
+
return <g data-held-decoration={id} strokeLinecap="round" strokeLinejoin="round">
|
|
5
|
+
{id === 'task-board' && <g transform="translate(233 640) scale(.8) translate(-840 -665)"><g>
|
|
6
|
+
<rect x="718" y="562" width="244" height="194" rx="36" fill="#fff" stroke="#fff" strokeWidth="30" />
|
|
7
|
+
<rect x="718" y="562" width="244" height="194" rx="36" fill="#fff" stroke="var(--dca-held-mark, #3161f7)" strokeWidth="11" />
|
|
8
|
+
<path d="M760 620h114" stroke="var(--dca-held-mark, #3161f7)" strokeWidth="18" strokeLinecap="round" />
|
|
9
|
+
<circle cx="775" cy="689" r="14" fill="var(--dca-held-mark, #3161f7)" />
|
|
10
|
+
<circle cx="842" cy="661" r="14" fill="var(--dca-held-accent, #f6b500)" />
|
|
11
|
+
<circle cx="907" cy="708" r="14" fill="var(--dca-held-mark, #3161f7)" />
|
|
12
|
+
<path d="M789 683 828 667 893 699" stroke="#202124" strokeWidth="13" strokeLinecap="round" strokeLinejoin="round" opacity=".72" />
|
|
13
|
+
</g>
|
|
14
|
+
</g>}
|
|
15
|
+
{id === 'wrench' && <g transform="translate(233 670) rotate(-60) scale(.65) translate(-642 -817)"><g>
|
|
16
|
+
<path d="M756 595c30-30 74-39 113-23l-56 56 46 46 56-56c16 39 7 83-23 113-32 32-80 39-119 20L674 850c-18 18-47 18-65 0s-18-47 0-65l99-99c-19-39-12-87 20-119Z" fill="#fff" stroke="#fff" strokeWidth="34" strokeLinejoin="round" />
|
|
17
|
+
<path d="M756 595c30-30 74-39 113-23l-56 56 46 46 56-56c16 39 7 83-23 113-32 32-80 39-119 20L674 850c-18 18-47 18-65 0s-18-47 0-65l99-99c-19-39-12-87 20-119Z" fill="#fff" stroke="var(--dca-held-mark, #3161f7)" strokeWidth="13" strokeLinejoin="round" />
|
|
18
|
+
<circle cx="642" cy="817" r="17" fill="var(--dca-held-accent, #f6b500)" />
|
|
19
|
+
<path d="M706 752 774 684" stroke="var(--dca-held-mark, #3161f7)" strokeWidth="20" strokeLinecap="round" opacity=".78" />
|
|
20
|
+
</g>
|
|
21
|
+
</g>}
|
|
22
|
+
{id === 'shield' && <g transform="translate(233 635) scale(.8) translate(-817 -690)"><g>
|
|
23
|
+
<path d="M817 553 929 594v76c0 80-46 134-112 162-66-28-112-82-112-162v-76Z" fill="#fff" stroke="#fff" strokeWidth="30" strokeLinejoin="round" />
|
|
24
|
+
<path d="M817 553 929 594v76c0 80-46 134-112 162-66-28-112-82-112-162v-76Z" fill="#fff" stroke="var(--dca-held-mark, #3161f7)" strokeWidth="11" strokeLinejoin="round" />
|
|
25
|
+
<path d="M761 683 802 722 881 632" stroke="var(--dca-held-mark, #3161f7)" strokeWidth="28" strokeLinecap="round" strokeLinejoin="round" />
|
|
26
|
+
</g>
|
|
27
|
+
</g>}
|
|
28
|
+
{id === 'magnifier' && <g transform="translate(233 670) rotate(-18) scale(.85) translate(-862 -735)"><g>
|
|
29
|
+
<g>
|
|
30
|
+
<path d="M862 646v112" stroke="#fff" strokeWidth="48" strokeLinecap="round" />
|
|
31
|
+
<path d="M862 646v112" stroke="var(--dca-held-mark, #3161f7)" strokeWidth="28" strokeLinecap="round" />
|
|
32
|
+
<circle cx="862" cy="562" r="82" fill="#fff" stroke="#fff" strokeWidth="30" />
|
|
33
|
+
<circle cx="862" cy="562" r="82" fill="#fff" fillOpacity=".72" stroke="var(--dca-held-mark, #3161f7)" strokeWidth="18" />
|
|
34
|
+
<path d="M828 534c21-22 54-30 82-19" stroke="#fff" strokeWidth="12" strokeLinecap="round" opacity=".9" />
|
|
35
|
+
</g>
|
|
36
|
+
</g>
|
|
37
|
+
</g>}
|
|
38
|
+
{id === 'control-panel' && <g transform="translate(233 640) scale(.8) translate(-839 -686)"><g>
|
|
39
|
+
<rect x="714" y="592" width="250" height="190" rx="38" fill="#fff" stroke="#fff" strokeWidth="30" />
|
|
40
|
+
<rect x="714" y="592" width="250" height="190" rx="38" fill="#fff" stroke="var(--dca-held-mark, #3161f7)" strokeWidth="11" />
|
|
41
|
+
<path d="M765 650h148" stroke="#202124" strokeWidth="14" strokeLinecap="round" opacity=".62" />
|
|
42
|
+
<path d="M765 718h148" stroke="#202124" strokeWidth="14" strokeLinecap="round" opacity=".62" />
|
|
43
|
+
<circle cx="818" cy="650" r="23" fill="var(--dca-held-mark, #3161f7)" stroke="#fff" strokeWidth="8" />
|
|
44
|
+
<circle cx="872" cy="718" r="23" fill="var(--dca-held-accent, #f6b500)" stroke="#fff" strokeWidth="8" />
|
|
45
|
+
</g>
|
|
46
|
+
</g>}
|
|
47
|
+
</g>
|
|
48
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useId, type CSSProperties, type JSX, type ReactNode } from 'react'
|
|
2
|
+
import { Faces } from './Faces.js'
|
|
3
|
+
import { mascotPaletteOf } from './palette.js'
|
|
4
|
+
|
|
5
|
+
// Paired arm layers share view-box pivots and paint rules in rig.css.
|
|
6
|
+
|
|
7
|
+
export type MascotExpression = 'neutral' | 'happy' | 'operator' | 'sleep'
|
|
8
|
+
|
|
9
|
+
export type MascotLight = 'default' | 'error' | 'success'
|
|
10
|
+
|
|
11
|
+
interface MascotRobotProps {
|
|
12
|
+
baseFace?: number
|
|
13
|
+
held?: ReactNode
|
|
14
|
+
accessory?: ReactNode
|
|
15
|
+
top?: ReactNode
|
|
16
|
+
surface?: ReactNode
|
|
17
|
+
expression?: MascotExpression
|
|
18
|
+
light?: MascotLight
|
|
19
|
+
size?: number
|
|
20
|
+
className?: string
|
|
21
|
+
style?: CSSProperties
|
|
22
|
+
/**
|
|
23
|
+
* Recolors the body / arm / face-mark gradients from the profile's palette. The antenna
|
|
24
|
+
* deliberately stays brand-yellow so its error/success status semantics survive.
|
|
25
|
+
*/
|
|
26
|
+
avatar?: { palette: number }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mixHex(a: string, b: string, amount: number): string {
|
|
30
|
+
const left = parseHex(a)
|
|
31
|
+
const right = parseHex(b)
|
|
32
|
+
const ratio = Math.max(0, Math.min(1, amount))
|
|
33
|
+
const mix = (from: number, to: number): number => Math.round(from + (to - from) * ratio)
|
|
34
|
+
return `#${toHex(mix(left.r, right.r))}${toHex(mix(left.g, right.g))}${toHex(mix(left.b, right.b))}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseHex(hex: string): { r: number; g: number; b: number } {
|
|
38
|
+
const normalized = hex.trim().replace(/^#/, '')
|
|
39
|
+
return {
|
|
40
|
+
r: Number.parseInt(normalized.slice(0, 2), 16),
|
|
41
|
+
g: Number.parseInt(normalized.slice(2, 4), 16),
|
|
42
|
+
b: Number.parseInt(normalized.slice(4, 6), 16)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toHex(value: number): string {
|
|
47
|
+
return value.toString(16).padStart(2, '0')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function MascotRig({
|
|
51
|
+
expression = 'neutral',
|
|
52
|
+
light = 'default',
|
|
53
|
+
size = 48,
|
|
54
|
+
className,
|
|
55
|
+
style,
|
|
56
|
+
avatar, top, surface, baseFace, accessory, held
|
|
57
|
+
}: MascotRobotProps): JSX.Element {
|
|
58
|
+
const uid = useId().replace(/:/g, '')
|
|
59
|
+
const blue = `dca-part-blue-${uid}`
|
|
60
|
+
const blueMark = `dca-part-blue-mark-${uid}`
|
|
61
|
+
const yellow = `dca-part-yellow-${uid}`
|
|
62
|
+
const softShadow = `dca-part-soft-shadow-${uid}`
|
|
63
|
+
const innerLift = `dca-part-inner-lift-${uid}`
|
|
64
|
+
const laptopClip = `dca-part-laptop-clip-${uid}`
|
|
65
|
+
const lightFill =
|
|
66
|
+
light === 'error' ? 'var(--dca-error, #dc2626)' : light === 'success' ? 'var(--dca-success, #16a34a)' : `url(#${yellow})`
|
|
67
|
+
const glowFill = light === 'error' ? 'var(--dca-error, #dc2626)' : light === 'success' ? 'var(--dca-success, #16a34a)' : '#f6b500'
|
|
68
|
+
|
|
69
|
+
const palette = mascotPaletteOf(avatar)
|
|
70
|
+
const body0 = palette.bodyD
|
|
71
|
+
const body1 = palette.bodyM
|
|
72
|
+
const body2 = palette.bodyL
|
|
73
|
+
const mark0 = palette.markD
|
|
74
|
+
const mark1 = palette.markM
|
|
75
|
+
const mark2 = palette.markL
|
|
76
|
+
const softShadowColor = palette.shadow
|
|
77
|
+
const innerLiftColor = avatar ? palette.shadow : '#163a88'
|
|
78
|
+
const raisedArmLeft = avatar ? mixHex(palette.bodyD, palette.bodyM, 0.22) : '#3161f7'
|
|
79
|
+
const raisedArmRight = avatar ? mixHex(palette.bodyM, palette.bodyL, 0.56) : '#7a96fb'
|
|
80
|
+
const propMark = avatar ? palette.markD : '#3161f7'
|
|
81
|
+
const laptopLine = palette.markL
|
|
82
|
+
const svgStyle = {
|
|
83
|
+
'--dca-held-mark': `url(#${blueMark})`,
|
|
84
|
+
'--dca-held-accent': `url(#${yellow})`,
|
|
85
|
+
'--dca-part-body-paint': `url(#${blue})`,
|
|
86
|
+
'--dca-part-raised-arm-left': raisedArmLeft,
|
|
87
|
+
'--dca-part-raised-arm-right': raisedArmRight,
|
|
88
|
+
'--dca-part-shadow-color': softShadowColor,
|
|
89
|
+
overflow: 'visible',
|
|
90
|
+
...style
|
|
91
|
+
} as CSSProperties
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<svg
|
|
95
|
+
width={size}
|
|
96
|
+
height={size}
|
|
97
|
+
viewBox="0 0 1024 1024"
|
|
98
|
+
fill="none"
|
|
99
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
100
|
+
className={className ? `dca-part-robot ${className}` : 'dca-part-robot'}
|
|
101
|
+
data-expression={expression}
|
|
102
|
+
style={svgStyle}
|
|
103
|
+
aria-hidden="true"
|
|
104
|
+
>
|
|
105
|
+
<defs>
|
|
106
|
+
<linearGradient className="dca-paint-body" id={blue} x1="279" y1="766" x2="736" y2="334" gradientUnits="userSpaceOnUse">
|
|
107
|
+
<stop offset="0" stopColor={body0} />
|
|
108
|
+
<stop offset=".46" stopColor={body1} />
|
|
109
|
+
<stop offset="1" stopColor={body2} />
|
|
110
|
+
</linearGradient>
|
|
111
|
+
<linearGradient className="dca-paint-mark" id={blueMark} x1="380" y1="696" x2="492" y2="557" gradientUnits="userSpaceOnUse">
|
|
112
|
+
<stop offset="0" stopColor={mark0} />
|
|
113
|
+
<stop offset=".55" stopColor={mark1} />
|
|
114
|
+
<stop offset="1" stopColor={mark2} />
|
|
115
|
+
</linearGradient>
|
|
116
|
+
<linearGradient id={yellow} x1="481" y1="174" x2="617" y2="713" gradientUnits="userSpaceOnUse">
|
|
117
|
+
<stop offset="0" stopColor="#ffcf11" />
|
|
118
|
+
<stop offset="1" stopColor="#f6b500" />
|
|
119
|
+
</linearGradient>
|
|
120
|
+
<filter id={softShadow} x="-12%" y="-12%" width="124%" height="124%">
|
|
121
|
+
<feDropShadow dx="0" dy="18" stdDeviation="24" floodColor={softShadowColor} floodOpacity=".18" />
|
|
122
|
+
</filter>
|
|
123
|
+
<filter id={innerLift} x="-8%" y="-8%" width="116%" height="116%">
|
|
124
|
+
<feDropShadow dx="0" dy="10" stdDeviation="16" floodColor={innerLiftColor} floodOpacity=".1" />
|
|
125
|
+
</filter>
|
|
126
|
+
<clipPath id={laptopClip}>
|
|
127
|
+
<rect x="358" y="716" width="308" height="128" rx="8" />
|
|
128
|
+
</clipPath>
|
|
129
|
+
</defs>
|
|
130
|
+
|
|
131
|
+
<g transform="translate(512 528) scale(1.3) translate(-512 -512)">
|
|
132
|
+
<g filter={`url(#${softShadow})`}>
|
|
133
|
+
<rect x="201" y="365" width="622" height="513" rx="151" fill="#fff" />
|
|
134
|
+
{/* Centered on the blue band (233 / 791), not offset toward the body as the
|
|
135
|
+
brand asset draws it, so the outline survives the raised-arm rotation. */}
|
|
136
|
+
<rect className="dca-part-arm-l-w" x="165" y="472" width="136" height="256" rx="58" fill="#fff" />
|
|
137
|
+
<rect className="dca-part-arm-r-w" x="723" y="472" width="136" height="256" rx="58" fill="#fff" />
|
|
138
|
+
{!top && <rect x="438" y="270" width="148" height="182" rx="2" fill="#fff" />}
|
|
139
|
+
{!top && <circle className="dca-part-antenna-w" cx="512" cy="229" r="116" fill="#fff" />}
|
|
140
|
+
</g>
|
|
141
|
+
|
|
142
|
+
<g filter={`url(#${innerLift})`}>
|
|
143
|
+
<rect x="243" y="408" width="538" height="426" rx="113" fill={`url(#${blue})`} />
|
|
144
|
+
<rect className="dca-part-arm-r-b" x="746" y="514" width="90" height="171" rx="19" fill={`url(#${blue})`} />
|
|
145
|
+
{!top && <rect x="479" y="337" width="66" height="119" rx="6" fill={`url(#${blue})`} />}
|
|
146
|
+
</g>
|
|
147
|
+
|
|
148
|
+
{surface}
|
|
149
|
+
<rect x="295" y="464" width="434" height="315" rx="78" fill="#fff" />
|
|
150
|
+
{!top && <circle className="dca-part-glow" cx="512" cy="229" r="96" fill={glowFill} />}
|
|
151
|
+
{top ?? <circle className="dca-part-light" cx="512" cy="229" r="73" fill={lightFill} />}
|
|
152
|
+
|
|
153
|
+
<g className="dca-part-arm-l">
|
|
154
|
+
<rect className="dca-part-arm-l-b" x="188" y="514" width="90" height="171" rx="19" fill={`url(#${blue})`} filter={`url(#${innerLift})`} />
|
|
155
|
+
{held}
|
|
156
|
+
</g>
|
|
157
|
+
{accessory}
|
|
158
|
+
<g className="dca-face-motion"><Faces baseFace={baseFace} mark={`url(#${blueMark})`} accent={`url(#${yellow})`} /></g>
|
|
159
|
+
|
|
160
|
+
{/* A white frame would melt into the white face screen behind it, so the lid
|
|
161
|
+
is dark with a white stroke ring. */}
|
|
162
|
+
<g className="dca-part-prop-laptop" transform="rotate(-2.5 512 800)">
|
|
163
|
+
<g filter={`url(#${softShadow})`}>
|
|
164
|
+
<rect x="338" y="698" width="348" height="164" rx="16" fill="#1d2433" stroke="#fff" strokeWidth="20" />
|
|
165
|
+
<rect x="300" y="862" width="424" height="26" rx="13" fill="#fff" />
|
|
166
|
+
</g>
|
|
167
|
+
<g clipPath={`url(#${laptopClip})`}>
|
|
168
|
+
<g className="dca-part-laptop-lines" strokeLinecap="round" strokeWidth="16" fill="none">
|
|
169
|
+
<path d="M382 744h118" stroke={laptopLine} />
|
|
170
|
+
<path d="M382 780h170" stroke="#5fd3a6" />
|
|
171
|
+
<path d="M382 816h84" stroke={laptopLine} opacity="0.75" />
|
|
172
|
+
<path className="dca-part-laptop-caret" d="M478 816h30" stroke="#ffcf11" />
|
|
173
|
+
<path d="M382 852h140" stroke="#5fd3a6" opacity="0.8" />
|
|
174
|
+
<path d="M382 888h96" stroke={laptopLine} />
|
|
175
|
+
</g>
|
|
176
|
+
</g>
|
|
177
|
+
</g>
|
|
178
|
+
|
|
179
|
+
{/* Anchored to the landed hand tip of the raised right arm
|
|
180
|
+
(translate(-48,88) rotate(-128°) scaleY(0.8) → tip ≈ 878,497), so the pole
|
|
181
|
+
overlaps the hand. tokens.css sequences arm-first-in / sign-first-out. */}
|
|
182
|
+
<g className="dca-part-prop-sign" filter={`url(#${softShadow})`}>
|
|
183
|
+
<rect x="866" y="356" width="24" height="150" rx="12" fill="#fff" />
|
|
184
|
+
<rect x="758" y="190" width="240" height="170" rx="26" fill="#fff" />
|
|
185
|
+
<path d="M843 247a37 37 0 0 1 70 12c0 24-36 36-36 36" stroke={propMark} strokeWidth="26" fill="none" strokeLinecap="round" />
|
|
186
|
+
<circle cx="878" cy="343" r="15" fill={propMark} />
|
|
187
|
+
</g>
|
|
188
|
+
</g>
|
|
189
|
+
</svg>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PrimaryId } from './appearanceModel.js'
|
|
2
|
+
import { Detail, Silhouette as S } from './DecorationShapes.js'
|
|
3
|
+
|
|
4
|
+
export function ObjectDecoration({ id }: { id: PrimaryId }) {
|
|
5
|
+
switch (id) {
|
|
6
|
+
case 'poop': return <>
|
|
7
|
+
<S d="M393 315c-17-37 10-65 57-67-12-33 15-48 39-53 21-5 29-17 27-35 52 20 69 43 55 75 47 0 72 35 52 73 70 9 81 92 8 92H389c-67 0-62-77 4-85Z" fill="#a97b5f" />
|
|
8
|
+
<path d="M397 313c64 19 157 22 227-5-9 32-171 60-227 5Zm53-65c35 16 80 15 121-13-1 31-82 49-121 13Z" fill="#805840" />
|
|
9
|
+
<Detail><path d="M391 365h142M468 290h37" stroke="#c49976" strokeWidth="14" strokeLinecap="round" /></Detail>
|
|
10
|
+
</>
|
|
11
|
+
case 'banana': return <>
|
|
12
|
+
<S d="M350 243c15 80 71 93 143 65 59-24 100-64 131-119l11-23 32 8-5 25c8 89-61 185-164 202-100 17-172-45-174-135l-7-21 24-15Z" fill="#f3cf62" />
|
|
13
|
+
<path d="M350 243c15 80 71 93 143 65 59-24 100-64 131-119-28 112-139 176-204 145-37-18-58-49-70-91Z" fill="#dbaa40" />
|
|
14
|
+
<path d="m624 189 11-23 32 8-5 25Zm-300 77-7-21 24-15 9 13 5 26Z" fill="#89705a" />
|
|
15
|
+
<Detail><path d="M422 375c81 23 170-30 209-107" stroke="#ffe7a0" strokeWidth="12" strokeLinecap="round" fill="none" /></Detail>
|
|
16
|
+
</>
|
|
17
|
+
case 'fried-egg': return <>
|
|
18
|
+
<S d="M343 330c-32-39 8-82 54-76 33-47 99-37 124-12 73-34 130 5 137 37 53 0 71 60 32 80-4 37-58 44-91 39H427c-39 6-101-25-84-68Z" fill="#fff9e8" />
|
|
19
|
+
<path d="M344 351c40 33 121 28 168 26 76 3 129 1 178-18-4 37-58 44-91 39H427c-34 5-87-18-83-47Z" fill="#e6d9bb" />
|
|
20
|
+
<ellipse cx="513" cy="299" rx="72" ry="56" fill="#f3be43" />
|
|
21
|
+
<Detail><path d="M475 283c9-14 24-18 38-18" stroke="#ffe39a" strokeWidth="13" strokeLinecap="round" /></Detail>
|
|
22
|
+
</>
|
|
23
|
+
case 'rubber-duck': return <>
|
|
24
|
+
<S d="M403 277c-13-10-20-29-17-47 8-57 94-71 121-21 16 28 5 52-5 64 42 17 83 19 115 4l39-25c13-8 22 0 16 15l-20 49c16 51-24 87-83 87H460c-58 0-89-47-57-85l-40-8c-24-5-28-21-3-27Z" fill="#f6d05e" />
|
|
25
|
+
<path d="M416 354c47 32 184 26 236-38 16 51-24 87-83 87H460c-38 0-65-20-66-44Z" fill="#e4ae3e" />
|
|
26
|
+
<path d="m407 277-47 6c-25 6-21 22 3 27l44 8c20-2 20-35 0-41Z" fill="#e58b4b" />
|
|
27
|
+
<circle cx="431" cy="246" r="9" fill="#6e563e" />
|
|
28
|
+
<Detail><path d="M500 318c16-8 51-8 63 7-11 22-39 27-63 16" stroke="#e4ae3e" strokeWidth="12" strokeLinecap="round" fill="none" /></Detail>
|
|
29
|
+
</>
|
|
30
|
+
case 'paper-boat': return <>
|
|
31
|
+
<S d="m330 297 74 0 102-115 118 115h70l-84 102H414Z" fill="#d6e7eb" />
|
|
32
|
+
<path d="m404 297 102-115 7 115Z" fill="#f1f7f5" /><path d="m506 182 118 115H513Z" fill="#92b5c1" />
|
|
33
|
+
<path d="m330 297 182 50 182-50-84 102H414Z" fill="#92b5c1" /><path d="m330 297 182 50-98 52Z" fill="#d6e7eb" />
|
|
34
|
+
<Detail><path d="m512 347 98 52" stroke="#7a9eac" strokeWidth="6" /></Detail>
|
|
35
|
+
</>
|
|
36
|
+
case 'traffic-cone': return <>
|
|
37
|
+
<S d="m485 211-80 151h-44c-18 0-21 37 0 37h302c21 0 18-37 0-37h-44l-80-151c-8-15-46-15-54 0Z" fill="#ed985f" />
|
|
38
|
+
<path d="m445 287 134 0 22 41H423Z" fill="#fff0dd" /><path d="m519 204 100 158h-45Z" fill="#c96c43" opacity=".35" />
|
|
39
|
+
<path d="M361 380h302" stroke="#c96c43" strokeWidth="31" strokeLinecap="round" />
|
|
40
|
+
<Detail><path d="m486 239-12 22" stroke="#ffc699" strokeWidth="12" strokeLinecap="round" /></Detail>
|
|
41
|
+
</>
|
|
42
|
+
case 'sprout': return <>
|
|
43
|
+
<S d="M501 399v-91c-93 26-151-28-157-93 76-19 143 12 167 65 13-87 76-117 148-99-8 83-58 130-134 126v92Z" fill="#89b875" />
|
|
44
|
+
<path d="M501 399v-91c-78 21-137-17-154-73 74 49 110 45 164 45 44-8 99-40 145-84-21 78-59 114-131 111v92Z" fill="#537f59" />
|
|
45
|
+
<Detail><path d="m378 236 114 57m49-12 79-69" stroke="#b5d38f" strokeWidth="10" strokeLinecap="round" /></Detail>
|
|
46
|
+
</>
|
|
47
|
+
case 'donut': return <>
|
|
48
|
+
<path d="M512 170a116 116 0 1 1 0 232 116 116 0 0 1 0-232Zm0 74a42 42 0 1 0 0 84 42 42 0 0 0 0-84Z" fill="#dbab70" fillRule="evenodd" stroke="#fff" strokeWidth="18" paintOrder="stroke fill" />
|
|
49
|
+
<path d="M403 322c49 72 169 73 218-4-14 51-56 84-109 84-52 0-94-31-109-80Z" fill="#bf8456" />
|
|
50
|
+
<path d="M396 284c0-153 231-153 232 0-2 23-26 20-33 0-9-27-13 5-31 4-6 0-10-3-10-14-13-47-75-41-83 2-4 19-18 27-25 6-11-32-20 22-39 18-8-1-11-8-11-16Z" fill="#e5a0b3" />
|
|
51
|
+
<Detail><path d="m449 230 12 9m58-41 1 14m55 21-9 10" stroke="#fff0ce" strokeWidth="10" strokeLinecap="round" /></Detail>
|
|
52
|
+
</>
|
|
53
|
+
default: return null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.dca-part-held {
|
|
2
|
+
opacity: 1;
|
|
3
|
+
transition: opacity 150ms ease 460ms;
|
|
4
|
+
}
|
|
5
|
+
.dca-robot:is([data-held-state='stowed'], [data-exiting='true']) .dca-part-held {
|
|
6
|
+
opacity: 0;
|
|
7
|
+
transition: opacity 140ms ease;
|
|
8
|
+
}
|
|
9
|
+
.dca-robot[data-gesture='blink'] .dca-part-caret { animation: none !important; }
|
|
10
|
+
|
|
11
|
+
/* Lift an upright tool beside the face rather than sweeping it across the screen. */
|
|
12
|
+
.dca-robot[data-held-state='holding'] .dca-action-celebrate :is(.dca-part-arm-l-w, .dca-part-arm-l) {
|
|
13
|
+
transform: translate(30px, -65px) rotate(20deg);
|
|
14
|
+
}
|