@lemoncat7/dsh-theme-xiaohei 0.3.1 → 0.3.2
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/README.md +7 -5
- package/lib/chrome/sidebar.d.ts +1 -1
- package/lib/chrome/sidebar.js +16 -0
- package/lib/client.js +410 -6
- package/lib/generated-character-motion.d.ts +4 -0
- package/lib/generated-character-motion.js +2 -0
- package/lib/metallic-brand-mark.js +16 -2
- package/lib/scene/character-motion-clock.d.ts +18 -0
- package/lib/scene/character-motion-clock.js +57 -0
- package/lib/scene/character-motion-types.d.ts +12 -0
- package/lib/scene/character-motion-types.js +2 -0
- package/lib/scene/character-motion.d.ts +6 -0
- package/lib/scene/character-motion.js +126 -0
- package/lib/scene/character-rig-renderer.d.ts +4 -0
- package/lib/scene/character-rig-renderer.js +85 -0
- package/lib/scene/character-rig.d.ts +20 -0
- package/lib/scene/character-rig.js +53 -0
- package/lib/scene/wallpaper-character-styles.js +10 -0
- package/lib/scene/wallpaper-character.d.ts +1 -1
- package/lib/scene/wallpaper-character.js +19 -1
- package/lib/sidebar-glass.js +13 -3
- package/lib/sidebar-reveal.d.ts +8 -0
- package/lib/sidebar-reveal.js +51 -0
- package/package.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { XIAOHEI_BRAND_AVATAR } from './generated-identity.js';
|
|
4
|
+
// Native wide/rail switches remount this slot. A tiny navigation mark does not
|
|
5
|
+
// need a fresh GPU context and shader compilation on every toggle.
|
|
6
|
+
let sidebarMetallicSnapshot;
|
|
4
7
|
const VERTEX_SHADER = `#version 300 es
|
|
5
8
|
precision highp float;
|
|
6
9
|
in vec2 a_position;
|
|
@@ -167,7 +170,10 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
|
|
|
167
170
|
const [ready, setReady] = useState(false);
|
|
168
171
|
const visualSize = Math.max(24, size + 4);
|
|
169
172
|
const context = size >= 32 ? 'hero' : 'sidebar';
|
|
173
|
+
const [snapshot, setSnapshot] = useState(context === 'sidebar' ? sidebarMetallicSnapshot : undefined);
|
|
170
174
|
useEffect(() => {
|
|
175
|
+
if (snapshot !== undefined)
|
|
176
|
+
return;
|
|
171
177
|
const canvas = canvasRef.current;
|
|
172
178
|
if (canvas === null)
|
|
173
179
|
return;
|
|
@@ -273,6 +279,11 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
|
|
|
273
279
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
274
280
|
gl.uniform1i(uniforms.u_tex, 0);
|
|
275
281
|
draw();
|
|
282
|
+
if (context === 'sidebar') {
|
|
283
|
+
sidebarMetallicSnapshot = canvas.toDataURL();
|
|
284
|
+
setSnapshot(sidebarMetallicSnapshot);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
276
287
|
setReady(true);
|
|
277
288
|
if (!reduceMotion.matches)
|
|
278
289
|
animationFrame = window.requestAnimationFrame(render);
|
|
@@ -295,12 +306,15 @@ export function XiaoheiMetallicBrandMark({ size, className }) {
|
|
|
295
306
|
gl.deleteTexture(texture);
|
|
296
307
|
gl.deleteBuffer(buffer);
|
|
297
308
|
gl.deleteProgram(program);
|
|
309
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
298
310
|
};
|
|
299
|
-
}, [visualSize]);
|
|
311
|
+
}, [visualSize, context, snapshot]);
|
|
300
312
|
const geometry = {
|
|
301
313
|
'--xiaohei-brand-mark-size': `${visualSize}px`,
|
|
302
314
|
};
|
|
303
|
-
return (_jsxs("span", { className: className === undefined ? 'xiaohei-brand-mark' : `${className} xiaohei-brand-mark`, "data-brand-context": context, "data-metallic-ready": ready ? 'true' : 'false', style: geometry, "aria-hidden": "true", children: [_jsx("img", { className: "xiaohei-brand-mark__fallback", src: XIAOHEI_BRAND_AVATAR, alt: "" }),
|
|
315
|
+
return (_jsxs("span", { className: className === undefined ? 'xiaohei-brand-mark' : `${className} xiaohei-brand-mark`, "data-brand-context": context, "data-metallic-ready": ready || snapshot !== undefined ? 'true' : 'false', style: geometry, "aria-hidden": "true", children: [_jsx("img", { className: "xiaohei-brand-mark__fallback", src: XIAOHEI_BRAND_AVATAR, alt: "" }), snapshot !== undefined
|
|
316
|
+
? _jsx("img", { className: "xiaohei-brand-mark__metal", src: snapshot, alt: "" })
|
|
317
|
+
: _jsx("canvas", { className: "xiaohei-brand-mark__metal", ref: canvasRef })] }));
|
|
304
318
|
}
|
|
305
319
|
/** Official sidebar brand-name occupant paired with the Xiaohei mark. */
|
|
306
320
|
export function XiaoheiBrandName() {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface Clock {
|
|
2
|
+
setTimeout(callback: () => void, delay: number): number;
|
|
3
|
+
clearTimeout(handle: number): void;
|
|
4
|
+
performance: {
|
|
5
|
+
now(): number;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export interface CharacterMotionClip {
|
|
9
|
+
duration: number;
|
|
10
|
+
}
|
|
11
|
+
/** One deadline; elapsed-time continuous sampling, capped at 30 fps. */
|
|
12
|
+
export declare function createCharacterMotionClock(clock: Clock, changed: (action: string | undefined, progress: number) => void, random?: () => number): {
|
|
13
|
+
setClips(next: Readonly<Record<string, CharacterMotionClip>> | undefined): void;
|
|
14
|
+
attention(): void;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=character-motion-clock.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** One deadline; elapsed-time continuous sampling, capped at 30 fps. */
|
|
2
|
+
export function createCharacterMotionClock(clock, changed, random = Math.random) {
|
|
3
|
+
let timer, disposed = false, turn = 0, active = '';
|
|
4
|
+
let clips;
|
|
5
|
+
const clear = () => { if (timer !== undefined)
|
|
6
|
+
clock.clearTimeout(timer); timer = undefined; };
|
|
7
|
+
function rest() {
|
|
8
|
+
active = '';
|
|
9
|
+
changed(undefined, 0);
|
|
10
|
+
if (clips && !disposed)
|
|
11
|
+
timer = clock.setTimeout(start, 2800 + random() * 2800);
|
|
12
|
+
}
|
|
13
|
+
function play(action) {
|
|
14
|
+
const clip = clips?.[action];
|
|
15
|
+
if (!clip || disposed)
|
|
16
|
+
return;
|
|
17
|
+
clear();
|
|
18
|
+
active = action;
|
|
19
|
+
const started = clock.performance.now();
|
|
20
|
+
const step = () => {
|
|
21
|
+
timer = undefined;
|
|
22
|
+
if (!clips || disposed)
|
|
23
|
+
return;
|
|
24
|
+
const t = Math.min(1, (clock.performance.now() - started) / clip.duration);
|
|
25
|
+
changed(active, t);
|
|
26
|
+
if (t >= 1) {
|
|
27
|
+
rest();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
timer = clock.setTimeout(step, 1000 / 30);
|
|
31
|
+
};
|
|
32
|
+
step();
|
|
33
|
+
}
|
|
34
|
+
function start() {
|
|
35
|
+
timer = undefined;
|
|
36
|
+
if (!clips || disposed)
|
|
37
|
+
return;
|
|
38
|
+
const gestures = Object.keys(clips).filter(name => name !== 'blink' && name !== 'attention');
|
|
39
|
+
play((turn++ % 2 === 0 && clips.blink) || !gestures.length ? 'blink' : gestures[Math.floor(random() * gestures.length)]);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
setClips(next) {
|
|
43
|
+
if (disposed || next === clips)
|
|
44
|
+
return;
|
|
45
|
+
clear();
|
|
46
|
+
clips = next;
|
|
47
|
+
active = '';
|
|
48
|
+
changed(undefined, 0);
|
|
49
|
+
if (clips)
|
|
50
|
+
timer = clock.setTimeout(start, 1200 + random() * 800);
|
|
51
|
+
},
|
|
52
|
+
attention() { if (!active)
|
|
53
|
+
play('attention'); },
|
|
54
|
+
dispose() { disposed = true; clear(); clips = undefined; changed(undefined, 0); },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=character-motion-clock.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type Point = readonly [number, number];
|
|
2
|
+
export interface CharacterLayer {
|
|
3
|
+
readonly src: string;
|
|
4
|
+
readonly box: readonly [number, number, number, number];
|
|
5
|
+
}
|
|
6
|
+
export interface CharacterRig {
|
|
7
|
+
readonly layers: Readonly<Record<string, CharacterLayer>>;
|
|
8
|
+
readonly ears: readonly Point[];
|
|
9
|
+
readonly size: Point;
|
|
10
|
+
}
|
|
11
|
+
export type CharacterMotionCatalog = Readonly<Record<string, Readonly<Record<'light' | 'dark', CharacterRig>>>>;
|
|
12
|
+
//# sourceMappingURL=character-motion-types.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Owns loading and lifecycle only. Rig math and painting stay independent. */
|
|
2
|
+
export declare function createCharacterMotion(doc: Document): {
|
|
3
|
+
setPose(nextPart: HTMLElement | undefined, nextPose: string): void;
|
|
4
|
+
dispose(): void;
|
|
5
|
+
};
|
|
6
|
+
//# sourceMappingURL=character-motion.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { CHARACTER_MOTION } from '../generated-character-motion.js';
|
|
2
|
+
import { createCharacterMotionClock } from './character-motion-clock.js';
|
|
3
|
+
import { createRigRenderer } from './character-rig-renderer.js';
|
|
4
|
+
/** Owns loading and lifecycle only. Rig math and painting stay independent. */
|
|
5
|
+
export function createCharacterMotion(doc) {
|
|
6
|
+
const win = doc.defaultView, reduced = win.matchMedia('(prefers-reduced-motion: reduce)');
|
|
7
|
+
const images = new Map(), loading = new Map();
|
|
8
|
+
let disposed = false, part, rig;
|
|
9
|
+
let canvas, render;
|
|
10
|
+
let pose = '', appearance = '', revision = 0, nearby = false, lastPointer = 0;
|
|
11
|
+
function decode(src) {
|
|
12
|
+
if (loading.has(src))
|
|
13
|
+
return loading.get(src);
|
|
14
|
+
if (images.has(src))
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
const image = new win.Image();
|
|
17
|
+
image.src = src;
|
|
18
|
+
const pending = image.decode().then(() => { if (!disposed)
|
|
19
|
+
images.set(src, image); }, () => { })
|
|
20
|
+
.finally(() => loading.delete(src));
|
|
21
|
+
loading.set(src, pending);
|
|
22
|
+
return pending;
|
|
23
|
+
}
|
|
24
|
+
const clock = createCharacterMotionClock(win, (action, progress) => {
|
|
25
|
+
if (!canvas || !part || !render)
|
|
26
|
+
return;
|
|
27
|
+
// Keep the same layered rest drawing after an action: no plate swap/pop.
|
|
28
|
+
render(action, progress);
|
|
29
|
+
part.dataset.motion = action ?? 'rig-rest';
|
|
30
|
+
});
|
|
31
|
+
function update() {
|
|
32
|
+
if (disposed)
|
|
33
|
+
return;
|
|
34
|
+
const mode = doc.documentElement.dataset.xiaoheiAppearance === 'light' ? 'light' : 'dark';
|
|
35
|
+
const moving = doc.documentElement.hasAttribute('data-xiaohei-sidebar-resizing');
|
|
36
|
+
const next = !doc.hidden && !reduced.matches && !moving && part ? CHARACTER_MOTION[pose]?.[mode] : undefined;
|
|
37
|
+
if (next === rig && appearance === mode)
|
|
38
|
+
return;
|
|
39
|
+
clock.setClips(undefined);
|
|
40
|
+
render = undefined;
|
|
41
|
+
const current = ++revision;
|
|
42
|
+
canvas?.remove();
|
|
43
|
+
canvas = undefined;
|
|
44
|
+
if (part)
|
|
45
|
+
part.dataset.motion = 'rest';
|
|
46
|
+
rig = next;
|
|
47
|
+
appearance = mode;
|
|
48
|
+
nearby = false;
|
|
49
|
+
if (!next || !part)
|
|
50
|
+
return;
|
|
51
|
+
const currentPart = part;
|
|
52
|
+
void Promise.all(Object.values(next.layers).map(layer => decode(layer.src))).then(() => {
|
|
53
|
+
if (disposed || current !== revision || Object.values(next.layers).some(layer => !images.get(layer.src)))
|
|
54
|
+
return;
|
|
55
|
+
canvas = doc.createElement('canvas');
|
|
56
|
+
canvas.className = 'xiaohei-character-motion';
|
|
57
|
+
canvas.width = next.size[0];
|
|
58
|
+
canvas.height = next.size[1];
|
|
59
|
+
const ctx = canvas.getContext('2d');
|
|
60
|
+
if (!ctx) {
|
|
61
|
+
canvas = undefined;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
currentPart.append(canvas);
|
|
65
|
+
render = createRigRenderer(ctx, next, images);
|
|
66
|
+
clock.setClips({
|
|
67
|
+
blink: { duration: 330 },
|
|
68
|
+
...Object.fromEntries(next.ears.map((_, i) => ['ear' + i, { duration: 1050 }])),
|
|
69
|
+
...(next.layers.tail ? { tail: { duration: 2400 } } : {}),
|
|
70
|
+
attention: { duration: 2400 },
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const pointer = (event) => {
|
|
75
|
+
if (!part || !render || event.pointerType !== 'mouse')
|
|
76
|
+
return;
|
|
77
|
+
const now = win.performance.now();
|
|
78
|
+
if (now - lastPointer < 100)
|
|
79
|
+
return;
|
|
80
|
+
lastPointer = now;
|
|
81
|
+
const rect = part.getBoundingClientRect();
|
|
82
|
+
const near = event.clientX >= rect.left - 70 && event.clientX <= rect.right + 70
|
|
83
|
+
&& event.clientY >= rect.top - 50 && event.clientY <= rect.bottom + 50;
|
|
84
|
+
if (near && !nearby)
|
|
85
|
+
clock.attention();
|
|
86
|
+
nearby = near;
|
|
87
|
+
};
|
|
88
|
+
reduced.addEventListener('change', update);
|
|
89
|
+
doc.addEventListener('visibilitychange', update);
|
|
90
|
+
doc.addEventListener('pointermove', pointer, { passive: true });
|
|
91
|
+
const observer = new win.MutationObserver(update);
|
|
92
|
+
observer.observe(doc.documentElement, { attributes: true, attributeFilter: ['data-xiaohei-appearance', 'data-xiaohei-sidebar-resizing'] });
|
|
93
|
+
return {
|
|
94
|
+
setPose(nextPart, nextPose) {
|
|
95
|
+
if (nextPart !== part || pose !== nextPose) {
|
|
96
|
+
revision++;
|
|
97
|
+
clock.setClips(undefined);
|
|
98
|
+
render = undefined;
|
|
99
|
+
canvas?.remove();
|
|
100
|
+
canvas = undefined;
|
|
101
|
+
rig = undefined;
|
|
102
|
+
if (part)
|
|
103
|
+
part.dataset.motion = 'rest';
|
|
104
|
+
}
|
|
105
|
+
part = nextPart;
|
|
106
|
+
pose = nextPose;
|
|
107
|
+
update();
|
|
108
|
+
},
|
|
109
|
+
dispose() {
|
|
110
|
+
disposed = true;
|
|
111
|
+
revision++;
|
|
112
|
+
clock.dispose();
|
|
113
|
+
canvas?.remove();
|
|
114
|
+
render = undefined;
|
|
115
|
+
images.clear();
|
|
116
|
+
loading.clear();
|
|
117
|
+
if (part)
|
|
118
|
+
part.dataset.motion = 'rest';
|
|
119
|
+
reduced.removeEventListener('change', update);
|
|
120
|
+
observer.disconnect();
|
|
121
|
+
doc.removeEventListener('visibilitychange', update);
|
|
122
|
+
doc.removeEventListener('pointermove', pointer);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=character-motion.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CharacterRig } from './character-motion-types.js';
|
|
2
|
+
/** Small CPU-skinned tail only; body and ears use normal image draws. */
|
|
3
|
+
export declare function createRigRenderer(ctx: CanvasRenderingContext2D, rig: CharacterRig, images: ReadonlyMap<string, HTMLImageElement | null>): (action: string | undefined, progress: number) => void;
|
|
4
|
+
//# sourceMappingURL=character-rig-renderer.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { bindVertex, rigAngles, skinVertex, solveBones, TAIL_JOINTS } from './character-rig.js';
|
|
2
|
+
/** Small CPU-skinned tail only; body and ears use normal image draws. */
|
|
3
|
+
export function createRigRenderer(ctx, rig, images) {
|
|
4
|
+
const tail = rig.layers.tail;
|
|
5
|
+
const vertices = [], triangles = [];
|
|
6
|
+
if (tail) {
|
|
7
|
+
const [x, y, r, b] = tail.box, cols = Math.ceil((r - x) / 10), rows = Math.ceil((b - y) / 10);
|
|
8
|
+
for (let row = 0; row <= rows; row++)
|
|
9
|
+
for (let col = 0; col <= cols; col++)
|
|
10
|
+
vertices.push(bindVertex(x + (r - x) * col / cols, y + (b - y) * row / rows));
|
|
11
|
+
for (let row = 0; row < rows; row++)
|
|
12
|
+
for (let col = 0; col < cols; col++) {
|
|
13
|
+
const i = row * (cols + 1) + col;
|
|
14
|
+
triangles.push([i, i + 1, i + cols + 1], [i + 1, i + cols + 2, i + cols + 1]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function layer(name) {
|
|
18
|
+
const layer = rig.layers[name], image = layer && images.get(layer.src);
|
|
19
|
+
if (layer && image) {
|
|
20
|
+
const [x, y, r, b] = layer.box;
|
|
21
|
+
ctx.drawImage(image, x, y, r - x, b - y);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function drawTail(progress) {
|
|
25
|
+
if (!tail)
|
|
26
|
+
return;
|
|
27
|
+
const image = images.get(tail.src);
|
|
28
|
+
if (!image)
|
|
29
|
+
return;
|
|
30
|
+
if (progress <= 0 || progress >= 1) {
|
|
31
|
+
layer('tail');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const joints = solveBones(TAIL_JOINTS, rigAngles(progress, TAIL_JOINTS.length, true));
|
|
35
|
+
const posed = vertices.map(v => skinVertex(v, joints));
|
|
36
|
+
const [left, top] = tail.box;
|
|
37
|
+
for (const indices of triangles) {
|
|
38
|
+
const [ia, ib, ic] = indices;
|
|
39
|
+
const a = vertices[ia], b = vertices[ib], c = vertices[ic];
|
|
40
|
+
const p = posed[ia], q = posed[ib], r = posed[ic];
|
|
41
|
+
const ux = b.x - a.x, uy = b.y - a.y, vx = c.x - a.x, vy = c.y - a.y, det = ux * vy - uy * vx;
|
|
42
|
+
const aa = ((q[0] - p[0]) * vy - (r[0] - p[0]) * uy) / det;
|
|
43
|
+
const bb = ((q[1] - p[1]) * vy - (r[1] - p[1]) * uy) / det;
|
|
44
|
+
const cc = ((r[0] - p[0]) * ux - (q[0] - p[0]) * vx) / det;
|
|
45
|
+
const dd = ((r[1] - p[1]) * ux - (q[1] - p[1]) * vx) / det;
|
|
46
|
+
ctx.save();
|
|
47
|
+
ctx.beginPath();
|
|
48
|
+
// Slightly overlap the interior triangles to avoid antialias hairlines.
|
|
49
|
+
const cx = (p[0] + q[0] + r[0]) / 3, cy = (p[1] + q[1] + r[1]) / 3;
|
|
50
|
+
[p, q, r].forEach((v, i) => {
|
|
51
|
+
const dx = v[0] - cx, dy = v[1] - cy, scale = 1 + 1.2 / Math.max(1, Math.hypot(dx, dy));
|
|
52
|
+
if (i)
|
|
53
|
+
ctx.lineTo(cx + dx * scale, cy + dy * scale);
|
|
54
|
+
else
|
|
55
|
+
ctx.moveTo(cx + dx * scale, cy + dy * scale);
|
|
56
|
+
});
|
|
57
|
+
ctx.closePath();
|
|
58
|
+
ctx.clip();
|
|
59
|
+
ctx.transform(aa, bb, cc, dd, p[0] - aa * (a.x - left) - cc * (a.y - top), p[1] - bb * (a.x - left) - dd * (a.y - top));
|
|
60
|
+
ctx.drawImage(image, 0, 0);
|
|
61
|
+
ctx.restore();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return (action, progress) => {
|
|
65
|
+
ctx.clearRect(0, 0, rig.size[0], rig.size[1]);
|
|
66
|
+
const ears = rigAngles(progress, rig.ears.length, false);
|
|
67
|
+
rig.ears.forEach(([x, y], i) => {
|
|
68
|
+
ctx.save();
|
|
69
|
+
ctx.translate(x, y);
|
|
70
|
+
ctx.rotate(action === 'attention' || action === 'ear' + i ? ears[i] : 0);
|
|
71
|
+
ctx.translate(-x, -y);
|
|
72
|
+
layer('ear' + i);
|
|
73
|
+
ctx.restore();
|
|
74
|
+
});
|
|
75
|
+
layer('body');
|
|
76
|
+
drawTail(action === 'tail' || action === 'attention' ? progress : 0);
|
|
77
|
+
if (action === 'blink') {
|
|
78
|
+
ctx.save();
|
|
79
|
+
ctx.globalAlpha = Math.min(1, Math.sin(Math.PI * progress) * 2);
|
|
80
|
+
layer('blink');
|
|
81
|
+
ctx.restore();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=character-rig-renderer.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Point } from './character-motion-types.js';
|
|
2
|
+
/** Bind-space joint positions. Each child inherits its parent's rotation. */
|
|
3
|
+
export declare const TAIL_JOINTS: readonly Point[];
|
|
4
|
+
export interface Joint {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
angle: number;
|
|
8
|
+
}
|
|
9
|
+
export interface Vertex {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
bone: number;
|
|
13
|
+
weight: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function solveBones(rest: readonly Point[], angles: readonly number[]): Joint[];
|
|
16
|
+
export declare function bindVertex(x: number, y: number, rest?: readonly Point[]): Vertex;
|
|
17
|
+
/** Linear blend skinning, inverse bind translation followed by posed rotation. */
|
|
18
|
+
export declare function skinVertex(v: Vertex, joints: readonly Joint[], rest?: readonly Point[]): Point;
|
|
19
|
+
export declare function rigAngles(progress: number, count: number, tail: boolean): number[];
|
|
20
|
+
//# sourceMappingURL=character-rig.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Bind-space joint positions. Each child inherits its parent's rotation. */
|
|
2
|
+
export const TAIL_JOINTS = [
|
|
3
|
+
[284, 328], [310, 347], [324, 373], [319, 398],
|
|
4
|
+
[307, 423], [307, 448], [319, 464],
|
|
5
|
+
];
|
|
6
|
+
export function solveBones(rest, angles) {
|
|
7
|
+
return rest.reduce((out, p, i) => {
|
|
8
|
+
const parent = out[i - 1];
|
|
9
|
+
const angle = (parent?.angle ?? 0) + (angles[i] ?? 0);
|
|
10
|
+
if (!parent)
|
|
11
|
+
out.push({ x: p[0], y: p[1], angle });
|
|
12
|
+
else {
|
|
13
|
+
const prev = rest[i - 1], dx = p[0] - prev[0], dy = p[1] - prev[1];
|
|
14
|
+
const c = Math.cos(parent.angle), s = Math.sin(parent.angle);
|
|
15
|
+
out.push({ x: parent.x + dx * c - dy * s, y: parent.y + dx * s + dy * c, angle });
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}, []);
|
|
19
|
+
}
|
|
20
|
+
export function bindVertex(x, y, rest = TAIL_JOINTS) {
|
|
21
|
+
let distance = Infinity, bone = 0, weight = 0;
|
|
22
|
+
for (let i = 0; i < rest.length - 1; i++) {
|
|
23
|
+
const a = rest[i], b = rest[i + 1], dx = b[0] - a[0], dy = b[1] - a[1];
|
|
24
|
+
const t = Math.max(0, Math.min(1, ((x - a[0]) * dx + (y - a[1]) * dy) / (dx * dx + dy * dy)));
|
|
25
|
+
const d = (x - a[0] - dx * t) ** 2 + (y - a[1] - dy * t) ** 2;
|
|
26
|
+
if (d < distance) {
|
|
27
|
+
distance = d;
|
|
28
|
+
bone = i;
|
|
29
|
+
weight = t;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { x, y, bone, weight };
|
|
33
|
+
}
|
|
34
|
+
/** Linear blend skinning, inverse bind translation followed by posed rotation. */
|
|
35
|
+
export function skinVertex(v, joints, rest = TAIL_JOINTS) {
|
|
36
|
+
let x = 0, y = 0;
|
|
37
|
+
for (let k = 0; k < 2; k++) {
|
|
38
|
+
const i = v.bone + k, p = rest[i], j = joints[i], w = k ? v.weight : 1 - v.weight;
|
|
39
|
+
const dx = v.x - p[0], dy = v.y - p[1], c = Math.cos(j.angle), s = Math.sin(j.angle);
|
|
40
|
+
x += w * (j.x + dx * c - dy * s);
|
|
41
|
+
y += w * (j.y + dx * s + dy * c);
|
|
42
|
+
}
|
|
43
|
+
return [x, y];
|
|
44
|
+
}
|
|
45
|
+
export function rigAngles(progress, count, tail) {
|
|
46
|
+
const t = Math.max(0, Math.min(1, progress));
|
|
47
|
+
const envelope = Math.sin(Math.PI * t) ** 2;
|
|
48
|
+
// Delayed distal joints create follow-through, with zero displacement/velocity
|
|
49
|
+
// at both ends; no discrete frame lookup and no idle simulation.
|
|
50
|
+
return Array.from({ length: count }, (_, i) => envelope * (tail ? (i ? .12 : 0) : .22)
|
|
51
|
+
* Math.sin(t * Math.PI * (tail ? 3 : 4) - i * (tail ? .65 : 1.1)));
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=character-rig.js.map
|
|
@@ -8,6 +8,9 @@ export const XIAOHEI_WALLPAPER_CHARACTER_CSS = `
|
|
|
8
8
|
pointer-events: none;
|
|
9
9
|
user-select: none;
|
|
10
10
|
}
|
|
11
|
+
html[data-xiaohei-sidebar-resizing] .xiaohei-wallpaper-character {
|
|
12
|
+
visibility: hidden;
|
|
13
|
+
}
|
|
11
14
|
html[data-xiaohei-appearance='light'] .xiaohei-wallpaper-character {
|
|
12
15
|
${CHARACTER_POSE_NAMES.map(pose => `--xiaohei-${pose}-art: url("${CHARACTER_POSES[pose].light}");`).join('\n ')}
|
|
13
16
|
}
|
|
@@ -21,6 +24,13 @@ html[data-xiaohei-appearance='light'] .xiaohei-wallpaper-character {
|
|
|
21
24
|
opacity: 0;
|
|
22
25
|
transition: opacity 220ms ease, transform 220ms cubic-bezier(.2,.8,.2,1);
|
|
23
26
|
}
|
|
27
|
+
.xiaohei-wallpaper-character .xiaohei-character-motion {
|
|
28
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
29
|
+
object-fit: contain; object-position: left center; pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
.xiaohei-wallpaper-character > span[data-motion]:not([data-motion='rest']) {
|
|
32
|
+
background-image: none;
|
|
33
|
+
}
|
|
24
34
|
${CHARACTER_POSE_NAMES.map(pose => `
|
|
25
35
|
.xiaohei-wallpaper-character__${pose}[data-ready] {
|
|
26
36
|
background-image: var(--xiaohei-${pose}-art);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare const XIAOHEI_WALLPAPER_CHARACTER_ID = "dsh-theme-xiaohei/wallpaper-character";
|
|
2
|
-
/**
|
|
2
|
+
/** Places the layered character only when the host geometry is settled. */
|
|
3
3
|
export declare function installXiaoheiWallpaperCharacter(doc?: Document | undefined): () => void;
|
|
4
4
|
//# sourceMappingURL=wallpaper-character.d.ts.map
|
|
@@ -4,8 +4,9 @@ import { XIAOHEI_SCENE_LAYER_ID } from './styles.js';
|
|
|
4
4
|
import { resolveCharacterPlacement } from './character-layout.js';
|
|
5
5
|
import { createCharacterIdleController } from './character-idle.js';
|
|
6
6
|
import { CHARACTER_POSES, CHARACTER_POSE_NAMES } from './character-poses.js';
|
|
7
|
+
import { createCharacterMotion } from './character-motion.js';
|
|
7
8
|
export const XIAOHEI_WALLPAPER_CHARACTER_ID = 'dsh-theme-xiaohei/wallpaper-character';
|
|
8
|
-
/**
|
|
9
|
+
/** Places the layered character only when the host geometry is settled. */
|
|
9
10
|
export function installXiaoheiWallpaperCharacter(doc = typeof document === 'undefined' ? undefined : document) {
|
|
10
11
|
const win = doc?.defaultView;
|
|
11
12
|
if (!doc || !win)
|
|
@@ -20,6 +21,7 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
|
|
|
20
21
|
const ready = new Set();
|
|
21
22
|
const loading = new Set();
|
|
22
23
|
const failed = new Set();
|
|
24
|
+
const motion = typeof win.matchMedia === 'function' ? createCharacterMotion(doc) : undefined;
|
|
23
25
|
const idle = createCharacterIdleController({ now: () => win.performance.now(),
|
|
24
26
|
setTimeout: (fn, delay) => win.setTimeout(fn, delay), clearTimeout: handle => win.clearTimeout(handle),
|
|
25
27
|
}, () => schedule());
|
|
@@ -43,6 +45,17 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
|
|
|
43
45
|
function reconcile() {
|
|
44
46
|
if (disposed || !doc)
|
|
45
47
|
return;
|
|
48
|
+
// The glass owner already tracks the native column's animated geometry.
|
|
49
|
+
// Do not measure controls or rebuild a different rig at intermediate widths.
|
|
50
|
+
if (doc.documentElement?.hasAttribute('data-xiaohei-sidebar-resizing')) {
|
|
51
|
+
dirty = true;
|
|
52
|
+
if (host && host.dataset.pose !== 'hidden')
|
|
53
|
+
host.dataset.pose = 'hidden';
|
|
54
|
+
idle.setPaused(true);
|
|
55
|
+
motion?.setPose(undefined, 'hidden');
|
|
56
|
+
scheduleSettled();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
46
59
|
const layer = doc.getElementById(XIAOHEI_SCENE_LAYER_ID);
|
|
47
60
|
const next = [
|
|
48
61
|
doc.querySelector(XIAOHEI_HOST_SELECTORS.sidebarShell),
|
|
@@ -62,6 +75,7 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
|
|
|
62
75
|
host?.remove();
|
|
63
76
|
host = undefined;
|
|
64
77
|
idle.setPaused(true);
|
|
78
|
+
motion?.setPose(undefined, 'hidden');
|
|
65
79
|
return;
|
|
66
80
|
}
|
|
67
81
|
if (!host || host.parentElement !== layer) {
|
|
@@ -115,9 +129,11 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
|
|
|
115
129
|
if (pose === 'hidden') {
|
|
116
130
|
if (host.dataset.pose !== 'hidden')
|
|
117
131
|
host.dataset.pose = 'hidden';
|
|
132
|
+
motion?.setPose(undefined, 'hidden');
|
|
118
133
|
return;
|
|
119
134
|
}
|
|
120
135
|
if (!ready.has(pose)) {
|
|
136
|
+
motion?.setPose(undefined, 'hidden');
|
|
121
137
|
// Do not retain the old location across a layout change: it could now
|
|
122
138
|
// overlap a message. Idle changes keep the already decoded seated pose.
|
|
123
139
|
if (placement.pose !== 'seated' || !['seated', 'chin', 'doze'].includes(host.dataset.pose ?? ''))
|
|
@@ -152,6 +168,7 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
|
|
|
152
168
|
part.dataset.ready = 'true';
|
|
153
169
|
if (host.dataset.pose !== pose)
|
|
154
170
|
host.dataset.pose = pose;
|
|
171
|
+
motion?.setPose(part, pose);
|
|
155
172
|
}
|
|
156
173
|
function schedule() {
|
|
157
174
|
if (disposed || frame !== undefined)
|
|
@@ -185,6 +202,7 @@ export function installXiaoheiWallpaperCharacter(doc = typeof document === 'unde
|
|
|
185
202
|
unsubscribe();
|
|
186
203
|
observer?.disconnect();
|
|
187
204
|
idle.dispose();
|
|
205
|
+
motion?.dispose();
|
|
188
206
|
for (const event of events)
|
|
189
207
|
doc.removeEventListener(event, activity, true);
|
|
190
208
|
doc.removeEventListener('visibilitychange', visibility);
|
package/lib/sidebar-glass.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { XIAOHEI_SCENE_LAYER_ID, XIAOHEI_SCENE_WORLD_CLASS } from './scene.js';
|
|
2
2
|
import { subscribeXiaoheiHostDom } from './host-dom.js';
|
|
3
3
|
import { XIAOHEI_HOST_SELECTORS } from './host-contract.js';
|
|
4
|
+
import { createSidebarReveal } from './sidebar-reveal.js';
|
|
4
5
|
/** Stable id for the paint-only glass surface behind DSH's native sidebar. */
|
|
5
6
|
export const XIAOHEI_SIDEBAR_GLASS_ID = 'dsh-theme-xiaohei/sidebar-glass';
|
|
6
7
|
const HORIZONTAL_INSET_START = 7;
|
|
@@ -35,6 +36,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
|
|
|
35
36
|
let resizing = false;
|
|
36
37
|
let appliedBounds;
|
|
37
38
|
let geometryDirty = true;
|
|
39
|
+
const reveal = createSidebarReveal(doc);
|
|
38
40
|
const clearResizeState = () => {
|
|
39
41
|
if (resizeSettleTimer !== undefined)
|
|
40
42
|
win.clearTimeout(resizeSettleTimer);
|
|
@@ -63,15 +65,19 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
|
|
|
63
65
|
return;
|
|
64
66
|
// RO runs after host layout and before paint. Update the independent
|
|
65
67
|
// paint layer now, not in next frame's RAF (which visibly trails).
|
|
66
|
-
applyBounds();
|
|
68
|
+
const changed = applyBounds();
|
|
67
69
|
geometryDirty = false;
|
|
68
|
-
|
|
70
|
+
if (changed)
|
|
71
|
+
markResizeActivity();
|
|
69
72
|
})
|
|
70
73
|
: undefined;
|
|
71
74
|
const applyBounds = () => {
|
|
72
75
|
if (glass === undefined || sidebarColumn === undefined)
|
|
73
|
-
return;
|
|
76
|
+
return false;
|
|
74
77
|
const bounds = resolveXiaoheiSidebarGlassBounds(sidebarColumn.getBoundingClientRect());
|
|
78
|
+
reveal.resize(bounds.width + HORIZONTAL_INSET_START + HORIZONTAL_INSET_END);
|
|
79
|
+
const changed = appliedBounds !== undefined && (bounds.left !== appliedBounds.left || bounds.top !== appliedBounds.top ||
|
|
80
|
+
bounds.width !== appliedBounds.width || bounds.height !== appliedBounds.height);
|
|
75
81
|
if (bounds.left !== appliedBounds?.left) {
|
|
76
82
|
glass.style.setProperty('--xiaohei-sidebar-glass-left', `${bounds.left}px`);
|
|
77
83
|
}
|
|
@@ -85,6 +91,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
|
|
|
85
91
|
glass.style.setProperty('--xiaohei-sidebar-glass-height', `${bounds.height}px`);
|
|
86
92
|
}
|
|
87
93
|
appliedBounds = bounds;
|
|
94
|
+
return changed;
|
|
88
95
|
};
|
|
89
96
|
const reconcile = () => {
|
|
90
97
|
if (disposed)
|
|
@@ -99,6 +106,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
|
|
|
99
106
|
resizeObserver?.observe(sidebarColumn);
|
|
100
107
|
}
|
|
101
108
|
if (sceneLayer === null || sidebarColumn === undefined) {
|
|
109
|
+
reveal.setColumn(undefined);
|
|
102
110
|
clearResizeState();
|
|
103
111
|
glass?.remove();
|
|
104
112
|
glass = undefined;
|
|
@@ -106,6 +114,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
|
|
|
106
114
|
geometryDirty = true;
|
|
107
115
|
return;
|
|
108
116
|
}
|
|
117
|
+
reveal.setColumn(sidebarColumn);
|
|
109
118
|
if (glass === undefined || glass.parentElement !== sceneLayer) {
|
|
110
119
|
doc.getElementById(XIAOHEI_SIDEBAR_GLASS_ID)?.remove();
|
|
111
120
|
glass = doc.createElement('div');
|
|
@@ -147,6 +156,7 @@ export function installXiaoheiSidebarGlass(doc = typeof document === 'undefined'
|
|
|
147
156
|
animationFrame = undefined;
|
|
148
157
|
unsubscribeHostDom();
|
|
149
158
|
resizeObserver?.disconnect();
|
|
159
|
+
reveal.dispose();
|
|
150
160
|
win.removeEventListener('resize', onViewportResize);
|
|
151
161
|
clearResizeState();
|
|
152
162
|
glass?.remove();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Synchronize native wide content with the visible column, without changing
|
|
2
|
+
* Host layout/state or adding a competing animation timer. */
|
|
3
|
+
export declare function createSidebarReveal(doc: Document): {
|
|
4
|
+
setColumn(column: HTMLElement | undefined): void;
|
|
5
|
+
resize(nextWidth: number): void;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=sidebar-reveal.d.ts.map
|