@hyperframes/core 0.9.19 → 0.9.21

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/dist/index.d.ts CHANGED
@@ -68,3 +68,4 @@ export { canaryBucket, evaluateCanary, parseCanaryOverride, type CanaryDecision,
68
68
  export { CANARIES, canaryEnvVar, findCanary, overdueCanaries, type CanaryDefinition, } from "./canaryRegistry.js";
69
69
  export { HF_VFX_ATTR, HF_VFX_CHAIN_VERSION, HF_VFX, chainCapture, getVfxDef, parseVfxChain, serializeVfxChain, normalizeVfxParams, type HfVfxCapture, type HfVfxChain, type HfVfxDef, type HfVfxNode, type HfVfxParam, type HfVfxParamValues, } from "./vfx.js";
70
70
  export { PAGE_DEVICE_SCALE_GLOBAL, PAGE_MOTION_BLUR_OFF_GLOBAL, acceleratedCanvasTrackerSource, } from "./pageMotionBlur.js";
71
+ export { MOTION_BLUR_FINGERPRINT_FIELDS, type MotionBlurFingerprint, type MotionBlurFingerprintField, } from "./motionBlurFingerprint.js";
package/dist/index.js CHANGED
@@ -71,3 +71,4 @@ export { CANARIES, canaryEnvVar, findCanary, overdueCanaries, } from "./canaryRe
71
71
  // VFX chain (data-vfx-chain) — defs, chain parse/serialize/normalize
72
72
  export { HF_VFX_ATTR, HF_VFX_CHAIN_VERSION, HF_VFX, chainCapture, getVfxDef, parseVfxChain, serializeVfxChain, normalizeVfxParams, } from "./vfx.js";
73
73
  export { PAGE_DEVICE_SCALE_GLOBAL, PAGE_MOTION_BLUR_OFF_GLOBAL, acceleratedCanvasTrackerSource, } from "./pageMotionBlur.js";
74
+ export { MOTION_BLUR_FINGERPRINT_FIELDS, } from "./motionBlurFingerprint.js";
@@ -6,6 +6,7 @@ export type HyperframePickerBoundingBox = {
6
6
  };
7
7
  export type HyperframePickerElementInfo = {
8
8
  id: string | null;
9
+ className?: string | null;
9
10
  tagName: string;
10
11
  selector: string;
11
12
  label: string;
@@ -0,0 +1,4 @@
1
+ export declare const MOTION_BLUR_FINGERPRINT_FIELDS: readonly ["canvas2d", "webgl", "video", "iframe", "customElement", "svgClip", "mask", "scrollContainer", "threeD", "overflowingMover", "frontOfMover", "blendMode", "fixedInMover", "pseudoOutside"];
2
+ export type MotionBlurFingerprintField = (typeof MOTION_BLUR_FINGERPRINT_FIELDS)[number];
3
+ export type MotionBlurFingerprint = Record<MotionBlurFingerprintField, number>;
4
+ export declare function emptyMotionBlurFingerprint(): MotionBlurFingerprint;
@@ -0,0 +1,34 @@
1
+ export const MOTION_BLUR_FINGERPRINT_FIELDS = [
2
+ "canvas2d",
3
+ "webgl",
4
+ "video",
5
+ "iframe",
6
+ "customElement",
7
+ "svgClip",
8
+ "mask",
9
+ "scrollContainer",
10
+ "threeD",
11
+ "overflowingMover",
12
+ "frontOfMover",
13
+ "blendMode",
14
+ "fixedInMover",
15
+ "pseudoOutside",
16
+ ];
17
+ export function emptyMotionBlurFingerprint() {
18
+ return {
19
+ canvas2d: 0,
20
+ webgl: 0,
21
+ video: 0,
22
+ iframe: 0,
23
+ customElement: 0,
24
+ svgClip: 0,
25
+ mask: 0,
26
+ scrollContainer: 0,
27
+ threeD: 0,
28
+ overflowingMover: 0,
29
+ frontOfMover: 0,
30
+ blendMode: 0,
31
+ fixedInMover: 0,
32
+ pseudoOutside: 0,
33
+ };
34
+ }
@@ -1,3 +1,4 @@
1
+ import { wallSetTimeout } from "./wallClock.js";
1
2
  import { swallow } from "./diagnostics.js";
2
3
  import { inspectRuntimeProtocol, runtimeProtocolFpsToNumber, runtimeProtocolMetadata, } from "./protocol.js";
3
4
  let runtimeProtocolFps = 30;
@@ -134,7 +135,7 @@ function flashElements(selectors, duration) {
134
135
  const els = document.querySelectorAll(selector);
135
136
  els.forEach((el) => {
136
137
  el.classList.add("__hf-flash");
137
- setTimeout(() => el.classList.remove("__hf-flash"), duration);
138
+ wallSetTimeout(() => el.classList.remove("__hf-flash"), duration);
138
139
  });
139
140
  }
140
141
  catch (err) {
@@ -30,6 +30,8 @@ export declare class TransportClock {
30
30
  private _lastReadMs;
31
31
  private _lastNow;
32
32
  private _audioStart;
33
+ private _reading;
34
+ private _ahead;
33
35
  constructor(opts?: {
34
36
  initialTime?: number;
35
37
  rate?: number;
@@ -38,6 +40,9 @@ export declare class TransportClock {
38
40
  });
39
41
  now(): number;
40
42
  private _elementTimeNeverBehind;
43
+ private _betweenUpdates;
44
+ private _runAhead;
45
+ aheadOf(el: HTMLMediaElement): number;
41
46
  private _heldUntilAudioOutputStarts;
42
47
  private _pendingAudioStart;
43
48
  /** Folds a >500ms gap since the last read into `_playStartMs` so it's never reported as played time. */
@@ -6,6 +6,15 @@ const STALL_ADJUSTED_LAG_MS = 33;
6
6
  const HAVE_FUTURE_DATA = 3;
7
7
  const AUDIO_START_WAIT_MS = 1500;
8
8
  const MEDIA_SYNC_STEER_TOLERANCE_SECONDS = 0.04;
9
+ const MAX_MEDIA_STEP_SECONDS = 0.25;
10
+ const MEDIA_STEPS_KEPT = 8;
11
+ const furthestAhead = (reading) => reading ? Math.max(0, ...reading.steps) : 0;
12
+ function noteStep(reading, time) {
13
+ const step = time - reading.time;
14
+ if (step > 0 && step <= MAX_MEDIA_STEP_SECONDS)
15
+ reading.steps = [...reading.steps.slice(1 - MEDIA_STEPS_KEPT), step];
16
+ reading.time = time;
17
+ }
9
18
  export class TransportClock {
10
19
  _baseTime = 0;
11
20
  _playStartMs = null;
@@ -20,6 +29,8 @@ export class TransportClock {
20
29
  _lastReadMs = null;
21
30
  _lastNow = 0;
22
31
  _audioStart = null;
32
+ _reading = null;
33
+ _ahead = null;
23
34
  constructor(opts) {
24
35
  this._baseTime = opts?.initialTime ?? 0;
25
36
  this._rate = opts?.rate ?? 1;
@@ -27,6 +38,7 @@ export class TransportClock {
27
38
  this._nowMs = opts?.nowMs ?? (() => performance.now());
28
39
  }
29
40
  now() {
41
+ this._ahead = null;
30
42
  if (this._playStartMs === null)
31
43
  return this._baseTime;
32
44
  // Audio-master: drift is impossible because audio IS the clock. Clearing
@@ -63,13 +75,42 @@ export class TransportClock {
63
75
  const spec = rate ?? 1;
64
76
  const time = timeAtSourceTime(spec, el.currentTime - mediaStart) + compositionStart;
65
77
  const held = this._heldUntilAudioOutputStarts(el.seeking, time);
66
- if (held !== undefined)
78
+ if (held !== undefined) {
79
+ this._reading = null;
67
80
  return held;
68
- if (time >= this._lastNow)
69
- return time;
81
+ }
82
+ const between = this._betweenUpdates(el, time);
83
+ if (between !== null)
84
+ return between;
70
85
  const wrapped = el.loop && this._lastNow - time > timeAtSourceTime(spec, el.duration) / 2;
71
86
  return !wrapped && el.readyState < HAVE_FUTURE_DATA ? this._lastNow : null;
72
87
  }
88
+ // Chromium moves currentTime in 10-50 ms steps, so reading it as is holds a frame, then jumps. Run on at the play
89
+ // rate, kept within [element time, element time + its largest recent step], never back; null when time is behind
90
+ // the playhead by more than that step (a late clip, not a step).
91
+ _betweenUpdates(el, time) {
92
+ const nowMs = this._nowMs();
93
+ const last = this._reading?.el === el ? this._reading : null;
94
+ if (time < this._lastNow - furthestAhead(last))
95
+ return null;
96
+ if (!last) {
97
+ this._reading = { el, time, steps: [], from: time, fromMs: nowMs };
98
+ return this._runAhead(el, time, Math.max(time, this._lastNow));
99
+ }
100
+ noteStep(last, time);
101
+ const run = last.from + ((nowMs - last.fromMs) / 1000) * this._rate;
102
+ const held = Math.min(Math.max(run, time), time + furthestAhead(last));
103
+ if (held !== run)
104
+ Object.assign(last, { from: held, fromMs: nowMs });
105
+ return this._runAhead(el, time, Math.max(held, this._lastNow));
106
+ }
107
+ _runAhead(el, time, now) {
108
+ this._ahead = { el, seconds: now - time };
109
+ return now;
110
+ }
111
+ aheadOf(el) {
112
+ return this._ahead?.el === el ? this._ahead.seconds : 0;
113
+ }
73
114
  _heldUntilAudioOutputStarts(seeking, time) {
74
115
  const pending = this._pendingAudioStart();
75
116
  if (seeking)
@@ -110,6 +151,7 @@ export class TransportClock {
110
151
  this._playStartMs = this._nowMs();
111
152
  this._audioStart = { sinceMs: this._playStartMs, at: this._baseTime, from: null };
112
153
  this._lastNow = this._baseTime;
154
+ this._reading = null;
113
155
  // Not a stall: the gap since the clock was last read (possibly a long
114
156
  // paused idle) says nothing about lost playback time, since none was
115
157
  // playing. `_applyStallCorrection` treats null as "nothing to compare
@@ -122,6 +164,7 @@ export class TransportClock {
122
164
  return false;
123
165
  this._baseTime = this.now();
124
166
  this._playStartMs = null;
167
+ this._reading = null;
125
168
  return true;
126
169
  }
127
170
  seek(timeSeconds) {
@@ -130,6 +173,7 @@ export class TransportClock {
130
173
  : Math.max(0, timeSeconds);
131
174
  this._baseTime = clamped;
132
175
  this._lastNow = clamped;
176
+ this._reading = null;
133
177
  if (this._playStartMs !== null) {
134
178
  this._playStartMs = this._nowMs();
135
179
  this._audioStart = { sinceMs: this._playStartMs, at: clamped, from: null };
@@ -143,6 +187,7 @@ export class TransportClock {
143
187
  }
144
188
  setRate(rate) {
145
189
  const safe = Number.isFinite(rate) && rate > 0 ? Math.max(0.1, Math.min(5, rate)) : 1;
190
+ this._reading = null;
146
191
  if (this._playStartMs !== null) {
147
192
  this._baseTime = this.now();
148
193
  this._playStartMs = this._nowMs();
@@ -189,6 +234,7 @@ export class TransportClock {
189
234
  this._lastReadMs = null;
190
235
  }
191
236
  this._audioSource = null;
237
+ this._reading = null;
192
238
  }
193
239
  /** The element the playhead currently follows, if the source is one. */
194
240
  // fallow-ignore-next-line unused-class-member
@@ -0,0 +1,8 @@
1
+ type TimeoutId = ReturnType<typeof setTimeout>;
2
+ type IntervalId = ReturnType<typeof setInterval>;
3
+ type Handler = (...args: never[]) => void;
4
+ export declare function wallSetTimeout(handler: Handler, timeout?: number, ...args: unknown[]): TimeoutId;
5
+ export declare function wallClearTimeout(id: TimeoutId | null | undefined): void;
6
+ export declare function wallSetInterval(handler: Handler, timeout?: number, ...args: unknown[]): IntervalId;
7
+ export declare function wallClearInterval(id: IntervalId | null | undefined): void;
8
+ export {};
@@ -0,0 +1,13 @@
1
+ const shim = () => globalThis.__HF_VIRTUAL_TIME__;
2
+ export function wallSetTimeout(handler, timeout, ...args) {
3
+ return (shim()?.originalSetTimeout ?? setTimeout)(handler, timeout, ...args);
4
+ }
5
+ export function wallClearTimeout(id) {
6
+ (shim()?.originalClearTimeout ?? clearTimeout)(id ?? undefined);
7
+ }
8
+ export function wallSetInterval(handler, timeout, ...args) {
9
+ return (shim()?.originalSetInterval ?? setInterval)(handler, timeout, ...args);
10
+ }
11
+ export function wallClearInterval(id) {
12
+ (shim()?.originalClearInterval ?? clearInterval)(id ?? undefined);
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/core",
3
- "version": "0.9.19",
3
+ "version": "0.9.21",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -437,9 +437,9 @@
437
437
  "linkedom": "^0.18.12",
438
438
  "postcss": "^8.5.8",
439
439
  "wawoff2": "^2.0.1",
440
- "@hyperframes/parsers": "0.9.19",
441
- "@hyperframes/lint": "0.9.19",
442
- "@hyperframes/studio-server": "0.9.19"
440
+ "@hyperframes/lint": "0.9.21",
441
+ "@hyperframes/parsers": "0.9.21",
442
+ "@hyperframes/studio-server": "0.9.21"
443
443
  },
444
444
  "devDependencies": {
445
445
  "@fontsource/archivo-black": "^5.2.8",