@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.
@@ -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
- render(action, progress);
29
- part.dataset.motion = action ?? 'rig-rest';
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
- clock.setClips(undefined);
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: 330 },
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 < 100)
79
- return;
80
- lastPointer = now;
81
- const rect = part.getBoundingClientRect();
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 && !nearby)
85
- clock.attention();
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>): (action: string | undefined, progress: number) => void;
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 { bindVertex, rigAngles, skinVertex, solveBones, TAIL_JOINTS } from './character-rig.js';
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 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();
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 === '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
- }
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) : .22)
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
@@ -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 = () => shell?.className.includes('_collapsed') ? 'rail'
9
- : shell?.className.includes('_fading') ? 'closing' : 'wide';
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(shell, { attributes: true, attributeFilter: ['class', 'style'] });
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(); },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lemoncat7/dsh-theme-xiaohei",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "A Luo Xiaohei-inspired animated dark theme for DeepSeek Harness",
5
5
  "keywords": [
6
6
  "deepseek-harness",