@peekling/runtime 0.1.0 → 0.1.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 +59 -8
- package/dist/canvas-renderer.d.ts +17 -0
- package/dist/canvas-renderer.d.ts.map +1 -0
- package/dist/canvas-renderer.js +192 -0
- package/dist/canvas.d.ts +5 -0
- package/dist/canvas.d.ts.map +1 -0
- package/dist/canvas.js +6 -0
- package/dist/configuration-validation.d.ts.map +1 -1
- package/dist/configuration-validation.js +176 -1
- package/dist/content.d.ts +3 -0
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +33 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +4 -1
- package/dist/interaction.d.ts +37 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +204 -0
- package/dist/overrides.d.ts +8 -0
- package/dist/overrides.d.ts.map +1 -1
- package/dist/overrides.js +18 -16
- package/dist/path-sampler.d.ts +9 -0
- package/dist/path-sampler.d.ts.map +1 -0
- package/dist/path-sampler.js +47 -0
- package/dist/peekling.css +1 -1
- package/dist/peekling.css.sri +1 -1
- package/dist/peekling.js +1365 -86
- package/dist/peekling.js.sri +1 -1
- package/dist/peekling.min.js +1 -1
- package/dist/peekling.min.js.sri +1 -1
- package/dist/plan-compiler.d.ts +2 -0
- package/dist/plan-compiler.d.ts.map +1 -1
- package/dist/plan-compiler.js +71 -14
- package/dist/plan.d.ts +27 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +281 -22
- package/dist/registry.js +2 -2
- package/dist/renderer.d.ts +18 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +1 -0
- package/dist/runtime-validation.d.ts.map +1 -1
- package/dist/runtime-validation.js +35 -0
- package/dist/runtime.d.ts +49 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +365 -14
- package/dist/targets.d.ts +11 -0
- package/dist/targets.d.ts.map +1 -0
- package/dist/targets.js +106 -0
- package/dist/types.d.ts +61 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -1
package/dist/runtime.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { ContentRenderer } from "./content.js";
|
|
2
2
|
import { InputCollector } from "./input.js";
|
|
3
|
+
import { CharacterInteractionController, } from "./interaction.js";
|
|
3
4
|
import { browserCompletionEvent, validateEventPayload } from "./events.js";
|
|
4
5
|
import { directionTo } from "./direction.js";
|
|
5
6
|
import { DiagnosticChannel } from "./diagnostics.js";
|
|
6
|
-
import { EVENT_NAME_PATTERN } from "./contracts.js";
|
|
7
|
+
import { EVENT_NAME_PATTERN, isSurfaceColor } from "./contracts.js";
|
|
7
8
|
import { DEFAULT_MAX_DENSITY, DEFAULT_POSITION } from "./defaults.js";
|
|
8
9
|
import { animationCycleMs, locomotionAdvance, locomotionSample, } from "./locomotion.js";
|
|
9
10
|
import { loadNativePack, requiredAtlasDensity, } from "./loader.js";
|
|
10
11
|
import { resolveCapabilityName } from "./normalized.js";
|
|
11
|
-
import { PlanRuntime, compiledPlanSectionRequirements,
|
|
12
|
+
import { PlanRuntime, compiledPlanSectionRequirements, createPresetPlan, } from "./plan.js";
|
|
12
13
|
import { compilePlan } from "./plan-compiler.js";
|
|
13
14
|
import { validateRuntimeConfiguration, validRuntimeName, } from "./runtime-validation.js";
|
|
14
15
|
import { runtimeMessage } from "./runtime-diagnostics.js";
|
|
15
16
|
import { OverrideManager, rejectedOverrideHandle } from "./overrides.js";
|
|
16
|
-
import {
|
|
17
|
+
import { createAtlasRenderer, } from "./renderer.js";
|
|
17
18
|
import { resolveState } from "./resolver.js";
|
|
18
19
|
import { SectionTracker } from "./sections.js";
|
|
20
|
+
import { SvgPathSampler } from "./path-sampler.js";
|
|
21
|
+
import { TargetTracker } from "./targets.js";
|
|
19
22
|
import { registerSiteVisibility } from "./visibility.js";
|
|
20
23
|
import { snapshotConfiguration } from "./own-data.js";
|
|
21
24
|
import { NATIVE_DIRECTIONS, } from "./types.js";
|
|
@@ -23,6 +26,10 @@ const SUSPEND_HOST = 1;
|
|
|
23
26
|
const SUSPEND_PAGE = 2;
|
|
24
27
|
const SUSPEND_SITE = 4;
|
|
25
28
|
const MAX_TIMER_DELAY = 2_147_483_647;
|
|
29
|
+
const DEFAULT_GRAVITY = 1_800;
|
|
30
|
+
const DEFAULT_THROW_SPEED = 1_800;
|
|
31
|
+
const DEFAULT_BOUNCE = 0.35;
|
|
32
|
+
const DEFAULT_FLOOR_INSET = 8;
|
|
26
33
|
const EMPTY_CONTENT_SELECTIONS = Object.freeze([]);
|
|
27
34
|
function browserSelectorIsValid(document, selector) {
|
|
28
35
|
try {
|
|
@@ -86,10 +93,34 @@ function hatchOptions(input) {
|
|
|
86
93
|
}
|
|
87
94
|
return input;
|
|
88
95
|
}
|
|
96
|
+
function checkedIndicator(input) {
|
|
97
|
+
if (input === null)
|
|
98
|
+
return;
|
|
99
|
+
const value = snapshotConfiguration(input);
|
|
100
|
+
if (!value ||
|
|
101
|
+
typeof value !== "object" ||
|
|
102
|
+
Array.isArray(value) ||
|
|
103
|
+
(value.kind !== undefined &&
|
|
104
|
+
value.kind !== "dot" &&
|
|
105
|
+
value.kind !== "count") ||
|
|
106
|
+
typeof value.label !== "string" ||
|
|
107
|
+
value.label.length < 1 ||
|
|
108
|
+
value.label.length > 120 ||
|
|
109
|
+
/[\u0000-\u001f\u007f]/.test(value.label) ||
|
|
110
|
+
(value.count !== undefined &&
|
|
111
|
+
(!Number.isInteger(value.count) ||
|
|
112
|
+
value.count < 0 ||
|
|
113
|
+
value.count > 999)) ||
|
|
114
|
+
(value.color !== undefined && !isSurfaceColor(value.color)) ||
|
|
115
|
+
(value.visible !== undefined && typeof value.visible !== "boolean")) {
|
|
116
|
+
throw new TypeError(runtimeMessage("indicator.invalid", "Indicator is invalid"));
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
89
120
|
/** Package-internal implementation. It is not a supported import. */
|
|
90
121
|
export class PeeklingRuntime {
|
|
91
|
-
static hatch(input) {
|
|
92
|
-
return new PeeklingRuntime(hatchOptions(input));
|
|
122
|
+
static hatch(input, rendererFactory = createAtlasRenderer) {
|
|
123
|
+
return new PeeklingRuntime(hatchOptions(input), rendererFactory);
|
|
93
124
|
}
|
|
94
125
|
/** Package-internal presentation update used only by the Web Component. */
|
|
95
126
|
updateComponentName(value) {
|
|
@@ -128,7 +159,9 @@ export class PeeklingRuntime {
|
|
|
128
159
|
#loaded;
|
|
129
160
|
#renderer;
|
|
130
161
|
#contentRenderer;
|
|
162
|
+
#interactionController;
|
|
131
163
|
#sections;
|
|
164
|
+
#targets;
|
|
132
165
|
#input;
|
|
133
166
|
#raf = 0;
|
|
134
167
|
#planTimer = 0;
|
|
@@ -164,10 +197,18 @@ export class PeeklingRuntime {
|
|
|
164
197
|
#instanceId;
|
|
165
198
|
#diagnostics;
|
|
166
199
|
#styles;
|
|
200
|
+
#rendererFactory;
|
|
167
201
|
#rejectedOverrideId = 0;
|
|
168
202
|
#reportedNotReadyEvent = false;
|
|
169
203
|
#reportedNotReadyOverride = false;
|
|
170
|
-
|
|
204
|
+
#trackTargets = false;
|
|
205
|
+
#pathSampler;
|
|
206
|
+
#indicator;
|
|
207
|
+
#dragging = false;
|
|
208
|
+
#throwing = false;
|
|
209
|
+
#throwVelocity = { x: 0, y: 0 };
|
|
210
|
+
#landUntil = 0;
|
|
211
|
+
constructor(options = {}, rendererFactory = createAtlasRenderer) {
|
|
171
212
|
options = snapshotConfiguration(options);
|
|
172
213
|
const document = options.document ?? globalThis.document;
|
|
173
214
|
const window = options.window ?? globalThis.window;
|
|
@@ -175,7 +216,10 @@ export class PeeklingRuntime {
|
|
|
175
216
|
throw new Error(runtimeMessage("browser.context", "Browser document/window required"));
|
|
176
217
|
this.#document = document;
|
|
177
218
|
this.#window = window;
|
|
219
|
+
this.#pathSampler = new SvgPathSampler(document);
|
|
178
220
|
this.#options = options;
|
|
221
|
+
this.#indicator = options.indicator;
|
|
222
|
+
this.#rendererFactory = rendererFactory;
|
|
179
223
|
this.#now = window.performance?.now?.bind(window.performance) ?? Date.now;
|
|
180
224
|
this.#instanceId = `instance:${Math.floor(this.#now())}:${Math.random().toString(36).slice(2, 8)}`;
|
|
181
225
|
this.#diagnostics = new DiagnosticChannel({
|
|
@@ -293,6 +337,31 @@ export class PeeklingRuntime {
|
|
|
293
337
|
this.#setSuspended(SUSPEND_HOST, false);
|
|
294
338
|
this.#notice("lifecycle.resumed", "info", "lifecycle");
|
|
295
339
|
}
|
|
340
|
+
refreshTargets() {
|
|
341
|
+
this.#targets?.refresh();
|
|
342
|
+
}
|
|
343
|
+
setIndicator(indicator) {
|
|
344
|
+
if (this.#destroyed)
|
|
345
|
+
return;
|
|
346
|
+
this.#indicator = checkedIndicator(indicator);
|
|
347
|
+
this.#interactionController?.updateIndicator(this.#indicator);
|
|
348
|
+
}
|
|
349
|
+
setContentVisible(visible) {
|
|
350
|
+
if (this.#destroyed || !this.#contentRenderer)
|
|
351
|
+
return false;
|
|
352
|
+
const expanded = this.#contentRenderer.setUserHidden(!visible);
|
|
353
|
+
this.#interactionController?.setExpanded(expanded);
|
|
354
|
+
this.#wake();
|
|
355
|
+
return expanded;
|
|
356
|
+
}
|
|
357
|
+
toggleContent() {
|
|
358
|
+
if (this.#destroyed || !this.#contentRenderer)
|
|
359
|
+
return false;
|
|
360
|
+
const expanded = this.#contentRenderer.toggleUserHidden();
|
|
361
|
+
this.#interactionController?.setExpanded(expanded);
|
|
362
|
+
this.#wake();
|
|
363
|
+
return expanded;
|
|
364
|
+
}
|
|
296
365
|
destroy() {
|
|
297
366
|
this.#destroy("destroyed");
|
|
298
367
|
}
|
|
@@ -307,7 +376,9 @@ export class PeeklingRuntime {
|
|
|
307
376
|
this.#input?.destroy();
|
|
308
377
|
this.#plan?.reset();
|
|
309
378
|
this.#sections?.destroy();
|
|
379
|
+
this.#targets?.destroy();
|
|
310
380
|
this.#contentRenderer?.destroy();
|
|
381
|
+
this.#interactionController?.destroy();
|
|
311
382
|
this.#renderer?.destroy();
|
|
312
383
|
this.#unregisterVisibility?.();
|
|
313
384
|
this.#unregisterVisibility = undefined;
|
|
@@ -334,10 +405,18 @@ export class PeeklingRuntime {
|
|
|
334
405
|
async #initialize() {
|
|
335
406
|
try {
|
|
336
407
|
const validateSelector = (selector) => browserSelectorIsValid(this.#document, selector);
|
|
408
|
+
const targetIds = new Set(Object.keys(this.#options.targets ?? {}));
|
|
409
|
+
for (const selector of Object.values(this.#options.targets ?? {})) {
|
|
410
|
+
if (!validateSelector(selector)) {
|
|
411
|
+
throw new TypeError(runtimeMessage("target.selector", "Target selector is invalid"));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
337
414
|
if (this.#options.plan !== undefined) {
|
|
338
415
|
compilePlan(this.#options.plan, {
|
|
339
416
|
contentIds: new Set(Object.keys(this.#content)),
|
|
417
|
+
targets: targetIds,
|
|
340
418
|
validateSelector,
|
|
419
|
+
validatePath: (path) => this.#pathSampler.validate(path),
|
|
341
420
|
});
|
|
342
421
|
}
|
|
343
422
|
const browserWindow = this.#window;
|
|
@@ -396,17 +475,22 @@ export class PeeklingRuntime {
|
|
|
396
475
|
this.#loaded = loaded;
|
|
397
476
|
const locomotion = hasLocomotion(loaded.content);
|
|
398
477
|
const capabilities = new Set(locomotion ? ["locomotion"] : []);
|
|
399
|
-
const compiled = compilePlan(this.#options.plan ??
|
|
478
|
+
const compiled = compilePlan(this.#options.plan ??
|
|
479
|
+
createPresetPlan(this.#options.preset ?? "companion", locomotion), {
|
|
400
480
|
states: new Set(Object.keys(loaded.content.states)),
|
|
401
481
|
capabilities,
|
|
402
482
|
contentIds: new Set(Object.keys(this.#content)),
|
|
483
|
+
targets: targetIds,
|
|
403
484
|
validateSelector,
|
|
485
|
+
validatePath: (path) => this.#pathSampler.validate(path),
|
|
404
486
|
});
|
|
405
487
|
this.#plan = new PlanRuntime(compiled);
|
|
406
488
|
this.#overrides = new OverrideManager({
|
|
407
489
|
states: new Set(Object.keys(loaded.content.states)),
|
|
408
490
|
capabilities,
|
|
409
491
|
contentIds: new Set(Object.keys(this.#content)),
|
|
492
|
+
targets: targetIds,
|
|
493
|
+
validatePath: (path) => this.#pathSampler.validate(path),
|
|
410
494
|
}, this.#now, (message) => this.#diagnose(message, "override.rejected", "warning", "override"));
|
|
411
495
|
if (this.#displayName === "") {
|
|
412
496
|
this.#displayName = loaded.content.displayName;
|
|
@@ -425,16 +509,53 @@ export class PeeklingRuntime {
|
|
|
425
509
|
this.#position = resolveInitialPosition(this.#options.position ?? DEFAULT_POSITION, this.#window.innerWidth, this.#window.innerHeight, (loaded.content.atlas.logicalWidth ?? loaded.content.atlas.cellWidth) *
|
|
426
510
|
scale, (loaded.content.atlas.logicalHeight ??
|
|
427
511
|
loaded.content.atlas.cellHeight) * scale);
|
|
428
|
-
this.#renderer =
|
|
512
|
+
this.#renderer = this.#rendererFactory({
|
|
513
|
+
document: this.#document,
|
|
514
|
+
pack: loaded.content,
|
|
515
|
+
atlasUrl: loaded.atlasObjectUrl,
|
|
516
|
+
scale,
|
|
517
|
+
styles: this.#styles,
|
|
518
|
+
});
|
|
429
519
|
if (Object.keys(this.#content).length ||
|
|
430
520
|
this.#displayName !== undefined) {
|
|
431
521
|
this.#ensureContentRenderer();
|
|
432
522
|
}
|
|
523
|
+
if (this.#options.interaction !== false) {
|
|
524
|
+
const interaction = this.#options.interaction ?? {};
|
|
525
|
+
const pressAction = interaction.press ??
|
|
526
|
+
(interaction.pressEvent ? "emit" : "toggle-content");
|
|
527
|
+
const controlsContent = Boolean(this.#contentRenderer) &&
|
|
528
|
+
(pressAction === "toggle-content" ||
|
|
529
|
+
pressAction === "show-content" ||
|
|
530
|
+
pressAction === "hide-content");
|
|
531
|
+
if (interaction.contentInitiallyHidden) {
|
|
532
|
+
this.#contentRenderer?.setUserHidden(true);
|
|
533
|
+
}
|
|
534
|
+
this.#interactionController = new CharacterInteractionController({
|
|
535
|
+
document: this.#document,
|
|
536
|
+
styles: this.#styles,
|
|
537
|
+
label: interaction.label ??
|
|
538
|
+
`Interact with ${loaded.content.displayName || "Peekling"}`,
|
|
539
|
+
draggable: interaction.drag !== false,
|
|
540
|
+
...(controlsContent
|
|
541
|
+
? { expanded: !interaction.contentInitiallyHidden }
|
|
542
|
+
: {}),
|
|
543
|
+
...(this.#indicator ? { indicator: this.#indicator } : {}),
|
|
544
|
+
onPress: () => this.#handleCharacterPress(),
|
|
545
|
+
onDragStart: () => this.#startCharacterDrag(),
|
|
546
|
+
onDragMove: (position, velocity) => this.#moveCharacterDrag(position, velocity),
|
|
547
|
+
onDragEnd: (velocity, moved, pointer) => this.#endCharacterDrag(velocity, moved, pointer),
|
|
548
|
+
});
|
|
549
|
+
if (interaction.contentInitiallyHidden) {
|
|
550
|
+
this.#interactionController.setExpanded(false);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
433
553
|
try {
|
|
434
554
|
await Promise.all([
|
|
435
555
|
this.#visibilityReady,
|
|
436
556
|
this.#renderer.ready,
|
|
437
557
|
this.#contentRenderer?.ready,
|
|
558
|
+
this.#interactionController?.ready,
|
|
438
559
|
]);
|
|
439
560
|
}
|
|
440
561
|
catch (cause) {
|
|
@@ -443,10 +564,14 @@ export class PeeklingRuntime {
|
|
|
443
564
|
}
|
|
444
565
|
const requirements = compiledPlanSectionRequirements(compiled);
|
|
445
566
|
this.#createInput();
|
|
567
|
+
if (targetIds.size) {
|
|
568
|
+
this.#targets = new TargetTracker(this.#document, this.#window, this.#options.targets, () => this.#wake());
|
|
569
|
+
}
|
|
446
570
|
if (requirements.selectors.length) {
|
|
447
571
|
this.#sections = new SectionTracker(this.#document, this.#window, requirements.selectors, requirements.thresholds, () => this.#wake(), (fact) => this.#input?.captureReaction("section.visibility", fact, "browser"));
|
|
448
572
|
}
|
|
449
573
|
this.#renderer.setHidden(this.#siteHidden);
|
|
574
|
+
this.#interactionController?.setHidden(this.#siteHidden);
|
|
450
575
|
if (this.#suspensions) {
|
|
451
576
|
this.#input?.setSuspended(true);
|
|
452
577
|
this.#sections?.setSuspended(true);
|
|
@@ -517,6 +642,122 @@ export class PeeklingRuntime {
|
|
|
517
642
|
});
|
|
518
643
|
return this.#contentRenderer;
|
|
519
644
|
}
|
|
645
|
+
#handleCharacterPress() {
|
|
646
|
+
if (this.#destroyed)
|
|
647
|
+
return;
|
|
648
|
+
const interaction = this.#options.interaction || {};
|
|
649
|
+
if (interaction.clearIndicatorOnPress === true && this.#indicator) {
|
|
650
|
+
this.setIndicator(null);
|
|
651
|
+
}
|
|
652
|
+
const action = interaction.press ?? (interaction.pressEvent ? "emit" : "toggle-content");
|
|
653
|
+
if (action === "emit") {
|
|
654
|
+
if (interaction.pressEvent)
|
|
655
|
+
this.emit(interaction.pressEvent);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (action === "none")
|
|
659
|
+
return;
|
|
660
|
+
if (action === "show-content")
|
|
661
|
+
this.setContentVisible(true);
|
|
662
|
+
else if (action === "hide-content")
|
|
663
|
+
this.setContentVisible(false);
|
|
664
|
+
else
|
|
665
|
+
this.toggleContent();
|
|
666
|
+
}
|
|
667
|
+
#startCharacterDrag() {
|
|
668
|
+
if (this.#destroyed || this.#siteHidden)
|
|
669
|
+
return;
|
|
670
|
+
this.#dragging = true;
|
|
671
|
+
this.#throwing = false;
|
|
672
|
+
this.#landUntil = 0;
|
|
673
|
+
this.#throwVelocity = { x: 0, y: 0 };
|
|
674
|
+
this.#wake();
|
|
675
|
+
}
|
|
676
|
+
#moveCharacterDrag(position, velocity) {
|
|
677
|
+
if (!this.#dragging || this.#destroyed)
|
|
678
|
+
return;
|
|
679
|
+
const size = this.#characterSize();
|
|
680
|
+
this.#position = size
|
|
681
|
+
? clampPosition(position, { width: this.#window.innerWidth, height: this.#window.innerHeight }, size.width, size.height)
|
|
682
|
+
: { ...position };
|
|
683
|
+
this.#throwVelocity = { ...velocity };
|
|
684
|
+
this.#wake();
|
|
685
|
+
}
|
|
686
|
+
#endCharacterDrag(velocity, moved, pointer) {
|
|
687
|
+
if (!this.#dragging || this.#destroyed)
|
|
688
|
+
return;
|
|
689
|
+
this.#dragging = false;
|
|
690
|
+
if (!moved) {
|
|
691
|
+
this.#throwVelocity = { x: 0, y: 0 };
|
|
692
|
+
this.#wake();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const interaction = this.#options.interaction || {};
|
|
696
|
+
if (interaction.catchTarget &&
|
|
697
|
+
this.#targets?.contains(interaction.catchTarget, pointer, interaction.catchMargin ?? 24) &&
|
|
698
|
+
this.#catchCharacter(interaction.catchTarget)) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (this.#reducedMotion) {
|
|
702
|
+
this.#landCharacter();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (interaction.throw === false) {
|
|
706
|
+
this.#throwVelocity = { x: 0, y: 0 };
|
|
707
|
+
this.#wake();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const maximum = interaction.maxThrowSpeed ?? DEFAULT_THROW_SPEED;
|
|
711
|
+
const magnitude = Math.hypot(velocity.x, velocity.y);
|
|
712
|
+
const factor = magnitude > maximum && magnitude ? maximum / magnitude : 1;
|
|
713
|
+
this.#throwVelocity = {
|
|
714
|
+
x: velocity.x * factor,
|
|
715
|
+
y: velocity.y * factor,
|
|
716
|
+
};
|
|
717
|
+
this.#throwing = true;
|
|
718
|
+
this.#wake();
|
|
719
|
+
}
|
|
720
|
+
#characterSize() {
|
|
721
|
+
const loaded = this.#loaded?.content;
|
|
722
|
+
if (!loaded)
|
|
723
|
+
return;
|
|
724
|
+
const scale = this.#options.scale ?? loaded.defaultScale;
|
|
725
|
+
return {
|
|
726
|
+
width: (loaded.atlas.logicalWidth ?? loaded.atlas.cellWidth) * scale,
|
|
727
|
+
height: (loaded.atlas.logicalHeight ?? loaded.atlas.cellHeight) * scale,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
#landCharacter() {
|
|
731
|
+
const size = this.#characterSize();
|
|
732
|
+
if (size) {
|
|
733
|
+
const floorInset = (this.#options.interaction || {}).floorInset ?? DEFAULT_FLOOR_INSET;
|
|
734
|
+
this.#position = clampPosition({
|
|
735
|
+
x: this.#position.x,
|
|
736
|
+
y: this.#window.innerHeight - size.height / 2 - floorInset,
|
|
737
|
+
}, { width: this.#window.innerWidth, height: this.#window.innerHeight }, size.width, size.height);
|
|
738
|
+
}
|
|
739
|
+
this.#dragging = false;
|
|
740
|
+
this.#throwing = false;
|
|
741
|
+
this.#throwVelocity = { x: 0, y: 0 };
|
|
742
|
+
this.#landUntil = this.#now() + 360;
|
|
743
|
+
this.#wake();
|
|
744
|
+
}
|
|
745
|
+
#catchCharacter(targetId) {
|
|
746
|
+
const target = this.#targets?.snapshot(true)[targetId];
|
|
747
|
+
const size = this.#characterSize();
|
|
748
|
+
if (!target || !size)
|
|
749
|
+
return false;
|
|
750
|
+
const interaction = this.#options.interaction || {};
|
|
751
|
+
const point = characterTargetPoint(target, interaction.catchAnchor ?? "center", size);
|
|
752
|
+
this.#position = clampPosition(point, { width: this.#window.innerWidth, height: this.#window.innerHeight }, size.width, size.height);
|
|
753
|
+
this.#throwing = false;
|
|
754
|
+
this.#throwVelocity = { x: 0, y: 0 };
|
|
755
|
+
this.#landUntil = this.#now() + 360;
|
|
756
|
+
if (interaction.catchEvent)
|
|
757
|
+
this.emit(interaction.catchEvent);
|
|
758
|
+
this.#wake();
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
520
761
|
#diagnose(message, code = "runtime.notice", severity = "warning", phase = "internal", eventId, metadata, cause) {
|
|
521
762
|
this.#diagnostics.emit({
|
|
522
763
|
code,
|
|
@@ -591,6 +832,7 @@ export class PeeklingRuntime {
|
|
|
591
832
|
return;
|
|
592
833
|
this.#siteHidden = hidden;
|
|
593
834
|
this.#renderer?.setHidden(hidden);
|
|
835
|
+
this.#interactionController?.setHidden(hidden);
|
|
594
836
|
if (this.#destroyed)
|
|
595
837
|
return;
|
|
596
838
|
if (hidden && this.#contentRenderer) {
|
|
@@ -602,6 +844,9 @@ export class PeeklingRuntime {
|
|
|
602
844
|
return;
|
|
603
845
|
}
|
|
604
846
|
if (hidden) {
|
|
847
|
+
this.#dragging = false;
|
|
848
|
+
this.#throwing = false;
|
|
849
|
+
this.#throwVelocity = { x: 0, y: 0 };
|
|
605
850
|
this.#overrides?.clear("dismissed");
|
|
606
851
|
this.#setSuspended(SUSPEND_SITE, true);
|
|
607
852
|
this.#notice("lifecycle.dismissed", "info", "lifecycle");
|
|
@@ -717,8 +962,13 @@ export class PeeklingRuntime {
|
|
|
717
962
|
.upgrade(required)
|
|
718
963
|
.then((changed) => {
|
|
719
964
|
if (changed && !this.#destroyed && this.#loaded === loaded) {
|
|
720
|
-
this.#renderer
|
|
721
|
-
|
|
965
|
+
const renderer = this.#renderer;
|
|
966
|
+
if (!renderer)
|
|
967
|
+
return;
|
|
968
|
+
return Promise.resolve(renderer.swapAtlas(loaded.content, loaded.atlasObjectUrl)).then(() => {
|
|
969
|
+
if (!this.#destroyed && this.#loaded === loaded)
|
|
970
|
+
this.#wake();
|
|
971
|
+
});
|
|
722
972
|
}
|
|
723
973
|
})
|
|
724
974
|
.catch((error) => {
|
|
@@ -795,10 +1045,13 @@ export class PeeklingRuntime {
|
|
|
795
1045
|
pointer: input.pointer,
|
|
796
1046
|
position: this.#position,
|
|
797
1047
|
viewport,
|
|
1048
|
+
characterSize,
|
|
798
1049
|
lastActivityAt: input.lastActivityAt,
|
|
799
1050
|
reducedMotion: this.#reducedMotion,
|
|
800
1051
|
pageVisible: !this.#document.hidden,
|
|
801
1052
|
sections,
|
|
1053
|
+
targets: this.#targets?.snapshot(this.#trackTargets),
|
|
1054
|
+
samplePath: (path, progress) => this.#pathSampler.sample(path, progress),
|
|
802
1055
|
reaction,
|
|
803
1056
|
};
|
|
804
1057
|
return {
|
|
@@ -822,6 +1075,7 @@ export class PeeklingRuntime {
|
|
|
822
1075
|
overrides.observeEvent(reaction.name, reaction.source);
|
|
823
1076
|
}
|
|
824
1077
|
const request = overrides.compose(ordinary, world);
|
|
1078
|
+
this.#trackTargets = request.motion?.trackTarget === true;
|
|
825
1079
|
if (this.#reducedMotion) {
|
|
826
1080
|
delete request.motion;
|
|
827
1081
|
if (request.capability === "locomotion")
|
|
@@ -829,6 +1083,25 @@ export class PeeklingRuntime {
|
|
|
829
1083
|
if (!request.state)
|
|
830
1084
|
request.state = "idle";
|
|
831
1085
|
}
|
|
1086
|
+
const interaction = this.#options.interaction || {};
|
|
1087
|
+
if (this.#dragging) {
|
|
1088
|
+
delete request.motion;
|
|
1089
|
+
delete request.capability;
|
|
1090
|
+
request.state = interaction.dragState ?? "scroll:fly";
|
|
1091
|
+
}
|
|
1092
|
+
else if (this.#throwing) {
|
|
1093
|
+
delete request.motion;
|
|
1094
|
+
delete request.capability;
|
|
1095
|
+
request.state =
|
|
1096
|
+
this.#throwVelocity.y < 0
|
|
1097
|
+
? (interaction.riseState ?? "scroll:fly")
|
|
1098
|
+
: (interaction.fallState ?? "scroll:fall");
|
|
1099
|
+
}
|
|
1100
|
+
else if (this.#landUntil > world.now) {
|
|
1101
|
+
delete request.motion;
|
|
1102
|
+
delete request.capability;
|
|
1103
|
+
request.state = interaction.landState ?? "success";
|
|
1104
|
+
}
|
|
832
1105
|
const diagnostics = [];
|
|
833
1106
|
if (reaction) {
|
|
834
1107
|
this.#seenReaction = reaction.id;
|
|
@@ -873,18 +1146,71 @@ export class PeeklingRuntime {
|
|
|
873
1146
|
}
|
|
874
1147
|
return this.#frameActive(frame);
|
|
875
1148
|
}
|
|
1149
|
+
#advanceThrow(snapshot) {
|
|
1150
|
+
if (!this.#throwing)
|
|
1151
|
+
return;
|
|
1152
|
+
const interaction = this.#options.interaction || {};
|
|
1153
|
+
const dt = snapshot.frame.stepMs / 1_000;
|
|
1154
|
+
const bounce = interaction.bounce ?? DEFAULT_BOUNCE;
|
|
1155
|
+
const halfWidth = snapshot.characterSize.width / 2;
|
|
1156
|
+
const halfHeight = snapshot.characterSize.height / 2;
|
|
1157
|
+
const floor = snapshot.world.viewport.height -
|
|
1158
|
+
halfHeight -
|
|
1159
|
+
(interaction.floorInset ?? DEFAULT_FLOOR_INSET);
|
|
1160
|
+
let velocityX = this.#throwVelocity.x;
|
|
1161
|
+
let velocityY = this.#throwVelocity.y + (interaction.gravity ?? DEFAULT_GRAVITY) * dt;
|
|
1162
|
+
let x = this.#position.x + velocityX * dt;
|
|
1163
|
+
let y = this.#position.y + velocityY * dt;
|
|
1164
|
+
const right = snapshot.world.viewport.width - halfWidth;
|
|
1165
|
+
if (x < halfWidth) {
|
|
1166
|
+
x = halfWidth;
|
|
1167
|
+
velocityX = Math.abs(velocityX) * bounce;
|
|
1168
|
+
}
|
|
1169
|
+
else if (x > right) {
|
|
1170
|
+
x = right;
|
|
1171
|
+
velocityX = -Math.abs(velocityX) * bounce;
|
|
1172
|
+
}
|
|
1173
|
+
if (y < halfHeight) {
|
|
1174
|
+
y = halfHeight;
|
|
1175
|
+
velocityY = Math.abs(velocityY) * bounce;
|
|
1176
|
+
}
|
|
1177
|
+
const next = { x, y };
|
|
1178
|
+
if (interaction.catchTarget &&
|
|
1179
|
+
this.#targets?.contains(interaction.catchTarget, next, interaction.catchMargin ?? 24) &&
|
|
1180
|
+
this.#catchCharacter(interaction.catchTarget)) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (y >= floor) {
|
|
1184
|
+
this.#position = { x, y: floor };
|
|
1185
|
+
this.#landCharacter();
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
this.#position = next;
|
|
1189
|
+
this.#throwVelocity = { x: velocityX * 0.995, y: velocityY };
|
|
1190
|
+
}
|
|
876
1191
|
#calculatePresentation(snapshot, evaluation) {
|
|
877
1192
|
const { frame, characterSize, world } = snapshot;
|
|
878
1193
|
const { request } = evaluation;
|
|
879
1194
|
const loaded = frame.loaded.content;
|
|
880
1195
|
if (!this.#reducedMotion)
|
|
881
1196
|
this.#animationClock += Math.max(0, frame.elapsed);
|
|
1197
|
+
this.#advanceThrow(snapshot);
|
|
882
1198
|
const requested = request.capability === "locomotion" && request.motion
|
|
883
1199
|
? loaded.directionalStates?.[directionTo({ x: 0, y: 0 }, { x: request.motion.x, y: request.motion.y })]
|
|
884
1200
|
: resolveCapabilityName(request.state, loaded);
|
|
885
1201
|
const resolved = resolveState(requested, loaded.states);
|
|
886
1202
|
let lift = 0;
|
|
887
|
-
if (request.motion) {
|
|
1203
|
+
if (request.motion?.direct) {
|
|
1204
|
+
const distance = request.motion.maxDistance ?? 0;
|
|
1205
|
+
this.#position = clampPosition({
|
|
1206
|
+
x: this.#position.x + request.motion.x * distance,
|
|
1207
|
+
y: this.#position.y + request.motion.y * distance,
|
|
1208
|
+
}, world.viewport, characterSize.width, characterSize.height);
|
|
1209
|
+
lift = request.motion.lift ?? 0;
|
|
1210
|
+
this.#locomotionState = "";
|
|
1211
|
+
this.#locomotionElapsed = 0;
|
|
1212
|
+
}
|
|
1213
|
+
else if (request.motion) {
|
|
888
1214
|
if (this.#locomotionState !== resolved.name) {
|
|
889
1215
|
this.#locomotionState = resolved.name;
|
|
890
1216
|
this.#locomotionElapsed = 0;
|
|
@@ -904,8 +1230,9 @@ export class PeeklingRuntime {
|
|
|
904
1230
|
y: this.#position.y + request.motion.y * distance,
|
|
905
1231
|
}, world.viewport, characterSize.width, characterSize.height);
|
|
906
1232
|
lift =
|
|
907
|
-
|
|
908
|
-
|
|
1233
|
+
request.motion.lift ??
|
|
1234
|
+
locomotionSample(loaded.locomotionMotion, nextCycles).lift *
|
|
1235
|
+
characterSize.height;
|
|
909
1236
|
}
|
|
910
1237
|
else {
|
|
911
1238
|
this.#locomotionState = "";
|
|
@@ -932,12 +1259,18 @@ export class PeeklingRuntime {
|
|
|
932
1259
|
if (!this.#frameActive(frame))
|
|
933
1260
|
return;
|
|
934
1261
|
frame.renderer.render(presentation.state, this.#animationClock, this.#position, snapshot.devicePixelRatio, presentation.lift);
|
|
1262
|
+
if (!this.#frameActive(frame))
|
|
1263
|
+
return;
|
|
1264
|
+
this.#interactionController?.render(this.#position, snapshot.characterSize, presentation.lift);
|
|
935
1265
|
if (!this.#frameActive(frame))
|
|
936
1266
|
return;
|
|
937
1267
|
this.#lastTick = frame.now;
|
|
938
1268
|
this.#schedulePlan(frame);
|
|
939
1269
|
if (this.#frameActive(frame) &&
|
|
940
|
-
(evaluation.request.motion ||
|
|
1270
|
+
(evaluation.request.motion ||
|
|
1271
|
+
frame.input.reaction ||
|
|
1272
|
+
this.#throwing ||
|
|
1273
|
+
this.#dragging))
|
|
941
1274
|
this.#wake();
|
|
942
1275
|
}
|
|
943
1276
|
#endPage() {
|
|
@@ -1013,3 +1346,21 @@ function clampPosition(point, viewport, width, height) {
|
|
|
1013
1346
|
y: Math.max(height / 2, Math.min(viewport.height - height / 2, point.y)),
|
|
1014
1347
|
};
|
|
1015
1348
|
}
|
|
1349
|
+
function characterTargetPoint(target, anchor, character) {
|
|
1350
|
+
const center = {
|
|
1351
|
+
x: target.left + target.width / 2,
|
|
1352
|
+
y: target.top + target.height / 2,
|
|
1353
|
+
};
|
|
1354
|
+
switch (anchor) {
|
|
1355
|
+
case "top":
|
|
1356
|
+
return { x: center.x, y: target.top - character.height / 2 };
|
|
1357
|
+
case "right":
|
|
1358
|
+
return { x: target.right + character.width / 2, y: center.y };
|
|
1359
|
+
case "bottom":
|
|
1360
|
+
return { x: center.x, y: target.bottom + character.height / 2 };
|
|
1361
|
+
case "left":
|
|
1362
|
+
return { x: target.left - character.width / 2, y: center.y };
|
|
1363
|
+
default:
|
|
1364
|
+
return center;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Point, TargetSnapshot } from "./types.js";
|
|
2
|
+
/** Tracks host-approved geometry without moving, styling, or reparenting host DOM. */
|
|
3
|
+
export declare class TargetTracker {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(document: Document, window: Window, selectors: Readonly<Record<string, string>>, wake: () => void);
|
|
6
|
+
snapshot(force?: boolean): Readonly<Record<string, TargetSnapshot>>;
|
|
7
|
+
contains(id: string, point: Readonly<Point>, margin?: number): boolean;
|
|
8
|
+
refresh: () => void;
|
|
9
|
+
destroy(): void;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=targets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../src/targets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIxD,sFAAsF;AACtF,qBAAa,aAAa;;IAYxB,YACE,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC3C,IAAI,EAAE,MAAM,IAAI,EAwBjB;IAED,QAAQ,CAAC,KAAK,UAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAwChE;IAED,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,SAAI,GAAG,OAAO,CAShE;IAED,OAAO,QAAO,IAAI,CAIhB;IAEF,OAAO,IAAI,IAAI,CAQd;CAGF"}
|