@dotcraft/avatar 0.7.3 → 0.7.5

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.
Files changed (77) hide show
  1. package/README.md +1 -1
  2. package/dist/BackDecorations.d.ts.map +1 -1
  3. package/dist/BackDecorations.js +100 -6
  4. package/dist/BackDecorations.js.map +1 -1
  5. package/dist/CloudDragon.d.ts +3 -0
  6. package/dist/CloudDragon.d.ts.map +1 -0
  7. package/dist/CloudDragon.js +70 -0
  8. package/dist/CloudDragon.js.map +1 -0
  9. package/dist/DecorationShapes.d.ts +5 -0
  10. package/dist/DecorationShapes.d.ts.map +1 -1
  11. package/dist/DecorationShapes.js +4 -0
  12. package/dist/DecorationShapes.js.map +1 -1
  13. package/dist/Decorations.d.ts.map +1 -1
  14. package/dist/Decorations.js +3 -2
  15. package/dist/Decorations.js.map +1 -1
  16. package/dist/FaceDecorations.d.ts.map +1 -1
  17. package/dist/FaceDecorations.js +105 -5
  18. package/dist/FaceDecorations.js.map +1 -1
  19. package/dist/HandDecorations.d.ts.map +1 -1
  20. package/dist/HandDecorations.js +19 -2
  21. package/dist/HandDecorations.js.map +1 -1
  22. package/dist/HatDecorations.d.ts.map +1 -1
  23. package/dist/HatDecorations.js +1 -0
  24. package/dist/HatDecorations.js.map +1 -1
  25. package/dist/ObjectDecorations.d.ts.map +1 -1
  26. package/dist/ObjectDecorations.js +59 -0
  27. package/dist/ObjectDecorations.js.map +1 -1
  28. package/dist/SkinDecorations.d.ts.map +1 -1
  29. package/dist/SkinDecorations.js +37 -8
  30. package/dist/SkinDecorations.js.map +1 -1
  31. package/dist/accessories.css +4 -2
  32. package/dist/composer/ComposerMascot.d.ts +1 -1
  33. package/dist/composer/ComposerMascot.d.ts.map +1 -1
  34. package/dist/composer/ComposerMascot.js +5 -4
  35. package/dist/composer/ComposerMascot.js.map +1 -1
  36. package/dist/composer/styles/energy.css +23 -3
  37. package/dist/composer/types.d.ts +3 -0
  38. package/dist/composer/types.d.ts.map +1 -1
  39. package/dist/decorationCatalog.d.ts.map +1 -1
  40. package/dist/decorationCatalog.js +91 -0
  41. package/dist/decorationCatalog.js.map +1 -1
  42. package/dist/decorationMotion.d.ts.map +1 -1
  43. package/dist/decorationMotion.js +5 -2
  44. package/dist/decorationMotion.js.map +1 -1
  45. package/dist/effects.css +65 -4
  46. package/dist/items.d.ts +546 -0
  47. package/dist/items.d.ts.map +1 -1
  48. package/dist/items.js +42 -13
  49. package/dist/items.js.map +1 -1
  50. package/dist/paintMaterials.d.ts +36 -0
  51. package/dist/paintMaterials.d.ts.map +1 -1
  52. package/dist/paintMaterials.js +9 -0
  53. package/dist/paintMaterials.js.map +1 -1
  54. package/dist/spine.d.ts +15 -0
  55. package/dist/spine.d.ts.map +1 -0
  56. package/dist/spine.js +53 -0
  57. package/dist/spine.js.map +1 -0
  58. package/package.json +1 -1
  59. package/src/BackDecorations.tsx +250 -5
  60. package/src/CloudDragon.tsx +110 -0
  61. package/src/DecorationShapes.tsx +4 -0
  62. package/src/Decorations.tsx +4 -3
  63. package/src/FaceDecorations.tsx +254 -11
  64. package/src/HandDecorations.tsx +223 -1
  65. package/src/HatDecorations.tsx +8 -0
  66. package/src/ObjectDecorations.tsx +180 -0
  67. package/src/SkinDecorations.tsx +77 -12
  68. package/src/accessories.css +4 -2
  69. package/src/composer/ComposerMascot.tsx +5 -4
  70. package/src/composer/styles/energy.css +23 -3
  71. package/src/composer/types.ts +3 -0
  72. package/src/decorationCatalog.ts +91 -0
  73. package/src/decorationMotion.ts +5 -2
  74. package/src/effects.css +65 -4
  75. package/src/items.ts +43 -13
  76. package/src/paintMaterials.ts +9 -0
  77. package/src/spine.ts +61 -0
@@ -31,6 +31,159 @@ function Ufo() {
31
31
  </g>
32
32
  }
33
33
 
34
+ function MiniVolcano() {
35
+ return <>
36
+ <Glow blur={20} className="dca-fx-pulse"><ellipse cx="512" cy="244" rx="90" ry="48" fill="#ff8a3d" opacity=".75" /></Glow>
37
+ <g fill="#a59c99" stroke="#fff" strokeWidth="10" paintOrder="stroke fill">
38
+ <path className="dca-fx dca-fx-steam" d="M476 228a16 16 0 1 1 32 0a16 16 0 1 1-32 0ZM492 218a22 22 0 1 1 44 0a22 22 0 1 1-44 0ZM521 230a15 15 0 1 1 30 0a15 15 0 1 1-30 0Z" />
39
+ <path className="dca-fx dca-fx-steam" d="M506 178a14 14 0 1 1 28 0a14 14 0 1 1-28 0ZM522 170a15 15 0 1 1 30 0a15 15 0 1 1-30 0Z" style={{ animationDelay: '-.9s' }} />
40
+ <circle className="dca-fx dca-fx-steam" cx="514" cy="134" r="11" style={{ animationDelay: '-1.7s' }} />
41
+ </g>
42
+ <S d="M372 400C416 396 448 340 460 262q2-16 16-16h72q14 0 16 16c12 78 44 134 88 138Z" fill="#564545" />
43
+ <path d="M532 246h16q14 0 16 16c12 78 44 134 88 138h-76c-18-44-32-100-44-154Z" fill="#3f3232" />
44
+ <path d="M460 262q2-16 16-16h72q14 0 16 16l4 28c0 26-16 26-16 0q-10-8-20-2c0 16-14 16-14 0q-12-8-24 0c0 22-16 22-16 0l-20-2Z" fill="#e8451f" />
45
+ <ellipse cx="512" cy="256" rx="42" ry="9" fill="#ffb347" />
46
+ </>
47
+ }
48
+
49
+ const talons = 'M512 360v36M548 358v38M518 399h-28q-8 0-8 8M554 399h-28q-8 0-8 8'
50
+ function PhoenixPerch() {
51
+ return <>
52
+ <Glow blur={24} className="dca-fx-pulse"><ellipse cx="596" cy="252" rx="132" ry="104" fill="#ffb347" opacity=".45" /></Glow>
53
+ <g className="dca-fx-flame" style={{ transformOrigin: '596px 330px' }}>
54
+ <S d="M590 356C640 364 700 330 724 282Q706 300 682 290Q722 248 716 196Q696 222 668 236Q684 184 654 142C644 190 596 236 570 300Z" fill="#ffb02e" />
55
+ <path d="M600 340C640 344 684 320 700 290Q684 296 668 292Q694 256 690 222Q672 244 654 252Q664 212 646 180C636 216 608 250 590 300Z" fill="#ffe27a" />
56
+ </g>
57
+ <g className="dca-fx-flame" style={{ transformOrigin: '466px 226px', animationDelay: '-.21s' }}>
58
+ <S d="M444 230C436 206 446 182 466 168Q468 190 480 196Q496 176 522 174Q506 190 504 200Q518 196 532 204C516 222 500 232 488 236Z" fill="#ffb02e" />
59
+ </g>
60
+ <g fill="none" strokeLinecap="round" strokeLinejoin="round">
61
+ <path d={talons} stroke="#fff" strokeWidth="25" /><path d={talons} stroke="#6b4a36" strokeWidth="11" />
62
+ </g>
63
+ <S d="M424 258a38 38 0 1 1 76 0a38 38 0 1 1-76 0ZM460 304a74 48 14 1 1 144 36a74 48 14 1 1-144-36Z" fill="#ee6a2c" />
64
+ <path d="M488 306c30-22 92-22 136 12l-28 6 18 18-30 2 10 16c-40 4-84-4-106-54Z" fill="#c2412a" />
65
+ <path d="M430 248 396 262 432 274Z" fill="#ffcf4a" stroke="#fff" strokeWidth="10" strokeLinejoin="round" paintOrder="stroke fill" />
66
+ <circle cx="452" cy="252" r="7" fill="#3b2418" />
67
+ </>
68
+ }
69
+
70
+ function SatelliteDish() {
71
+ return <>
72
+ <S d="M472 404v-12q0-14 14-14h12v-72h28v72h12q14 0 14 14v12Z" fill="#8b95a5" />
73
+ <g transform="translate(512 306) rotate(26)">
74
+ <S d="M-150-76A150 34 0 0 1 150-76A150 76 0 0 1-150-76Z" fill="#8b95a5" />
75
+ <ellipse cy="-76" rx="150" ry="34" fill="#dce8f4" />
76
+ <path d="M0-76v-84" stroke="#fff" strokeWidth="30" strokeLinecap="round" /><path d="M0-76v-84" stroke="#8b95a5" strokeWidth="14" strokeLinecap="round" />
77
+ <circle cy="-168" r="24" fill="#f6b500" stroke="#fff" strokeWidth="12" paintOrder="stroke fill" />
78
+ </g>
79
+ </>
80
+ }
81
+
82
+ function WindUpKey() {
83
+ return <>
84
+ <S d="M497 380V256h30v124Z" fill="#c99139" />
85
+ <S d="M478 404v-14q0-14 14-14h40q14 0 14 14v14Z" fill="#c99139" />
86
+ <g className="dca-fx-spin-flat-slow" style={{ transformOrigin: '512px 236px' }}>
87
+ <path d="M494 212h36a70 70 0 1 1 0 48h-36a70 70 0 1 1 0-48ZM400 236a30 30 0 1 0 60 0a30 30 0 1 0-60 0ZM564 236a30 30 0 1 0 60 0a30 30 0 1 0-60 0Z" fill="#efc65c" fillRule="evenodd" stroke="#fff" strokeWidth="18" strokeLinejoin="round" paintOrder="stroke fill" />
88
+ </g>
89
+ </>
90
+ }
91
+
92
+ const crabLegs = 'M436 354 400 366 388 404M458 370 434 382 428 406M588 354 624 366 636 404M566 370 590 382 596 406'
93
+ const crabClaw = (x: number, flip: number) => <g transform={`translate(${x} 238) scale(${flip} 1) rotate(-14)`}>
94
+ <path d="M14 90 0 18" stroke="#fff" strokeWidth="42" strokeLinecap="round" /><path d="M14 90 0 18" stroke="#e8654f" strokeWidth="24" strokeLinecap="round" />
95
+ <S d="M-34-6C-68-44-60-98-16-116C-28-84-26-58 0-36Z" fill="#e8654f" />
96
+ <g className="dca-fx-swing" style={{ transformOrigin: '0px -8px' }}><S d="M26-20C50-44 48-80 22-100C16-74 10-58-8-40Z" fill="#e8654f" /></g>
97
+ <S d="M-38 0a38 38 0 1 1 76 0a38 38 0 1 1-76 0Z" fill="#e8654f" />
98
+ </g>
99
+ function Crab() {
100
+ return <>
101
+ <g fill="none" strokeLinecap="round" strokeLinejoin="round"><path d={crabLegs} stroke="#fff" strokeWidth="30" /><path d={crabLegs} stroke="#c24a3a" strokeWidth="14" /></g>
102
+ {crabClaw(404, 1)}{crabClaw(620, -1)}
103
+ <S d="M404 330a108 54 0 1 1 216 0a108 54 0 1 1-216 0Z" fill="#e8654f" />
104
+ <path d="M408 344c36 28 172 28 208 0-10 24-52 40-104 40s-94-16-104-40Z" fill="#c24a3a" />
105
+ <g stroke="#fff" strokeWidth="10" paintOrder="stroke fill"><circle cx="484" cy="300" r="15" fill="#fff4ef" /><circle cx="540" cy="300" r="15" fill="#fff4ef" /></g>
106
+ <g fill="#2b2f3a"><circle cx="486" cy="301" r="7" /><circle cx="542" cy="301" r="7" /></g>
107
+ </>
108
+ }
109
+
110
+ function Cupcake() {
111
+ return <>
112
+ <S d="M499 236v-62a13 13 0 0 1 26 0v62Z" fill="#7fc7d9" />
113
+ <g className="dca-fx-flame" style={{ transformOrigin: '512px 166px' }}>
114
+ <S d="M512 116c13 15 19 25 19 35a19 19 0 0 1-38 0c0-10 6-20 19-35Z" fill="#ffb347" stroke={12} />
115
+ <path d="M512 136c6 8 9 13 9 18a9 9 0 0 1-18 0c0-5 3-10 9-18Z" fill="#ffe08a" />
116
+ </g>
117
+ <S d="M414 328h196l-24 76H438Z" fill="#7fc7d9" />
118
+ <Detail><path d="M466 342l5 50M512 342v52M558 342l-5 50" stroke="#5aa6ba" strokeWidth="11" strokeLinecap="round" /></Detail>
119
+ <S d="M422 296h180a28 28 0 0 1 0 56H422a28 28 0 0 1 0-56ZM448 256h128a26 26 0 0 1 0 52H448a26 26 0 0 1 0-52ZM476 222h72a22 22 0 0 1 0 44h-72a22 22 0 0 1 0-44Z" fill="#f2a0b4" />
120
+ <path d="M394 324a28 28 0 0 0 28 28h180a28 28 0 0 0 28-28c-34 13-202 13-236 0Z" fill="#e07f99" />
121
+ </>
122
+ }
123
+
124
+ const cog = (cx: number, cy: number, teeth: number, pitch: number, phase: number) => {
125
+ const outer = pitch + 13, root = pitch - 13
126
+ const at = (r: number, deg: number) => `${(cx + r * Math.cos(deg * Math.PI / 180)).toFixed(1)} ${(cy + r * Math.sin(deg * Math.PI / 180)).toFixed(1)}`
127
+ const tip = 11 / outer * 180 / Math.PI, base = 15 / root * 180 / Math.PI
128
+ return `M${Array.from({ length: teeth }, (_, k) => {
129
+ const a = phase + k * 360 / teeth
130
+ return [at(root, a - base), at(outer, a - tip), at(outer, a + tip), at(root, a + base)].join('L')
131
+ }).join('L')}Z`
132
+ }
133
+ // Both cogs rest on a tooth tip; the offset centers and 51° phase keep the 8- and 6-tooth cogs meshed at a 6:4.5s turn.
134
+ function Gears() {
135
+ return <>
136
+ <g className="dca-fx-rotate-fast" style={{ transformOrigin: '417px 291px' }}>
137
+ <S d={cog(417, 291, 8, 96, 0)} fill="#8b95a5" />
138
+ <circle cx="417" cy="291" r="32" fill="#f6b500" stroke="#fff" strokeWidth="10" paintOrder="stroke fill" />
139
+ </g>
140
+ <g className="dca-fx-rotate-fast" style={{ transformOrigin: '583px 317px', animationDirection: 'reverse', animationDuration: '4.5s' }}>
141
+ <S d={cog(583, 317, 6, 72, 51)} fill="#a7b1c0" />
142
+ <circle cx="583" cy="317" r="25" fill="#f6b500" stroke="#fff" strokeWidth="10" paintOrder="stroke fill" />
143
+ </g>
144
+ </>
145
+ }
146
+
147
+ const raindrop = 'M0-24C6-12 14-2 14 8A14 14 0 0 1-14 8C-14-2-6-12 0-24Z'
148
+ const rain: [number, number, string][] = [[446, 312, '0s'], [446, 362, '-1.3s'], [512, 326, '-.6s'], [512, 372, '-1.9s'], [578, 308, '-1s'], [578, 358, '-2.2s']]
149
+ const cloud = 'M404 218h216a36 36 0 0 1 0 72H404a36 36 0 0 1 0-72ZM396 208a50 50 0 1 1 100 0a50 50 0 1 1-100 0ZM466 186a64 64 0 1 1 128 0a64 64 0 1 1-128 0ZM570 222a44 44 0 1 1 88 0a44 44 0 1 1-88 0Z'
150
+ function RainCloud() {
151
+ const clip = useClipId()
152
+ return <>
153
+ <path d="M446 260V386M512 260V386M578 260V386" stroke="#4f7cf6" strokeOpacity=".35" strokeWidth="28" strokeLinecap="round" />
154
+ <g fill="#4f7cf6" stroke="#fff" strokeWidth="8" paintOrder="stroke fill">
155
+ {rain.map(([x, y, delay]) => <g key={`${x}-${y}`} transform={`translate(${x} ${y})`}><path className="dca-fx dca-fx-steam" d={raindrop} style={{ animationDirection: 'reverse', animationDelay: delay }} /></g>)}
156
+ </g>
157
+ <g className="dca-fx-hover">
158
+ <defs><clipPath id={clip}><path d={cloud} /></clipPath></defs>
159
+ <S d={cloud} fill="#dfe5ee" />
160
+ <path d={cloud} transform="translate(10 16)" fill="#9aa6b8" clipPath={`url(#${clip})`} />
161
+ </g>
162
+ </>
163
+ }
164
+
165
+ function ManekiNeko() {
166
+ const clip = useClipId()
167
+ const body = 'M408 404c-10-60 10-104 48-124h112c38 20 58 64 48 124ZM412 232a100 84 0 1 1 200 0a100 84 0 1 1-200 0Z'
168
+ return <>
169
+ <defs><clipPath id={clip}><path d={body} /></clipPath></defs>
170
+ <Glow blur={24} className="dca-fx-pulse"><ellipse cx="530" cy="270" rx="150" ry="136" fill="#ffd970" opacity=".55" /></Glow>
171
+ <g className="dca-fx-swing" style={{ transformOrigin: '588px 316px' }}>
172
+ <path d="M588 316 630 214" stroke="#fff" strokeWidth="76" strokeLinecap="round" /><path d="M588 316 630 214" stroke="#efc65c" strokeWidth="58" strokeLinecap="round" />
173
+ <S d="M596 194a38 38 0 1 1 76 0a38 38 0 1 1-76 0Z" fill="#efc65c" />
174
+ <circle cx="634" cy="198" r="15" fill="#fff3c4" />
175
+ </g>
176
+ <S d="M424 196 436 116 490 160ZM600 196 588 116 534 160Z" fill="#efc65c" />
177
+ <S d={body} fill="#efc65c" />
178
+ <path d="M440 172 446 138 474 160ZM584 172 578 138 550 160Z" fill="#e8654f" />
179
+ <path d="M430 300c40 22 124 22 164 0v24c-40 22-124 22-164 0Z" fill="#e8654f" />
180
+ <circle cx="512" cy="338" r="15" fill="#f6b500" stroke="#fff" strokeWidth="8" paintOrder="stroke fill" />
181
+ <path d="M470 232q14-16 28 0M526 232q14-16 28 0" stroke="#6e4a2e" strokeWidth="10" strokeLinecap="round" fill="none" />
182
+ <path d="M504 254h16l-8 9Z" fill="#e8654f" />
183
+ <g clipPath={`url(#${clip})`}><g transform="translate(520 0) scale(.3 1)"><g className="dca-fx dca-fx-sheen" fill="#fff"><path d="M70 100h110L30 420H-80Z" opacity=".5" /></g></g></g>
184
+ </>
185
+ }
186
+
34
187
  export function ObjectDecoration({ id }: { id: HeadId }) {
35
188
  switch (id) {
36
189
  case 'ringed-planet': return <RingedPlanet />
@@ -69,6 +222,8 @@ export function ObjectDecoration({ id }: { id: HeadId }) {
69
222
  <S d="M512 200 560 320 512 404 464 320Z" fill="#8b5cf6" /><path d="M512 200 536 320 512 404Z" fill="#c4b5fd" />
70
223
  </>
71
224
  case 'ufo': return <Ufo />
225
+ case 'mini-volcano': return <MiniVolcano />
226
+ case 'phoenix-perch': return <PhoenixPerch />
72
227
  case 'poop': return <>
73
228
  <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" />
74
229
  <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" />
@@ -116,6 +271,31 @@ export function ObjectDecoration({ id }: { id: HeadId }) {
116
271
  <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" />
117
272
  <Detail><path d="m449 230 12 9m58-41 1 14m55 21-9 10" stroke="#fff0ce" strokeWidth="10" strokeLinecap="round" /></Detail>
118
273
  </>
274
+ // The bottom edge slopes 8° so the rotation lands it flat on the top edge.
275
+ case 'floppy-disk': return <g transform="rotate(-8 512 404)">
276
+ <S d="M388 176h234l30 30v218l-280-40V192c0-9 7-16 16-16Z" fill="#3c4658" />
277
+ <path d="M440 176h156v94c0 6-4 10-10 10H450c-6 0-10-4-10-10Z" fill="#a7b1c0" /><rect x="400" y="302" width="224" height="62" rx="10" fill="#fff" />
278
+ <Detail><rect x="548" y="196" width="30" height="64" rx="6" fill="#3c4658" /></Detail>
279
+ </g>
280
+ case 'rocket': return <>
281
+ <g className="dca-fx dca-fx-flame" style={{ transformOrigin: '512px 330px' }}>
282
+ <path d="M512 398c-24-18-34-40-30-68h60c4 28-6 50-30 68Z" fill="#ffb347" /><path d="M512 380c-12-14-16-30-12-50h24c4 20 0 36-12 50Z" fill="#ffe08a" />
283
+ </g>
284
+ <S d="M446 248c-34 20-52 56-54 100v44c0 8 8 11 14 6l40-40Zm132 0c34 20 52 56 54 100v44c0 8-8 11-14 6l-40-40Z" fill="#e8654f" />
285
+ <S d="M512 128c48 28 68 86 68 154v36c0 7-5 12-12 12H456c-7 0-12-5-12-12v-36c0-68 20-126 68-154Z" fill="#dce8f4" />
286
+ <path d="M512 128c29 17 48 44 58 78H454c10-34 29-61 58-78Z" fill="#e8654f" />
287
+ <Detail><circle cx="512" cy="258" r="26" fill="#3c4658" /></Detail>
288
+ </>
289
+ case 'satellite-dish': return <SatelliteDish />
290
+ case 'cursor-arrow': return <g transform="translate(540 400) scale(1.15 -1.15)">
291
+ <S d="M0 0V176L44 136 74 200 104 186 74 124H132Z" fill="#2b2f3a" stroke={20} /><path d="M0 0 74 124H132Z" fill="#5b6577" />
292
+ </g>
293
+ case 'wind-up-key': return <WindUpKey />
294
+ case 'crab': return <Crab />
295
+ case 'cupcake': return <Cupcake />
296
+ case 'gears': return <Gears />
297
+ case 'rain-cloud': return <RainCloud />
298
+ case 'maneki-neko': return <ManekiNeko />
119
299
  default: return null
120
300
  }
121
301
  }
@@ -4,14 +4,33 @@ import type { BodyPaint } from './MascotRig.js'
4
4
  import { isPaintSkin, paintMaterials, type PaintSkinId } from './paintMaterials.js'
5
5
  import { materialBounds } from './RigMaterial.js'
6
6
 
7
- const dots = [[280, 440], [372, 432], [464, 446], [556, 432], [648, 446], [740, 438], [268, 560], [758, 530], [262, 680], [762, 650], [292, 806], [400, 812], [512, 800], [624, 812], [732, 806], [268, 760]]
8
7
  const star4 = (r: number) => `M0 ${-r}l${r * .28} ${r * .72}l${r * .72} ${r * .28}l${-r * .72} ${r * .28}l${-r * .28} ${r * .72}l${-r * .28} ${-r * .72}l${-r * .72} ${-r * .28}l${r * .72} ${-r * .28}Z`
8
+ const ray = (deg: number, spread: number) => {
9
+ const at = (a: number) => `${(512 + 640 * Math.cos(a * Math.PI / 180)).toFixed(0)} ${(621 + 640 * Math.sin(a * Math.PI / 180)).toFixed(0)}`
10
+ return `M512 621L${at(deg - spread / 2)}L${at(deg + spread / 2)}Z`
11
+ }
9
12
 
10
13
  export function SkinOverlay({ id }: { id: SkinId }) {
11
14
  switch (id) {
12
15
  case 'stripes': return <g fill="#fff" opacity=".34" data-skin-overlay={id}>
13
16
  {[60, 250, 440, 630].map(x => <path key={x} d={`M${x} 834 L${x + 220} 408 h80 L${x + 80} 834Z`} />)}
14
17
  </g>
18
+ case 'split': return <rect x="512" width="512" height="1024" fill="#fff" opacity=".34" data-skin-overlay={id} />
19
+ case 'hoops': return <g fill="#fff" opacity=".34" data-skin-overlay={id}>{[408, 558, 708].map(y => <rect key={y} y={y} width="1024" height="56" />)}</g>
20
+ case 'sunburst': return <g fill="#fff" opacity=".3" data-skin-overlay={id}>{Array.from({ length: 12 }, (_, index) => <path key={index} d={ray(index * 30, 15)} />)}</g>
21
+ case 'dipped': return <rect y="600" width="1024" height="424" fill="var(--dca-part-shadow-color, #0b1020)" opacity=".4" data-skin-overlay={id} />
22
+ case 'pinwheel': return <g fill="#fff" opacity=".3" data-skin-overlay={id}>
23
+ <g className="dca-fx-pinwheel" style={{ transformOrigin: '512px 621px' }}>{[45, 135, 225, 315].map(deg => <path key={deg} d={ray(deg, 45)} />)}</g>
24
+ </g>
25
+ case 'quartered': return <path d="M512 0H1024V600H512ZM0 600H512V1024H0Z" fill="#fff" opacity=".34" data-skin-overlay={id} />
26
+ case 'bowtie': return <path d={`${ray(0, 90)}${ray(180, 90)}`} fill="#fff" opacity=".34" data-skin-overlay={id} />
27
+ // Each ring is 1.793x the one inside it, the end scale of dca-fx-sonar, so the loop hands every ring to the next.
28
+ case 'sonar': return <g fill="none" stroke="#fff" opacity=".34" data-skin-overlay={id}>
29
+ <g className="dca-fx-sonar" style={{ transformOrigin: '512px 621px' }}>
30
+ {[[98, 28], [175.5, 51], [314.5, 91]].map(([r, width]) => <circle key={r} cx="512" cy="621" r={r} strokeWidth={width} />)}
31
+ </g>
32
+ </g>
33
+ case 'tide': return <path className="dca-fx-tide" d={`M-17 630q50-100 100 0${'t100 0'.repeat(10)}V1400H-17Z`} fill="var(--dca-part-shadow-color, #0b1020)" opacity=".4" data-skin-overlay={id} />
15
34
  default: return null
16
35
  }
17
36
  }
@@ -20,28 +39,75 @@ const sheen = <g className="dca-fx dca-fx-sheen" fill="#fff">
20
39
  <path d="M120 834 L420 408 h58 L178 834Z" opacity=".38" />
21
40
  <path d="M200 834 L500 408 h22 L222 834Z" opacity=".55" />
22
41
  </g>
42
+ const stars = <g fill="#fff">{[[372, 432], [556, 432], [740, 438], [758, 530], [762, 650], [400, 812], [624, 812], [268, 760]].map(([cx, cy], index) => <path key={`${cx}-${cy}`} className="dca-fx dca-fx-node" d={star4(index % 2 ? 10 : 15)} transform={`translate(${cx} ${cy})`} style={{ animationDelay: `${(index * .43) % 1.8}s` }} />)}</g>
43
+ const scanline = <g className="dca-fx dca-fx-scanline" fill="#7dff8a">
44
+ <rect y="250" width="1024" height="110" opacity=".14" />
45
+ <rect y="320" width="1024" height="40" opacity=".6" />
46
+ </g>
47
+ const rainbow = (className: string, opacity?: string) => <g className={`dca-fx dca-fx-sheen ${className}`} opacity={opacity}>
48
+ {['#ff5f6d', '#ffa53a', '#ffe066', '#6ee07a', '#4fb4ff', '#a276ff'].map((fill, k) => <path key={fill} d={`M${60 + 25 * k} 834 L${360 + 25 * k} 408 h25 L${85 + 25 * k} 834Z`} fill={fill} opacity=".75" />)}
49
+ </g>
50
+ const prism = <>{rainbow('dca-fx-prism')}{rainbow('dca-fx-prism-echo', '0')}</>
51
+ const accretion = <g className="dca-fx dca-fx-void" fill="#ffb04a" style={{ transformOrigin: '512px 621px' }}>
52
+ {[[0, .9], [-18, .6], [-36, .35], [-54, .15]].map(([deg, opacity]) => <path key={deg} d={ray(deg, 18)} opacity={opacity} />)}
53
+ </g>
54
+ const effects: Partial<Record<PaintSkinId, ReactNode>> = { chrome: sheen, holographic: sheen, gold: sheen, candy: sheen, galaxy: stars, terminal: scanline, prism, void: accretion }
23
55
 
56
+ const sweep = (skin: string, stops: ReactNode) => (id: string) => <linearGradient id={id} className={`dca-skin-paint dca-skin-${skin}`} x1="279" y1="766" x2="736" y2="334" gradientUnits="userSpaceOnUse">{stops}</linearGradient>
24
57
  const gradients: Record<PaintSkinId, (id: string) => ReactNode> = {
25
- chrome: id => <linearGradient id={id} className="dca-skin-paint dca-skin-chrome" x1="279" y1="766" x2="736" y2="334" gradientUnits="userSpaceOnUse">
58
+ chrome: sweep('chrome', <>
26
59
  <stop offset="0" stopColor="#3c4658" /><stop offset=".36" stopColor="#9aa6b8" /><stop offset=".5" stopColor="#f4f7fb" />
27
60
  <stop offset=".54" stopColor="#6b7688" /><stop offset=".8" stopColor="#c3ccd8" /><stop offset="1" stopColor="#ffffff" />
28
- </linearGradient>,
29
- holographic: id => <linearGradient id={id} className="dca-skin-paint dca-skin-holo" x1="279" y1="766" x2="736" y2="334" gradientUnits="userSpaceOnUse">
61
+ </>),
62
+ holographic: sweep('holo', <>
30
63
  <stop offset="0" stopColor="#ff8ad4" /><stop offset=".25" stopColor="#ffd28a" /><stop offset=".5" stopColor="#c9ff8a" />
31
64
  <stop offset=".75" stopColor="#8ad8ff" /><stop offset="1" stopColor="#c98aff" />
32
- </linearGradient>,
33
- gold: id => <linearGradient id={id} className="dca-skin-paint dca-skin-gold" x1="279" y1="766" x2="736" y2="334" gradientUnits="userSpaceOnUse">
65
+ </>),
66
+ gold: sweep('gold', <>
34
67
  <stop offset="0" stopColor="#b8862c" /><stop offset=".4" stopColor="#f6d365" /><stop offset=".52" stopColor="#fff3c4" />
35
68
  <stop offset=".64" stopColor="#d9a53a" /><stop offset="1" stopColor="#ffe9a8" />
36
- </linearGradient>,
37
- lava: id => <linearGradient id={id} className="dca-skin-paint dca-skin-lava" x1="279" y1="766" x2="736" y2="334" gradientUnits="userSpaceOnUse">
69
+ </>),
70
+ lava: sweep('lava', <>
38
71
  <stop offset="0" stopColor="#4a1208" /><stop offset=".3" stopColor="#e8451f" /><stop offset=".55" stopColor="#ffb347" />
39
72
  <stop offset=".8" stopColor="#e8451f" /><stop offset="1" stopColor="#7a1f0e" />
40
- </linearGradient>,
41
- galaxy: id => <linearGradient id={id} className="dca-skin-paint dca-skin-galaxy" x1="279" y1="766" x2="736" y2="334" gradientUnits="userSpaceOnUse">
73
+ </>),
74
+ galaxy: sweep('galaxy', <>
42
75
  <stop offset="0" stopColor="#1b1040" /><stop offset=".3" stopColor="#4c1d95" /><stop offset=".55" stopColor="#2563eb" />
43
76
  <stop offset=".8" stopColor="#0b3d62" /><stop offset="1" stopColor="#7c3aed" />
77
+ </>),
78
+ bumblebee: id => <linearGradient id={id} className="dca-skin-paint dca-skin-bumblebee" x1="279" y1="766" x2="431" y2="622" gradientUnits="userSpaceOnUse" spreadMethod="repeat">
79
+ <stop offset="0" stopColor="#f6c343" /><stop offset=".5" stopColor="#f6c343" /><stop offset=".5" stopColor="#2b2f3a" /><stop offset="1" stopColor="#2b2f3a" />
80
+ </linearGradient>,
81
+ aurora: sweep('aurora', <>
82
+ <stop offset="0" stopColor="#0a1f33" /><stop offset=".3" stopColor="#1fbf8f" /><stop offset=".55" stopColor="#5eead4" />
83
+ <stop offset=".8" stopColor="#7c6cf0" /><stop offset="1" stopColor="#122046" />
84
+ </>),
85
+ terminal: sweep('terminal', <>
86
+ <stop offset="0" stopColor="#0b0f14" /><stop offset=".55" stopColor="#111a22" /><stop offset="1" stopColor="#1b2a2a" />
87
+ </>),
88
+ patina: sweep('patina', <>
89
+ <stop offset="0" stopColor="#2a8a78" /><stop offset=".38" stopColor="#58b39a" /><stop offset=".5" stopColor="#b8733f" />
90
+ <stop offset=".78" stopColor="#d98e52" /><stop offset="1" stopColor="#f2b27a" />
91
+ </>),
92
+ thermal: sweep('thermal', <>
93
+ <stop offset="0" stopColor="#1a0b5e" /><stop offset=".3" stopColor="#5b1fa8" /><stop offset=".55" stopColor="#c02a6b" />
94
+ <stop offset=".8" stopColor="#ff7a1f" /><stop offset="1" stopColor="#3b1488" />
95
+ </>),
96
+ candy: sweep('candy', <>
97
+ <stop offset="0" stopColor="#2a0309" /><stop offset=".3" stopColor="#7d0a1c" /><stop offset=".55" stopColor="#c8102e" />
98
+ <stop offset=".8" stopColor="#ff4d63" /><stop offset="1" stopColor="#a30c24" />
99
+ </>),
100
+ racer: id => <linearGradient id={id} className="dca-skin-paint dca-skin-racer" x1="0" y1="408" x2="0" y2="834" gradientUnits="userSpaceOnUse">
101
+ <stop offset="0" stopColor="#7cbbe3" /><stop offset=".45" stopColor="#66a9d8" /><stop offset=".45" stopColor="#f28a1c" />
102
+ <stop offset=".73" stopColor="#f28a1c" /><stop offset=".73" stopColor="#5a9fd0" /><stop offset="1" stopColor="#4f94c6" />
44
103
  </linearGradient>,
104
+ prism: sweep('prism', <>
105
+ <stop offset="0" stopColor="#2b2f4f" /><stop offset=".5" stopColor="#4f5580" /><stop offset="1" stopColor="#7b82b0" />
106
+ </>),
107
+ void: id => <radialGradient id={id} className="dca-skin-paint dca-skin-void" cx="512" cy="621" r="345" gradientUnits="userSpaceOnUse">
108
+ <stop offset="0" stopColor="#030206" /><stop offset=".55" stopColor="#06040d" /><stop offset=".72" stopColor="#140a2a" />
109
+ <stop offset=".88" stopColor="#3a1a78" /><stop offset="1" stopColor="#6a3fd0" />
110
+ </radialGradient>,
45
111
  }
46
112
  export function skinPaint(id: SkinId): BodyPaint | undefined {
47
113
  return isPaintSkin(id) ? { render: gradients[id], shadow: paintMaterials[id].shadow } : undefined
@@ -49,8 +115,7 @@ export function skinPaint(id: SkinId): BodyPaint | undefined {
49
115
  export function SkinPaintSurface({ id }: { id: SkinId }) {
50
116
  if (!isPaintSkin(id)) return null
51
117
  return <g data-skin-overlay={id}>
52
- {id !== 'galaxy' && id !== 'lava' && sheen}
53
- {id === 'galaxy' && <g fill="#fff">{dots.filter((_, index) => index % 2 === 1).map(([cx, cy], index) => <path key={`${cx}-${cy}`} className="dca-fx dca-fx-node" d={star4(index % 2 ? 10 : 15)} transform={`translate(${cx} ${cy})`} style={{ animationDelay: `${(index * .43) % 1.8}s` }} />)}</g>}
118
+ {effects[id]}
54
119
  <rect className="dca-fx dca-skin-energy" {...materialBounds} fill={paintMaterials[id].accent} opacity="0" />
55
120
  </g>
56
121
  }
@@ -1,8 +1,10 @@
1
- .dca-part-held {
1
+ .dca-part-held,
2
+ .dca-part-temple {
2
3
  opacity: 1;
3
4
  transition: opacity 150ms ease 460ms;
4
5
  }
5
- .dca-robot:is([data-held-state='stowed'], [data-exiting='true']) .dca-part-held {
6
+ .dca-robot:is([data-held-state='stowed'], [data-exiting='true']) .dca-part-held,
7
+ .dca-action-hold-sign .dca-part-temple {
6
8
  opacity: 0;
7
9
  transition: opacity 140ms ease;
8
10
  }
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useLayoutEffect, useRef, useState, type AnimationEvent, type CSSProperties } from 'react'
2
- import { Avatar } from '../Avatar.js'
2
+ import { AppearanceAvatar } from '../Avatar.js'
3
3
  import { deriveAppearance, mascotPaletteOf, type AvatarPose } from '../index.js'
4
4
  import { useComposerAvatarBehavior } from './useComposerAvatarBehavior.js'
5
5
  import { consumeMascotHandoff, recordMascotHandoff } from './mascotHandoff.js'
@@ -8,9 +8,10 @@ import { useComposerMotion } from './useComposerMotion.js'
8
8
  import { MASCOT_SIZE, MASCOT_SCALE, MASCOT_HIDDEN_RATIO, MASCOT_RAISE, MASCOT_SLEEP_AFTER_MS, MASCOT_WAVE_DURATION_MS, MASCOT_SPARKLES } from './constants.js'
9
9
  import { useMascotActiveIdle } from './useMascotActiveIdle.js'
10
10
  import type { ComposerMascotProps, ComposerMascotContext, MascotExpression, MascotLight } from './types.js'
11
- export function ComposerMascot({ name, motion = 'system', theme = 'dark', focused = false, dragOver = false, bounceSignal = 0, interaction, reasoningEffort = 'off', speed = 'standard', contextMax = false, anchorOffset = 0, anchorPushSignal = 0, handoff = false, renderCharacter, renderMenu, onNameRendered }: ComposerMascotProps) {
11
+ export function ComposerMascot({ name, appearance, motion = 'system', theme = 'dark', focused = false, dragOver = false, bounceSignal = 0, interaction, reasoningEffort = 'off', speed = 'standard', contextMax = false, anchorOffset = 0, anchorPushSignal = 0, handoff = false, renderCharacter, renderMenu, onNameRendered }: ComposerMascotProps) {
12
12
  const reduced = !useComposerMotion(motion)
13
13
  const { avatar, profileTransition, profileTransitionRevision } = useComposerProfile(name, reduced)
14
+ const look = appearance && !avatar ? appearance : deriveAppearance(avatar ?? '')
14
15
  useEffect(() => { onNameRendered?.(avatar) }, [avatar, onNameRendered])
15
16
  const [menuPos, setMenuPos] = useState<{ x: number; y: number } | null>(null)
16
17
  const [ambientSleeping, setSleeping] = useState(false)
@@ -86,7 +87,7 @@ export function ComposerMascot({ name, motion = 'system', theme = 'dark', focuse
86
87
  reducedMotion: reduced
87
88
  })
88
89
  clearGestureRef.current = avatarBehavior.clearGesture
89
- const mascotPalette = mascotPaletteOf(deriveAppearance(avatar ?? ''))
90
+ const mascotPalette = mascotPaletteOf(look)
90
91
  const activity: ComposerMascotContext["activity"] = light === 'error'
91
92
  ? 'error'
92
93
  : light === 'success'
@@ -316,7 +317,7 @@ export function ComposerMascot({ name, motion = 'system', theme = 'dark', focuse
316
317
  .filter(Boolean)
317
318
  .join(' ') || undefined
318
319
 
319
- const character = <Avatar name={avatar ?? ''} size={MASCOT_SIZE} state={avatarBehavior.pose}
320
+ const character = <AppearanceAvatar appearance={look} size={MASCOT_SIZE} state={avatarBehavior.pose}
320
321
  expression={avatarBehavior.expression} gesture={avatarBehavior.gesture}
321
322
  gestureSequence={avatarBehavior.gestureSequence} onGestureComplete={avatarBehavior.completeGesture}
322
323
  eventSequence={bounceSignal + greetingSequence} motion={reduced ? 'off' : 'on'} />
@@ -92,12 +92,30 @@
92
92
  animation: dca-skin-energy 2.8s ease-in-out infinite;
93
93
  }
94
94
 
95
- [data-mascot-effort='medium'] .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy, .dca-skin-holo) stop { animation-duration: 4.6s; }
96
- [data-mascot-effort='high'] .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy, .dca-skin-holo) stop { animation-duration: 3.2s; }
97
- [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy, .dca-skin-holo) stop { animation-duration: 2.2s; }
95
+ [data-mascot-effort='medium'] .dca-robot[data-effects='live'] .dca-skin-paint stop { animation-duration: 4.6s; }
96
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] .dca-skin-paint stop { animation-duration: 3.2s; }
97
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] .dca-skin-paint stop { animation-duration: 2.2s; }
98
+ :is([data-mascot-effort='high'], [data-mascot-effort='extraHigh'], [data-mascot-context='max']) .dca-robot[data-effects='live'] .dca-skin-thermal stop { animation-name: dca-fx-thermal-hot; }
98
99
  [data-mascot-effort='medium'] .dca-robot[data-effects='live'] .dca-fx-sheen { animation-duration: 3.4s; }
99
100
  [data-mascot-effort='high'] .dca-robot[data-effects='live'] .dca-fx-sheen { animation-duration: 2.4s; }
100
101
  [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] .dca-fx-sheen { animation-duration: 1.6s; }
102
+ [data-mascot-effort='medium'] .dca-robot[data-effects='live'] .dca-fx-scanline { animation-duration: 2.6s; }
103
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] .dca-fx-scanline { animation-duration: 1.8s; }
104
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] .dca-fx-scanline { animation-duration: 1.2s; }
105
+ [data-mascot-effort='medium'] .dca-robot[data-effects='live'] .dca-fx-pinwheel { animation-duration: 16s; }
106
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] .dca-fx-pinwheel { animation-duration: 11s; }
107
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] .dca-fx-pinwheel { animation-duration: 8s; }
108
+ [data-mascot-effort='medium'] .dca-robot[data-effects='live'] .dca-fx-sonar { animation-duration: 2.6s; }
109
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] .dca-fx-sonar { animation-duration: 1.8s; }
110
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] .dca-fx-sonar { animation-duration: 1.2s; }
111
+ [data-mascot-effort='medium'] .dca-robot[data-effects='live'] .dca-fx-tide { animation-duration: 5s; }
112
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] .dca-fx-tide { animation-duration: 3.6s; }
113
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] .dca-fx-tide { animation-duration: 2.6s; }
114
+ [data-mascot-effort='medium'] .dca-robot[data-effects='live'] .dca-fx-void { animation-duration: 9s; }
115
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] .dca-fx-void { animation-duration: 6.5s; }
116
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] .dca-fx-void { animation-duration: 4.5s; }
117
+ :is([data-mascot-effort='high'], [data-mascot-effort='extraHigh'], [data-mascot-context='max']) .dca-robot[data-effects='live'] .dca-fx-void { animation-name: dca-fx-void-hot; }
118
+ :is([data-mascot-effort='extraHigh'], [data-mascot-context='max']) .dca-robot[data-effects='live'] .dca-fx-prism-echo { opacity: 1; }
101
119
  [data-mascot-effort='high'] .dca-robot[data-effects='live'] [data-skin-overlay='galaxy'] .dca-fx-node { animation-duration: 1.1s; }
102
120
  [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] [data-skin-overlay='galaxy'] .dca-fx-node { animation-duration: .7s; }
103
121
 
@@ -111,6 +129,8 @@
111
129
  animation: dca-skin-energy 3.8s ease-in-out infinite;
112
130
  }
113
131
 
132
+ [data-mascot-context='max'] .dca-robot[data-effects='live'] .dca-skin-thermal stop { animation-duration: 3.8s; }
133
+
114
134
  [data-mascot-context='max'] .dca-robot .dca-paint-body stop:nth-child(1) {
115
135
  animation: composer-mascot-max-coat-dark 3.8s ease-in-out infinite;
116
136
  }
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react'
2
+ import type { Appearance } from '../appearanceModel.js'
2
3
  export type MascotExpression = 'neutral' | 'happy' | 'operator' | 'sleep'
3
4
  export type MascotLight = 'default' | 'error' | 'success'
4
5
  export interface ComposerMascotContext {
@@ -14,6 +15,8 @@ export interface ComposerMascotContext {
14
15
  }
15
16
  export interface ComposerMascotProps {
16
17
  name?: string
18
+ /** What the default companion wears; ignored while a name is rendered, which keeps deriving its own look. */
19
+ appearance?: Appearance
17
20
  motion?: 'system' | 'on' | 'off'
18
21
  theme?: 'light' | 'dark'
19
22
  focused?: boolean