mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
docs(examples): reduced-motion play starts both films; precise media-start layers
- replica-compare: the voluntary control now starts and pauses BOTH films (not just the reference), and the replica-sync effect attaches in view regardless of the preference, so a reduced-motion visitor who presses play sees the whole synchronized pair. Added a focused source-level assertion to the motion-check suite (the repo has no React runtime harness for snippets). - html-schema: describe each layer precisely instead of grouping the CLI — timing compiler, HTML parser, producer audio, and 'hyperframes validate' read only data-media-start; runtime, Studio and 'hyperframes snapshot' read data-playback-start first (Studio also writes it). Round-3 findings from Magi.
This commit is contained in:
@@ -122,7 +122,7 @@ Audio has no visual lifecycle.
|
||||
|
||||
| Attribute | Applies to | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `data-media-start` / `data-playback-start` | Video, audio, nested composition | Offset into the source file, used by trim and split. The two names are read by different layers with different precedence: the timing compiler and HTML parser read only `data-media-start`, while the runtime player, Studio, and the CLI read `data-playback-start` first and fall back to `data-media-start`. Studio writes `data-playback-start` for nested compositions and any element authored with it. Set **one**; authoring both is what causes the two layers to diverge. |
|
||||
| `data-media-start` / `data-playback-start` | Video, audio, nested composition | Offset into the source file, used by trim and split. The two names are read by different layers, and not all of them agree. **Read only `data-media-start`:** the timing compiler, the HTML parser, the producer's audio extraction, and `hyperframes validate`. **Read `data-playback-start` first, falling back to `data-media-start`:** the runtime player, Studio, and `hyperframes snapshot` — and Studio also writes `data-playback-start` for nested compositions and any element authored with it. Set **one** value; authoring both is what makes these two groups diverge. |
|
||||
| `data-playback-rate` | Video, audio, nested composition | Playback multiplier from `0.1` to `5` |
|
||||
| `data-volume` | Video and audio | Static volume from `0` to `1` |
|
||||
| `data-has-audio="true"` | Video | Declares that the video contributes audio |
|
||||
|
||||
@@ -65,11 +65,13 @@ export const ReplicaCompare = ({
|
||||
}
|
||||
}, [reduced, inView]);
|
||||
|
||||
// Keep the replica (right) locked to the reference (left) clock.
|
||||
// Keep the replica (right) locked to the reference (left) clock. Attached
|
||||
// whenever the pair is in view — including under reduced motion — so that once
|
||||
// the reference starts (autoplay, or a voluntary press) the replica follows.
|
||||
useEffect(() => {
|
||||
const a = refVideo.current;
|
||||
const b = repVideo.current;
|
||||
if (!a || !b || reduced || !inView) return;
|
||||
if (!a || !b || !inView) return;
|
||||
|
||||
const resync = () => {
|
||||
if (Number.isFinite(a.currentTime) && Math.abs((b.currentTime || 0) - a.currentTime) > 0.15) {
|
||||
@@ -95,13 +97,28 @@ export const ReplicaCompare = ({
|
||||
};
|
||||
}, [reduced, inView]);
|
||||
|
||||
// Start/pause both films together — the sync effect keeps the replica locked to
|
||||
// the reference clock once the reference is playing.
|
||||
const startBoth = () => {
|
||||
refVideo.current?.play().catch(() => {});
|
||||
repVideo.current?.play().catch(() => {});
|
||||
};
|
||||
const pauseBoth = () => {
|
||||
refVideo.current?.pause();
|
||||
repVideo.current?.pause();
|
||||
};
|
||||
|
||||
const toggleSound = () => {
|
||||
const a = refVideo.current;
|
||||
if (!a) return;
|
||||
const next = !muted;
|
||||
setMuted(next);
|
||||
a.muted = next; // only the reference carries audio; replica stays silent
|
||||
if (!next) a.play().catch(() => {});
|
||||
if (!next) {
|
||||
startBoth(); // voluntary play — starts the whole pair, incl. under reduced motion
|
||||
} else if (reduced) {
|
||||
pauseBoth(); // no autoplay to fall back to under reduced motion
|
||||
}
|
||||
};
|
||||
|
||||
const active = inView && !reduced;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
auditSnippets,
|
||||
@@ -8,6 +11,8 @@ import {
|
||||
splitComponents,
|
||||
} from "./check-docs-snippet-motion.mjs";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const GUARDED = `export const Grid = () => {
|
||||
const [reducedMotion, setReducedMotion] = useState(
|
||||
() =>
|
||||
@@ -121,3 +126,20 @@ test("forwarding a caller's autoPlay prop does not make a component owe the guar
|
||||
test("every autoplaying component in docs/snippets currently satisfies the guard", () => {
|
||||
assert.deepEqual(auditSnippets(), []);
|
||||
});
|
||||
|
||||
// Reduced motion disables autoplay, but the visible control must still start the
|
||||
// whole comparison — not just the reference — or a reduced-motion visitor who
|
||||
// presses it sees half the pair. Source-level, since the repo has no React
|
||||
// runtime harness for docs snippets (this whole check is source-level for that
|
||||
// reason).
|
||||
test("ReplicaCompare's control starts both films and does not gate sync on reduced motion", () => {
|
||||
const source = readFileSync(join(here, "../docs/snippets/replica-compare.jsx"), "utf8");
|
||||
// The voluntary-play path starts both the reference and the replica.
|
||||
assert.match(source, /refVideo\.current\?\.play\(\)/, "reference is started");
|
||||
assert.match(source, /repVideo\.current\?\.play\(\)/, "replica is started too");
|
||||
// The replica-sync effect attaches in view regardless of the preference, so a
|
||||
// voluntary play under reduced motion still pulls the replica along.
|
||||
const syncGate = source.match(/const b = repVideo\.current;\s*\n\s*if \(([^)]*)\) return;/);
|
||||
assert.ok(syncGate, "found the replica-sync guard");
|
||||
assert.doesNotMatch(syncGate[1], /reduced/, "sync must not early-return on reduced motion");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user