@tongue-core/ui 0.1.0-rc.2 → 0.1.0-rc.4
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 +14 -2
- package/dist/player/index.d.ts +1 -1
- package/dist/player/index.js +5 -4
- package/dist/player/player.d.ts +4 -3
- package/dist/player/player.js +6 -2
- package/dist/player/source.d.ts +22 -3
- package/dist/player/source.js +40 -12
- package/dist/react/timeline.d.ts +2 -0
- package/dist/react/timeline.js +1 -1
- package/dist/timeline/model.d.ts +2 -0
- package/dist/timeline/model.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -38,9 +38,21 @@ player.fit(canvas.clientWidth); // render a
|
|
|
38
38
|
const bitmap = await player.preview(900, 160); // a thumbnail, when the workers are free
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
The picture keeps the film's shape in whatever box the page gives the canvas: unless the page
|
|
42
|
+
sets its own `object-fit`, the player sets `contain`, so a box of another shape letterboxes the
|
|
43
|
+
frame instead of stretching it.
|
|
44
|
+
|
|
41
45
|
A build is static files: `build.json`, `film.json`, `scenes/*.js`, `assets/…` (`tongue build`
|
|
42
|
-
writes one). The player fetches the
|
|
43
|
-
|
|
46
|
+
writes one). The player fetches them from the build's folder or, given `read` instead of `build`,
|
|
47
|
+
reads them a file at a time from wherever the build is kept (a Git commit, memory, an archive):
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// Each path as the build gives it: "build.json", "scenes/title.5ebf1e9a05d5.js", "assets/fonts/inter.ttf"…
|
|
51
|
+
await player.load({ read: (path, signal) => commit.bytes(path, signal) });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Either way it reads the manifests and bundles, then the asset files frames need, nearest the
|
|
55
|
+
playhead first, each checked against its hash. **A frame is drawn only once every file it needs
|
|
44
56
|
is there**; until then the last frame stays up. Video plays from the proxy segments import makes
|
|
45
57
|
(H.264, decoded with WebCodecs in a worker), sound from the soundtrack mixed a few seconds at a time.
|
|
46
58
|
Frames, video frames and sound are byte-for-byte what Bun makes from the same build.
|
package/dist/player/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createPlayer, type Player, type PlayerOptions, type PlayerEvents, type PlayerEvent, type Readiness, type Shown, type LoadedFilm, type Pixels, type Mix } from "./player.js";
|
|
2
2
|
export { Cancelled, type FrameResult } from "./pool.js";
|
|
3
|
-
export { openSource, buildBase, SourceError, type BuildSource, type OpenedSource } from "./source.js";
|
|
3
|
+
export { openFilm, openSource, openFiles, buildBase, SourceError, type FilmSource, type BuildSource, type FileSource, type OpenedBuild, type OpenedSource } from "./source.js";
|
|
4
4
|
export { FilmNeeds, type Span, type Range } from "./needs.js";
|
|
5
5
|
export { AssetLoader, type AssetFileInfo, type AssetStatus, type AssetProgress, type FetchBytes } from "./assets.js";
|
|
6
6
|
export { FrameCache, type Cached, type Closable } from "./frames.js";
|
package/dist/player/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @tongue-core/ui/player: a film's player, without React or Effect. It opens a build from wherever it is
|
|
2
|
-
// served (build.json, film.json, the scene
|
|
3
|
-
//
|
|
4
|
-
// @tongue-core/ui/player/worker; bundle it and
|
|
2
|
+
// served, or reads it a file at a time from wherever it is kept (build.json, film.json, the scene
|
|
3
|
+
// bundles, then the asset files frames need), draws exact frames with the engine in Web Workers, and
|
|
4
|
+
// plays them with the soundtrack. The worker script is @tongue-core/ui/player/worker; bundle it and
|
|
5
|
+
// pass its URL.
|
|
5
6
|
export { createPlayer } from "./player.js";
|
|
6
7
|
export { Cancelled } from "./pool.js";
|
|
7
|
-
export { openSource, buildBase, SourceError } from "./source.js";
|
|
8
|
+
export { openFilm, openSource, openFiles, buildBase, SourceError } from "./source.js";
|
|
8
9
|
export { FilmNeeds } from "./needs.js";
|
|
9
10
|
export { AssetLoader } from "./assets.js";
|
|
10
11
|
export { FrameCache } from "./frames.js";
|
package/dist/player/player.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { BuildDoc, FilmDoc } from "@tongue-core/project/doc";
|
|
|
3
3
|
import { type AssetProgress } from "./assets.js";
|
|
4
4
|
import { type Range } from "./needs.js";
|
|
5
5
|
import { type Scales } from "./schedule.js";
|
|
6
|
-
import { type
|
|
6
|
+
import { type FilmSource } from "./source.js";
|
|
7
7
|
import { type PlayState } from "./transport.js";
|
|
8
8
|
export interface PlayerOptions {
|
|
9
9
|
/** Where frames are drawn. Its pixel size follows the film's size at the still scale. */
|
|
@@ -72,8 +72,9 @@ export interface Mix {
|
|
|
72
72
|
readonly right: Float32Array;
|
|
73
73
|
}
|
|
74
74
|
export interface Player {
|
|
75
|
-
/** Loads a build
|
|
76
|
-
|
|
75
|
+
/** Loads a build, served as files (`{ build }`) or read a file at a time (`{ read }`); again after a
|
|
76
|
+
* rebuild or for another version: the playhead and range are kept, unchanged files aren't read again. */
|
|
77
|
+
load(source: FilmSource): Promise<LoadedFilm>;
|
|
77
78
|
/** Loads the current build again. */
|
|
78
79
|
reload(): Promise<LoadedFilm>;
|
|
79
80
|
seek(frame: number): void;
|
package/dist/player/player.js
CHANGED
|
@@ -5,7 +5,7 @@ import { FrameCache } from "./frames.js";
|
|
|
5
5
|
import { FilmNeeds } from "./needs.js";
|
|
6
6
|
import { Cancelled, WorkerPool } from "./pool.js";
|
|
7
7
|
import { bufferedAhead, clampFrame, startBuffer, wantedFrames } from "./schedule.js";
|
|
8
|
-
import {
|
|
8
|
+
import { openFilm } from "./source.js";
|
|
9
9
|
import { Transport } from "./transport.js";
|
|
10
10
|
/** Which workers use a file: audio the soundtrack's, proxy segments the video worker, a video's
|
|
11
11
|
* facts both (frames lay out with them; the decoder reads its proxy from them), the rest frames. */
|
|
@@ -31,6 +31,10 @@ const SETTLE_MS = 90;
|
|
|
31
31
|
export function createPlayer(options) {
|
|
32
32
|
const canvas = options.canvas;
|
|
33
33
|
const ctx = canvas.getContext("2d");
|
|
34
|
+
// The picture keeps the film's shape in whatever box the page gives the canvas: letterboxed,
|
|
35
|
+
// never stretched. A page that sets its own object-fit keeps it.
|
|
36
|
+
if (canvas.style.objectFit === "")
|
|
37
|
+
canvas.style.objectFit = "contain";
|
|
34
38
|
if (!ctx)
|
|
35
39
|
throw new Error("The player needs a canvas with a 2D context.");
|
|
36
40
|
const pool = new WorkerPool({ workerUrl: options.workerUrl, workers: options.workers ?? defaultWorkers() });
|
|
@@ -210,7 +214,7 @@ export function createPlayer(options) {
|
|
|
210
214
|
async function load(source) {
|
|
211
215
|
const mine = ++generation;
|
|
212
216
|
lastSource = source;
|
|
213
|
-
const opened = await
|
|
217
|
+
const opened = await openFilm(source, typeof location !== "undefined" ? location.href : "http://localhost/");
|
|
214
218
|
if (mine !== generation)
|
|
215
219
|
throw new Cancelled();
|
|
216
220
|
const doc = opened.doc;
|
package/dist/player/source.d.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { type BuildDoc, type FilmDoc } from "@tongue-core/project/doc";
|
|
2
2
|
import type { AssetFileInfo, FetchBytes } from "./assets.js";
|
|
3
|
+
/** A build served as files: its folder's URL. */
|
|
3
4
|
export interface BuildSource {
|
|
4
5
|
/** The build's folder (where build.json is), absolute or relative to the page. */
|
|
5
6
|
readonly build: string | URL;
|
|
6
7
|
/** Another manifest in the build, such as one scene on its own. Default build.json. */
|
|
7
8
|
readonly manifest?: string;
|
|
8
9
|
}
|
|
10
|
+
/** A build read a file at a time, from wherever it is kept: a Git commit, memory, an archive. */
|
|
11
|
+
export interface FileSource {
|
|
12
|
+
/** One file of the build, by its path in the build as build.json and film.json give it:
|
|
13
|
+
* `build.json`, `film.json`, `scenes/title.5ebf1e9a05d5.js`, `assets/fonts/inter.ttf`.
|
|
14
|
+
* Rejects when the file can't be read. */
|
|
15
|
+
readonly read: (path: string, signal?: AbortSignal) => Promise<Uint8Array>;
|
|
16
|
+
/** Another manifest in the build, such as one scene on its own. Default build.json. */
|
|
17
|
+
readonly manifest?: string;
|
|
18
|
+
}
|
|
19
|
+
/** What a player loads: a build served as files, or one read a file at a time. */
|
|
20
|
+
export type FilmSource = BuildSource | FileSource;
|
|
9
21
|
/** A build, opened: its documents, its text files for the workers, and its asset files. */
|
|
10
|
-
export interface
|
|
11
|
-
readonly base: URL;
|
|
22
|
+
export interface OpenedBuild {
|
|
12
23
|
readonly manifest: string;
|
|
13
24
|
readonly build: BuildDoc;
|
|
14
25
|
readonly doc: FilmDoc;
|
|
@@ -19,10 +30,18 @@ export interface OpenedSource {
|
|
|
19
30
|
/** Reads one imported asset file by its path in the assets folder. */
|
|
20
31
|
readonly fetchAsset: FetchBytes;
|
|
21
32
|
}
|
|
33
|
+
/** A build served as files, opened: also the folder it is served from. */
|
|
34
|
+
export interface OpenedSource extends OpenedBuild {
|
|
35
|
+
readonly base: URL;
|
|
36
|
+
}
|
|
22
37
|
export declare class SourceError extends Error {
|
|
23
38
|
readonly name = "SourceError";
|
|
24
39
|
}
|
|
25
40
|
/** The folder URL of a build, ending in a slash so paths resolve inside it. */
|
|
26
41
|
export declare function buildBase(build: string | URL, page: string): URL;
|
|
27
|
-
/** Opens a build
|
|
42
|
+
/** Opens a build served as files: every path resolved against its folder's URL. */
|
|
28
43
|
export declare function openSource(source: BuildSource, page: string, signal?: AbortSignal): Promise<OpenedSource>;
|
|
44
|
+
/** Opens a build read a file at a time; an asset file is read at its path in the assets folder. */
|
|
45
|
+
export declare function openFiles(source: FileSource, signal?: AbortSignal): Promise<OpenedBuild>;
|
|
46
|
+
/** Opens either kind of source; a served build's folder is resolved against `page`. */
|
|
47
|
+
export declare function openFilm(source: FilmSource, page: string, signal?: AbortSignal): Promise<OpenedBuild>;
|
package/dist/player/source.js
CHANGED
|
@@ -13,27 +13,55 @@ export function buildBase(build, page) {
|
|
|
13
13
|
/** Fetches with revalidation: a server answers 304 for what hasn't changed, so a rebuild is seen at once. */
|
|
14
14
|
async function fetchOk(url, signal) {
|
|
15
15
|
const response = await fetch(url, { cache: "no-cache", signal }).catch((e) => {
|
|
16
|
-
throw new SourceError(`${url.href} couldn't be fetched: ${
|
|
16
|
+
throw new SourceError(`${url.href} couldn't be fetched: ${message(e)}`);
|
|
17
17
|
});
|
|
18
18
|
if (!response.ok)
|
|
19
19
|
throw new SourceError(`${url.href}: ${response.status} ${response.statusText}`);
|
|
20
20
|
return response;
|
|
21
21
|
}
|
|
22
|
-
const
|
|
22
|
+
const message = (e) => (e instanceof Error ? e.message : String(e));
|
|
23
|
+
const failure = (e) => new SourceError(message(e));
|
|
23
24
|
/** Opens a build: its manifest and film.json, then every scene bundle at once. */
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const manifest = source.manifest ?? "build.json";
|
|
27
|
-
const text = async (path) => (await fetchOk(new URL(path, base), signal)).text();
|
|
28
|
-
const buildText = await text(manifest);
|
|
25
|
+
async function open(manifest, reader) {
|
|
26
|
+
const buildText = await reader.text(manifest);
|
|
29
27
|
const build = await Effect.runPromise(parseBuildDoc(buildText)).catch((e) => { throw failure(e); });
|
|
30
|
-
const filmText = await text(build.film);
|
|
28
|
+
const filmText = await reader.text(build.film);
|
|
31
29
|
const doc = await Effect.runPromise(parseFilmDoc(filmText)).catch((e) => { throw failure(e); });
|
|
32
|
-
const bundles = await Promise.all(doc.scenes.map(async (scene) => [scene.bundle, await text(scene.bundle)]));
|
|
30
|
+
const bundles = await Promise.all(doc.scenes.map(async (scene) => [scene.bundle, await reader.text(scene.bundle)]));
|
|
33
31
|
const files = { [manifest]: buildText, [build.film]: filmText };
|
|
34
32
|
for (const [path, code] of bundles)
|
|
35
33
|
files[path] = code;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
return { manifest, build, doc, files, assets: build.assets.files, fetchAsset: reader.assets(build.assets.dir) };
|
|
35
|
+
}
|
|
36
|
+
/** Opens a build served as files: every path resolved against its folder's URL. */
|
|
37
|
+
export async function openSource(source, page, signal) {
|
|
38
|
+
const base = buildBase(source.build, page);
|
|
39
|
+
const opened = await open(source.manifest ?? "build.json", {
|
|
40
|
+
text: async (path) => (await fetchOk(new URL(path, base), signal)).text(),
|
|
41
|
+
assets: (dir) => {
|
|
42
|
+
const assetsBase = buildBase(dir, base.href);
|
|
43
|
+
return async (path, abort) => new Uint8Array(await (await fetchOk(new URL(path, assetsBase), abort)).arrayBuffer());
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return { ...opened, base };
|
|
47
|
+
}
|
|
48
|
+
/** A file in one of the build's folders: "assets" and "fonts/inter.ttf" → "assets/fonts/inter.ttf". */
|
|
49
|
+
function inFolder(dir, path) {
|
|
50
|
+
const folder = dir.replace(/\/+$/, "");
|
|
51
|
+
return folder === "" || folder === "." ? path : `${folder}/${path}`;
|
|
52
|
+
}
|
|
53
|
+
/** Opens a build read a file at a time; an asset file is read at its path in the assets folder. */
|
|
54
|
+
export async function openFiles(source, signal) {
|
|
55
|
+
const bytes = (path, abort) => source.read(path, abort).catch((e) => {
|
|
56
|
+
throw new SourceError(`${path} couldn't be read: ${message(e)}`);
|
|
57
|
+
});
|
|
58
|
+
const utf8 = new TextDecoder();
|
|
59
|
+
return open(source.manifest ?? "build.json", {
|
|
60
|
+
text: async (path) => utf8.decode(await bytes(path, signal)),
|
|
61
|
+
assets: (dir) => (path, abort) => bytes(inFolder(dir, path), abort),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Opens either kind of source; a served build's folder is resolved against `page`. */
|
|
65
|
+
export function openFilm(source, page, signal) {
|
|
66
|
+
return "read" in source ? openFiles(source, signal) : openSource(source, page, signal);
|
|
39
67
|
}
|
package/dist/react/timeline.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export declare const Clip: import("react").NamedExoticComponent<Styled & {
|
|
|
46
46
|
/** A transition: the stretch where two clips overlap. */
|
|
47
47
|
export declare const Transition: import("react").NamedExoticComponent<Styled & {
|
|
48
48
|
readonly transition: TimelineTransition;
|
|
49
|
+
readonly title?: string;
|
|
49
50
|
readonly children?: ReactNode;
|
|
50
51
|
}>;
|
|
51
52
|
/** The playhead. It moves without React: its frame is a CSS variable on itself, set as reports
|
|
@@ -88,6 +89,7 @@ export declare const Timeline: {
|
|
|
88
89
|
}>;
|
|
89
90
|
Transition: import("react").NamedExoticComponent<Styled & {
|
|
90
91
|
readonly transition: TimelineTransition;
|
|
92
|
+
readonly title?: string;
|
|
91
93
|
readonly children?: ReactNode;
|
|
92
94
|
}>;
|
|
93
95
|
Playhead: typeof Playhead;
|
package/dist/react/timeline.js
CHANGED
|
@@ -169,7 +169,7 @@ export const Clip = memo(function Clip(props) {
|
|
|
169
169
|
/** A transition: the stretch where two clips overlap. */
|
|
170
170
|
export const Transition = memo(function Transition(props) {
|
|
171
171
|
const t = props.transition;
|
|
172
|
-
return (_jsx("div", { "data-tl-transition": "", "data-style": t.style, className: props.className, style: { position: "absolute", top: 0, bottom: 0, left: px("--t-start"), width: px("--t-frames"), contain: "strict", "--t-start": t.start, "--t-frames": t.end - t.start, pointerEvents: "none", ...props.style }, children: props.children }));
|
|
172
|
+
return (_jsx("div", { "data-tl-transition": "", "data-style": t.style, title: props.title, className: props.className, style: { position: "absolute", top: 0, bottom: 0, left: px("--t-start"), width: px("--t-frames"), contain: "strict", "--t-start": t.start, "--t-frames": t.end - t.start, pointerEvents: "none", ...props.style }, children: props.children }));
|
|
173
173
|
});
|
|
174
174
|
/** The playhead. It moves without React: its frame is a CSS variable on itself, set as reports
|
|
175
175
|
* come in. When it moves out of view (playing on, a jump) the view follows it (`follow`, on by
|
package/dist/timeline/model.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface TimelineClip {
|
|
|
15
15
|
export interface TimelineTransition {
|
|
16
16
|
readonly row: string;
|
|
17
17
|
readonly style: TransitionDoc["style"];
|
|
18
|
+
/** Everything film.json says about it: a dissolve's noise and edge, a wipe's direction. */
|
|
19
|
+
readonly doc: TransitionDoc;
|
|
18
20
|
readonly from: string;
|
|
19
21
|
readonly to: string;
|
|
20
22
|
/** Film frames [start, end). */
|
package/dist/timeline/model.js
CHANGED
|
@@ -31,7 +31,7 @@ export function timelineModel(doc) {
|
|
|
31
31
|
const transitions = track.transitions.map((t) => {
|
|
32
32
|
edges.add(t.start);
|
|
33
33
|
edges.add(t.end);
|
|
34
|
-
return { row: track.id, style: t.style, from: t.from, to: t.to, start: t.start, end: t.end };
|
|
34
|
+
return { row: track.id, style: t.style, doc: t, from: t.from, to: t.to, start: t.start, end: t.end };
|
|
35
35
|
});
|
|
36
36
|
return { id: track.id, kind: track.kind, clips: rowClips, transitions };
|
|
37
37
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tongue-core/ui",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.4",
|
|
4
4
|
"description": "The pieces of a film's interface: the player, the timeline's state and geometry, and unstyled React primitives. Build your own studio from them.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"README.md"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@tongue-core/engine": "0.1.0-rc.
|
|
40
|
-
"@tongue-core/film": "0.1.0-rc.
|
|
41
|
-
"@tongue-core/project": "0.1.0-rc.
|
|
39
|
+
"@tongue-core/engine": "0.1.0-rc.4",
|
|
40
|
+
"@tongue-core/film": "0.1.0-rc.4",
|
|
41
|
+
"@tongue-core/project": "0.1.0-rc.4"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@effect/atom-react": "^4.0.6",
|