dsh-offwork 1.1.0 → 1.1.1
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 +9 -0
- package/README.md +19 -16
- package/assets/animations/NOTICE.md +15 -0
- package/assets/animations/alpha-probe.webm +0 -0
- package/assets/animations/ornate-drink.webm +0 -0
- package/assets/animations/ornate-greet.webm +0 -0
- package/assets/animations/ornate-idle.webm +0 -0
- package/assets/animations/whale-drink.webm +0 -0
- package/assets/animations/whale-greet.webm +0 -0
- package/assets/characters/NOTICE.md +4 -0
- package/assets/characters/whale-actions.webp +0 -0
- package/assets/characters/whale-ornate-actions.webp +0 -0
- package/client.js +502 -38
- package/client.source.js +85 -21
- package/package.json +4 -1
- package/pet-interactions.js +3 -3
- package/pet-motion.js +110 -0
- package/pet-surfaces.js +2 -2
- package/pet-video.js +231 -0
- package/pet-view.js +61 -10
- package/tests/pet-character.test.mjs +8 -2
- package/tests/pet-motion.test.mjs +139 -0
- package/tests/pet-video.test.mjs +337 -0
- package/tools/build.mjs +28 -2
- package/tools/check-package.mjs +11 -0
- package/ui-utils.js +7 -1
package/client.source.js
CHANGED
|
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
const h = React.createElement;
|
|
7
7
|
/* PURE_MODULES */
|
|
8
8
|
/* PET_SPRITES */
|
|
9
|
+
/* PET_CLIPS */
|
|
9
10
|
const messages = {
|
|
10
11
|
zh: {
|
|
11
12
|
...COMPANION_MESSAGES.zh,
|
|
@@ -432,8 +433,10 @@ window.__ModuleLoader__.load({
|
|
|
432
433
|
apply(ctx) {
|
|
433
434
|
for (const [lang, dict] of Object.entries(messages))
|
|
434
435
|
ctx.effect(() => ctx.locale.register('offwork-clock', lang, dict));
|
|
435
|
-
const viewKey = 'dsh.offwork-clock.view.
|
|
436
|
-
const previousViewKey = 'dsh.offwork-clock.view.
|
|
436
|
+
const viewKey = 'dsh.offwork-clock.view.v7';
|
|
437
|
+
const previousViewKey = 'dsh.offwork-clock.view.v6';
|
|
438
|
+
const olderViewKey = 'dsh.offwork-clock.view.v5';
|
|
439
|
+
const oldestViewKey = 'dsh.offwork-clock.view.v4';
|
|
437
440
|
const legacyKey = 'dsh.offwork-clock.view.v3';
|
|
438
441
|
|
|
439
442
|
function Clock(props) {
|
|
@@ -505,6 +508,8 @@ window.__ModuleLoader__.load({
|
|
|
505
508
|
const [waterConfirm, setWaterConfirm] = React.useState(null);
|
|
506
509
|
const [waterNotice, setWaterNotice] = React.useState(false);
|
|
507
510
|
const reactionState = React.useRef(null);
|
|
511
|
+
const petMotion = React.useRef(null);
|
|
512
|
+
const petReactionToken = React.useRef(null);
|
|
508
513
|
const waterRuntime = React.useRef(null);
|
|
509
514
|
const waterOwner = React.useRef(null);
|
|
510
515
|
if (!waterOwner.current)
|
|
@@ -655,7 +660,11 @@ window.__ModuleLoader__.load({
|
|
|
655
660
|
|
|
656
661
|
React.useEffect(() => {
|
|
657
662
|
try {
|
|
658
|
-
const saved =
|
|
663
|
+
const saved =
|
|
664
|
+
localStorage.getItem(viewKey) ??
|
|
665
|
+
localStorage.getItem(previousViewKey) ??
|
|
666
|
+
localStorage.getItem(olderViewKey) ??
|
|
667
|
+
localStorage.getItem(oldestViewKey);
|
|
659
668
|
const legacy = !saved ? JSON.parse(localStorage.getItem(legacyKey) || 'null') : null;
|
|
660
669
|
setView(
|
|
661
670
|
normalizeView(
|
|
@@ -1191,7 +1200,10 @@ window.__ModuleLoader__.load({
|
|
|
1191
1200
|
d.moved = true;
|
|
1192
1201
|
petDragged.current = true;
|
|
1193
1202
|
const next = clamp({ x: e.clientX - d.dx, y: e.clientY - d.dy });
|
|
1194
|
-
if (petMode)
|
|
1203
|
+
if (petMode) {
|
|
1204
|
+
setDragPosition(next);
|
|
1205
|
+
petMotion.current?.drag(e.clientX - d.x0);
|
|
1206
|
+
}
|
|
1195
1207
|
lastPosition.current = next;
|
|
1196
1208
|
if (box.current) {
|
|
1197
1209
|
box.current.style.left = next.x + 'px';
|
|
@@ -1203,6 +1215,7 @@ window.__ModuleLoader__.load({
|
|
|
1203
1215
|
const d = dragState.current;
|
|
1204
1216
|
if (!d) return;
|
|
1205
1217
|
dragState.current = null;
|
|
1218
|
+
if (petMode && d.moved) petMotion.current?.release(e.type !== 'pointerup');
|
|
1206
1219
|
if (e.type === 'pointercancel' || e.type === 'lostpointercapture')
|
|
1207
1220
|
petDragged.current = false;
|
|
1208
1221
|
if (d.moved && lastPosition.current) commitPosition(lastPosition.current);
|
|
@@ -1472,6 +1485,56 @@ window.__ModuleLoader__.load({
|
|
|
1472
1485
|
motion: view.companionMotion && !reactionState.current?.stopped,
|
|
1473
1486
|
pageVisible: waterVisible && !document.hidden && !editing && !waterEditing,
|
|
1474
1487
|
});
|
|
1488
|
+
React.useLayoutEffect(() => {
|
|
1489
|
+
if (!petMode) return;
|
|
1490
|
+
const artwork = box.current?.querySelector('.owc-pet-handle > .owc-pet-art');
|
|
1491
|
+
if (!artwork) return;
|
|
1492
|
+
const base = createPetMotion(artwork, window, { enabled: false });
|
|
1493
|
+
const controller = createPetVideoMotion(
|
|
1494
|
+
artwork,
|
|
1495
|
+
window,
|
|
1496
|
+
base,
|
|
1497
|
+
PET_CLIPS.models[stageModel],
|
|
1498
|
+
PET_CLIPS.probe,
|
|
1499
|
+
);
|
|
1500
|
+
petMotion.current = controller;
|
|
1501
|
+
return () => {
|
|
1502
|
+
controller.destroy();
|
|
1503
|
+
petMotion.current = null;
|
|
1504
|
+
};
|
|
1505
|
+
}, [petMode, stageModel]);
|
|
1506
|
+
React.useLayoutEffect(() => {
|
|
1507
|
+
petMotion.current?.configure({
|
|
1508
|
+
enabled:
|
|
1509
|
+
savedView.companionMotion &&
|
|
1510
|
+
waterVisible &&
|
|
1511
|
+
waterReady &&
|
|
1512
|
+
!waterError &&
|
|
1513
|
+
!error &&
|
|
1514
|
+
!editing &&
|
|
1515
|
+
!waterEditing &&
|
|
1516
|
+
!ui.menu,
|
|
1517
|
+
resting: ['quiet', 'rest', 'offline'].includes(companion.pose),
|
|
1518
|
+
});
|
|
1519
|
+
}, [
|
|
1520
|
+
petMode,
|
|
1521
|
+
stageModel,
|
|
1522
|
+
savedView.companionMotion,
|
|
1523
|
+
waterVisible,
|
|
1524
|
+
waterReady,
|
|
1525
|
+
waterError,
|
|
1526
|
+
error,
|
|
1527
|
+
editing,
|
|
1528
|
+
waterEditing,
|
|
1529
|
+
!!ui.menu,
|
|
1530
|
+
companion.pose,
|
|
1531
|
+
]);
|
|
1532
|
+
React.useLayoutEffect(() => {
|
|
1533
|
+
const token = waterNotice?.token;
|
|
1534
|
+
if (petReactionToken.current === token) return;
|
|
1535
|
+
petReactionToken.current = token;
|
|
1536
|
+
if (petMode && companion.animate) petMotion.current?.play('celebrate');
|
|
1537
|
+
}, [petMode, waterNotice?.token, companion.animate]);
|
|
1475
1538
|
const companionCard = (compact = false) =>
|
|
1476
1539
|
companion.visible &&
|
|
1477
1540
|
h(
|
|
@@ -2346,7 +2409,7 @@ window.__ModuleLoader__.load({
|
|
|
2346
2409
|
? petArtwork(
|
|
2347
2410
|
h,
|
|
2348
2411
|
companion,
|
|
2349
|
-
view.petSize * (petModel(view.petModel).renderer === '
|
|
2412
|
+
view.petSize * (petModel(view.petModel).renderer === 'svg' ? 1 : 0.65),
|
|
2350
2413
|
'',
|
|
2351
2414
|
view.petModel,
|
|
2352
2415
|
PET_SPRITES,
|
|
@@ -2592,15 +2655,13 @@ window.__ModuleLoader__.load({
|
|
|
2592
2655
|
function closePetPanel() {
|
|
2593
2656
|
navigateScreen(null);
|
|
2594
2657
|
}
|
|
2595
|
-
function
|
|
2658
|
+
function interactWithPet(event) {
|
|
2596
2659
|
if (petDragged.current && event.detail !== 0) {
|
|
2597
2660
|
petDragged.current = false;
|
|
2598
2661
|
return;
|
|
2599
2662
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
event,
|
|
2603
|
-
);
|
|
2663
|
+
petDragged.current = false;
|
|
2664
|
+
if (!ui.pending) petMotion.current?.play('greet');
|
|
2604
2665
|
}
|
|
2605
2666
|
function closePetMenu(restore = true) {
|
|
2606
2667
|
dispatchUI({ type: 'dismiss-menu' });
|
|
@@ -3071,7 +3132,7 @@ window.__ModuleLoader__.load({
|
|
|
3071
3132
|
{
|
|
3072
3133
|
ref: box,
|
|
3073
3134
|
className: 'owc-v2 owc-pet-shell',
|
|
3074
|
-
'data-version': '1.1.
|
|
3135
|
+
'data-version': '1.1.1',
|
|
3075
3136
|
'data-pet-model': stageModel,
|
|
3076
3137
|
'data-status': safeState.status,
|
|
3077
3138
|
'aria-label': t('companionLabel'),
|
|
@@ -3113,10 +3174,13 @@ window.__ModuleLoader__.load({
|
|
|
3113
3174
|
{
|
|
3114
3175
|
type: 'button',
|
|
3115
3176
|
className: 'owc-pet-handle',
|
|
3116
|
-
'aria-label': t('
|
|
3117
|
-
'aria-expanded': !!petShowing,
|
|
3177
|
+
'aria-label': t('petGreet'),
|
|
3118
3178
|
title: t('companionName') + ' · ' + t('petHint'),
|
|
3119
|
-
onClick:
|
|
3179
|
+
onClick: interactWithPet,
|
|
3180
|
+
onPointerEnter: (event) => {
|
|
3181
|
+
if (event.pointerType === 'mouse' && !dragState.current)
|
|
3182
|
+
petMotion.current?.play('look');
|
|
3183
|
+
},
|
|
3120
3184
|
onKeyDown: gripKeys,
|
|
3121
3185
|
},
|
|
3122
3186
|
petArtwork(
|
|
@@ -3126,14 +3190,14 @@ window.__ModuleLoader__.load({
|
|
|
3126
3190
|
waterNotice?.token,
|
|
3127
3191
|
stageModel,
|
|
3128
3192
|
PET_SPRITES,
|
|
3193
|
+
true,
|
|
3129
3194
|
),
|
|
3130
3195
|
),
|
|
3131
3196
|
h(
|
|
3132
|
-
'
|
|
3197
|
+
'div',
|
|
3133
3198
|
{
|
|
3134
|
-
type: 'button',
|
|
3135
3199
|
className: 'owc-pet-clock',
|
|
3136
|
-
|
|
3200
|
+
role: 'timer',
|
|
3137
3201
|
'aria-label': t('petClock'),
|
|
3138
3202
|
},
|
|
3139
3203
|
iconSvg('alarm', 12),
|
|
@@ -3145,13 +3209,13 @@ window.__ModuleLoader__.load({
|
|
|
3145
3209
|
petShowing &&
|
|
3146
3210
|
ui.screen !== 'home' &&
|
|
3147
3211
|
h(
|
|
3148
|
-
'
|
|
3212
|
+
'div',
|
|
3149
3213
|
{
|
|
3150
|
-
type: 'button',
|
|
3151
3214
|
className: 'owc-pet-water-alert owc-water-due',
|
|
3152
3215
|
'data-water-event': waterState.eventId,
|
|
3216
|
+
role: 'note',
|
|
3153
3217
|
'aria-label': t('petWaterAlert'),
|
|
3154
|
-
|
|
3218
|
+
title: t('petWaterAlert'),
|
|
3155
3219
|
style: {
|
|
3156
3220
|
left: waterAlertLayout.left,
|
|
3157
3221
|
top: waterAlertLayout.top,
|
|
@@ -3258,7 +3322,7 @@ window.__ModuleLoader__.load({
|
|
|
3258
3322
|
(collapsed && waterQuickOpen ? ' owc-quick' : '') +
|
|
3259
3323
|
(editing || waterEditing ? ' owc-editing' : ''),
|
|
3260
3324
|
'aria-label': t('title'),
|
|
3261
|
-
'data-version': '1.1.
|
|
3325
|
+
'data-version': '1.1.1',
|
|
3262
3326
|
'data-status': safeState?.status || 'setup',
|
|
3263
3327
|
style: cardStyle,
|
|
3264
3328
|
onPointerDown: dragDown,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-offwork",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Harness 页面陪伴角色、下班倒计时、实时收入与喝水提醒。Character companion, clock-out countdown, earnings and hydration reminders for DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
"ui-utils.js",
|
|
41
41
|
"companion.js",
|
|
42
42
|
"pet-view.js",
|
|
43
|
+
"pet-motion.js",
|
|
44
|
+
"pet-video.js",
|
|
43
45
|
"pet-interactions.js",
|
|
44
46
|
"pet-surfaces.js",
|
|
45
47
|
"hydration.js",
|
|
@@ -48,6 +50,7 @@
|
|
|
48
50
|
"locale/*.json",
|
|
49
51
|
"icon.svg",
|
|
50
52
|
"assets/characters/*",
|
|
53
|
+
"assets/animations/*",
|
|
51
54
|
"LICENSE",
|
|
52
55
|
"README.md",
|
|
53
56
|
"CHANGELOG.md",
|
package/pet-interactions.js
CHANGED
|
@@ -48,7 +48,7 @@ export const PET_INTERACTION_MESSAGES = {
|
|
|
48
48
|
petWaterCard: '喝水',
|
|
49
49
|
petCountdownCard: '下班倒计时',
|
|
50
50
|
petEarningsCard: '收入',
|
|
51
|
-
petWaterAlert: '
|
|
51
|
+
petWaterAlert: '喝水提醒已到,可从右键菜单打开喝水卡片',
|
|
52
52
|
petPausedHint: '本班已暂停,可手动恢复。',
|
|
53
53
|
petMenu: '宠物菜单',
|
|
54
54
|
petBack: '返回',
|
|
@@ -77,7 +77,7 @@ export const PET_INTERACTION_MESSAGES = {
|
|
|
77
77
|
petWaterCard: 'Water',
|
|
78
78
|
petCountdownCard: 'Shift countdown',
|
|
79
79
|
petEarningsCard: 'Earnings',
|
|
80
|
-
petWaterAlert: '
|
|
80
|
+
petWaterAlert: 'Water is due; open Water from the context menu',
|
|
81
81
|
petPausedHint: 'Paused for this shift. Resume when ready.',
|
|
82
82
|
petMenu: 'Pet menu',
|
|
83
83
|
petBack: 'Back',
|
|
@@ -106,7 +106,7 @@ export const PET_INTERACTION_MESSAGES = {
|
|
|
106
106
|
export const PET_INTERACTION_STYLE = `
|
|
107
107
|
.owc-pet-workspace .owc-glass{opacity:var(--owc-alpha)}
|
|
108
108
|
.owc-pet-water-card.owc-water-due{border-radius:14px}
|
|
109
|
-
.owc-pet-water-alert{position:fixed!important;width:140px;box-sizing:border-box;min-height:30px!important;border:1px solid var(--owc-accent)!important;background:var(--dsw-alias-bg-overlay)!important;box-shadow:0 4px 12px #18354b18;font-size:11px!important;z-index:2}
|
|
109
|
+
.owc-pet-water-alert{display:flex;align-items:center;justify-content:center;gap:6px;padding:8px 12px;border-radius:12px;cursor:default;position:fixed!important;width:140px;box-sizing:border-box;min-height:30px!important;border:1px solid var(--owc-accent)!important;background:var(--dsw-alias-bg-overlay)!important;box-shadow:0 4px 12px #18354b18;font-size:11px!important;z-index:2}
|
|
110
110
|
.owc-pet-panel.owc-pet-workspace{display:flex;flex-direction:column;overflow:hidden}
|
|
111
111
|
.owc-pet-workspace>.owc-head{position:relative;flex:none;min-height:46px;padding:8px 12px;box-sizing:border-box}
|
|
112
112
|
.owc-pet-workspace>.owc-pet-scroll{position:relative;min-height:0;flex:1;overflow:auto;overscroll-behavior:contain;scrollbar-width:none}
|
package/pet-motion.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Independent, decorative motion only. Never writes preferences or drinking records.
|
|
2
|
+
const PET_ACTIONS = {
|
|
3
|
+
look: { duration: 650, transforms: ['none', 'rotate(-1deg) translateY(-1px)', 'none'] },
|
|
4
|
+
greet: {
|
|
5
|
+
duration: 900,
|
|
6
|
+
transforms: ['none', 'translateY(2px) rotate(-2deg)', 'translateY(-4px) rotate(1deg)', 'none'],
|
|
7
|
+
},
|
|
8
|
+
celebrate: {
|
|
9
|
+
duration: 1200,
|
|
10
|
+
transforms: [
|
|
11
|
+
'none',
|
|
12
|
+
'translateY(-7px) rotate(-1deg)',
|
|
13
|
+
'translateY(1px)',
|
|
14
|
+
'translateY(-3px) rotate(1deg)',
|
|
15
|
+
'none',
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
settle: {
|
|
19
|
+
duration: 550,
|
|
20
|
+
transforms: ['none', 'translateY(2px) scaleY(.985)', 'translateY(-2px)', 'none'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createPetMotion(element, host, options = {}) {
|
|
25
|
+
const document = host.document;
|
|
26
|
+
const reduced = host.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
27
|
+
let enabled = options.enabled ?? true;
|
|
28
|
+
let resting = options.resting ?? false;
|
|
29
|
+
let animation = null;
|
|
30
|
+
let destroyed = false;
|
|
31
|
+
const allowed = () => !destroyed && enabled && !document.hidden && !reduced?.matches;
|
|
32
|
+
function stop() {
|
|
33
|
+
const previous = animation;
|
|
34
|
+
animation = null;
|
|
35
|
+
previous?.cancel();
|
|
36
|
+
delete element.dataset.action;
|
|
37
|
+
element.style.transform = '';
|
|
38
|
+
}
|
|
39
|
+
function sync() {
|
|
40
|
+
if (!allowed()) stop();
|
|
41
|
+
element.dataset.motion = allowed() && !resting ? 'idle' : 'off';
|
|
42
|
+
}
|
|
43
|
+
function play(action) {
|
|
44
|
+
if (!allowed() || !Object.hasOwn(PET_ACTIONS, action) || !element.animate) return false;
|
|
45
|
+
if (element.dataset.action === 'drag') return false;
|
|
46
|
+
if (action === 'look' && element.dataset.action) return false;
|
|
47
|
+
if (action === 'greet' && element.dataset.action === 'celebrate') return false;
|
|
48
|
+
stop();
|
|
49
|
+
const spec = PET_ACTIONS[action];
|
|
50
|
+
element.dataset.action = action;
|
|
51
|
+
try {
|
|
52
|
+
const running = element.animate(
|
|
53
|
+
spec.transforms.map((transform) => ({ transform })),
|
|
54
|
+
{
|
|
55
|
+
duration: spec.duration,
|
|
56
|
+
easing: 'ease-in-out',
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
animation = running;
|
|
60
|
+
running.finished.then(
|
|
61
|
+
() => {
|
|
62
|
+
if (animation === running) {
|
|
63
|
+
animation = null;
|
|
64
|
+
delete element.dataset.action;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
() => {
|
|
68
|
+
if (animation === running) {
|
|
69
|
+
animation = null;
|
|
70
|
+
delete element.dataset.action;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
stop();
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
document.addEventListener('visibilitychange', sync);
|
|
81
|
+
reduced?.addEventListener?.('change', sync);
|
|
82
|
+
sync();
|
|
83
|
+
return {
|
|
84
|
+
play,
|
|
85
|
+
configure(next) {
|
|
86
|
+
if (next.resting && !resting) stop();
|
|
87
|
+
enabled = next.enabled;
|
|
88
|
+
resting = next.resting;
|
|
89
|
+
sync();
|
|
90
|
+
},
|
|
91
|
+
drag(direction) {
|
|
92
|
+
if (!allowed()) return;
|
|
93
|
+
stop();
|
|
94
|
+
element.dataset.action = 'drag';
|
|
95
|
+
element.style.transform = `translateY(-2px) rotate(${direction < 0 ? -2 : 2}deg)`;
|
|
96
|
+
},
|
|
97
|
+
release(cancelled = false) {
|
|
98
|
+
const dragged = element.dataset.action === 'drag';
|
|
99
|
+
stop();
|
|
100
|
+
if (dragged && !cancelled) play('settle');
|
|
101
|
+
},
|
|
102
|
+
destroy() {
|
|
103
|
+
destroyed = true;
|
|
104
|
+
stop();
|
|
105
|
+
element.dataset.motion = 'off';
|
|
106
|
+
document.removeEventListener('visibilitychange', sync);
|
|
107
|
+
reduced?.removeEventListener?.('change', sync);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/pet-surfaces.js
CHANGED
|
@@ -122,7 +122,7 @@ export const PET_SURFACE_STYLE = `
|
|
|
122
122
|
.owc-v2:not(.owc-pet-shell) .owc-head{position:sticky;top:0;z-index:2;background:var(--owc-surface)}
|
|
123
123
|
.owc-v2:not(.owc-pet-shell) .owc-save{position:sticky;bottom:0;z-index:2;background:var(--owc-surface);padding:12px 0}
|
|
124
124
|
.owc-v2 .owc-pet-toast{overflow-wrap:anywhere;overflow:auto;scrollbar-width:none}
|
|
125
|
-
.owc-v2
|
|
125
|
+
.owc-v2 .owc-pet-water-alert{border-color:var(--owc-accent)!important;color:var(--owc-accent)}
|
|
126
126
|
.owc-v2 .owc-glass{background:var(--owc-surface);border:1px solid var(--owc-border);box-shadow:var(--owc-shadow);opacity:var(--owc-alpha,.94)}
|
|
127
127
|
.owc-v2 .owc-pet-workspace{border-radius:var(--owc-radius);container-type:inline-size}
|
|
128
128
|
.owc-v2 .owc-head{margin:0;padding:10px 16px;gap:8px;min-height:52px;border-bottom:1px solid var(--owc-border);box-sizing:border-box}
|
|
@@ -182,7 +182,7 @@ export const PET_SURFACE_STYLE = `
|
|
|
182
182
|
.owc-v2 .owc-menu-natural{padding:6px;display:flow-root;box-sizing:border-box;min-width:0}
|
|
183
183
|
.owc-v2 .owc-pet-context button{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;justify-content:flex-start;font-size:13px}
|
|
184
184
|
.owc-v2 .owc-pet-context hr{border-color:var(--owc-border);margin:6px 4px}
|
|
185
|
-
.owc-v2 .owc-pet-toast,.owc-v2
|
|
185
|
+
.owc-v2 .owc-pet-toast,.owc-v2 .owc-pet-clock,.owc-v2 .owc-pet-water-alert{background:var(--owc-surface)!important;color:var(--owc-text);border:1px solid var(--owc-border)!important;box-shadow:var(--owc-shadow)}
|
|
186
186
|
.owc-v2 .owc-pet-toast{border-radius:12px;font-size:12px}
|
|
187
187
|
.owc-v2 .owc-pet-unsaved{padding:20px 16px}
|
|
188
188
|
.owc-v2 .owc-pet-unsaved p{font-size:13px;overflow-wrap:anywhere}
|
package/pet-video.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Independent transparent-video playback. Presentation only; never records an action.
|
|
2
|
+
const PET_VIDEO_SUPPORT = new WeakMap();
|
|
3
|
+
|
|
4
|
+
export function petVideoAlphaSupported(host, source) {
|
|
5
|
+
if (PET_VIDEO_SUPPORT.has(host)) return PET_VIDEO_SUPPORT.get(host);
|
|
6
|
+
const result = new Promise((resolve) => {
|
|
7
|
+
const video = host.document.createElement('video');
|
|
8
|
+
let timer;
|
|
9
|
+
const finish = (supported) => {
|
|
10
|
+
host.clearTimeout(timer);
|
|
11
|
+
video.onloadeddata = video.onerror = null;
|
|
12
|
+
video.pause();
|
|
13
|
+
video.removeAttribute('src');
|
|
14
|
+
video.load();
|
|
15
|
+
resolve(supported);
|
|
16
|
+
};
|
|
17
|
+
if (!source || !video.canPlayType('video/webm; codecs="vp9"')) return finish(false);
|
|
18
|
+
video.muted = true;
|
|
19
|
+
video.playsInline = true;
|
|
20
|
+
video.preload = 'auto';
|
|
21
|
+
video.onloadeddata = () => {
|
|
22
|
+
try {
|
|
23
|
+
const canvas = host.document.createElement('canvas');
|
|
24
|
+
canvas.width = canvas.height = 16;
|
|
25
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
26
|
+
context.drawImage(video, 0, 0, 16, 16);
|
|
27
|
+
finish(
|
|
28
|
+
context.getImageData(1, 1, 1, 1).data[3] > 240 &&
|
|
29
|
+
context.getImageData(14, 14, 1, 1).data[3] < 8,
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
finish(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
video.onerror = () => finish(false);
|
|
36
|
+
timer = host.setTimeout(() => finish(false), 3000);
|
|
37
|
+
video.src = source;
|
|
38
|
+
video.load();
|
|
39
|
+
});
|
|
40
|
+
PET_VIDEO_SUPPORT.set(host, result);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createPetVideoMotion(element, host, base, clips, probeSource) {
|
|
45
|
+
const layer = element.querySelector('.owc-pet-video-layer');
|
|
46
|
+
if (!layer || !clips) return base;
|
|
47
|
+
const document = host.document;
|
|
48
|
+
const reduced = host.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
49
|
+
const capability = petVideoAlphaSupported(host, probeSource);
|
|
50
|
+
const failed = new Set();
|
|
51
|
+
const retired = new Map();
|
|
52
|
+
let enabled = false,
|
|
53
|
+
resting = false,
|
|
54
|
+
dragging = false,
|
|
55
|
+
unsupported = false,
|
|
56
|
+
idleTimer = null,
|
|
57
|
+
destroyed = false,
|
|
58
|
+
current = null;
|
|
59
|
+
const allowed = () => enabled && !destroyed && !document.hidden && !reduced?.matches;
|
|
60
|
+
function clearIdleTimer() {
|
|
61
|
+
host.clearTimeout(idleTimer);
|
|
62
|
+
idleTimer = null;
|
|
63
|
+
}
|
|
64
|
+
function startIdle() {
|
|
65
|
+
if (
|
|
66
|
+
allowed() &&
|
|
67
|
+
!resting &&
|
|
68
|
+
!dragging &&
|
|
69
|
+
!current &&
|
|
70
|
+
!unsupported &&
|
|
71
|
+
Object.hasOwn(clips, 'idle') &&
|
|
72
|
+
!failed.has('idle')
|
|
73
|
+
)
|
|
74
|
+
play('idle');
|
|
75
|
+
}
|
|
76
|
+
function scheduleIdle(delay = 0) {
|
|
77
|
+
clearIdleTimer();
|
|
78
|
+
if (!Object.hasOwn(clips, 'idle') || failed.has('idle') || unsupported || destroyed) return;
|
|
79
|
+
idleTimer = host.setTimeout(() => {
|
|
80
|
+
idleTimer = null;
|
|
81
|
+
startIdle();
|
|
82
|
+
}, delay);
|
|
83
|
+
}
|
|
84
|
+
function dispose(video) {
|
|
85
|
+
video.onplaying = video.onended = video.onerror = video.onwaiting = video.onstalled = null;
|
|
86
|
+
video.pause();
|
|
87
|
+
video.removeAttribute('src');
|
|
88
|
+
video.load();
|
|
89
|
+
video.remove();
|
|
90
|
+
}
|
|
91
|
+
function stop(fade = false) {
|
|
92
|
+
clearIdleTimer();
|
|
93
|
+
const run = current;
|
|
94
|
+
if (!run) return;
|
|
95
|
+
current = null;
|
|
96
|
+
host.clearTimeout(run.timer);
|
|
97
|
+
delete element.dataset.videoPlaying;
|
|
98
|
+
if (element.dataset.action === run.action) delete element.dataset.action;
|
|
99
|
+
if (!run.video) return;
|
|
100
|
+
const video = run.video;
|
|
101
|
+
video.onplaying = video.onended = video.onerror = video.onwaiting = video.onstalled = null;
|
|
102
|
+
video.pause();
|
|
103
|
+
video.style.opacity = '0';
|
|
104
|
+
if (fade && allowed()) {
|
|
105
|
+
const timer = host.setTimeout(() => {
|
|
106
|
+
retired.delete(video);
|
|
107
|
+
dispose(video);
|
|
108
|
+
}, 150);
|
|
109
|
+
retired.set(video, timer);
|
|
110
|
+
} else dispose(video);
|
|
111
|
+
}
|
|
112
|
+
function fallback(run, broken = false) {
|
|
113
|
+
if (current !== run) return;
|
|
114
|
+
if (broken) failed.add(run.action);
|
|
115
|
+
stop();
|
|
116
|
+
if (allowed() && run.action !== 'idle') {
|
|
117
|
+
base.play(run.action);
|
|
118
|
+
scheduleIdle(1400);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function play(action) {
|
|
122
|
+
if (!allowed() || dragging || element.dataset.action === 'drag') return false;
|
|
123
|
+
if (action === 'idle' && (resting || current || unsupported)) return false;
|
|
124
|
+
if (
|
|
125
|
+
current &&
|
|
126
|
+
(action === 'look' ||
|
|
127
|
+
current.action === action ||
|
|
128
|
+
(current.action === 'celebrate' && action === 'greet'))
|
|
129
|
+
)
|
|
130
|
+
return false;
|
|
131
|
+
if (!Object.hasOwn(clips, action) || failed.has(action) || unsupported) {
|
|
132
|
+
if (action === 'idle') return false;
|
|
133
|
+
if (current?.action === 'idle') stop();
|
|
134
|
+
const played = base.play(action);
|
|
135
|
+
if (played) scheduleIdle(1400);
|
|
136
|
+
return played;
|
|
137
|
+
}
|
|
138
|
+
stop();
|
|
139
|
+
base.release(true);
|
|
140
|
+
const run = { action, video: null, timer: null };
|
|
141
|
+
current = run;
|
|
142
|
+
element.dataset.action = action;
|
|
143
|
+
run.timer = host.setTimeout(() => fallback(run, true), 9000);
|
|
144
|
+
capability.then((supported) => {
|
|
145
|
+
if (current !== run || !allowed()) return;
|
|
146
|
+
if (!supported) {
|
|
147
|
+
unsupported = true;
|
|
148
|
+
return fallback(run);
|
|
149
|
+
}
|
|
150
|
+
const video = document.createElement('video');
|
|
151
|
+
run.video = video;
|
|
152
|
+
video.muted = true;
|
|
153
|
+
video.defaultMuted = true;
|
|
154
|
+
video.playsInline = true;
|
|
155
|
+
video.loop = action === 'idle';
|
|
156
|
+
video.disablePictureInPicture = true;
|
|
157
|
+
video.preload = 'auto';
|
|
158
|
+
video.setAttribute('aria-hidden', 'true');
|
|
159
|
+
video.setAttribute('playsinline', '');
|
|
160
|
+
video.onplaying = () => {
|
|
161
|
+
if (current !== run || !allowed()) return;
|
|
162
|
+
if (!Number.isFinite(video.duration) || video.duration <= 0 || video.duration > 15)
|
|
163
|
+
return fallback(run, true);
|
|
164
|
+
host.clearTimeout(run.timer);
|
|
165
|
+
run.timer =
|
|
166
|
+
action === 'idle'
|
|
167
|
+
? null
|
|
168
|
+
: host.setTimeout(() => fallback(run, true), video.duration * 1000 + 2000);
|
|
169
|
+
element.dataset.videoPlaying = 'true';
|
|
170
|
+
};
|
|
171
|
+
video.onwaiting = video.onstalled = () => {
|
|
172
|
+
if (current !== run) return;
|
|
173
|
+
host.clearTimeout(run.timer);
|
|
174
|
+
run.timer = host.setTimeout(() => fallback(run, true), 4000);
|
|
175
|
+
};
|
|
176
|
+
video.onended = () => {
|
|
177
|
+
if (current === run) {
|
|
178
|
+
stop(true);
|
|
179
|
+
scheduleIdle();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
video.onerror = () => fallback(run, true);
|
|
183
|
+
video.src = clips[action];
|
|
184
|
+
layer.appendChild(video);
|
|
185
|
+
try {
|
|
186
|
+
Promise.resolve(video.play()).catch(() => fallback(run, true));
|
|
187
|
+
} catch {
|
|
188
|
+
fallback(run, true);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const visibility = () => {
|
|
194
|
+
if (!allowed()) stop();
|
|
195
|
+
else scheduleIdle();
|
|
196
|
+
};
|
|
197
|
+
document.addEventListener('visibilitychange', visibility);
|
|
198
|
+
reduced?.addEventListener?.('change', visibility);
|
|
199
|
+
return {
|
|
200
|
+
play,
|
|
201
|
+
configure(next) {
|
|
202
|
+
if (!next.enabled || (next.resting && !resting)) stop();
|
|
203
|
+
enabled = next.enabled;
|
|
204
|
+
resting = next.resting;
|
|
205
|
+
base.configure(next);
|
|
206
|
+
startIdle();
|
|
207
|
+
},
|
|
208
|
+
drag(direction) {
|
|
209
|
+
dragging = true;
|
|
210
|
+
stop();
|
|
211
|
+
base.drag(direction);
|
|
212
|
+
},
|
|
213
|
+
release(cancelled) {
|
|
214
|
+
dragging = false;
|
|
215
|
+
base.release(cancelled);
|
|
216
|
+
scheduleIdle(cancelled ? 0 : 650);
|
|
217
|
+
},
|
|
218
|
+
destroy() {
|
|
219
|
+
destroyed = true;
|
|
220
|
+
stop();
|
|
221
|
+
for (const [video, timer] of retired) {
|
|
222
|
+
host.clearTimeout(timer);
|
|
223
|
+
dispose(video);
|
|
224
|
+
}
|
|
225
|
+
retired.clear();
|
|
226
|
+
document.removeEventListener('visibilitychange', visibility);
|
|
227
|
+
reduced?.removeEventListener?.('change', visibility);
|
|
228
|
+
base.destroy();
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|