@glissade/cli 0.2.0 → 0.4.0
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 +22 -0
- package/dist/captions.js +71 -0
- package/dist/cli.js +52 -2
- package/dist/import.js +31 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +2 -1
- package/dist/narrate.js +20 -0
- package/dist/render.js +16 -1
- package/package.json +8 -6
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @glissade/cli
|
|
2
|
+
|
|
3
|
+
`gs` — headless rendering and the dev/capture loop. `gs render` evaluates every frame, rasterizes on Skia (no browser anywhere), writes a PNG sequence or muxes mp4/webm via FFmpeg with sample-accurate audio. v2: `gs dev --record` serves a scene with its state machines mounted and writes input-trace sidecars; `gs render --trace/--state` are the deterministic export routes for interactive scenes.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm i -D @glissade/cli
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
gs render scene.ts --out out.mp4 --fps 60
|
|
11
|
+
gs dev scene.ts --record # capture a take
|
|
12
|
+
gs render scene.ts --trace scene.button.take1.trace.json --out take.mp4
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Part of glissade
|
|
16
|
+
|
|
17
|
+
*(glide & slide)* — programmatic motion graphics for TypeScript: realtime-first in any web page, deterministic headless video export from the same code, a visual studio over the same document. No generator functions.
|
|
18
|
+
|
|
19
|
+
- [Repository & full README](https://github.com/tyevco/glissade)
|
|
20
|
+
- [Getting started](https://github.com/tyevco/glissade/blob/main/docs/getting-started.md) · [Concepts](https://github.com/tyevco/glissade/blob/main/docs/concepts.md) · [Interactivity](https://github.com/tyevco/glissade/blob/main/docs/interactivity.md)
|
|
21
|
+
|
|
22
|
+
Apache-2.0.
|
package/dist/captions.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { compileTimeline, key, timeline, track } from "@glissade/core";
|
|
5
|
+
import { toSrt, toVtt } from "@glissade/narrate";
|
|
6
|
+
//#region src/captions.ts
|
|
7
|
+
/**
|
|
8
|
+
* Render-time caption handling (--captions burn|sidecar|off). Captions are
|
|
9
|
+
* ordinary document data (a string track + a Text node), so hiding them is a
|
|
10
|
+
* DOCUMENT operation: an override track on 'captions/opacity' merged over
|
|
11
|
+
* the scene doc — never a scene-graph mutation.
|
|
12
|
+
*/
|
|
13
|
+
var captions_exports = /* @__PURE__ */ __exportAll({
|
|
14
|
+
hideCaptionsDoc: () => hideCaptionsDoc,
|
|
15
|
+
parseCaptionsMode: () => parseCaptionsMode,
|
|
16
|
+
timingPathFor: () => timingPathFor,
|
|
17
|
+
writeCaptionSidecars: () => writeCaptionSidecars
|
|
18
|
+
});
|
|
19
|
+
function parseCaptionsMode(raw) {
|
|
20
|
+
if (raw === void 0 || raw === "" || raw === "burn") return "burn";
|
|
21
|
+
if (raw === "sidecar" || raw === "off") return raw;
|
|
22
|
+
throw new Error(`--captions must be burn, sidecar, or off (got '${raw}')`);
|
|
23
|
+
}
|
|
24
|
+
/** `<module>.narration.timing.json`, when the scene has been narrated. */
|
|
25
|
+
function timingPathFor(modulePath) {
|
|
26
|
+
const candidate = modulePath.replace(/\.[jt]sx?$/, "") + ".narration.timing.json";
|
|
27
|
+
return existsSync(candidate) ? candidate : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Hide the caption node for sidecar/off renders. The override spans the whole
|
|
31
|
+
* duration: coalescing is last-insertion-wins only inside the later track's
|
|
32
|
+
* key range (§2.2), so a point key would lose to an authored opacity track.
|
|
33
|
+
*/
|
|
34
|
+
function hideCaptionsDoc(doc, captionsId = "captions") {
|
|
35
|
+
const duration = compileTimeline(doc).duration;
|
|
36
|
+
const hide = track(`${captionsId}/opacity`, "number", [key(0, 0, { interp: "hold" }), key(Math.max(duration, 1e-6), 0)]);
|
|
37
|
+
return timeline({
|
|
38
|
+
duration,
|
|
39
|
+
...doc.fps !== void 0 ? { fps: doc.fps } : {},
|
|
40
|
+
...doc.assets !== void 0 ? { assets: doc.assets } : {},
|
|
41
|
+
children: [{
|
|
42
|
+
timeline: doc,
|
|
43
|
+
at: 0,
|
|
44
|
+
mode: "add"
|
|
45
|
+
}, {
|
|
46
|
+
timeline: {
|
|
47
|
+
version: 1,
|
|
48
|
+
tracks: [hide]
|
|
49
|
+
},
|
|
50
|
+
at: 0,
|
|
51
|
+
mode: "add"
|
|
52
|
+
}]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Write .srt + .vtt next to the render output; returns the paths. */
|
|
56
|
+
function writeCaptionSidecars(timingPath, outPath) {
|
|
57
|
+
const timing = JSON.parse(readFileSync(timingPath, "utf8"));
|
|
58
|
+
const isFile = /\.(mp4|webm)$/i.test(outPath);
|
|
59
|
+
const dir = isFile ? dirname(outPath) : outPath;
|
|
60
|
+
const stem = isFile ? basename(outPath).replace(/\.(mp4|webm)$/i, "") : "captions";
|
|
61
|
+
const srt = join(dir, `${stem}.srt`);
|
|
62
|
+
const vtt = join(dir, `${stem}.vtt`);
|
|
63
|
+
writeFileSync(srt, toSrt(timing));
|
|
64
|
+
writeFileSync(vtt, toVtt(timing));
|
|
65
|
+
return {
|
|
66
|
+
srt,
|
|
67
|
+
vtt
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { parseCaptionsMode as n, captions_exports as t };
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { i as render } from "./render.js";
|
|
3
|
+
import { n as parseCaptionsMode } from "./captions.js";
|
|
3
4
|
//#region src/cli.ts
|
|
4
5
|
/**
|
|
5
6
|
* gs — the glissade CLI (DESIGN.md §5.7).
|
|
@@ -9,6 +10,13 @@ function fail(msg) {
|
|
|
9
10
|
console.error(`gs: ${msg}`);
|
|
10
11
|
process.exit(1);
|
|
11
12
|
}
|
|
13
|
+
function parseCaptionsModeOrFail(raw) {
|
|
14
|
+
try {
|
|
15
|
+
return parseCaptionsMode(raw);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
12
20
|
function parseArgs(argv) {
|
|
13
21
|
const positional = [];
|
|
14
22
|
const flags = /* @__PURE__ */ new Map();
|
|
@@ -34,6 +42,8 @@ function parseArgs(argv) {
|
|
|
34
42
|
const USAGE = `usage:
|
|
35
43
|
gs render <scene-module> [options]
|
|
36
44
|
gs dev <scene-module> [--record] [--port <n>]
|
|
45
|
+
gs import <lottie.json> [--out <dir>] [--allow-degraded]
|
|
46
|
+
gs narrate <scene-module|script.narration.json> [--provider <id>] [--force]
|
|
37
47
|
|
|
38
48
|
render options:
|
|
39
49
|
--out <path> output directory for a PNG sequence, or .mp4/.webm (needs ffmpeg). default: ./out
|
|
@@ -42,20 +52,59 @@ render options:
|
|
|
42
52
|
--trace <file> replay an InputTrace and bake it (machine scenes, §A.6)
|
|
43
53
|
--state <name> render one machine state's timeline linearly
|
|
44
54
|
--force downgrade a trace hash mismatch to a warning
|
|
55
|
+
--captions <m> burn (default) | sidecar | off; burn/sidecar also write .srt/.vtt
|
|
45
56
|
|
|
46
57
|
dev options:
|
|
47
58
|
--record add a Record button; writes .trace.json sidecars next to the module
|
|
48
59
|
--port <n> listen port (default: any free port)
|
|
60
|
+
|
|
61
|
+
import options:
|
|
62
|
+
--out <dir> output directory for the generated scene module (default: .)
|
|
63
|
+
--allow-degraded downgrade degradable rejections (expressions, merge-paths modes != 1) to warnings
|
|
64
|
+
|
|
65
|
+
narrate options (the explicit TTS prepare step; render itself stays offline):
|
|
66
|
+
--provider <id> fake | espeak | openai (default: the script's provider, else espeak)
|
|
67
|
+
--force ignore the cache and re-synthesize every segment
|
|
49
68
|
`;
|
|
50
69
|
async function main() {
|
|
51
70
|
const [command, ...rest] = process.argv.slice(2);
|
|
52
|
-
if (command !== "render" && command !== "dev") {
|
|
71
|
+
if (command !== "render" && command !== "dev" && command !== "import" && command !== "narrate") {
|
|
53
72
|
console.error(USAGE);
|
|
54
73
|
process.exit(command === void 0 || command === "help" || command === "--help" ? 0 : 1);
|
|
55
74
|
}
|
|
56
75
|
const { positional, flags } = parseArgs(rest);
|
|
57
76
|
const modulePath = positional[0];
|
|
58
|
-
if (!modulePath) fail(`missing <scene-module
|
|
77
|
+
if (!modulePath) fail(`missing ${command === "import" ? "<lottie.json>" : "<scene-module>"}\n${USAGE}`);
|
|
78
|
+
if (command === "narrate") {
|
|
79
|
+
const { narrateCommand } = await import("./narrate.js");
|
|
80
|
+
try {
|
|
81
|
+
const result = await narrateCommand({
|
|
82
|
+
input: modulePath,
|
|
83
|
+
...flags.has("provider") ? { provider: flags.get("provider") } : {},
|
|
84
|
+
...flags.has("force") ? { force: true } : {}
|
|
85
|
+
});
|
|
86
|
+
const parts = [result.synthesized.length > 0 ? `synthesized ${result.synthesized.join(", ")}` : null, result.reused.length > 0 ? `reused ${result.reused.length} cached` : null].filter(Boolean);
|
|
87
|
+
process.stderr.write(`gs narrate: ${parts.join("; ") || "nothing to do"} → ${result.timingPath}\n`);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (command === "import") {
|
|
94
|
+
const { importCommand } = await import("./import.js").then((n) => n.n);
|
|
95
|
+
try {
|
|
96
|
+
const result = await importCommand({
|
|
97
|
+
input: modulePath,
|
|
98
|
+
out: flags.get("out") ?? ".",
|
|
99
|
+
allowDegraded: flags.has("allow-degraded")
|
|
100
|
+
});
|
|
101
|
+
for (const w of result.warnings) process.stderr.write(`gs import: warning: ${w}\n`);
|
|
102
|
+
process.stderr.write(`gs import: wrote ${result.out}\n`);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
59
108
|
if (command === "dev") {
|
|
60
109
|
const { dev } = await import("./dev.js").then((n) => n.n);
|
|
61
110
|
const portFlag = flags.get("port");
|
|
@@ -85,6 +134,7 @@ async function main() {
|
|
|
85
134
|
...flags.has("trace") ? { trace: flags.get("trace") } : {},
|
|
86
135
|
...flags.has("state") ? { state: flags.get("state") } : {},
|
|
87
136
|
...flags.has("force") ? { force: true } : {},
|
|
137
|
+
captions: parseCaptionsModeOrFail(flags.get("captions")),
|
|
88
138
|
onProgress: (n, total) => {
|
|
89
139
|
if (n % 30 === 0 || n === total) process.stderr.write(`\rrendering ${n}/${total} frames`);
|
|
90
140
|
}
|
package/dist/import.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { generateSceneModule, importLottie } from "@glissade/lottie";
|
|
5
|
+
//#region src/import.ts
|
|
6
|
+
/**
|
|
7
|
+
* gs import (lottie-import.md §3.1): Lottie/bodymovin .json → a generated
|
|
8
|
+
* TypeScript scene module (nodes + inline Timeline) consumable by gs render.
|
|
9
|
+
*/
|
|
10
|
+
var import_exports = /* @__PURE__ */ __exportAll({ importCommand: () => importCommand });
|
|
11
|
+
async function importCommand(opts) {
|
|
12
|
+
const inputAbs = resolve(opts.input);
|
|
13
|
+
let json;
|
|
14
|
+
try {
|
|
15
|
+
json = JSON.parse(readFileSync(inputAbs, "utf8"));
|
|
16
|
+
} catch (err) {
|
|
17
|
+
throw new Error(`${opts.input}: ${err instanceof Error ? err.message : String(err)}`);
|
|
18
|
+
}
|
|
19
|
+
const result = importLottie(json, { allowDegraded: opts.allowDegraded === true });
|
|
20
|
+
const code = generateSceneModule(result, { source: basename(inputAbs) });
|
|
21
|
+
const outDir = resolve(opts.out);
|
|
22
|
+
mkdirSync(outDir, { recursive: true });
|
|
23
|
+
const outFile = join(outDir, `${basename(inputAbs).replace(/\.json$/i, "")}.ts`);
|
|
24
|
+
writeFileSync(outFile, code);
|
|
25
|
+
return {
|
|
26
|
+
out: outFile,
|
|
27
|
+
warnings: result.warnings
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { import_exports as n, importCommand as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Scene, SceneModule, VideoFrameSource } from "@glissade/scene";
|
|
2
|
-
import { Image } from "@napi-rs/canvas";
|
|
3
2
|
import { AudioClip, Key, Timeline } from "@glissade/core";
|
|
3
|
+
import { Image } from "@napi-rs/canvas";
|
|
4
4
|
|
|
5
5
|
//#region src/render.d.ts
|
|
6
6
|
|
|
@@ -16,6 +16,8 @@ interface RenderOptions {
|
|
|
16
16
|
state?: string;
|
|
17
17
|
/** Downgrade a trace hash mismatch to a warning. */
|
|
18
18
|
force?: boolean;
|
|
19
|
+
/** burn (default): captions render in-frame; sidecar/off hide the caption node. */
|
|
20
|
+
captions?: 'burn' | 'sidecar' | 'off';
|
|
19
21
|
onProgress?: (frame: number, total: number) => void;
|
|
20
22
|
}
|
|
21
23
|
declare class SceneModuleError extends Error {
|
|
@@ -139,4 +141,21 @@ interface DevServer {
|
|
|
139
141
|
}
|
|
140
142
|
declare function dev(opts: DevOptions): Promise<DevServer>;
|
|
141
143
|
//#endregion
|
|
142
|
-
|
|
144
|
+
//#region src/import.d.ts
|
|
145
|
+
/**
|
|
146
|
+
* gs import (lottie-import.md §3.1): Lottie/bodymovin .json → a generated
|
|
147
|
+
* TypeScript scene module (nodes + inline Timeline) consumable by gs render.
|
|
148
|
+
*/
|
|
149
|
+
interface ImportOptions {
|
|
150
|
+
input: string;
|
|
151
|
+
/** Output directory; the module is written as <basename>.ts inside it. */
|
|
152
|
+
out: string;
|
|
153
|
+
allowDegraded?: boolean;
|
|
154
|
+
}
|
|
155
|
+
interface ImportCommandResult {
|
|
156
|
+
out: string;
|
|
157
|
+
warnings: string[];
|
|
158
|
+
}
|
|
159
|
+
declare function importCommand(opts: ImportOptions): Promise<ImportCommandResult>;
|
|
160
|
+
//#endregion
|
|
161
|
+
export { AudioMixError, type AudioMixPlan, type DevOptions, type DevServer, type EncoderChoice, FfmpegVideoFrameSource, type ImportCommandResult, type ImportOptions, MachineExportError, type MachineRenderFlags, NoEncoderError, type RenderOptions, SceneModuleError, type VideoInfo, VideoProbeError, atempoChain, availableEncoders, dev, ffmpegAvailable, gainExpression, importCommand, loadSceneModule, parseEncoderList, pickEncoder, planAudioMix, probeVideo, render, resolveAssetPath, resolveRenderDoc };
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,5 @@ import { a as planAudioMix, i as gainExpression, n as atempoChain, o as resolveA
|
|
|
4
4
|
import { a as pickEncoder, i as parseEncoderList, n as availableEncoders, t as NoEncoderError } from "./encoders.js";
|
|
5
5
|
import { r as resolveRenderDoc, t as MachineExportError } from "./machines.js";
|
|
6
6
|
import { t as dev } from "./dev.js";
|
|
7
|
-
|
|
7
|
+
import { t as importCommand } from "./import.js";
|
|
8
|
+
export { AudioMixError, FfmpegVideoFrameSource, MachineExportError, NoEncoderError, SceneModuleError, VideoProbeError, atempoChain, availableEncoders, dev, ffmpegAvailable, gainExpression, importCommand, loadSceneModule, parseEncoderList, pickEncoder, planAudioMix, probeVideo, render, resolveAssetPath, resolveRenderDoc };
|
package/dist/narrate.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { scriptPathFor, synthesizeScript } from "@glissade/narrate/providers";
|
|
2
|
+
//#region src/narrate.ts
|
|
3
|
+
/**
|
|
4
|
+
* gs narrate — the explicit TTS prepare step. Provider calls happen here and
|
|
5
|
+
* only here; gs render consumes the committed timing manifest + cached wavs,
|
|
6
|
+
* fully offline.
|
|
7
|
+
*/
|
|
8
|
+
async function narrateCommand(opts) {
|
|
9
|
+
const result = await synthesizeScript(scriptPathFor(opts.input), {
|
|
10
|
+
...opts.provider !== void 0 ? { provider: opts.provider } : {},
|
|
11
|
+
...opts.force !== void 0 ? { force: opts.force } : {}
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
timingPath: result.timingPath,
|
|
15
|
+
synthesized: result.synthesized,
|
|
16
|
+
reused: result.reused
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { narrateCommand };
|
package/dist/render.js
CHANGED
|
@@ -31,12 +31,15 @@ async function render(opts) {
|
|
|
31
31
|
const mod = await loadSceneModule(opts.modulePath);
|
|
32
32
|
const scene = mod.createScene();
|
|
33
33
|
const { resolveRenderDoc } = await import("./machines.js").then((n) => n.n);
|
|
34
|
-
|
|
34
|
+
let doc = resolveRenderDoc(mod, scene, {
|
|
35
35
|
...opts.trace !== void 0 ? { trace: opts.trace } : {},
|
|
36
36
|
...opts.state !== void 0 ? { state: opts.state } : {},
|
|
37
37
|
...opts.force !== void 0 ? { force: opts.force } : {}
|
|
38
38
|
});
|
|
39
39
|
const fps = opts.fps ?? doc.fps ?? 60;
|
|
40
|
+
const captionsMode = opts.captions ?? "burn";
|
|
41
|
+
const { hideCaptionsDoc, timingPathFor, writeCaptionSidecars } = await import("./captions.js").then((n) => n.t);
|
|
42
|
+
if (captionsMode !== "burn" && scene.resolveTarget("captions/opacity") !== void 0) doc = hideCaptionsDoc(doc);
|
|
40
43
|
const { compileTimeline } = await import("@glissade/core");
|
|
41
44
|
const compiled = compileTimeline(doc);
|
|
42
45
|
const duration = compiled.duration;
|
|
@@ -79,8 +82,19 @@ async function render(opts) {
|
|
|
79
82
|
}
|
|
80
83
|
backend.dispose();
|
|
81
84
|
for (const source of videoSources) source.close();
|
|
85
|
+
const emitSidecars = (target) => {
|
|
86
|
+
if (captionsMode === "off") return;
|
|
87
|
+
const timingPath = timingPathFor(opts.modulePath);
|
|
88
|
+
if (!timingPath) {
|
|
89
|
+
if (captionsMode === "sidecar") process.stderr.write("note: --captions sidecar: no narration timing manifest found; run gs narrate first\n");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const { srt, vtt } = writeCaptionSidecars(timingPath, target);
|
|
93
|
+
process.stderr.write(`captions: ${srt}, ${vtt}\n`);
|
|
94
|
+
};
|
|
82
95
|
if (!isVideo) {
|
|
83
96
|
if (compiled.audio.length > 0) process.stderr.write("note: PNG-sequence output ignores timeline audio; render to .mp4/.webm to mix it\n");
|
|
97
|
+
emitSidecars(framesDir);
|
|
84
98
|
return {
|
|
85
99
|
frames: total,
|
|
86
100
|
out: framesDir
|
|
@@ -88,6 +102,7 @@ async function render(opts) {
|
|
|
88
102
|
}
|
|
89
103
|
const outAbs = resolve(opts.out);
|
|
90
104
|
mkdirSync(dirname(outAbs), { recursive: true });
|
|
105
|
+
emitSidecars(outAbs);
|
|
91
106
|
const isWebm = /\.webm$/i.test(outAbs);
|
|
92
107
|
const container = isWebm ? "webm" : "mp4";
|
|
93
108
|
const { pickEncoder } = await import("./encoders.js").then((n) => n.r);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
"@napi-rs/canvas": "^0.1.65",
|
|
21
21
|
"esbuild": "^0.28.0",
|
|
22
22
|
"jiti": "^2.4.2",
|
|
23
|
-
"@glissade/backend-skia": "0.
|
|
24
|
-
"@glissade/core": "0.
|
|
25
|
-
"@glissade/interact": "0.
|
|
26
|
-
"@glissade/
|
|
27
|
-
"@glissade/
|
|
23
|
+
"@glissade/backend-skia": "0.4.0",
|
|
24
|
+
"@glissade/core": "0.4.0",
|
|
25
|
+
"@glissade/interact": "0.4.0",
|
|
26
|
+
"@glissade/lottie": "0.4.0",
|
|
27
|
+
"@glissade/narrate": "0.4.0",
|
|
28
|
+
"@glissade/player": "0.4.0",
|
|
29
|
+
"@glissade/scene": "0.4.0"
|
|
28
30
|
},
|
|
29
31
|
"repository": {
|
|
30
32
|
"type": "git",
|