clauderipple 0.8.9 → 0.9.0
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/CHANGELOG.md +62 -0
- package/README.es.md +0 -4
- package/README.ja.md +0 -4
- package/README.ko.md +5 -1
- package/README.md +5 -1
- package/README.pt-BR.md +0 -4
- package/README.zh-CN.md +0 -4
- package/dist/app/dist/main.js +23 -3
- package/dist/app/package.json +1 -1
- package/dist/cli/src/codex.js +3 -1
- package/dist/cli/src/index.js +1 -1
- package/dist/cli/src/settings.js +5 -2
- package/dist/cli/src/tray.js +41 -5
- package/dist/mod/.claude-plugin/plugin.json +9 -1
- package/dist/mod/hooks/cache.ts +60 -0
- package/dist/mod/hooks/compact.ts +40 -0
- package/dist/mod/hooks/handoff.ts +43 -0
- package/dist/mod/hooks/i18n.ts +2716 -0
- package/dist/mod/hooks/judge.ts +226 -0
- package/dist/mod/hooks/ladder.js +179 -0
- package/dist/mod/hooks/ladder.ts +174 -0
- package/dist/mod/hooks/modes.ts +93 -0
- package/dist/mod/hooks/register.tsx +1458 -256
- package/dist/mod/hooks/session.ts +62 -0
- package/dist/mod/hooks/settings.js +342 -0
- package/dist/mod/hooks/settings.ts +321 -0
- package/dist/mod/hooks/stats.ts +59 -0
- package/dist/mod/hooks/ui/agents.tsx +138 -0
- package/dist/mod/hooks/ui/alerts.tsx +370 -0
- package/dist/mod/hooks/ui/art.ts +628 -0
- package/dist/mod/hooks/ui/band.tsx +836 -0
- package/dist/mod/hooks/ui/kit.tsx +258 -0
- package/dist/mod/hooks/ui/log.tsx +109 -0
- package/dist/mod/hooks/ui/parts.tsx +217 -0
- package/dist/mod/hooks/ui/ripple.ts +614 -0
- package/dist/mod/hooks/ui/settings.tsx +376 -0
- package/dist/mod/hooks/ui/setup.tsx +148 -0
- package/dist/mod/hooks/ui/theme.ts +217 -0
- package/dist/mod/hooks/ui/view.ts +371 -0
- package/dist/mod/hooks/worker.ts +1 -1
- package/dist/mod/hooks/workers.ts +217 -0
- package/dist/mod/tests/register.test.tsx +1730 -0
- package/dist/mod/tests/ui.test.tsx +853 -0
- package/dist/mod/tsconfig.json +2 -1
- package/dist/mod/types/index.d.ts +320 -1
- package/dist/router/src/admin.js +195 -53
- package/dist/router/src/agents.js +24 -5
- package/dist/router/src/bootstrap.js +4 -2
- package/dist/router/src/compat.js +10 -2
- package/dist/router/src/config.js +3 -0
- package/dist/router/src/health.js +33 -0
- package/dist/router/src/index.js +1 -0
- package/dist/router/src/ingress/models.js +4 -1
- package/dist/router/src/mod-settings.js +119 -0
- package/dist/router/src/model-policy.js +6 -0
- package/dist/router/src/model-scores.js +159 -0
- package/dist/router/src/picker.js +7 -0
- package/dist/router/src/providers/anthropic-account-pool.js +10 -4
- package/dist/router/src/providers/anthropic-accounts.js +26 -1
- package/dist/router/src/providers/chatgpt/catalog.js +45 -14
- package/dist/router/src/providers/chatgpt/index.js +9 -3
- package/dist/router/src/providers/chatgpt/translate.js +9 -5
- package/dist/router/src/providers/google/index.js +6 -2
- package/dist/router/src/providers/openai/index.js +4 -1
- package/dist/router/src/providers/openai/translate.js +1 -1
- package/dist/router/src/proxy.js +14 -3
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +932 -209
- package/dist/ui/favicon-32.png +0 -0
- package/dist/ui/favicon.svg +12 -0
- package/dist/ui/i18n.js +923 -237
- package/dist/ui/index.html +159 -87
- package/dist/ui/style.css +789 -408
- package/docs/ARCHITECTURE.md +134 -2
- package/package.json +7 -3
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
// 리플이 (Ripple), the mod's mascot: a hollow ripple ring with a face, a wave crest on top and two
|
|
2
|
+
// ripple ears, alive wherever it is drawn. Every shape is the approved sheet "리플이 v2 (물마루)"
|
|
3
|
+
// (design-drafts/ripple-char-v2.html and brand/, 2026-10-11) on its 56-unit grid; the motion is ours.
|
|
4
|
+
//
|
|
5
|
+
// What the host allows, and so what this draws (see parts.tsx):
|
|
6
|
+
// - A plain Svg image (never isInteractive) keeps its CSS motion and does not blink when the desktop
|
|
7
|
+
// redraws (seen in the owner's app, 2026-10-11). But each redraw is a new image, so the motion
|
|
8
|
+
// starts over: every loop opens on a natural pose (the rest pose, or a part that is hidden at
|
|
9
|
+
// rest, like a ripple about to spread), and the big moments (a look, a hop, a yawn) come seconds
|
|
10
|
+
// in, so the first second after a redraw is quiet and reads as going on.
|
|
11
|
+
// - A face is one fixed string per state; only the gauge varies, in 5% steps, so a figure that
|
|
12
|
+
// creeps does not make a new picture every redraw.
|
|
13
|
+
// - Idle life is several loops of unrelated lengths (blinks on 7, 11 and 13 s, a look on 13, a hop
|
|
14
|
+
// on 17), so the sum never looks mechanical. One loop per element: blinks nest a group each.
|
|
15
|
+
// - Under prefers-reduced-motion nothing moves: the markup alone is the rest pose. No filters, no
|
|
16
|
+
// external references, no script.
|
|
17
|
+
// - Two details: `full` (34 px up) is the colour sheet, the face in ink; `mini` (below, the band's
|
|
18
|
+
// 20 px) is the mono template the sheet gives for small sizes, every part in the tone, heavier
|
|
19
|
+
// lines, no mouth or brows, its moves a little larger so they read.
|
|
20
|
+
// - Colours are the palette variables svgDoc sets (theme.ts), so light and dark differ by tokens.
|
|
21
|
+
//
|
|
22
|
+
// Pure: no imports at run time, so node tests can load it.
|
|
23
|
+
|
|
24
|
+
import type { Palette } from './theme'
|
|
25
|
+
|
|
26
|
+
export type Face =
|
|
27
|
+
| 'calm' | 'thinking' | 'working' | 'drowsy' | 'sleepy' | 'uneasy' | 'strained' | 'alarmed'
|
|
28
|
+
| 'dazed' | 'asking' | 'squeezing' | 'going' | 'pleased' | 'sorry'
|
|
29
|
+
export type FaceTone = 'accent' | 'ok' | 'warn' | 'bad'
|
|
30
|
+
export type Detail = 'full' | 'mini'
|
|
31
|
+
|
|
32
|
+
export const FACES: readonly Face[] = ['calm', 'thinking', 'working', 'drowsy', 'sleepy', 'uneasy', 'strained', 'alarmed', 'dazed', 'asking', 'squeezing', 'going', 'pleased', 'sorry']
|
|
33
|
+
|
|
34
|
+
/** The grid every face is drawn on: the SVG's viewBox is `0 0 56 56` at any size. */
|
|
35
|
+
export const GRID = 56
|
|
36
|
+
|
|
37
|
+
/** From this size up the full drawing (the sheet's 34 px badge floor); below it, the mini one. */
|
|
38
|
+
export const MIN_SIZE = 34
|
|
39
|
+
|
|
40
|
+
export function detailOf(size: number): Detail {
|
|
41
|
+
return size < MIN_SIZE ? 'mini' : 'full'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const FACE_TONE: Record<Face, FaceTone> = {
|
|
45
|
+
calm: 'accent',
|
|
46
|
+
thinking: 'accent',
|
|
47
|
+
working: 'accent',
|
|
48
|
+
drowsy: 'accent',
|
|
49
|
+
sleepy: 'accent',
|
|
50
|
+
uneasy: 'warn',
|
|
51
|
+
strained: 'warn',
|
|
52
|
+
alarmed: 'bad',
|
|
53
|
+
dazed: 'bad',
|
|
54
|
+
asking: 'accent',
|
|
55
|
+
squeezing: 'accent',
|
|
56
|
+
going: 'accent',
|
|
57
|
+
pleased: 'ok',
|
|
58
|
+
sorry: 'bad',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The faces whose ring is a gauge when given a value; their strain grows with it. */
|
|
62
|
+
export const GAUGE_FACES: readonly Face[] = ['uneasy', 'strained', 'alarmed']
|
|
63
|
+
|
|
64
|
+
/** A gauge value as drawn: whole multiples of 5 from 0 to 100. */
|
|
65
|
+
export function gaugeStep(percent: number): number {
|
|
66
|
+
if (!Number.isFinite(percent)) return 0
|
|
67
|
+
return Math.min(100, Math.max(0, Math.round(percent / 5) * 5))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const v = (key: keyof Palette): string => 'var(--' + key + ')'
|
|
71
|
+
const n = (x: number): string => String(Math.round(x * 100) / 100)
|
|
72
|
+
|
|
73
|
+
// ---- Parts ---------------------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/** The crest's variants: its base sits on the outer edge of the ring at −100°…−64°, buried in it. */
|
|
76
|
+
const CREST = {
|
|
77
|
+
up: 'M24.7 12.29C24.2 6.2 29.2 2.4 35 2.6C39.6 2.8 42.6 6.2 41.6 9.8C40.6 8 38.6 7 36.8 7.6C35 8.2 35 11 36.33 13.92Z',
|
|
78
|
+
tall: 'M24.7 12.29C24.4 5.6 28.6 0.8 34.2 0.6C37.6 0.5 40.2 2.2 40.8 4.6C39.2 3.8 37.2 4 36.2 5.4C35 7.2 35.2 10.6 36.33 13.92Z',
|
|
79
|
+
short: 'M24.7 12.29C25 8.4 28.4 6.4 32 6.6C34.8 6.8 36.8 8.4 36.8 10.2C36 9.6 35.2 9.8 34.9 10.6C34.7 11.6 35.4 12.8 36.33 13.92Z',
|
|
80
|
+
wind: 'M24.7 12.29C25.6 6.4 31.6 3.6 38 4C42 4.2 44.6 6 45.2 8.4C43.4 7.4 40.8 7.4 38.8 8.4C37.2 9.4 36 11.6 36.33 13.92Z',
|
|
81
|
+
wilt: 'M24.7 12.29C25.4 6 31.4 3.8 36.6 4.6C41.6 5.4 44.4 9.6 43.6 14.6C42.4 12 40.2 10.6 38.2 11C37.2 11.4 36.6 12.6 36.33 13.92Z',
|
|
82
|
+
left: 'M31.3 12.29C31.8 6.2 26.8 2.4 21 2.6C16.4 2.8 13.4 6.2 14.4 9.8C15.4 8 17.4 7 19.2 7.6C21 8.2 21 11 19.67 13.92Z',
|
|
83
|
+
} as const
|
|
84
|
+
|
|
85
|
+
/** The question's tail: two points on the ring's outer edge meeting at (8.6, 53.4). */
|
|
86
|
+
const TAIL = 'M18.5 47.45Q15.4 51.2 8.6 53.4Q11.8 48.2 13.03 42.7Z'
|
|
87
|
+
|
|
88
|
+
const RING = '<circle cx="28" cy="31" r="16.5" fill="none"'
|
|
89
|
+
/** The ears: the sheet's colour arcs (r 23.5, 2.4) and the mono template's (r 24.5, 4). */
|
|
90
|
+
const EARS: Record<Detail, { r: string; l: string; width: number }> = {
|
|
91
|
+
full: { r: 'M49.47 21.44A23.5 23.5 0 0 1 49.47 40.56', l: 'M6.53 40.56A23.5 23.5 0 0 1 6.53 21.44', width: 2.4 },
|
|
92
|
+
mini: { r: 'M50.02 20.26A24.5 24.5 0 0 1 50.02 41.74', l: 'M5.98 41.74A24.5 24.5 0 0 1 5.98 20.26', width: 4 },
|
|
93
|
+
}
|
|
94
|
+
const OUTER_R = 'M53.86 23.59A26.9 26.9 0 0 1 53.86 38.41'
|
|
95
|
+
const OUTER_L = 'M2.14 38.41A26.9 26.9 0 0 1 2.14 23.59'
|
|
96
|
+
|
|
97
|
+
/** What one drawing paints with: the tone, and the face's ink (the tone too on the mini). */
|
|
98
|
+
type Pen = { mini: boolean; tone: string; ink: string; muted: string; ring: number }
|
|
99
|
+
|
|
100
|
+
function penOf(face: Face, detail: Detail): Pen {
|
|
101
|
+
const tone = v(FACE_TONE[face])
|
|
102
|
+
const mini = detail === 'mini'
|
|
103
|
+
return { mini, tone, ink: mini ? tone : v('ink'), muted: mini ? tone : v('muted'), ring: mini ? 6.4 : 5 }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stroke(d: string, paint: string, width: number, extra = ''): string {
|
|
107
|
+
return '<path d="' + d + '" fill="none" stroke="' + paint + '" stroke-width="' + n(width) + '" stroke-linecap="round"' + extra + '/>'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A line of the face: ink, round caps and joins; heavier on the mini. */
|
|
111
|
+
function line(p: Pen, d: string, width = 2.2): string {
|
|
112
|
+
return stroke(d, p.ink, p.mini ? Math.max(3.2, width * 1.45) : width, ' stroke-linejoin="round"')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The capsule eyes, the left one at `x, y`, `h` tall; the mini's are the template's 5 wide, half again as tall. */
|
|
116
|
+
function capsules(p: Pen, x: number, y: number, h: number): string {
|
|
117
|
+
const w = p.mini ? 5 : 3.6
|
|
118
|
+
const hh = p.mini ? h * 1.5 : h
|
|
119
|
+
const top = y + h / 2 - hh / 2
|
|
120
|
+
return [x, x + 11.2].map(left => '<rect x="' + n(left + 1.8 - w / 2) + '" y="' + n(top) + '" width="' + n(w) + '" height="' + n(hh) + '" rx="' + n(w / 2) + '" fill="' + p.ink + '"/>').join('')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A water drop (sweat, tear) at its top point, scaled; muted, larger on the mini. */
|
|
124
|
+
function drop(p: Pen, x: number, y: number, scale: number): string {
|
|
125
|
+
const s = p.mini ? scale * 1.5 : scale
|
|
126
|
+
const r = 2.3 * s
|
|
127
|
+
return (
|
|
128
|
+
'<path d="M' + n(x) + ' ' + n(y) + 'q' + n(-r) + ' ' + n(3.1 * s) + ' ' + n(-r) + ' ' + n(4.5 * s) + 'a' + n(r) + ' ' + n(r) + ' 0 0 0 ' + n(2 * r) +
|
|
129
|
+
' 0q0 ' + n(-1.4 * s) + ' ' + n(-r) + ' ' + n(-4.5 * s) + 'z" fill="' + p.muted + '"/>'
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type Ears = 'on' | 'dim' | 'double' | 'none' | 'left' | 'waving'
|
|
134
|
+
|
|
135
|
+
/** The ears, each in a group of its own so one can wave. */
|
|
136
|
+
function ears(p: Pen, kind: Ears): string {
|
|
137
|
+
const e = EARS[p.mini ? 'mini' : 'full']
|
|
138
|
+
const ear = (d: string, opacity?: number) => stroke(d, p.tone, e.width, opacity === undefined ? '' : ' stroke-opacity="' + opacity + '"')
|
|
139
|
+
const L = (opacity?: number) => '<g class="earL">' + ear(e.l, opacity) + '</g>'
|
|
140
|
+
const R = (opacity?: number) => '<g class="earR">' + ear(e.r, opacity) + '</g>'
|
|
141
|
+
switch (kind) {
|
|
142
|
+
case 'none':
|
|
143
|
+
return ''
|
|
144
|
+
case 'left':
|
|
145
|
+
return L(0.55)
|
|
146
|
+
case 'dim':
|
|
147
|
+
return L(0.4) + R(0.4)
|
|
148
|
+
case 'waving':
|
|
149
|
+
return L(0.4) + R()
|
|
150
|
+
case 'on':
|
|
151
|
+
return L() + R()
|
|
152
|
+
case 'double':
|
|
153
|
+
// The mini has no room past the ears: its alarm is the pulse alone.
|
|
154
|
+
return L() + R() + (p.mini ? '' : stroke(OUTER_R, p.tone, 2, ' stroke-opacity="0.55"') + stroke(OUTER_L, p.tone, 2, ' stroke-opacity="0.55"'))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Copies of the ears that spread outward like water, unseen at rest. */
|
|
159
|
+
function waves(p: Pen, sides: 'both' | 'right'): string {
|
|
160
|
+
const e = EARS[p.mini ? 'mini' : 'full']
|
|
161
|
+
const wave = (cls: string, d: string) => stroke(d, p.tone, e.width * 0.8, ' class="' + cls + '" opacity="0"')
|
|
162
|
+
return (sides === 'both' ? wave('wL', e.l) : '') + wave('wR', e.r)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The ring as a gauge: a faint track and the value from twelve o'clock, clockwise. */
|
|
166
|
+
function gauge(p: Pen, percent: number): string {
|
|
167
|
+
const step = gaugeStep(percent)
|
|
168
|
+
const track = RING + ' stroke="' + v('faint') + '" stroke-width="' + p.ring + '"/>'
|
|
169
|
+
if (step === 0) return track
|
|
170
|
+
if (step === 100) return RING + ' stroke="' + p.tone + '" stroke-width="' + p.ring + '"/>'
|
|
171
|
+
const a = ((-90 + 3.6 * step) * Math.PI) / 180
|
|
172
|
+
const x = 28 + 16.5 * Math.cos(a)
|
|
173
|
+
const y = 31 + 16.5 * Math.sin(a)
|
|
174
|
+
return track + stroke('M28 14.5A16.5 16.5 0 ' + (step > 50 ? 1 : 0) + ' 1 ' + n(x) + ' ' + n(y), p.tone, p.ring)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The ring in progress: eight dashes, the first under the crest. It turns by whole dashes. */
|
|
178
|
+
function dashed(p: Pen): string {
|
|
179
|
+
return '<g class="spin">' + RING + ' stroke="' + p.tone + '" stroke-width="' + p.ring + '" stroke-dasharray="9.2 3.76" transform="rotate(-99 28 31)"/></g>'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Ripples that spread from the ring while it thinks, unseen at rest. */
|
|
183
|
+
function rings(p: Pen): string {
|
|
184
|
+
return ['r1', 'r2'].map(cls => '<circle class="' + cls + '" cx="28" cy="31" r="19" fill="none" stroke="' + p.tone + '" stroke-width="' + (p.mini ? 2.6 : 1.6) + '" opacity="0"/>').join('')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** A thought's bubbles rising to the upper right, small to large. */
|
|
188
|
+
function bubbles(p: Pen): string {
|
|
189
|
+
const w = p.mini ? 2.4 : 1.5
|
|
190
|
+
return [[43.6, 14.2, 1.3], [47.6, 9.2, 1.9], [51.6, 3.9, 2.6]]
|
|
191
|
+
.map(([x, y, r], i) => '<circle class="bub bub' + (i + 1) + '" cx="' + x + '" cy="' + y + '" r="' + r + '" fill="none" stroke="' + p.tone + '" stroke-width="' + w + '"/>')
|
|
192
|
+
.join('')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function zs(p: Pen): string {
|
|
196
|
+
if (p.mini) return '<path class="z1" d="M42.6 2.6h6.4l-6.4 6.8h6.4" fill="none" stroke="' + p.muted + '" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
197
|
+
return (
|
|
198
|
+
'<path class="z1" d="M44.4 4.6h4l-4 4.4h4" fill="none" stroke="' + p.muted + '" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
199
|
+
'<path class="z2" d="M50 10.6h2.8l-2.8 3h2.8" fill="none" stroke="' + p.muted + '" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" opacity=".7"/>'
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---- Faces ---------------------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
type Spec = {
|
|
206
|
+
crest: keyof typeof CREST
|
|
207
|
+
ring: 'solid' | 'gauge' | 'dashed' | 'broken'
|
|
208
|
+
ears: Ears
|
|
209
|
+
/** The eyes, in nested groups of their own so they can blink on several loops. */
|
|
210
|
+
eyes: (p: Pen) => string
|
|
211
|
+
/** Where the eyes' middle is, for their blink. */
|
|
212
|
+
eyesY: number
|
|
213
|
+
/** Brows: the full drawing only. */
|
|
214
|
+
brows?: string
|
|
215
|
+
/** The mouth, in a group of its own: the full drawing only. */
|
|
216
|
+
mouth?: string
|
|
217
|
+
/** The mouth that opens in a yawn, unseen at rest. */
|
|
218
|
+
yawn?: boolean
|
|
219
|
+
/** Muted props (z, sweat, tear), drawn last. */
|
|
220
|
+
prop?: (p: Pen) => string
|
|
221
|
+
/** Crest and ring at this opacity, in one group so their overlap does not darken. */
|
|
222
|
+
faded?: number
|
|
223
|
+
tail?: boolean
|
|
224
|
+
/** Ripples spreading from the ears. */
|
|
225
|
+
waves?: 'both' | 'right'
|
|
226
|
+
/** Under the body: ripples from the ring, the done halo. */
|
|
227
|
+
behind?: (p: Pen) => string
|
|
228
|
+
/** Outside the figure, so a tilt does not carry it: the asking bubbles. */
|
|
229
|
+
outside?: (p: Pen) => string
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const ink = (d: string, width = 2.2) => stroke(d, v('ink'), width, ' stroke-linejoin="round"')
|
|
233
|
+
|
|
234
|
+
const SPECS: Record<Face, Spec> = {
|
|
235
|
+
calm: { crest: 'up', ring: 'solid', ears: 'on', waves: 'both', eyes: p => capsules(p, 20.6, 26.2, 5.6), eyesY: 29, mouth: ink('M24.8 36.2q3.2 2.6 6.4 0') },
|
|
236
|
+
// Eyes up and to the right, a lopsided "hmm"; ripples spread while it weighs.
|
|
237
|
+
thinking: {
|
|
238
|
+
crest: 'up',
|
|
239
|
+
ring: 'solid',
|
|
240
|
+
ears: 'dim',
|
|
241
|
+
eyes: p => capsules(p, 21.4, 24.4, 5.2),
|
|
242
|
+
eyesY: 27,
|
|
243
|
+
mouth: ink('M26.2 37.6q2.2 .7 4.4 -.5'),
|
|
244
|
+
behind: rings,
|
|
245
|
+
},
|
|
246
|
+
// Focused: eyes a little low, the mouth set, the ring turning.
|
|
247
|
+
working: { crest: 'up', ring: 'dashed', ears: 'dim', waves: 'both', eyes: p => capsules(p, 20.6, 27, 5), eyesY: 29.5, mouth: ink('M26 37.2h4') },
|
|
248
|
+
// Heavy lids over half eyes; nods off and catches itself, now and then a yawn.
|
|
249
|
+
drowsy: {
|
|
250
|
+
crest: 'up',
|
|
251
|
+
ring: 'solid',
|
|
252
|
+
ears: 'dim',
|
|
253
|
+
faded: 0.75,
|
|
254
|
+
eyes: p => (p.mini ? capsules(p, 20.6, 29, 3) : capsules(p, 20.6, 29, 2.8) + line(p, 'M19.9 28.4h5', 1.8) + line(p, 'M31.1 28.4h5', 1.8)),
|
|
255
|
+
eyesY: 30.2,
|
|
256
|
+
mouth: ink('M26.6 37.2q1.4 1 2.8 0'),
|
|
257
|
+
yawn: true,
|
|
258
|
+
},
|
|
259
|
+
sleepy: {
|
|
260
|
+
crest: 'short',
|
|
261
|
+
ring: 'solid',
|
|
262
|
+
ears: 'none',
|
|
263
|
+
faded: 0.45,
|
|
264
|
+
eyes: p => line(p, 'M20 29q2.4 2.2 4.8 0') + line(p, 'M31.2 29q2.4 2.2 4.8 0'),
|
|
265
|
+
eyesY: 30.1,
|
|
266
|
+
mouth: ink('M26.6 37.2q1.4 1 2.8 0'),
|
|
267
|
+
yawn: true,
|
|
268
|
+
prop: zs,
|
|
269
|
+
},
|
|
270
|
+
uneasy: {
|
|
271
|
+
crest: 'up',
|
|
272
|
+
ring: 'gauge',
|
|
273
|
+
ears: 'dim',
|
|
274
|
+
eyes: p => capsules(p, 20.6, 26.9, 5),
|
|
275
|
+
eyesY: 29.4,
|
|
276
|
+
brows: ink('M19.4 24.4L25 22.4', 2) + ink('M36.6 24.4L31 22.4', 2),
|
|
277
|
+
mouth: ink('M22.9 37.2q1.275 -1.7 2.55 0t2.55 0t2.55 0t2.55 0'),
|
|
278
|
+
prop: p => '<g class="sweat">' + drop(p, 46.6, 13.8, 1) + '</g>',
|
|
279
|
+
},
|
|
280
|
+
strained: {
|
|
281
|
+
crest: 'tall',
|
|
282
|
+
ring: 'gauge',
|
|
283
|
+
ears: 'on',
|
|
284
|
+
eyes: p => capsules(p, 20.6, 27.6, 4.4),
|
|
285
|
+
eyesY: 29.8,
|
|
286
|
+
brows: ink('M19.6 23.6h5.4', 2) + ink('M31 23.6h5.4', 2),
|
|
287
|
+
mouth: ink('M24.9 37.4h6.2'),
|
|
288
|
+
prop: p => '<g class="sweat">' + drop(p, 46.6, 13.8, 1) + '</g>',
|
|
289
|
+
},
|
|
290
|
+
alarmed: {
|
|
291
|
+
crest: 'tall',
|
|
292
|
+
ring: 'gauge',
|
|
293
|
+
ears: 'double',
|
|
294
|
+
eyes: p => (p.mini ? capsules(p, 20.6, 25.2, 7.6) : '<circle cx="22.4" cy="29" r="3.3" fill="' + p.ink + '"/><circle cx="33.6" cy="29" r="3.3" fill="' + p.ink + '"/>'),
|
|
295
|
+
eyesY: 29,
|
|
296
|
+
brows: ink('M18.8 22.4q3.2 -2.2 6.6 -0.8', 2) + ink('M37.2 22.4q-3.2 -2.2 -6.6 -0.8', 2),
|
|
297
|
+
mouth: '<ellipse cx="28" cy="37.8" rx="2.6" ry="3.1" fill="' + v('ink') + '"/>',
|
|
298
|
+
prop: p => '<g class="sweat">' + drop(p, 46.6, 13.8, 1) + '</g><g class="sweat2">' + drop(p, 10.4, 15.2, 0.8) + '</g>',
|
|
299
|
+
},
|
|
300
|
+
dazed: {
|
|
301
|
+
crest: 'short',
|
|
302
|
+
ring: 'broken',
|
|
303
|
+
ears: 'left',
|
|
304
|
+
eyes: p => '<g class="e1">' + line(p, 'M20.2 26.8l4.4 4.4m0 -4.4l-4.4 4.4') + '</g><g class="e2">' + line(p, 'M31.4 26.8l4.4 4.4m0 -4.4l-4.4 4.4') + '</g>',
|
|
305
|
+
eyesY: 29,
|
|
306
|
+
mouth: ink('M23.6 37.8l2.2 -1.8l2.2 1.8l2.2 -1.8l2.2 1.8'),
|
|
307
|
+
},
|
|
308
|
+
asking: {
|
|
309
|
+
crest: 'left',
|
|
310
|
+
ring: 'solid',
|
|
311
|
+
ears: 'dim',
|
|
312
|
+
tail: true,
|
|
313
|
+
eyes: p => capsules(p, 21.4, 25, 5.6),
|
|
314
|
+
eyesY: 27.8,
|
|
315
|
+
brows: ink('M30.4 22.2q2.6 -1.8 5.4 -0.6', 2),
|
|
316
|
+
mouth: '<ellipse cx="28" cy="37.2" rx="1.7" ry="2" fill="' + v('ink') + '"/>',
|
|
317
|
+
outside: bubbles,
|
|
318
|
+
},
|
|
319
|
+
squeezing: {
|
|
320
|
+
crest: 'up',
|
|
321
|
+
ring: 'dashed',
|
|
322
|
+
ears: 'dim',
|
|
323
|
+
eyes: p => line(p, 'M20.4 26.6l3.4 2.4l-3.4 2.4') + line(p, 'M35.6 26.6l-3.4 2.4l3.4 2.4'),
|
|
324
|
+
eyesY: 29,
|
|
325
|
+
mouth: ink('M25.6 37.4h4.8'),
|
|
326
|
+
},
|
|
327
|
+
// Off it goes: the right ear waves, a ripple leaves from it.
|
|
328
|
+
going: { crest: 'wind', ring: 'dashed', ears: 'waving', waves: 'right', eyes: p => capsules(p, 22.2, 26.2, 5.6), eyesY: 29, mouth: ink('M26.6 37.2q1.4 1 2.8 0') },
|
|
329
|
+
pleased: {
|
|
330
|
+
crest: 'up',
|
|
331
|
+
ring: 'solid',
|
|
332
|
+
ears: 'on',
|
|
333
|
+
waves: 'both',
|
|
334
|
+
eyes: p => line(p, 'M20 30q2.4 -3.4 4.8 0') + line(p, 'M31.2 30q2.4 -3.4 4.8 0'),
|
|
335
|
+
eyesY: 28.3,
|
|
336
|
+
mouth: '<path d="M23.8 35.4h8.4a4.2 4.2 0 0 1 -8.4 0z" fill="' + v('ink') + '"/>',
|
|
337
|
+
// The done ripple: unseen at rest, spreads once from the ring.
|
|
338
|
+
behind: p => '<circle class="halo" cx="28" cy="31" r="19" fill="none" stroke="' + p.tone + '" stroke-width="' + (p.mini ? 2.6 : 1.6) + '" opacity="0"/>',
|
|
339
|
+
},
|
|
340
|
+
sorry: {
|
|
341
|
+
crest: 'wilt',
|
|
342
|
+
ring: 'solid',
|
|
343
|
+
ears: 'none',
|
|
344
|
+
eyes: p => capsules(p, 20.6, 27.6, 4.4),
|
|
345
|
+
eyesY: 29.8,
|
|
346
|
+
brows: ink('M19.2 24.8L24.6 22.2', 2) + ink('M36.8 24.8L31.4 22.2', 2),
|
|
347
|
+
mouth: ink('M24.8 38.4q3.2 -2.6 6.4 0'),
|
|
348
|
+
prop: p => '<g class="tear">' + drop(p, 36.4, 33.2, 0.8) + '</g>',
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function ringOf(p: Pen, s: Spec, percent?: number): string {
|
|
353
|
+
if (s.ring === 'broken') {
|
|
354
|
+
// Broken at 22°…54°; the shard (28°…46°) knocked 6.2 out along 38° and turned 18°.
|
|
355
|
+
return (
|
|
356
|
+
stroke('M37.7 44.35A16.5 16.5 0 1 1 43.3 37.18', p.tone, p.ring) +
|
|
357
|
+
'<g class="shard">' + stroke('M42.57 38.75A16.5 16.5 0 0 1 39.46 42.87', p.tone, p.ring, ' transform="translate(4.89 3.82) rotate(18 41.18 40.93)"') + '</g>'
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
if (s.ring === 'dashed') return dashed(p)
|
|
361
|
+
if (s.ring === 'gauge' && percent !== undefined) return gauge(p, percent)
|
|
362
|
+
return RING + ' stroke="' + p.tone + '" stroke-width="' + p.ring + '"/>'
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The face's markup on the 56 grid, in the drawing order of the sheet: crest, tail, ring, ears,
|
|
367
|
+
* eyes, face, props. `percent` draws the ring of uneasy, strained and alarmed as a gauge. The
|
|
368
|
+
* groups are what the motion moves: `.fig` the whole figure (hops, tilts), `.pose` its shape
|
|
369
|
+
* (breath, squash, wobble), `.face` the look, `.b1`…`.b3` the blinks.
|
|
370
|
+
*/
|
|
371
|
+
export function rippleBody(face: Face, percent?: number, detail: Detail = 'full'): string {
|
|
372
|
+
const s = SPECS[face]
|
|
373
|
+
const p = penOf(face, detail)
|
|
374
|
+
const crest = '<g class="crest"><g class="crest2"><path d="' + CREST[s.crest] + '" fill="' + p.tone + '"/></g></g>' + (s.tail ? '<path d="' + TAIL + '" fill="' + p.tone + '"/>' : '')
|
|
375
|
+
const ring = ringOf(p, s, percent)
|
|
376
|
+
const body = s.faded ? '<g opacity="' + s.faded + '">' + crest + ring + '</g>' : crest + ring
|
|
377
|
+
const eyes = '<g class="eyes"><g class="b1"><g class="b2"><g class="b3">' + s.eyes(p) + '</g></g></g></g>'
|
|
378
|
+
const mouth = p.mini
|
|
379
|
+
? ''
|
|
380
|
+
: (s.brows ?? '') + (s.mouth ? '<g class="mouth">' + s.mouth + '</g>' : '') +
|
|
381
|
+
(s.yawn ? '<ellipse class="yawn" cx="28" cy="37.8" rx="2.4" ry="3" fill="' + v('ink') + '" opacity="0"/>' : '')
|
|
382
|
+
return (
|
|
383
|
+
'<g class="fig"><g class="pose">' + (s.behind ? s.behind(p) : '') + body +
|
|
384
|
+
'<g class="ears">' + ears(p, s.ears) + '</g>' + (s.waves ? waves(p, s.waves) : '') +
|
|
385
|
+
'<g class="face">' + eyes + mouth + '</g>' +
|
|
386
|
+
(s.prop ? '<g class="prop">' + s.prop(p) + '</g>' : '') + '</g></g>' +
|
|
387
|
+
(s.outside ? s.outside(p) : '')
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---- Motion --------------------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
/** One animation: what it moves, its keyframes, how long, and whether it waits or plays once. */
|
|
394
|
+
type Loop = { at: string; name: string; frames: string; seconds: number; timing?: 'linear' | 'ease-out'; delay?: number; once?: boolean }
|
|
395
|
+
|
|
396
|
+
const loop = (at: string, name: string, frames: string, seconds: number, more: Omit<Loop, 'at' | 'name' | 'frames' | 'seconds'> = {}): Loop => ({ at, name, frames, seconds, ...more })
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The keyframes, `k` the scale of their travel (larger on the mini, where a unit is a third of a
|
|
400
|
+
* pixel). Each opens on `0%` at the rest pose or with its part hidden, and each but the turns
|
|
401
|
+
* (`spin`, `dizzy`: a dash, or an × a quarter round, draws the same) ends where it began.
|
|
402
|
+
*/
|
|
403
|
+
function frames(k: number) {
|
|
404
|
+
const px = (x: number) => n(x * k) + 'px'
|
|
405
|
+
const tx = (x: number) => 'translateX(' + px(x) + ')'
|
|
406
|
+
const ty = (y: number) => 'translateY(' + px(y) + ')'
|
|
407
|
+
return {
|
|
408
|
+
breathe: '0%,100%{transform:none}50%{transform:scale(1.012,1.028)}',
|
|
409
|
+
// Three blinks on loops of 7, 11 and 13 s: they fall at 2.2, 5.9, 7.6, 9.2, 16.2, 18.7…
|
|
410
|
+
blinkA: '0%,100%{transform:none}29%,33%{transform:none}31%{transform:scaleY(.1)}',
|
|
411
|
+
blinkB: '0%,100%{transform:none}68%,70.6%{transform:none}69.3%{transform:scaleY(.1)}',
|
|
412
|
+
blinkC: '0%,100%{transform:none}44%,46.2%{transform:none}45.1%{transform:scaleY(.1)}',
|
|
413
|
+
sway: '0%,100%{transform:none}30%{transform:rotate(-5deg)}70%{transform:rotate(3deg)}',
|
|
414
|
+
look: '0%,100%{transform:none}38%{transform:none}43%,53%{transform:' + tx(-1.6) + '}58%,72%{transform:none}77%,86%{transform:' + tx(1.6) + '}91%{transform:none}',
|
|
415
|
+
// A crouch, a small hop (kept under the top edge), a squash on landing.
|
|
416
|
+
hop: '0%,100%{transform:none}60%{transform:none}62%{transform:scale(1.03,.96)}64.5%{transform:translateY(-2px)}67%{transform:scale(1.03,.97)}69%{transform:none}',
|
|
417
|
+
wave: '0%,100%{opacity:0;transform:none}8%{opacity:.6}60%{opacity:0;transform:scale(1.14)}',
|
|
418
|
+
ripple: '0%,100%{opacity:0;transform:none}10%{opacity:.5}75%{opacity:0;transform:scale(1.34)}',
|
|
419
|
+
twirl: '0%,100%{transform:none}20%{transform:rotate(-14deg)}45%{transform:rotate(6deg)}70%{transform:rotate(-8deg)}',
|
|
420
|
+
ponder: '0%,100%{transform:none}40%,60%{transform:translate(' + px(1) + ',' + px(-0.6) + ')}',
|
|
421
|
+
spin: '0%{transform:none}100%{transform:rotate(45deg)}',
|
|
422
|
+
busy: '0%,100%{transform:none}50%{transform:' + ty(-0.8) + '}',
|
|
423
|
+
nod: '0%,100%{transform:none}45%{transform:none}62%{transform:' + ty(1.2) + ' rotate(3deg)}70%{transform:' + ty(1.5) + ' rotate(3.5deg)}74%{transform:' + ty(-0.3) + '}78%{transform:none}',
|
|
424
|
+
scan: '0%,100%{transform:none}20%,40%{transform:' + tx(-1.2) + '}60%,80%{transform:' + tx(1.2) + '}',
|
|
425
|
+
bob: '0%,100%{transform:none}50%{transform:rotate(-6deg)}',
|
|
426
|
+
heavy: '0%,100%{transform:none}18%{transform:none}26%,38%{transform:scaleY(.12)}44%{transform:none}',
|
|
427
|
+
yawn: '0%,100%{opacity:0;transform:scale(.3)}66%{opacity:0;transform:scale(.3)}74%,84%{opacity:1;transform:none}90%{opacity:0;transform:scale(.3)}',
|
|
428
|
+
gape: '0%,100%{opacity:1}66%{opacity:1}70%,86%{opacity:0}90%{opacity:1}',
|
|
429
|
+
stretch: '0%,100%{transform:none}66%{transform:none}76%,84%{transform:scale(.98,1.04)}90%{transform:none}',
|
|
430
|
+
snore: '0%,100%{transform:none}50%{transform:scale(1.02,1.04)}',
|
|
431
|
+
zrise: '0%,100%{opacity:1;transform:none}70%{opacity:0;transform:translate(' + px(2) + ',' + px(-3) + ')}71%{opacity:0;transform:none}',
|
|
432
|
+
fret: '0%,100%{transform:none}30%{transform:' + tx(-0.8) + '}70%{transform:' + tx(0.8) + '}',
|
|
433
|
+
drip: '0%,100%{opacity:1;transform:none}55%{opacity:0;transform:' + ty(3) + '}56%{opacity:0;transform:none}',
|
|
434
|
+
quiver: '0%,100%{transform:none}15%{transform:rotate(3deg)}30%{transform:rotate(-3deg)}45%{transform:none}',
|
|
435
|
+
pulse: '0%,100%{opacity:1}50%{opacity:.3}',
|
|
436
|
+
jolt: '0%,100%{transform:none}10%{transform:' + ty(-1) + '}22%{transform:none}',
|
|
437
|
+
dizzy: '0%{transform:none}100%{transform:rotate(360deg)}',
|
|
438
|
+
orbit: '0%,100%{transform:none}25%{transform:translate(' + px(0.8) + ',' + px(-0.4) + ') rotate(2deg)}50%{transform:' + ty(-0.7) + '}75%{transform:translate(' + px(-0.8) + ',' + px(-0.4) + ') rotate(-2deg)}',
|
|
439
|
+
shard: '0%,100%{transform:none}50%{transform:translate(' + px(1.2) + ',' + px(1.6) + ') rotate(10deg)}',
|
|
440
|
+
tilt: '0%,100%{transform:none}14%,70%{transform:rotate(-7deg)}86%{transform:none}',
|
|
441
|
+
pop: '0%,100%{opacity:0;transform:scale(.2)}20%{opacity:1;transform:scale(1.15)}26%,72%{opacity:1;transform:none}84%{opacity:0;transform:none}',
|
|
442
|
+
squash: '0%,100%{transform:none}30%{transform:scale(1.08,.9)}62%{transform:scale(.95,1.06)}82%{transform:none}',
|
|
443
|
+
squeeze: '0%,100%{transform:none}30%{transform:scale(.93)}62%{transform:scale(1.04)}',
|
|
444
|
+
wag: '0%,100%{transform:none}12%{transform:rotate(-28deg)}26%{transform:rotate(10deg)}40%{transform:rotate(-24deg)}54%{transform:none}',
|
|
445
|
+
leave: '0%,100%{opacity:0;transform:none}10%{opacity:.7}55%{opacity:0;transform:' + tx(6) + '}',
|
|
446
|
+
flutter: '0%,100%{transform:none}50%{transform:rotate(-4deg)}',
|
|
447
|
+
lean: '0%,100%{transform:none}50%{transform:' + tx(0.8) + '}',
|
|
448
|
+
// Done: a crouch, a jump (under the top edge), a squash, a little rebound, still.
|
|
449
|
+
bounce: '0%{transform:none}16%{transform:scale(1.05,.93)}40%{transform:translateY(-2.2px)}62%{transform:scale(1.04,.96)}80%{transform:translateY(-.8px)}100%{transform:none}',
|
|
450
|
+
halo: '0%,100%{opacity:0;transform:none}12%{opacity:.55}90%{opacity:0;transform:scale(1.42)}',
|
|
451
|
+
sink: '0%,100%{transform:none}50%{transform:' + ty(1) + '}',
|
|
452
|
+
droop: '0%,100%{transform:none}50%{transform:rotate(5deg)}',
|
|
453
|
+
tear: '0%,100%{opacity:1;transform:none}55%{opacity:0;transform:' + ty(2.6) + '}56%{opacity:0;transform:none}',
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
type Frames = ReturnType<typeof frames>
|
|
458
|
+
|
|
459
|
+
/** The idle life: breath, three blinks, the crest's sway, a look either way, a hop, the ears' ripples. */
|
|
460
|
+
function idle(f: Frames, delay = 0, blink = true): Loop[] {
|
|
461
|
+
const d = delay ? { delay } : {}
|
|
462
|
+
return [
|
|
463
|
+
loop('.pose', 'breathe', f.breathe, 4.2, d),
|
|
464
|
+
loop('.crest', 'sway', f.sway, 5.4, d),
|
|
465
|
+
loop('.face', 'look', f.look, 13, d),
|
|
466
|
+
loop('.fig', 'hop', f.hop, 17, d),
|
|
467
|
+
loop('.wL,.wR', 'wave', f.wave, 3.6, { delay: delay + 0.6 }),
|
|
468
|
+
...(blink ? [loop('.b1', 'blinkA', f.blinkA, 7, d), loop('.b2', 'blinkB', f.blinkB, 11, d), loop('.b3', 'blinkC', f.blinkC, 13, d)] : []),
|
|
469
|
+
]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** How hard a gauge face strains at `percent`: its wobble's swing (deg) and beat (s). */
|
|
473
|
+
function strain(face: Face, percent: number): { swing: number; beat: number } {
|
|
474
|
+
const x = gaugeStep(percent) / 100
|
|
475
|
+
if (face === 'alarmed') return { swing: 3 + 2 * x, beat: 1.4 - 0.4 * x }
|
|
476
|
+
if (face === 'strained') return { swing: 2 + 3 * x, beat: 2.6 - 1.2 * x }
|
|
477
|
+
return { swing: 1.5 + 3 * x, beat: 3.2 - 1.4 * x }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The value a gauge face strains at when it is drawn without one. */
|
|
481
|
+
const DEFAULT_PERCENT: Partial<Record<Face, number>> = { uneasy: 60, strained: 85, alarmed: 97 }
|
|
482
|
+
|
|
483
|
+
function motionOf(face: Face, percent: number | undefined, detail: Detail): Loop[] {
|
|
484
|
+
const f = frames(detail === 'mini' ? 1.5 : 1)
|
|
485
|
+
const full = detail === 'full'
|
|
486
|
+
switch (face) {
|
|
487
|
+
case 'calm':
|
|
488
|
+
return idle(f)
|
|
489
|
+
case 'thinking':
|
|
490
|
+
return [
|
|
491
|
+
loop('.pose', 'breathe', f.breathe, 4.2),
|
|
492
|
+
loop('.crest', 'twirl', f.twirl, 2.6),
|
|
493
|
+
loop('.face', 'ponder', f.ponder, 5.2),
|
|
494
|
+
loop('.r1', 'ripple', f.ripple, 2.6),
|
|
495
|
+
loop('.r2', 'ripple', f.ripple, 2.6, { delay: 1.3 }),
|
|
496
|
+
loop('.b1', 'blinkA', f.blinkA, 7),
|
|
497
|
+
loop('.b2', 'blinkB', f.blinkB, 11),
|
|
498
|
+
]
|
|
499
|
+
case 'working':
|
|
500
|
+
return [
|
|
501
|
+
loop('.spin', 'spin', f.spin, 1.2, { timing: 'linear' }),
|
|
502
|
+
loop('.fig', 'busy', f.busy, 0.8),
|
|
503
|
+
loop('.crest', 'bob', f.bob, 0.8),
|
|
504
|
+
loop('.face', 'scan', f.scan, 2.4),
|
|
505
|
+
loop('.wL,.wR', 'wave', f.wave, 1.6),
|
|
506
|
+
loop('.b1', 'blinkA', f.blinkA, 7),
|
|
507
|
+
]
|
|
508
|
+
case 'drowsy':
|
|
509
|
+
return [
|
|
510
|
+
loop('.fig', 'nod', f.nod, 6),
|
|
511
|
+
loop('.b1', 'heavy', f.heavy, 7),
|
|
512
|
+
loop('.crest', 'sway', f.sway, 6.4),
|
|
513
|
+
...(full ? [loop('.pose', 'stretch', f.stretch, 9), loop('.yawn', 'yawn', f.yawn, 9), loop('.mouth', 'gape', f.gape, 9)] : []),
|
|
514
|
+
]
|
|
515
|
+
case 'sleepy':
|
|
516
|
+
return [
|
|
517
|
+
loop('.pose', 'snore', f.snore, 4.6),
|
|
518
|
+
loop('.z1', 'zrise', f.zrise, 3.6),
|
|
519
|
+
...(full ? [loop('.z2', 'zrise', f.zrise, 3.6, { delay: 1.2 }), loop('.fig', 'stretch', f.stretch, 11), loop('.yawn', 'yawn', f.yawn, 11), loop('.mouth', 'gape', f.gape, 11)] : []),
|
|
520
|
+
]
|
|
521
|
+
case 'uneasy':
|
|
522
|
+
case 'strained':
|
|
523
|
+
case 'alarmed': {
|
|
524
|
+
// The wobble swings wider and beats faster as the gauge fills; so does the sweat.
|
|
525
|
+
const { swing, beat } = strain(face, percent ?? DEFAULT_PERCENT[face]!)
|
|
526
|
+
const wob = '0%,100%{transform:none}25%{transform:rotate(' + n(swing) + 'deg)}75%{transform:rotate(' + n(-swing) + 'deg)}'
|
|
527
|
+
const b = Number(n(beat))
|
|
528
|
+
const sweat = Number(n(beat * 1.3))
|
|
529
|
+
if (face === 'uneasy') {
|
|
530
|
+
return [loop('.pose', 'wob', wob, b), loop('.face', 'fret', f.fret, 3.2), loop('.sweat', 'drip', f.drip, sweat), loop('.crest', 'sway', f.sway, 2.8), loop('.b1', 'blinkA', f.blinkA, 7), loop('.b2', 'blinkB', f.blinkB, 11)]
|
|
531
|
+
}
|
|
532
|
+
if (face === 'strained') {
|
|
533
|
+
return [loop('.pose', 'wob', wob, b), loop('.crest', 'quiver', f.quiver, b), loop('.ears', 'pulse', f.pulse, b), loop('.sweat', 'drip', f.drip, sweat), loop('.b1', 'blinkA', f.blinkA, 7)]
|
|
534
|
+
}
|
|
535
|
+
return [
|
|
536
|
+
loop('.pose', 'wob', wob, b),
|
|
537
|
+
loop('.crest', 'quiver', f.quiver, b),
|
|
538
|
+
loop('.ears', 'pulse', f.pulse, b),
|
|
539
|
+
loop('.face', 'jolt', f.jolt, b),
|
|
540
|
+
loop('.sweat', 'drip', f.drip, sweat),
|
|
541
|
+
loop('.sweat2', 'drip', f.drip, sweat, { delay: Number(n(sweat / 2)) }),
|
|
542
|
+
]
|
|
543
|
+
}
|
|
544
|
+
case 'dazed':
|
|
545
|
+
return [
|
|
546
|
+
loop('.e1', 'dizzy', f.dizzy, 1.8, { timing: 'linear' }),
|
|
547
|
+
loop('.e2', 'dizzy', f.dizzy, 1.8, { timing: 'linear' }),
|
|
548
|
+
loop('.fig', 'orbit', f.orbit, 2.4),
|
|
549
|
+
loop('.shard', 'shard', f.shard, 2.6),
|
|
550
|
+
loop('.crest', 'sway', f.sway, 3),
|
|
551
|
+
]
|
|
552
|
+
case 'asking':
|
|
553
|
+
return [
|
|
554
|
+
loop('.fig', 'tilt', f.tilt, 4.4),
|
|
555
|
+
loop('.bub1', 'pop', f.pop, 4.4),
|
|
556
|
+
loop('.bub2', 'pop', f.pop, 4.4, { delay: 0.25 }),
|
|
557
|
+
loop('.bub3', 'pop', f.pop, 4.4, { delay: 0.5 }),
|
|
558
|
+
loop('.b1', 'blinkA', f.blinkA, 7),
|
|
559
|
+
loop('.b2', 'blinkB', f.blinkB, 11),
|
|
560
|
+
]
|
|
561
|
+
case 'squeezing':
|
|
562
|
+
return [loop('.pose', 'squash', f.squash, 1.4), loop('.spin', 'spin', f.spin, 1.4, { timing: 'linear' }), loop('.ears', 'squeeze', f.squeeze, 1.4)]
|
|
563
|
+
case 'going':
|
|
564
|
+
return [
|
|
565
|
+
loop('.earR', 'wag', f.wag, 2.4),
|
|
566
|
+
loop('.wR', 'leave', f.leave, 2.4, { delay: 0.3 }),
|
|
567
|
+
loop('.spin', 'spin', f.spin, 1.6, { timing: 'linear' }),
|
|
568
|
+
loop('.crest', 'flutter', f.flutter, 0.8),
|
|
569
|
+
loop('.fig', 'lean', f.lean, 2.4),
|
|
570
|
+
]
|
|
571
|
+
case 'pleased':
|
|
572
|
+
// A happy bounce and its ripple, once; then the idle life, its eyes too glad to blink.
|
|
573
|
+
return [loop('.fig', 'bounce', f.bounce, 1.2, { timing: 'ease-out', once: true }), loop('.halo', 'halo', f.halo, 1.4, { timing: 'ease-out', once: true }), ...idle(f, 1.2, false)]
|
|
574
|
+
case 'sorry':
|
|
575
|
+
return [loop('.fig', 'sink', f.sink, 4.2), loop('.crest', 'droop', f.droop, 4.2), loop('.tear', 'tear', f.tear, 3.6), loop('.b2', 'blinkB', f.blinkB, 11)]
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Where each part turns: the ring's foot (the whole figure), its centre, the crest's base, the ears'
|
|
580
|
+
// roots, the middle of the eyes; the bubbles and the yawn about themselves.
|
|
581
|
+
const ORIGINS =
|
|
582
|
+
'g,circle,path,rect,ellipse{transform-box:view-box}' +
|
|
583
|
+
'.fig,.pose{transform-origin:28px 49.5px}' +
|
|
584
|
+
'.spin,.halo,.face,.ears,.wL,.wR,.r1,.r2{transform-origin:28px 31px}' +
|
|
585
|
+
'.crest,.crest2{transform-origin:30.5px 13px}' +
|
|
586
|
+
'.earR{transform-origin:45px 31px}' +
|
|
587
|
+
'.e1{transform-origin:22.4px 29px}.e2{transform-origin:33.6px 29px}' +
|
|
588
|
+
'.shard{transform-origin:46px 44px}' +
|
|
589
|
+
'.yawn,.mouth{transform-origin:28px 37.6px}' +
|
|
590
|
+
'.bub{transform-box:fill-box;transform-origin:center}'
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* The face's motion, for svgDoc's `css`; all of it inside a no-preference reduced-motion query, so
|
|
594
|
+
* with less motion asked for the drawing is its rest pose. `percent` is the gauge's, which sets how
|
|
595
|
+
* hard uneasy, strained and alarmed strain (in its 5% steps, like the ring).
|
|
596
|
+
*/
|
|
597
|
+
export function rippleCss(face: Face, percent?: number, detail: Detail = 'full'): string {
|
|
598
|
+
const moves = motionOf(face, percent, detail)
|
|
599
|
+
const keyframes = new Map<string, string>()
|
|
600
|
+
for (const move of moves) keyframes.set(move.name, move.frames)
|
|
601
|
+
// One rule per element, its animations in one list: two rules on one element would drop one.
|
|
602
|
+
const by = new Map<string, string[]>()
|
|
603
|
+
for (const move of moves) {
|
|
604
|
+
const one = move.name + ' ' + n(move.seconds) + 's ' + (move.timing ?? 'ease-in-out') + (move.delay ? ' ' + n(move.delay) + 's' : '') + (move.once ? ' 1' : ' infinite')
|
|
605
|
+
by.set(move.at, [...(by.get(move.at) ?? []), one])
|
|
606
|
+
}
|
|
607
|
+
const eyes = '.eyes,.b1,.b2,.b3{transform-origin:28px ' + SPECS[face].eyesY + 'px}'
|
|
608
|
+
return (
|
|
609
|
+
ORIGINS + eyes +
|
|
610
|
+
'@media (prefers-reduced-motion:no-preference){' +
|
|
611
|
+
[...keyframes].map(([name, body]) => '@keyframes ' + name + '{' + body + '}').join('') +
|
|
612
|
+
[...by].map(([at, list]) => at + '{animation:' + list.join(',') + '}').join('') + '}'
|
|
613
|
+
)
|
|
614
|
+
}
|