@xplainer/motion 0.0.0-stage → 0.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/LICENSE +110 -0
- package/NOTICE +68 -0
- package/README.md +78 -2
- package/dist/animations.d.ts +40 -0
- package/dist/animations.js +1 -0
- package/dist/captions.d.ts +70 -0
- package/dist/captions.js +1 -0
- package/dist/clock.d.ts +94 -0
- package/dist/clock.js +1 -0
- package/dist/components.d.ts +109 -0
- package/dist/components.js +1 -0
- package/dist/composition.d.ts +63 -0
- package/dist/composition.js +1 -0
- package/dist/context.d.ts +48 -0
- package/dist/context.js +1 -0
- package/dist/easing.d.ts +56 -0
- package/dist/easing.js +1 -0
- package/dist/failures.d.ts +25 -0
- package/dist/failures.js +1 -0
- package/dist/harness.d.ts +134 -0
- package/dist/harness.js +1 -0
- package/dist/hooks.d.ts +21 -0
- package/dist/hooks.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +1 -0
- package/dist/interpolate.d.ts +45 -0
- package/dist/interpolate.js +1 -0
- package/dist/provider.d.ts +15 -0
- package/dist/provider.js +1 -0
- package/dist/random.d.ts +24 -0
- package/dist/random.js +1 -0
- package/dist/runtime.d.ts +74 -0
- package/dist/runtime.js +1 -0
- package/dist/spring.d.ts +76 -0
- package/dist/spring.js +1 -0
- package/dist/static-fetch.d.ts +21 -0
- package/dist/static-fetch.js +1 -0
- package/dist/static-file.d.ts +21 -0
- package/dist/static-file.js +1 -0
- package/package.json +56 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Copyright (c) 2026 BrewMyTech.
|
|
2
|
+
|
|
3
|
+
This repository is NOT under a single licence. Part of it is open source and
|
|
4
|
+
part of it is proprietary, and the line between them is drawn per directory
|
|
5
|
+
below. Read the whole of this file before copying anything out of here.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
PART ONE — OPEN SOURCE, APACHE LICENCE 2.0
|
|
9
|
+
|
|
10
|
+
The eight packages published to npm — seven under the `@xplainer/` scope, plus
|
|
11
|
+
the unscoped `xplainer`, which is an alias for `@xplainer/cli` — are licensed
|
|
12
|
+
under the Apache Licence, Version 2.0:
|
|
13
|
+
|
|
14
|
+
apps/cli @xplainer/cli
|
|
15
|
+
packages/alias xplainer
|
|
16
|
+
packages/mcp-server @xplainer/mcp-server
|
|
17
|
+
packages/motion @xplainer/motion
|
|
18
|
+
packages/protocol @xplainer/protocol
|
|
19
|
+
packages/render-core @xplainer/render-core
|
|
20
|
+
packages/skill @xplainer/skill
|
|
21
|
+
packages/tts-client @xplainer/tts-client
|
|
22
|
+
|
|
23
|
+
Everything inside those eight directories is Apache-2.0, including the workspace
|
|
24
|
+
template and the scaffold templates under `packages/render-core`.
|
|
25
|
+
The full licence text is in LICENSE-APACHE-2.0 at the root of this repository,
|
|
26
|
+
and a copy of it is committed as `LICENSE` inside each of the eight package
|
|
27
|
+
directories so that it also travels inside every published tarball. The
|
|
28
|
+
attribution notice Apache-2.0 section 4(d) propagates is the NOTICE file, at
|
|
29
|
+
the root and inside each of the eight.
|
|
30
|
+
|
|
31
|
+
Each of those eight manifests declares `"license": "Apache-2.0"`. That SPDX
|
|
32
|
+
identifier is the authoritative statement for machine consumers.
|
|
33
|
+
|
|
34
|
+
Licensed under the Apache Licence, Version 2.0 (the "Licence"); you may
|
|
35
|
+
not use these files except in compliance with the Licence. You may obtain
|
|
36
|
+
a copy of the Licence at
|
|
37
|
+
|
|
38
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
39
|
+
|
|
40
|
+
Unless required by applicable law or agreed to in writing, software
|
|
41
|
+
distributed under the Licence is distributed on an "AS IS" BASIS, WITHOUT
|
|
42
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
43
|
+
Licence for the specific language governing permissions and limitations
|
|
44
|
+
under the Licence.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
PART TWO — PROPRIETARY, ALL RIGHTS RESERVED
|
|
48
|
+
|
|
49
|
+
Every file that neither Part One nor Part Three names is proprietary. No
|
|
50
|
+
permission is granted to use, copy, modify, merge, publish, distribute,
|
|
51
|
+
sublicense, or sell any part of it. This covers, without being limited to:
|
|
52
|
+
|
|
53
|
+
apps/desktop the desktop client (tier: open-later)
|
|
54
|
+
packages/config shared build presets and tooling (tier: open-later)
|
|
55
|
+
services/tts-sidecar the packaged TTS sidecar (tier: open-later)
|
|
56
|
+
|
|
57
|
+
infra/ (but not infra/ffmpeg/, which is Part Three), scripts/, docs/,
|
|
58
|
+
.github/ (but not .github/workflows/ffmpeg-macos.yml, also Part Three),
|
|
59
|
+
and every other file at the root
|
|
60
|
+
|
|
61
|
+
No `hosted` member remains in this repository. `apps/api`, `apps/web` and
|
|
62
|
+
`services/media-service` were relocated to a private repository on 2026-09-06
|
|
63
|
+
(ADR 0023) and this file stopped listing them when they went; that tier stays
|
|
64
|
+
proprietary wherever it lives.
|
|
65
|
+
|
|
66
|
+
The three `open-later` members listed above are on a path to being opened but
|
|
67
|
+
have NOT been relicensed: opening the published packages did not open them,
|
|
68
|
+
and each still declares `"license": "UNLICENSED"` in its own manifest. Nothing
|
|
69
|
+
in Part One extends to them. Making this repository public did not change that
|
|
70
|
+
either — it made them readable, not usable.
|
|
71
|
+
|
|
72
|
+
The tier each package belongs to is declared in its own package.json as
|
|
73
|
+
`"xplainer": { "tier": "hosted" | "open-later" }`, and the boundary between
|
|
74
|
+
the tiers is machine-checked. The tier field describes intent; this file
|
|
75
|
+
describes the grant. Where they appear to disagree, this file and the
|
|
76
|
+
`license` field of the package's own manifest govern.
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
PART THREE — THE macOS FFMPEG BUILD SCRIPTS, MIT LICENCE
|
|
80
|
+
|
|
81
|
+
The scripts that build the macOS FFmpeg which `xplainer setup` downloads, and
|
|
82
|
+
the workflow that runs them, are licensed under the MIT Licence:
|
|
83
|
+
|
|
84
|
+
infra/ffmpeg/ except infra/ffmpeg/keys/
|
|
85
|
+
.github/workflows/ffmpeg-macos.yml
|
|
86
|
+
|
|
87
|
+
They are published beside every build as part of its complete corresponding
|
|
88
|
+
source ("the scripts used to control compilation and installation of the
|
|
89
|
+
executable", GPLv2 section 3), so that whoever receives the binary can rebuild
|
|
90
|
+
it. The full licence text is in infra/ffmpeg/LICENSE. Each of those files
|
|
91
|
+
carries `SPDX-License-Identifier: MIT`.
|
|
92
|
+
|
|
93
|
+
infra/ffmpeg/keys/ holds the OpenPGP public keys of FFmpeg's release signer and
|
|
94
|
+
of zlib's maintainer, unmodified, so that the build can check their signatures.
|
|
95
|
+
They are their owners'; nothing in this file licenses them.
|
|
96
|
+
|
|
97
|
+
The FFmpeg binary those scripts produce is not covered by any part of this
|
|
98
|
+
file. It is FFmpeg, x264 and zlib under the GNU General Public License version
|
|
99
|
+
2 or later, and every download carries their licences in its LICENSE file.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
THIRD PARTY COMPONENTS
|
|
103
|
+
|
|
104
|
+
This software depends on components that others own, and `xplainer setup`
|
|
105
|
+
downloads more of them onto the machine it runs on, each under its own licence.
|
|
106
|
+
The Apache-2.0 grant in Part One covers this software only and grants you
|
|
107
|
+
nothing in respect of any of them. The NOTICE file names them and their terms.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
Questions: rishav@brewmytech.com
|
package/NOTICE
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
xplainer.video
|
|
2
|
+
Copyright 2026 BrewMyTech
|
|
3
|
+
|
|
4
|
+
This product includes software developed by BrewMyTech (https://xplainer.video).
|
|
5
|
+
|
|
6
|
+
The packages published to npm — those under the `@xplainer/` scope and the
|
|
7
|
+
unscoped `xplainer`, which is an alias for `@xplainer/cli` — are licensed under
|
|
8
|
+
the Apache Licence, Version 2.0. See the LICENSE file distributed with each
|
|
9
|
+
package, or LICENSE-APACHE-2.0 in the source repository.
|
|
10
|
+
|
|
11
|
+
--------------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
THIRD PARTY COMPONENTS
|
|
14
|
+
|
|
15
|
+
This software depends on components owned by others, each under its own
|
|
16
|
+
licence. Those licences govern those components.
|
|
17
|
+
|
|
18
|
+
`@xplainer/render-core` redistributes the CMU Pronouncing Dictionary
|
|
19
|
+
(`cmudict.dict`), Copyright (C) 1993-2015 Carnegie Mellon University, under the
|
|
20
|
+
2-clause BSD licence reproduced verbatim beside it at
|
|
21
|
+
`packages/render-core/src/g2p/data/cmudict.LICENSE` and shipped in the package
|
|
22
|
+
at `dist/g2p/data/cmudict.LICENSE`. That licence's clause 1 requires the notice
|
|
23
|
+
and the disclaimer to travel with the source, which is why the file is copied
|
|
24
|
+
into the tarball rather than referenced.
|
|
25
|
+
|
|
26
|
+
`@xplainer/render-core` also redistributes the tokenizer of
|
|
27
|
+
`onnx-community/Kokoro-82M-v1.0-ONNX-timestamped` (`kokoro-tokenizer.json`),
|
|
28
|
+
Apache-2.0, as the authoritative list of the symbols the speech model accepts.
|
|
29
|
+
The model weights themselves are neither bundled nor redistributed here; they
|
|
30
|
+
are fetched from their upstream home, pinned by revision and digest.
|
|
31
|
+
|
|
32
|
+
`@xplainer/cli` depends on xz-decompress
|
|
33
|
+
(https://github.com/httptoolkit/xz-decompress), MIT, to unpack the `.tar.xz`
|
|
34
|
+
FFmpeg builds `xplainer setup` downloads on Linux. It is installed from the npm
|
|
35
|
+
registry as a declared dependency, not bundled. It contains, compiled to
|
|
36
|
+
WebAssembly, xzwasm (Copyright (c) Steve Sanderson, MIT), XZ Embedded by Lasse
|
|
37
|
+
Collin and Igor Pavlov (public domain) and walloc (Copyright (c) 2020 Igalia,
|
|
38
|
+
S.L., MIT). Its package carries no licence file; those are the terms its
|
|
39
|
+
`package.json` and the header of its own code state.
|
|
40
|
+
|
|
41
|
+
`xplainer setup` fetches two more components onto the machine it runs on, and
|
|
42
|
+
neither is bundled in or redistributed by any package here. The speech model
|
|
43
|
+
weights and voice pack of `onnx-community/Kokoro-82M-v1.0-ONNX-timestamped` are
|
|
44
|
+
Apache-2.0 and come from that repository, pinned by revision and digest. ONNX
|
|
45
|
+
Runtime (`onnxruntime-node` and `onnxruntime-common`, Copyright (c) Microsoft
|
|
46
|
+
Corporation) is MIT and comes from the npm registry, pinned by version and
|
|
47
|
+
digest; only the host platform's build is kept. `setup` is the one command that
|
|
48
|
+
downloads, it says what it is fetching and what each artefact must hash to, and
|
|
49
|
+
nothing is installed behind your back.
|
|
50
|
+
|
|
51
|
+
`xplainer setup` also acquires FFmpeg (https://ffmpeg.org), which xplainer
|
|
52
|
+
runs as a separate program and does not link, modify or bundle. On macOS,
|
|
53
|
+
`xplainer setup` downloads an FFmpeg program built by xplainer.video from
|
|
54
|
+
unmodified FFmpeg 9.0.2, x264 b35605ace3ddf7c1a5d67a2eb553f034aef41d55 and zlib
|
|
55
|
+
1.3.2, licensed under the GNU General Public License version 2 or later. Its
|
|
56
|
+
complete corresponding source is listed at
|
|
57
|
+
https://cdn.xplainer.video/toolchain/v1/ffmpeg/9.0.2/source/README.txt. On
|
|
58
|
+
Linux and Windows it downloads the GPL build BtbN publishes as FFmpeg-Builds
|
|
59
|
+
(https://github.com/FFmpeg-Builds/FFmpeg-Builds), licensed under the GNU
|
|
60
|
+
General Public License version 3 or later, from that project's own release.
|
|
61
|
+
The GPL texts travel with each download, not in these packages.
|
|
62
|
+
docs/third-party-components.md in the source repository lists every
|
|
63
|
+
component `setup` acquires. FFmpeg is a trademark of Fabrice Bellard,
|
|
64
|
+
originator of the FFmpeg project.
|
|
65
|
+
|
|
66
|
+
Apache-2.0 governs this software. It does not govern the components named above
|
|
67
|
+
— the FFmpeg builds and everything else `xplainer setup` acquires among them —
|
|
68
|
+
and nothing here grants you any right to them.
|
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @xplainer/motion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**The API explainer scenes are written against, and the page-side harness the xplainer renderer
|
|
4
|
+
drives one frame at a time.** A scene is ordinary React: it reads the frame, computes its picture
|
|
5
|
+
from it, and the renderer captures that picture once per frame.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @xplainer/motion react react-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { AbsoluteFill, Easing, interpolate, spring, useCurrentFrame, useVideoConfig } from "@xplainer/motion";
|
|
13
|
+
|
|
14
|
+
export const Title: React.FC<{ text: string }> = ({ text }) => {
|
|
15
|
+
const frame = useCurrentFrame();
|
|
16
|
+
const { fps } = useVideoConfig();
|
|
17
|
+
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
|
|
18
|
+
const scale = spring({ frame, fps, config: { damping: 12 } });
|
|
19
|
+
const y = interpolate(frame, [0, 30], [40, 0], {
|
|
20
|
+
easing: Easing.out(Easing.cubic),
|
|
21
|
+
extrapolateRight: "clamp",
|
|
22
|
+
});
|
|
23
|
+
return (
|
|
24
|
+
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
|
25
|
+
<h1 style={{ opacity, transform: `translateY(${y}px) scale(${scale})` }}>{text}</h1>
|
|
26
|
+
</AbsoluteFill>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What is in it
|
|
32
|
+
|
|
33
|
+
| Kind | Exports |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Frame and video | `useCurrentFrame`, `useVideoConfig` |
|
|
36
|
+
| Motion | `interpolate`, `Easing`, `spring`, `measureSpring`, `random` |
|
|
37
|
+
| Timing and layout | `Sequence`, `Series`, `Loop`, `Freeze`, `AbsoluteFill` |
|
|
38
|
+
| Media | `Img`, `Audio` (the narration marker), `staticFile` |
|
|
39
|
+
| Render holds | `delayRender`, `continueRender`, `cancelRender` |
|
|
40
|
+
| The video itself | `registerRoot`, `Composition`, `registerVideo`, `CalculateMetadataFunction` |
|
|
41
|
+
| Captions | `paginateCaptions`, `Caption`, `TikTokPage` |
|
|
42
|
+
|
|
43
|
+
Every motion function is a pure function of the frame, so a frame rendered out of order, or a
|
|
44
|
+
single still, is the same picture the full render shows. `spring` is the closed-form solution of a
|
|
45
|
+
damped harmonic oscillator; `Easing` holds the textbook closed forms; `random(seed)` is
|
|
46
|
+
deterministic per seed.
|
|
47
|
+
|
|
48
|
+
`<Video>` and `<OffthreadVideo>` throw `NOT_SUPPORTED_IN_V1` when rendered: video assets are not
|
|
49
|
+
supported by this renderer yet, so use `<Img>` stills. A second `<Audio>` beside the narration fails
|
|
50
|
+
the same way, because the narration is the only audio track the video gets. `staticFile()` takes a
|
|
51
|
+
name inside the video's public directory and refuses one that starts with `/` or climbs out of it
|
|
52
|
+
with `..`.
|
|
53
|
+
|
|
54
|
+
## The harness
|
|
55
|
+
|
|
56
|
+
`@xplainer/motion/harness` is a separate entry point with side effects, imported only by the
|
|
57
|
+
renderer's generated bundle, before the scene code. It runs the page on film time — `Date`,
|
|
58
|
+
`performance.now`, `setTimeout`, `setInterval` and `requestAnimationFrame` all advance only when the
|
|
59
|
+
renderer asks for the next frame — and before each frame is captured it waits for render holds,
|
|
60
|
+
images and fonts, and for the page to stop changing, so an image a scene's effects mount a few
|
|
61
|
+
React updates after the frame is still in that frame; a frame that never stops changing is released
|
|
62
|
+
with a warning in the job log. `Math.random` in the page is seeded, so it repeats from run to run.
|
|
63
|
+
A scene never imports it.
|
|
64
|
+
|
|
65
|
+
The exported surface of the main entry point is recorded in `api/motion.api.md` in the repository.
|
|
66
|
+
|
|
67
|
+
## Docs
|
|
68
|
+
|
|
69
|
+
- [Architecture][architecture] — the members and the dependency direction
|
|
70
|
+
- [Decision records][adr]
|
|
71
|
+
- [Roadmap][roadmap] — what is built and what is not
|
|
72
|
+
|
|
73
|
+
## Licence
|
|
74
|
+
|
|
75
|
+
Apache-2.0. See `LICENSE` and `NOTICE`, both shipped inside this package.
|
|
76
|
+
|
|
77
|
+
[architecture]: https://github.com/BrewMyTech/xplainer.video/blob/main/docs/ARCHITECTURE.md
|
|
78
|
+
[adr]: https://github.com/BrewMyTech/xplainer.video/blob/main/docs/adr/README.md
|
|
79
|
+
[roadmap]: https://github.com/BrewMyTech/xplainer.video/blob/main/docs/ROADMAP.md
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Animations on film time.
|
|
3
|
+
*
|
|
4
|
+
* CSS animations, CSS transitions and `element.animate()` all run on the document timeline, which
|
|
5
|
+
* is wall time and is not something the harness can replace. So instead of replacing the clock,
|
|
6
|
+
* the harness pauses every animation on the document timeline after each frame commits and sets
|
|
7
|
+
* its `currentTime` itself: film time now, minus the film time at which the harness first saw that
|
|
8
|
+
* animation. An animation that starts when its element mounts at frame 40 is therefore at 0 ms on
|
|
9
|
+
* frame 40 and at 1000 ms on frame 70 at 30 fps, however long the capture of the frames between
|
|
10
|
+
* took.
|
|
11
|
+
*
|
|
12
|
+
* **The start is when the harness first saw the animation, so the order of `setFrame` calls
|
|
13
|
+
* matters.** A video rendered front to back sees every animation start at the frame its element
|
|
14
|
+
* mounted. A page asked for frame 70 alone, of an element that mounts at frame 40, has no earlier
|
|
15
|
+
* frame to have seen it on, and shows it at 0 ms — which is why the renderer steps a still's page
|
|
16
|
+
* through every frame before the one it captures, at the cost of setting each of them. A scene
|
|
17
|
+
* whose animation is `interpolate()` on `useCurrentFrame()` is a pure function of the frame and
|
|
18
|
+
* needs none of this.
|
|
19
|
+
*
|
|
20
|
+
* Scroll- and view-driven animations (`animation-timeline: scroll()` / `view()`) are not on the
|
|
21
|
+
* document timeline and are not touched: their progress is the scroll position, not time.
|
|
22
|
+
*/
|
|
23
|
+
/** The part of `Animation` the seeker touches, so Node tests can hand it plain objects. */
|
|
24
|
+
export type SeekableAnimation = {
|
|
25
|
+
pause(): void;
|
|
26
|
+
currentTime: CSSNumberish | null;
|
|
27
|
+
/** Compared by identity with the document's own timeline; nothing else is read from it. */
|
|
28
|
+
readonly timeline: unknown;
|
|
29
|
+
};
|
|
30
|
+
export declare class AnimationSeeker {
|
|
31
|
+
#private;
|
|
32
|
+
/**
|
|
33
|
+
* Pause and seek each animation on `timeline` — the document's — to film time; returns how many
|
|
34
|
+
* were seeked. Any other animation is left alone: one driven by a scroll or view timeline
|
|
35
|
+
* (`animation-timeline: scroll()`) measures its time in percentages of a scroll range, not in
|
|
36
|
+
* milliseconds, and setting a number of milliseconds on it throws. Its progress follows the
|
|
37
|
+
* scroll position, which a frame's own render already decides.
|
|
38
|
+
*/
|
|
39
|
+
seek(animations: Iterable<SeekableAnimation>, filmMs: number, timeline: unknown): number;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class AnimationSeeker{#firstSeen=new WeakMap;seek(animations,filmMs,timeline){let count=0;for(const animation of animations){if(animation.timeline!==timeline)continue;let start=this.#firstSeen.get(animation);start===void 0&&(start=filmMs,this.#firstSeen.set(animation,start)),animation.pause(),animation.currentTime=filmMs-start,count+=1}return count}}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caption paging: word-level `captions.json` into the pages the caption track shows one at a time.
|
|
3
|
+
*
|
|
4
|
+
* **The rule, which `packages/render-core` already writes `captions.json` against.** Its
|
|
5
|
+
* `buildCaptions()` gives every word but the first of the whole track a leading space and never
|
|
6
|
+
* gives one to folded punctuation (`packages/render-core/src/narrate/captions.ts`). The pager is
|
|
7
|
+
* the other half of that agreement:
|
|
8
|
+
*
|
|
9
|
+
* 1. Captions are taken in order. The first one opens the first page.
|
|
10
|
+
* 2. A caption **opens a new page only if its `text` begins with whitespace** and at least
|
|
11
|
+
* `combineWithinMs` have elapsed from the current page's start to the caption's `startMs`. The
|
|
12
|
+
* window is half-open: a caption exactly `combineWithinMs` after the page start opens the next
|
|
13
|
+
* page. A caption with no leading space never opens a page however late it is — it is the tail
|
|
14
|
+
* of the word before it, and splitting there would strand it.
|
|
15
|
+
* 3. The caption that opens a page has its leading whitespace trimmed, in the page `text` and in
|
|
16
|
+
* its token, so no page begins with a space. Every other caption is appended verbatim: page
|
|
17
|
+
* `text` is the plain concatenation of its tokens' `text`.
|
|
18
|
+
* 4. A page lasts until the next page starts; the last page lasts until the latest `endMs` among
|
|
19
|
+
* its own tokens. So consecutive pages tile the time between them with no gap for the caption
|
|
20
|
+
* band to flicker through, and the track ends when the speech does.
|
|
21
|
+
*
|
|
22
|
+
* Grouping is by elapsed time only; nothing here knows where a sentence or a narration segment
|
|
23
|
+
* ends. The scaffold's `Captions.tsx` gets sentence-aware pages by calling this once per sentence,
|
|
24
|
+
* which is why this function stays this small.
|
|
25
|
+
*
|
|
26
|
+
* Pure: no clock, no frame, no order dependence, so a frame rendered out of order or a still taken
|
|
27
|
+
* alone pages exactly as the full render does.
|
|
28
|
+
*/
|
|
29
|
+
/** One word of `captions.json`, in the shape `packages/protocol/schemas/captions.json` defines. */
|
|
30
|
+
export type Caption = {
|
|
31
|
+
text: string;
|
|
32
|
+
startMs: number;
|
|
33
|
+
endMs: number;
|
|
34
|
+
timestampMs: number | null;
|
|
35
|
+
confidence: number | null;
|
|
36
|
+
};
|
|
37
|
+
/** One caption inside a page, with its own span so the spoken word can be highlighted. */
|
|
38
|
+
export type TikTokToken = {
|
|
39
|
+
text: string;
|
|
40
|
+
fromMs: number;
|
|
41
|
+
toMs: number;
|
|
42
|
+
};
|
|
43
|
+
/** One page of caption text and the tokens it was built from. */
|
|
44
|
+
export type TikTokPage = {
|
|
45
|
+
text: string;
|
|
46
|
+
startMs: number;
|
|
47
|
+
durationMs: number;
|
|
48
|
+
tokens: TikTokToken[];
|
|
49
|
+
};
|
|
50
|
+
/** The fields of a caption the pager reads. Every `Caption` is one. */
|
|
51
|
+
export type PageableCaption = Pick<Caption, "text" | "startMs" | "endMs">;
|
|
52
|
+
export type PaginateCaptionsOptions = {
|
|
53
|
+
/** A caption starting this long after its page's start (or later) may open the next page. */
|
|
54
|
+
combineWithinMs: number;
|
|
55
|
+
};
|
|
56
|
+
export declare function paginateCaptions(captions: readonly PageableCaption[], options: PaginateCaptionsOptions): TikTokPage[];
|
|
57
|
+
export type CreateTikTokStyleCaptionsInput = {
|
|
58
|
+
captions: readonly PageableCaption[];
|
|
59
|
+
combineTokensWithinMilliseconds: number;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* `paginateCaptions` under its older name and argument shape.
|
|
63
|
+
*
|
|
64
|
+
* It exists so scenes written before this package — which imported the pager from a
|
|
65
|
+
* captions-specific module under this name — keep compiling through the renderer's import alias
|
|
66
|
+
* for one release. New code should call `paginateCaptions`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createTikTokStyleCaptions(input: CreateTikTokStyleCaptionsInput): {
|
|
69
|
+
pages: TikTokPage[];
|
|
70
|
+
};
|
package/dist/captions.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const LEADING_WHITESPACE=/^\s/u;export function paginateCaptions(captions,options){const{combineWithinMs}=options;if(!(Number.isFinite(combineWithinMs)&&combineWithinMs>=0))throw new RangeError(`paginateCaptions: combineWithinMs must be zero or a positive number, got ${String(combineWithinMs)}.`);const pages=[];let page;for(const caption of captions){if(page===void 0||LEADING_WHITESPACE.test(caption.text)&&caption.startMs-page.startMs>=combineWithinMs||page===void 0){const text=caption.text.trimStart();page={text,startMs:caption.startMs,durationMs:0,tokens:[{text,fromMs:caption.startMs,toMs:caption.endMs}]},pages.push(page);continue}page.text+=caption.text,page.tokens.push({text:caption.text,fromMs:caption.startMs,toMs:caption.endMs})}for(const[index,current]of pages.entries()){const next=pages[index+1],end=next===void 0?Math.max(...current.tokens.map(token=>token.toMs)):next.startMs;current.durationMs=Math.max(0,end-current.startMs)}return pages}export function createTikTokStyleCaptions(input){return{pages:paginateCaptions(input.captions,{combineWithinMs:input.combineTokensWithinMilliseconds})}}
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Film time: the clock a captured page runs on instead of the wall clock.
|
|
3
|
+
*
|
|
4
|
+
* A browser animates against wall time — `Date.now()`, `performance.now()`, `setTimeout`,
|
|
5
|
+
* `requestAnimationFrame` — and a capture that takes 40 ms per frame would otherwise see a scene
|
|
6
|
+
* whose timers ran 40 ms per frame instead of 33. The harness replaces all four with **film
|
|
7
|
+
* time**: `frame / fps · 1000` milliseconds, which only moves when the renderer asks for a frame.
|
|
8
|
+
* These are the pieces of that replacement that need no `window`, so they are tested in Node; the
|
|
9
|
+
* harness installs them.
|
|
10
|
+
*
|
|
11
|
+
* Three rules, each of which the spike found by being wrong first:
|
|
12
|
+
*
|
|
13
|
+
* - **Due timers fire in due order, ties in creation order** — the same order a browser would have
|
|
14
|
+
* fired them in, given time.
|
|
15
|
+
* - **A timer created while the queue is draining waits for the next drain**, even with delay 0.
|
|
16
|
+
* Otherwise `setTimeout(tick, 0)` inside `tick` is an infinite loop inside one frame.
|
|
17
|
+
* - **An interval fires as many times as fit**, so a 10 ms interval fires three times across one
|
|
18
|
+
* 33 ms frame rather than once.
|
|
19
|
+
*/
|
|
20
|
+
/** 2020-01-01T00:00:00Z: what `Date.now()` reads at frame 0, so dates are stable across renders. */
|
|
21
|
+
export declare const FILM_EPOCH_MS: number;
|
|
22
|
+
/** Film milliseconds at a frame. Multiplies first, so whole frames at integer fps stay exact. */
|
|
23
|
+
export declare function frameToFilmMs(frame: number, fps: number): number;
|
|
24
|
+
/**
|
|
25
|
+
* A `Date` that reads film time when asked for "now" and is the native `Date` otherwise.
|
|
26
|
+
*
|
|
27
|
+
* A `Proxy` rather than a subclass because `Date()` called without `new` must keep working (it
|
|
28
|
+
* returns a string), and `instanceof Date` must stay true for dates made either way; both fall out
|
|
29
|
+
* of forwarding everything but the three "now" paths to the native constructor.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createFrozenDate(NativeDate: DateConstructor, filmMs: () => number): DateConstructor;
|
|
32
|
+
/** `setTimeout` / `setInterval` against a clock somebody else advances. */
|
|
33
|
+
export declare class TimerQueue {
|
|
34
|
+
#private;
|
|
35
|
+
/**
|
|
36
|
+
* Schedule `callback` `delayMs` after `nowMs`. The delay is whatever the page passed, converted
|
|
37
|
+
* with `Number()` as a browser converts it — `setTimeout(fn, "100")` waits 100 ms — and one that
|
|
38
|
+
* converts to a non-finite or negative number is 0.
|
|
39
|
+
*/
|
|
40
|
+
add(callback: () => void, delayMs: unknown, repeat: boolean, nowMs: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* Reschedule every pending timer with an id of at least `fromId` as if it had been added at
|
|
43
|
+
* `nowMs`, keeping what has happened to it since: a one-shot timer becomes due its own delay
|
|
44
|
+
* after `nowMs`, and an interval that has fired `n` times becomes due where the same interval
|
|
45
|
+
* added at `nowMs` would be after `n` firings — the same phase, on the new origin. The due time is
|
|
46
|
+
* recomputed rather than shifted, so it is the same number to the last bit whatever it was
|
|
47
|
+
* scheduled from; a shift could land a timer due exactly on a frame one ulp past it. Returns how
|
|
48
|
+
* many it moved.
|
|
49
|
+
*
|
|
50
|
+
* This is how the harness takes the wall clock back out of a render-hold wait: a timer the wait
|
|
51
|
+
* created was scheduled from film time plus however long the wait had run, and once the hold is
|
|
52
|
+
* released it is rebased onto the frame's film instant, so when it fires no longer depends on how
|
|
53
|
+
* long the page took to release the hold.
|
|
54
|
+
*/
|
|
55
|
+
rebase(fromId: number, nowMs: number): number;
|
|
56
|
+
/** Cancel a timer or an interval. Anything that is not a live id is ignored, as in a browser. */
|
|
57
|
+
clear(id: unknown): void;
|
|
58
|
+
/**
|
|
59
|
+
* The id the next timer will get. Every timer created from now on has an id at least this
|
|
60
|
+
* large, which is how a caller marks "timers created after this point" for `runDue`.
|
|
61
|
+
*/
|
|
62
|
+
get nextId(): number;
|
|
63
|
+
/**
|
|
64
|
+
* The earliest due time among timers with an id of at least `fromId` (all of them by default),
|
|
65
|
+
* or `undefined` when there is none.
|
|
66
|
+
*/
|
|
67
|
+
nextDue(fromId?: number): number | undefined;
|
|
68
|
+
get size(): number;
|
|
69
|
+
/**
|
|
70
|
+
* Fire every timer due at or before `targetMs`, earliest first, ties in creation order. Timers
|
|
71
|
+
* created while this runs are left for the next call, and timers with an id below `fromId` are
|
|
72
|
+
* left alone entirely. Returns how many fired. `invoke` is how a callback is called, so the
|
|
73
|
+
* caller decides what a throwing one means.
|
|
74
|
+
*/
|
|
75
|
+
runDue(targetMs: number, invoke: (callback: () => void) => void, fromId?: number): number;
|
|
76
|
+
}
|
|
77
|
+
/** `requestAnimationFrame` as a queue the harness drains once per captured frame. */
|
|
78
|
+
export declare class FrameCallbackQueue {
|
|
79
|
+
#private;
|
|
80
|
+
request(callback: (timestamp: number) => void): number;
|
|
81
|
+
cancel(id: unknown): void;
|
|
82
|
+
get size(): number;
|
|
83
|
+
/** The id the next request will get; the same "requested after this point" mark as `TimerQueue`'s. */
|
|
84
|
+
get nextId(): number;
|
|
85
|
+
/** How many queued callbacks have an id of at least `fromId`. */
|
|
86
|
+
pending(fromId?: number): number;
|
|
87
|
+
/**
|
|
88
|
+
* Run every queued callback with an id of at least `fromId` (all of them by default) with
|
|
89
|
+
* `timestamp`, in request order, and leave the rest queued. A callback requested while this runs
|
|
90
|
+
* belongs to the next drain, and one cancelled by an earlier callback in the same drain does not
|
|
91
|
+
* run — both exactly as a browser runs a frame's callbacks.
|
|
92
|
+
*/
|
|
93
|
+
drain(timestamp: number, invoke: (callback: () => void) => void, fromId?: number): number;
|
|
94
|
+
}
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const FILM_EPOCH_MS=Date.UTC(2020,0,1);export function frameToFilmMs(frame,fps){return frame*1e3/fps}export function createFrozenDate(NativeDate,filmMs){const now=()=>FILM_EPOCH_MS+Math.floor(filmMs());return new Proxy(NativeDate,{construct(target,args,newTarget){return Reflect.construct(target,args.length===0?[now()]:args,newTarget)},apply(){return new NativeDate(now()).toString()},get(target,property,receiver){return property==="now"?now:Reflect.get(target,property,receiver)}})}export class TimerQueue{#timers=new Map;#nextId=1;add(callback,delayMs,repeat,nowMs){const requested=Number(delayMs),delay=Number.isFinite(requested)&&requested>0?requested:0,id=this.#nextId;return this.#nextId+=1,this.#timers.set(id,{id,due:nowMs+delay,delay,ticks:0,callback,interval:repeat?Math.max(1,delay):null}),id}rebase(fromId,nowMs){let moved=0;for(const timer of this.#timers.values()){if(timer.id<fromId)continue;let due=nowMs+timer.delay;if(timer.interval!==null)for(let tick=0;tick<timer.ticks;tick+=1)due+=timer.interval;timer.due=due,moved+=1}return moved}clear(id){typeof id=="number"&&this.#timers.delete(id)}get nextId(){return this.#nextId}nextDue(fromId=1){let earliest;for(const timer of this.#timers.values())timer.id<fromId||(earliest===void 0||timer.due<earliest)&&(earliest=timer.due);return earliest}get size(){return this.#timers.size}runDue(targetMs,invoke,fromId=1){const watermark=this.#nextId;let fired=0;for(;;){let next;for(const timer of this.#timers.values())timer.id<fromId||timer.id>=watermark||timer.due>targetMs||(next===void 0||timer.due<next.due||timer.due===next.due&&timer.id<next.id)&&(next=timer);if(next===void 0)return fired;next.interval===null?this.#timers.delete(next.id):(next.due+=next.interval,next.ticks+=1),fired+=1,invoke(next.callback)}}}export class FrameCallbackQueue{#callbacks=new Map;#nextId=1;request(callback){const id=this.#nextId;return this.#nextId+=1,this.#callbacks.set(id,callback),id}cancel(id){typeof id=="number"&&this.#callbacks.delete(id)}get size(){return this.#callbacks.size}get nextId(){return this.#nextId}pending(fromId=1){let count=0;for(const id of this.#callbacks.keys())id>=fromId&&(count+=1);return count}drain(timestamp,invoke,fromId=1){const handles=[...this.#callbacks.keys()].filter(id=>id>=fromId);let ran=0;for(const id of handles){const callback=this.#callbacks.get(id);callback!==void 0&&(this.#callbacks.delete(id),ran+=1,invoke(()=>callback(timestamp)))}return ran}}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The layout and timing components a scene is built from.
|
|
3
|
+
*
|
|
4
|
+
* Timing is one rule applied at every level (see `context.ts`): a component reads the frame its
|
|
5
|
+
* parent sees, decides whether its children exist at that frame, and re-provides the frame
|
|
6
|
+
* relative to itself. Children outside their window are **not mounted**, rather than hidden, so a
|
|
7
|
+
* scene's effects and `delayRender` calls run only while it is on screen.
|
|
8
|
+
*
|
|
9
|
+
* Media is deliberately narrow in this version. `<Img>` is a plain `<img>` and the harness waits
|
|
10
|
+
* for every image in the document before a frame is captured. `<Audio>` is a marker: the renderer
|
|
11
|
+
* muxes the narration track itself, so the element only exists to keep the engine-owned
|
|
12
|
+
* `Video.tsx` declarative about it, and a second one mounted beside it throws
|
|
13
|
+
* `NOT_SUPPORTED_IN_V1`, because the narration is the only audio track the video gets. `<Video>`
|
|
14
|
+
* and `<OffthreadVideo>` throw `NOT_SUPPORTED_IN_V1` the moment they render, because a
|
|
15
|
+
* frame-accurate video decoder is not part of this renderer and a silently blank rectangle would be
|
|
16
|
+
* worse than a named refusal. A silently missing sound would be worse in the same way.
|
|
17
|
+
*/
|
|
18
|
+
import { type ComponentProps, type CSSProperties, type ReactElement, type ReactNode } from "react";
|
|
19
|
+
export type AbsoluteFillProps = ComponentProps<"div">;
|
|
20
|
+
/** A `<div>` covering its positioned parent, laid out as a flex column. `style` wins over the fill. */
|
|
21
|
+
export declare function AbsoluteFill({ style, ...rest }: AbsoluteFillProps): ReactElement;
|
|
22
|
+
/** `absolute-fill` wraps the children in an `AbsoluteFill`; `none` renders them as they are. */
|
|
23
|
+
export type SequenceLayout = "absolute-fill" | "none";
|
|
24
|
+
export type SequenceProps = {
|
|
25
|
+
/** The frame, in the parent's timeline, at which this sequence's frame 0 falls. Default 0. */
|
|
26
|
+
from?: number;
|
|
27
|
+
/** How many frames the children stay mounted. Default `Infinity`. */
|
|
28
|
+
durationInFrames?: number;
|
|
29
|
+
/** A label for people reading the tree. The renderer ignores it. */
|
|
30
|
+
name?: string;
|
|
31
|
+
layout?: SequenceLayout;
|
|
32
|
+
/** Applied to the `AbsoluteFill` wrapper; ignored with `layout="none"`. */
|
|
33
|
+
style?: CSSProperties;
|
|
34
|
+
/** Applied to the `AbsoluteFill` wrapper; ignored with `layout="none"`. */
|
|
35
|
+
className?: string;
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Mount the children for frames `from ≤ f < from + durationInFrames` of the parent's timeline,
|
|
40
|
+
* and show them their own frame, `f − from`.
|
|
41
|
+
*
|
|
42
|
+
* `useVideoConfig().durationInFrames` inside is how long the children can actually be on screen:
|
|
43
|
+
* this sequence's own duration, clipped to what is left of the parent's window from `from` on — so
|
|
44
|
+
* an open sequence at frame 100 of a 300-frame parent reports 200, and a 50-frame sequence placed
|
|
45
|
+
* 20 frames before its parent's end reports 20.
|
|
46
|
+
*/
|
|
47
|
+
export declare function Sequence({ from, durationInFrames, layout, style, className, children, }: SequenceProps): ReactElement | null;
|
|
48
|
+
export type SeriesSequenceProps = Omit<SequenceProps, "from" | "durationInFrames"> & {
|
|
49
|
+
/** Required: the series places the next sequence where this one ends. */
|
|
50
|
+
durationInFrames: number;
|
|
51
|
+
/** Shift this sequence, and every one after it, by this many frames. Default 0. */
|
|
52
|
+
offset?: number;
|
|
53
|
+
};
|
|
54
|
+
export type SeriesProps = {
|
|
55
|
+
children?: ReactNode;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The sequences placed back to back. `Series.Sequence` is the only child it accepts, directly or
|
|
59
|
+
* inside Fragments (`<>…</>`, or an array from `.map`), which are opened in place.
|
|
60
|
+
*/
|
|
61
|
+
export declare const Series: ((props: SeriesProps) => ReactElement) & {
|
|
62
|
+
Sequence: (props: SeriesSequenceProps) => never;
|
|
63
|
+
};
|
|
64
|
+
export type LoopProps = {
|
|
65
|
+
/** The length of one iteration. */
|
|
66
|
+
durationInFrames: number;
|
|
67
|
+
/** How many iterations to play before unmounting. Default `Infinity`. */
|
|
68
|
+
times?: number;
|
|
69
|
+
name?: string;
|
|
70
|
+
layout?: SequenceLayout;
|
|
71
|
+
style?: CSSProperties;
|
|
72
|
+
className?: string;
|
|
73
|
+
children?: ReactNode;
|
|
74
|
+
};
|
|
75
|
+
/** Replay the children every `durationInFrames`; they see `frame mod durationInFrames`. */
|
|
76
|
+
export declare function Loop({ durationInFrames, times, layout, style, className, children, }: LoopProps): ReactElement | null;
|
|
77
|
+
export type FreezeProps = {
|
|
78
|
+
/** The frame the children see while frozen. */
|
|
79
|
+
frame: number;
|
|
80
|
+
/** Whether the freeze applies, or a test of the parent's frame. Default `true`. */
|
|
81
|
+
active?: boolean | ((frame: number) => boolean);
|
|
82
|
+
children?: ReactNode;
|
|
83
|
+
};
|
|
84
|
+
/** Show the children a fixed frame. Adds no wrapper element. */
|
|
85
|
+
export declare function Freeze({ frame, active, children }: FreezeProps): ReactElement;
|
|
86
|
+
export type ImgProps = ComponentProps<"img">;
|
|
87
|
+
/** An `<img>`. The harness holds every frame until each image in the document has loaded. */
|
|
88
|
+
export declare function Img(props: ImgProps): ReactElement;
|
|
89
|
+
export type AudioProps = {
|
|
90
|
+
src: string;
|
|
91
|
+
volume?: number | ((frame: number) => number);
|
|
92
|
+
muted?: boolean;
|
|
93
|
+
loop?: boolean;
|
|
94
|
+
name?: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* A marker for the narration track. Renders nothing; the renderer muxes the narration itself.
|
|
98
|
+
* A second concurrently mounted `<Audio>` throws `NOT_SUPPORTED_IN_V1` when it mounts, because the
|
|
99
|
+
* narration is the only audio track the video gets and the second would be silently missing.
|
|
100
|
+
*/
|
|
101
|
+
export declare function Audio(_props: AudioProps): null;
|
|
102
|
+
export type VideoProps = {
|
|
103
|
+
src: string;
|
|
104
|
+
[prop: string]: unknown;
|
|
105
|
+
};
|
|
106
|
+
/** Refused in this version: throws `NOT_SUPPORTED_IN_V1` when rendered. */
|
|
107
|
+
export declare function Video(_props: VideoProps): never;
|
|
108
|
+
/** Refused in this version: throws `NOT_SUPPORTED_IN_V1` when rendered. */
|
|
109
|
+
export declare function OffthreadVideo(_props: VideoProps): never;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Fragment as _Fragment,jsx as _jsx}from"react/jsx-runtime";import{Children,Fragment,isValidElement,useEffect}from"react";import{TimelineContext}from"./context.js";import{useTimeline}from"./hooks.js";import{mountAudio}from"./runtime.js";const FILL={position:"absolute",top:0,left:0,right:0,bottom:0,width:"100%",height:"100%",display:"flex",flexDirection:"column"};export function AbsoluteFill({style,...rest}){return _jsx("div",{...rest,style:{...FILL,...style}})}function assertFrom(from,caller){if(typeof from!="number"||!Number.isFinite(from))throw new TypeError(`${caller}: from must be a finite number, got ${String(from)}.`)}function assertDuration(duration,caller){if(typeof duration!="number"||Number.isNaN(duration)||duration<=0)throw new RangeError(`${caller}: durationInFrames must be a positive number or Infinity, got ${String(duration)}.`)}function Layout({layout,style,className,children}){return layout==="none"?_jsx(_Fragment,{children}):_jsx(AbsoluteFill,{style,className,children})}export function Sequence({from=0,durationInFrames=Number.POSITIVE_INFINITY,layout="absolute-fill",style,className,children}){const parent=useTimeline("<Sequence>");assertFrom(from,"<Sequence>"),assertDuration(durationInFrames,"<Sequence>");const frame=parent.frame-from;if(frame<0||frame>=durationInFrames)return null;const left=parent.durationInFrames-from,timeline={frame,durationInFrames:Math.min(durationInFrames,left>0?left:parent.durationInFrames)};return _jsx(TimelineContext.Provider,{value:timeline,children:_jsx(Layout,{layout,style,className,children})})}function SeriesSequence(_props){throw new Error("<Series.Sequence> was rendered on its own. It only has a meaning as a direct child of <Series>.")}function seriesChildren(children,prefix=""){return Children.toArray(children).flatMap(child=>{const key=`${prefix}${isValidElement(child)?String(child.key):""}`;return isValidElement(child)&&child.type===Fragment?seriesChildren(child.props.children,`${key}/`):[{child,key}]})}function SeriesRoot({children}){const items=seriesChildren(children);let cursor=0;const sequences=items.map(({child,key},index)=>{if(!isValidElement(child)||child.type!==SeriesSequence)throw new TypeError("<Series> accepts only <Series.Sequence> elements as its children.");const{durationInFrames,offset=0,...rest}=child.props;if(assertFrom(offset,"<Series.Sequence> offset"),assertDuration(durationInFrames,"<Series.Sequence>"),!Number.isFinite(durationInFrames)&&index!==items.length-1)throw new RangeError("<Series.Sequence>: only the last sequence of a <Series> may have an infinite durationInFrames.");const from=cursor+offset;return cursor=from+durationInFrames,_jsx(Sequence,{...rest,from,durationInFrames},key)});return _jsx(_Fragment,{children:sequences})}export const Series=Object.assign(SeriesRoot,{Sequence:SeriesSequence});export function Loop({durationInFrames,times=Number.POSITIVE_INFINITY,layout="absolute-fill",style,className,children}){const parent=useTimeline("<Loop>");if(!(Number.isFinite(durationInFrames)&&durationInFrames>0))throw new RangeError(`<Loop>: durationInFrames must be a positive finite number, got ${String(durationInFrames)}.`);if(typeof times!="number"||Number.isNaN(times)||times<0)throw new RangeError(`<Loop>: times must be zero or a positive number, got ${String(times)}.`);if(parent.frame<0)return null;const iteration=Math.floor(parent.frame/durationInFrames);if(iteration>=times)return null;const timeline={frame:parent.frame-iteration*durationInFrames,durationInFrames};return _jsx(TimelineContext.Provider,{value:timeline,children:_jsx(Layout,{layout,style,className,children})})}export function Freeze({frame,active=!0,children}){const parent=useTimeline("<Freeze>");if(typeof frame!="number"||Number.isNaN(frame))throw new TypeError(`<Freeze>: frame must be a number, got ${String(frame)}.`);if(!(typeof active=="function"?active(parent.frame):active))return _jsx(_Fragment,{children});const timeline={frame,durationInFrames:parent.durationInFrames};return _jsx(TimelineContext.Provider,{value:timeline,children})}export function Img(props){return _jsx("img",{alt:"",...props})}export function Audio(_props){return useEffect(()=>mountAudio(),[]),null}const VIDEO_NOT_SUPPORTED="NOT_SUPPORTED_IN_V1: video assets are not supported by the xplainer renderer; use <Img> stills instead";export function Video(_props){throw new Error(VIDEO_NOT_SUPPORTED)}export function OffthreadVideo(_props){throw new Error(VIDEO_NOT_SUPPORTED)}
|