@lemoncat7/dsh-theme-xiaohei 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/lib/bloub-heixiu/runtime.js +17 -3
- package/lib/chrome/conversation-messages.js +8 -8
- package/lib/chrome/sidebar.d.ts +1 -1
- package/lib/chrome/sidebar.js +13 -15
- package/lib/chrome/tokens.d.ts +1 -1
- package/lib/chrome/tokens.js +4 -4
- package/lib/client.js +413 -99
- package/lib/gaze.js +15 -2
- package/lib/metallic-brand-mark.js +13 -40
- package/lib/scene/character-expression.d.ts +4 -0
- package/lib/scene/character-expression.js +15 -0
- package/lib/scene/character-eye-material.d.ts +13 -0
- package/lib/scene/character-eye-material.js +88 -0
- package/lib/scene/character-eye-renderer.d.ts +5 -0
- package/lib/scene/character-eye-renderer.js +80 -0
- package/lib/scene/character-eye-rig.d.ts +20 -0
- package/lib/scene/character-eye-rig.js +35 -0
- package/lib/scene/character-gaze.d.ts +16 -0
- package/lib/scene/character-gaze.js +36 -0
- package/lib/scene/character-motion-clock.d.ts +1 -1
- package/lib/scene/character-motion-clock.js +17 -4
- package/lib/scene/character-motion.js +66 -12
- package/lib/scene/character-rig-renderer.d.ts +3 -2
- package/lib/scene/character-rig-renderer.js +28 -18
- package/lib/scene/character-rig.d.ts +2 -0
- package/lib/scene/character-rig.js +9 -1
- package/lib/sidebar-reveal.js +11 -3
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CHARACTER_MOTION } from '../generated-character-motion.js';
|
|
2
2
|
import { createCharacterMotionClock } from './character-motion-clock.js';
|
|
3
3
|
import { createRigRenderer } from './character-rig-renderer.js';
|
|
4
|
+
import { EYE_RIGS } from './character-eye-rig.js';
|
|
5
|
+
import { createCharacterGaze } from './character-gaze.js';
|
|
4
6
|
/** Owns loading and lifecycle only. Rig math and painting stay independent. */
|
|
5
7
|
export function createCharacterMotion(doc) {
|
|
6
8
|
const win = doc.defaultView, reduced = win.matchMedia('(prefers-reduced-motion: reduce)');
|
|
@@ -8,6 +10,16 @@ export function createCharacterMotion(doc) {
|
|
|
8
10
|
let disposed = false, part, rig;
|
|
9
11
|
let canvas, render;
|
|
10
12
|
let pose = '', appearance = '', revision = 0, nearby = false, lastPointer = 0;
|
|
13
|
+
let attentionTimer, lastAttention = -Infinity;
|
|
14
|
+
let gazePoint = [0, 0], activeAction, activeProgress = 0;
|
|
15
|
+
let bounds;
|
|
16
|
+
const paint = () => { if (render && part) {
|
|
17
|
+
render(activeAction, activeProgress, gazePoint);
|
|
18
|
+
part.dataset.motion = activeAction ?? 'rig-rest';
|
|
19
|
+
} };
|
|
20
|
+
const gaze = createCharacterGaze(win, point => { gazePoint = point; paint(); });
|
|
21
|
+
const cancelAttention = () => { if (attentionTimer !== undefined)
|
|
22
|
+
win.clearTimeout(attentionTimer); attentionTimer = undefined; };
|
|
11
23
|
function decode(src) {
|
|
12
24
|
if (loading.has(src))
|
|
13
25
|
return loading.get(src);
|
|
@@ -25,8 +37,9 @@ export function createCharacterMotion(doc) {
|
|
|
25
37
|
if (!canvas || !part || !render)
|
|
26
38
|
return;
|
|
27
39
|
// Keep the same layered rest drawing after an action: no plate swap/pop.
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
activeAction = action;
|
|
41
|
+
activeProgress = progress;
|
|
42
|
+
paint();
|
|
30
43
|
});
|
|
31
44
|
function update() {
|
|
32
45
|
if (disposed)
|
|
@@ -36,8 +49,13 @@ export function createCharacterMotion(doc) {
|
|
|
36
49
|
const next = !doc.hidden && !reduced.matches && !moving && part ? CHARACTER_MOTION[pose]?.[mode] : undefined;
|
|
37
50
|
if (next === rig && appearance === mode)
|
|
38
51
|
return;
|
|
39
|
-
|
|
52
|
+
cancelAttention();
|
|
40
53
|
render = undefined;
|
|
54
|
+
clock.setClips(undefined);
|
|
55
|
+
gaze.reset();
|
|
56
|
+
bounds = undefined;
|
|
57
|
+
activeAction = undefined;
|
|
58
|
+
activeProgress = 0;
|
|
41
59
|
const current = ++revision;
|
|
42
60
|
canvas?.remove();
|
|
43
61
|
canvas = undefined;
|
|
@@ -62,9 +80,9 @@ export function createCharacterMotion(doc) {
|
|
|
62
80
|
return;
|
|
63
81
|
}
|
|
64
82
|
currentPart.append(canvas);
|
|
65
|
-
render = createRigRenderer(ctx, next, images);
|
|
83
|
+
render = createRigRenderer(ctx, next, images, EYE_RIGS[pose]);
|
|
66
84
|
clock.setClips({
|
|
67
|
-
blink: { duration:
|
|
85
|
+
blink: { duration: 280 },
|
|
68
86
|
...Object.fromEntries(next.ears.map((_, i) => ['ear' + i, { duration: 1050 }])),
|
|
69
87
|
...(next.layers.tail ? { tail: { duration: 2400 } } : {}),
|
|
70
88
|
attention: { duration: 2400 },
|
|
@@ -75,17 +93,45 @@ export function createCharacterMotion(doc) {
|
|
|
75
93
|
if (!part || !render || event.pointerType !== 'mouse')
|
|
76
94
|
return;
|
|
77
95
|
const now = win.performance.now();
|
|
78
|
-
if (now - lastPointer
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
if (!bounds || now - lastPointer >= 100) {
|
|
97
|
+
bounds = part.getBoundingClientRect();
|
|
98
|
+
lastPointer = now;
|
|
99
|
+
}
|
|
100
|
+
const rect = bounds, eyes = EYE_RIGS[pose];
|
|
101
|
+
if (rig && eyes?.length) {
|
|
102
|
+
const scale = Math.min(rect.width / rig.size[0], rect.height / rig.size[1]);
|
|
103
|
+
const eyeX = eyes.reduce((sum, eye) => sum + eye.iris[0], 0) / eyes.length / 2;
|
|
104
|
+
const eyeY = eyes.reduce((sum, eye) => sum + eye.iris[1], 0) / eyes.length / 2;
|
|
105
|
+
const cx = rect.left + eyeX * scale;
|
|
106
|
+
const cy = rect.top + (rect.height - rig.size[1] * scale) / 2 + eyeY * scale;
|
|
107
|
+
gaze.target((event.clientX - cx) / Math.max(220, win.innerWidth * .32), (event.clientY - cy) / Math.max(180, win.innerHeight * .32));
|
|
108
|
+
}
|
|
82
109
|
const near = event.clientX >= rect.left - 70 && event.clientX <= rect.right + 70
|
|
83
110
|
&& event.clientY >= rect.top - 50 && event.clientY <= rect.bottom + 50;
|
|
84
|
-
if (near
|
|
85
|
-
|
|
111
|
+
if (!near)
|
|
112
|
+
cancelAttention();
|
|
113
|
+
if (near && !nearby && now - lastAttention > 6000) {
|
|
114
|
+
const current = revision;
|
|
115
|
+
// A passing pointer should not trigger a performance. Brief dwell gives
|
|
116
|
+
// an intentional approach one quiet, coordinated response.
|
|
117
|
+
attentionTimer = win.setTimeout(() => {
|
|
118
|
+
attentionTimer = undefined;
|
|
119
|
+
if (!disposed && nearby && current === revision) {
|
|
120
|
+
lastAttention = win.performance.now();
|
|
121
|
+
clock.attention();
|
|
122
|
+
}
|
|
123
|
+
}, 180);
|
|
124
|
+
}
|
|
86
125
|
nearby = near;
|
|
87
126
|
};
|
|
88
127
|
reduced.addEventListener('change', update);
|
|
128
|
+
const neutral = () => { gaze.target(0, 0); nearby = false; cancelAttention(); };
|
|
129
|
+
const leave = (event) => { if (!event.relatedTarget)
|
|
130
|
+
neutral(); };
|
|
131
|
+
const resize = () => { bounds = undefined; };
|
|
132
|
+
doc.addEventListener('pointerout', leave);
|
|
133
|
+
win.addEventListener('blur', neutral);
|
|
134
|
+
win.addEventListener('resize', resize);
|
|
89
135
|
doc.addEventListener('visibilitychange', update);
|
|
90
136
|
doc.addEventListener('pointermove', pointer, { passive: true });
|
|
91
137
|
const observer = new win.MutationObserver(update);
|
|
@@ -93,9 +139,12 @@ export function createCharacterMotion(doc) {
|
|
|
93
139
|
return {
|
|
94
140
|
setPose(nextPart, nextPose) {
|
|
95
141
|
if (nextPart !== part || pose !== nextPose) {
|
|
142
|
+
cancelAttention();
|
|
96
143
|
revision++;
|
|
97
|
-
clock.setClips(undefined);
|
|
98
144
|
render = undefined;
|
|
145
|
+
clock.setClips(undefined);
|
|
146
|
+
gaze.reset();
|
|
147
|
+
bounds = undefined;
|
|
99
148
|
canvas?.remove();
|
|
100
149
|
canvas = undefined;
|
|
101
150
|
rig = undefined;
|
|
@@ -108,8 +157,10 @@ export function createCharacterMotion(doc) {
|
|
|
108
157
|
},
|
|
109
158
|
dispose() {
|
|
110
159
|
disposed = true;
|
|
160
|
+
cancelAttention();
|
|
111
161
|
revision++;
|
|
112
162
|
clock.dispose();
|
|
163
|
+
gaze.dispose();
|
|
113
164
|
canvas?.remove();
|
|
114
165
|
render = undefined;
|
|
115
166
|
images.clear();
|
|
@@ -120,6 +171,9 @@ export function createCharacterMotion(doc) {
|
|
|
120
171
|
observer.disconnect();
|
|
121
172
|
doc.removeEventListener('visibilitychange', update);
|
|
122
173
|
doc.removeEventListener('pointermove', pointer);
|
|
174
|
+
doc.removeEventListener('pointerout', leave);
|
|
175
|
+
win.removeEventListener('blur', neutral);
|
|
176
|
+
win.removeEventListener('resize', resize);
|
|
123
177
|
},
|
|
124
178
|
};
|
|
125
179
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { CharacterRig } from './character-motion-types.js';
|
|
1
|
+
import type { CharacterRig, Point } from './character-motion-types.js';
|
|
2
|
+
import type { EyeRig } from './character-eye-rig.js';
|
|
2
3
|
/** 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
|
|
4
|
+
export declare function createRigRenderer(ctx: CanvasRenderingContext2D, rig: CharacterRig, images: ReadonlyMap<string, HTMLImageElement | null>, eyes?: readonly EyeRig[]): (action: string | undefined, progress: number, gaze?: Point) => void;
|
|
4
5
|
//# sourceMappingURL=character-rig-renderer.d.ts.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { attentionBlink, blinkClosure } from './character-expression.js';
|
|
2
|
+
import { createEyeRenderer } from './character-eye-renderer.js';
|
|
3
|
+
import { bindVertex, earOffset, rigAngles, skinVertex, solveBones, TAIL_JOINTS } from './character-rig.js';
|
|
2
4
|
/** Small CPU-skinned tail only; body and ears use normal image draws. */
|
|
3
|
-
export function createRigRenderer(ctx, rig, images) {
|
|
5
|
+
export function createRigRenderer(ctx, rig, images, eyes = []) {
|
|
6
|
+
const body = images.get(rig.layers.body.src);
|
|
7
|
+
const drawEyes = body ? createEyeRenderer(ctx, body, eyes) : undefined;
|
|
4
8
|
const tail = rig.layers.tail;
|
|
5
9
|
const vertices = [], triangles = [];
|
|
6
10
|
if (tail) {
|
|
@@ -61,25 +65,31 @@ export function createRigRenderer(ctx, rig, images) {
|
|
|
61
65
|
ctx.restore();
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
|
-
return (action, progress) => {
|
|
68
|
+
return (action, progress, gaze = [0, 0]) => {
|
|
65
69
|
ctx.clearRect(0, 0, rig.size[0], rig.size[1]);
|
|
66
|
-
const
|
|
67
|
-
rig.ears.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
const earProgress = action === 'attention' ? Math.max(0, Math.min(1, (progress - .03) / .65)) : progress;
|
|
71
|
+
const ears = rigAngles(earProgress, rig.ears.length, false);
|
|
72
|
+
rig.ears.forEach(([, root], i) => {
|
|
73
|
+
const part = rig.layers['ear' + i], image = part && images.get(part.src);
|
|
74
|
+
if (!part || !image)
|
|
75
|
+
return;
|
|
76
|
+
const angle = action === 'attention' || action === 'ear' + i ? ears[i] : 0;
|
|
77
|
+
if (Math.abs(angle) < 1e-8) {
|
|
78
|
+
layer('ear' + i);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const [x, top, right, bottom] = part.box;
|
|
82
|
+
// Non-overlapping source rows preserve alpha and never rotate a rectangle
|
|
83
|
+
// of hair out from behind the head. The lower attachment stays identical.
|
|
84
|
+
for (let row = 0; row < image.height; row++) {
|
|
85
|
+
const y = top + row * (bottom - top) / image.height;
|
|
86
|
+
ctx.drawImage(image, 0, row, image.width, 1, x + earOffset(y, top, root, angle), y, right - x, (bottom - top) / image.height);
|
|
87
|
+
}
|
|
74
88
|
});
|
|
75
89
|
layer('body');
|
|
76
|
-
drawTail(action === '
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
ctx.globalAlpha = Math.min(1, Math.sin(Math.PI * progress) * 2);
|
|
80
|
-
layer('blink');
|
|
81
|
-
ctx.restore();
|
|
82
|
-
}
|
|
90
|
+
drawTail(action === 'attention' ? Math.max(0, Math.min(1, (progress - .22) / .78)) : action === 'tail' ? progress : 0);
|
|
91
|
+
const closure = action === 'blink' ? blinkClosure(progress) : action === 'attention' ? attentionBlink(progress) : 0;
|
|
92
|
+
drawEyes?.(gaze, closure);
|
|
83
93
|
};
|
|
84
94
|
}
|
|
85
95
|
//# sourceMappingURL=character-rig-renderer.js.map
|
|
@@ -12,6 +12,8 @@ export interface Vertex {
|
|
|
12
12
|
bone: number;
|
|
13
13
|
weight: number;
|
|
14
14
|
}
|
|
15
|
+
/** Keep the ear root (and any hair below it) fixed; only flex the upper tip. */
|
|
16
|
+
export declare function earOffset(y: number, top: number, root: number, angle: number): number;
|
|
15
17
|
export declare function solveBones(rest: readonly Point[], angles: readonly number[]): Joint[];
|
|
16
18
|
export declare function bindVertex(x: number, y: number, rest?: readonly Point[]): Vertex;
|
|
17
19
|
/** Linear blend skinning, inverse bind translation followed by posed rotation. */
|
|
@@ -3,6 +3,14 @@ export const TAIL_JOINTS = [
|
|
|
3
3
|
[284, 328], [310, 347], [324, 373], [319, 398],
|
|
4
4
|
[307, 423], [307, 448], [319, 464],
|
|
5
5
|
];
|
|
6
|
+
/** Keep the ear root (and any hair below it) fixed; only flex the upper tip. */
|
|
7
|
+
export function earOffset(y, top, root, angle) {
|
|
8
|
+
const span = Math.max(1, root - top);
|
|
9
|
+
const weight = Math.max(0, Math.min(1, (root - y) / span));
|
|
10
|
+
if (weight === 0)
|
|
11
|
+
return 0;
|
|
12
|
+
return Math.sin(angle) * Math.min(20, span * .55) * weight * weight;
|
|
13
|
+
}
|
|
6
14
|
export function solveBones(rest, angles) {
|
|
7
15
|
return rest.reduce((out, p, i) => {
|
|
8
16
|
const parent = out[i - 1];
|
|
@@ -47,7 +55,7 @@ export function rigAngles(progress, count, tail) {
|
|
|
47
55
|
const envelope = Math.sin(Math.PI * t) ** 2;
|
|
48
56
|
// Delayed distal joints create follow-through, with zero displacement/velocity
|
|
49
57
|
// at both ends; no discrete frame lookup and no idle simulation.
|
|
50
|
-
return Array.from({ length: count }, (_, i) => envelope * (tail ? (i ? .12 : 0) : .
|
|
58
|
+
return Array.from({ length: count }, (_, i) => envelope * (tail ? (i ? .12 : 0) : .32)
|
|
51
59
|
* Math.sin(t * Math.PI * (tail ? 3 : 4) - i * (tail ? .65 : 1.1)));
|
|
52
60
|
}
|
|
53
61
|
//# sourceMappingURL=character-rig.js.map
|
package/lib/sidebar-reveal.js
CHANGED
|
@@ -5,8 +5,16 @@ export function createSidebarReveal(doc) {
|
|
|
5
5
|
let observer;
|
|
6
6
|
let phase = '', width = 0, waiting = false;
|
|
7
7
|
const attribute = 'data-xiaohei-sidebar-reveal';
|
|
8
|
-
const readPhase = () =>
|
|
9
|
-
|
|
8
|
+
const readPhase = () => {
|
|
9
|
+
const state = shell?.closest('[data-sidebar-collapsed]')?.getAttribute('data-sidebar-collapsed');
|
|
10
|
+
if (state === 'true')
|
|
11
|
+
return 'rail';
|
|
12
|
+
if (state === 'false')
|
|
13
|
+
return 'wide';
|
|
14
|
+
if (shell?.querySelector("button[aria-label='Collapse sidebar'], button[aria-label='收起侧边栏']"))
|
|
15
|
+
return 'wide';
|
|
16
|
+
return 'rail';
|
|
17
|
+
};
|
|
10
18
|
const apply = () => {
|
|
11
19
|
if (!shell)
|
|
12
20
|
return;
|
|
@@ -41,7 +49,7 @@ export function createSidebarReveal(doc) {
|
|
|
41
49
|
if (!shell)
|
|
42
50
|
return;
|
|
43
51
|
observer = new doc.defaultView.MutationObserver(changed);
|
|
44
|
-
observer.observe(
|
|
52
|
+
observer.observe(column, { subtree: true, childList: true, attributes: true, attributeFilter: ['data-sidebar-collapsed', 'aria-label', 'style'] });
|
|
45
53
|
apply();
|
|
46
54
|
},
|
|
47
55
|
resize(nextWidth) { width = nextWidth; apply(); },
|