@docyrus/animaguru 0.17.0 → 0.19.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 +63 -3
- package/dist/animaguru.js +840 -322
- package/package.json +1 -1
- package/skills/animaguru/SKILL.md +9 -1
- package/skills/animaguru/references/sharing.md +1 -1
- package/skills/animaguru-video/SKILL.md +7 -1
- package/skills/animaguru-video/references/short-form-ad.md +1 -0
- package/templates/video/index.html +1 -1
- package/templates/video/scripts/export-mp4.mjs +3 -0
- package/templates/video/shared/locales/en/player.json +1 -0
- package/templates/video/src/player.ts +43 -8
- package/templates/video/src/style.css +6 -1
package/package.json
CHANGED
|
@@ -61,7 +61,7 @@ animaguru assets generate PROJECT_ID --type speech --name "Welcome" \
|
|
|
61
61
|
--prompt "Welcome, traveler." --voice Kore --delivery "Warm and welcoming" --yes
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Images use the staff-managed route automatically. Use `--model` to select a model for video, music, speech or sound effects; discover the choices with `catalog` and the server's configured subset with `assets list`. Asset defaults are per type, independently of sprite project defaults. Image backgrounds are retained unless `--transparent` is supplied. CLI 0.17.0 adds `--references`, `--mask` and the 16:9 and 9:16 image shapes. `--references SHA,SHA` (up to four) makes an image follow this project's saved images, uploads or sprite art. To keep a recurring character consistent, generate a character sheet once and reference its `file` SHA in every later image; refer to references in the prompt as Image 1, Image 2 and so on, and say what to take from each. `--aspect-ratio` is 1:1, 3:2, 2:3, 16:9 or 9:16; use 16:9 or 9:16 for video scenes. `--mask x,y,width,height[;…]` (up to eight rectangles, in fractions of Image 1 from the top left) edits only those parts of Image 1, the first reference, and keeps the rest and Image 1's shape. Look at the image before choosing the rectangles. Videos require an image asset's `file` SHA from `assets list`, or a character's `art` SHA from `projects get`, in the same project. Validate video options against the selected model's `videoProfiles` in `catalog`; omitted video settings use five seconds and that provider's first resolution/mode. MiniMax H3 Max and H3 Max Turbo (`fal/minimax-h3-max`, `fal/minimax-h3-max-turbo`) also take `--workflow text-to-video` (with `--aspect-ratio`) and `--workflow extend-video` (with `--source VIDEO_SHA`, 2K resolution, `--extend-output`, and up to three comma-separated `--audio-references`). From images, they accept `--end-reference` with or without `--reference` and a `--soundtrack AUDIO_SHA`. All workflows take `--prompt-expansion` and `--seed`. These need CLI 0.8.0 or newer. For Gemini speech, `--prompt` is spoken verbatim and `--delivery` is a separate instruction. Music uses Lyria; the Gemini TTS models are for speech.
|
|
64
|
+
Images use the staff-managed route automatically. Use `--model` to select a model for video, music, speech or sound effects; discover the choices with `catalog` and the server's configured subset with `assets list`. Asset defaults are per type, independently of sprite project defaults. Image backgrounds are retained unless `--transparent` is supplied. CLI 0.17.0 adds `--references`, `--mask` and the 16:9 and 9:16 image shapes. `--references SHA,SHA` (up to four) makes an image follow this project's saved images, uploads or sprite art. To keep a recurring character consistent, generate a character sheet once and reference its `file` SHA in every later image; refer to references in the prompt as Image 1, Image 2 and so on, and say what to take from each. `--aspect-ratio` is 1:1, 3:2, 2:3, 16:9 or 9:16; use 16:9 or 9:16 for video scenes. `--mask x,y,width,height[;…]` (up to eight rectangles, in fractions of Image 1 from the top left) edits only those parts of Image 1, the first reference, and keeps the rest and Image 1's shape. Look at the image before choosing the rectangles. Videos require an image asset's `file` SHA from `assets list`, or a character's `art` SHA from `projects get`, in the same project. Validate video options against the selected model's `videoProfiles` in `catalog`; omitted video settings use five seconds and that provider's first resolution/mode. MiniMax H3 Max and H3 Max Turbo (`fal/minimax-h3-max`, `fal/minimax-h3-max-turbo`) also take `--workflow text-to-video` (with `--aspect-ratio`) and `--workflow extend-video` (with `--source VIDEO_SHA`, 2K resolution, `--extend-output`, and up to three comma-separated `--audio-references`). From images, they accept `--end-reference` with or without `--reference` and a `--soundtrack AUDIO_SHA`. All workflows take `--prompt-expansion` and `--seed`. These need CLI 0.8.0 or newer. CLI 0.18.0 adds H3 Max's (not Turbo's) `--workflow reference-to-video` (keep a character across clips from up to nine `--references`, three `--reference-videos` and three `--audio-references`, named Image 1, Video 1, Audio 1 in the prompt; optional `--reference`, `--end-reference`, `--middle-reference` with `--middle-seconds`), `lip-sync` (the face in `--reference` says `--soundtrack` speech of 5 s–15 min; no `--prompt`; `--transcribe`), `camera-controls` (`--camera "time,azimuth,elevation,distance;…"`, 2–12 keyframes around `--reference`; without a prompt only the camera moves), `insert-video` (a 5–13 s scene inside `--source` from `--insert-at` to `--resume-at`, guided by `--references`/`--reference-videos`), `recast` (people in `--references` replace those in `--source`; pass `--subject-consent` only after each person agreed), `relight` (`--source` relit from a `--lighting` sphere image; no prompt), `3d-to-video` (a 3D render `--source` into footage, from `--references` or `--generated-references`) and `style` (`--style vhs|retro-toon-70s|low-poly|hand-drawn|16bit-pixel`, `--damage` for VHS, 768p, 5–15 whole seconds). Lip-sync, recast, relight and 3d-to-video take their input's length; their dry runs, like reference-to-video's and insert-video's, are server estimates because the price depends on the files. H3 takes fractional `--seconds` (6.5). For Gemini speech, `--prompt` is spoken verbatim and `--delivery` is a separate instruction. Music uses Lyria; the Gemini TTS models are for speech.
|
|
65
65
|
|
|
66
66
|
For ElevenLabs speech, choose `eleven_v4` or `eleven_v4_turbo`, pass an ElevenLabs voice ID with `--voice` (George is the default), and put audio tags in `--prompt` rather than using `--delivery`. For sound effects, use `--type sfx`; the only model is `eleven_text_to_sound_v2`. SFX supports `--seconds` from 0.5 to 30 (default 5), `--loop`, and `--prompt-influence` from 0 to 1 (default 0.3). These additions require CLI 0.5.0 or newer; check `catalog` before using them with an installed CLI.
|
|
67
67
|
|
|
@@ -94,6 +94,14 @@ Run tooling from the workspace root. `npm run dev` opens a hub with separate ani
|
|
|
94
94
|
|
|
95
95
|
For hosting, run `animaguru submit animations/NAME --yes`, or `animaguru submit --animation NAME --yes` at the root. Existing sharing and spending authorization rules apply. Multiple films require an explicit selection; do not silently submit the first folder. Put playback narration in the film's `public/assets/`, configure its public-relative master path in `film.config.json`, and retain original requests/receipts in `production/`. HTML5 export rejects a configured soundtrack missing from its package. Manual playback is audible; autoplay and capture start muted.
|
|
96
96
|
|
|
97
|
+
## Frame checks
|
|
98
|
+
|
|
99
|
+
CLI 0.19.0 adds `animaguru review check [DIRECTORY] [--animation NAME] --yes`. After `npm run review`, it sends the film's review frames to AnimaGuru's frame checker (Cloudflare's Clef-omni) and returns each frame's probability of an empty frame, cut-off or overlapping text, broken letters and hard-to-read text, plus a polish score; flagged frames come first and the full answers go to `production/review/check.json`. It charges a fraction of a credit per frame (`--dry-run` for the most). `--source frames` checks up to 12 full-size frames from `npm run frames -- --full`; `--questions FILE` adds up to 16 questions of your own. Open the flagged frames first. See the `animaguru-video` skill.
|
|
100
|
+
|
|
101
|
+
## Reference videos
|
|
102
|
+
|
|
103
|
+
CLI 0.19.0 adds `animaguru reference analyze VIDEO --yes` for a video someone shares to have the same kind of video made for their own brand or story. VIDEO is a local MP4, MOV or WebM file (up to 100 MB and ten minutes) or an HTTPS link to the video file itself; YouTube, Instagram and TikTok pages cannot be fetched, so ask for the file. Elorian's Glimpse watches and listens to it and returns a remake brief: format, story structure, style, palette, typography, motion and transitions, audio with a transcript, timed scenes, the original brand's elements to replace, and what makes it work. It takes several minutes (about eight for a 6-second clip), saves the analysis to `production/references/NAME.json` in a video project (`--out FILE` elsewhere) and is charged what it costs: about 22 to 36 credits for a 6-second clip in testing, more for a longer video (`--dry-run` shows the hold, 200 credits at the default low effort). Tell the user the cost before running it. `--focus TEXT` says what matters most; `--effort low|medium|xhigh` trades cost for depth; `--no-wait` returns the ID for `animaguru reference get ID --wait`. MCP clients use `analyze_video` with a link and poll `get_video_analysis`. Remake from the brief with the user's own brand kit and story: keep the structure, pacing and techniques, never the original's brand, claims, people or footage. See the `animaguru-video` skill.
|
|
104
|
+
|
|
97
105
|
## Motion library
|
|
98
106
|
|
|
99
107
|
`animaguru motion search DESCRIPTION [--category ID] [--labels a,b] [--code]` finds ready-made PixiJS motion examples (kinetic typography, app UI and more) built on the video film kit; `animaguru motion get ID --write src/scenes` installs one into a film. See the `animaguru-video` skill. Searching is free.
|
|
@@ -4,7 +4,7 @@ Read from the `animaguru` skill only when the user asks to host, share or upload
|
|
|
4
4
|
|
|
5
5
|
## Submit a finished animation to AnimaGuru
|
|
6
6
|
|
|
7
|
-
`submit` hosts a finished animation, separate from paid asset generation and `files export`. In a scaffolded video project, `animaguru submit --yes` builds its portable HTML5 ZIP and uploads it privately. Use `--file` for an existing AnimaGuru HTML5 export or MP4; only the export is uploaded, never the source workspace. `--dry-run` inspects the file, metadata and audience locally without signing in or uploading.
|
|
7
|
+
`submit` hosts a finished animation, separate from paid asset generation and `files export`. In a scaffolded video project, `animaguru submit --yes` builds its portable HTML5 ZIP and uploads it privately. Use `--file` for an existing AnimaGuru HTML5 export or MP4; only the export is uploaded, never the source workspace. `--dry-run` inspects the file, metadata and audience locally without signing in or uploading. The video is kept in the selected workspace's library, or a synced project's workspace; pass `--workspace personal` or `--workspace TEAM_ID` when the user names the library to use.
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
animaguru submit --share "reviewer@example.com" --yes
|
|
@@ -46,6 +46,10 @@ Cover the missing essentials, grouping related questions:
|
|
|
46
46
|
|
|
47
47
|
Save the answers in `production/brief.md`. For a short vertical social ad (Reels, Shorts, TikTok, about 15–60 s), read `references/short-form-ad.md` next: it is the end-to-end command sequence, so the CLI, catalog and older films need no exploration. Develop the story, sequence and pacing from that brief and the selected category. The visual template must not impose a family audience, emotional arc, fixed scene sequence, film length, narration tone or CTA. Once the brief is established, proceed with routine creative decisions without repeated approvals.
|
|
48
48
|
|
|
49
|
+
### A reference video to remake
|
|
50
|
+
|
|
51
|
+
When the user shares a video and asks for the same kind of film for their own brand or story, analyse it first: `animaguru reference analyze FILE_OR_LINK --animation NAME --yes` (a local MP4, MOV or WebM file, or an HTTPS link to the video file; for a YouTube, Instagram or TikTok page, ask for the file). It takes several minutes and about 22 to 36 credits for a short clip, more for a longer one, so tell the user before running it; it saves `production/references/NAME.json`. Read its scenes, format, story, style, audio and `brand_elements`, and let it answer the interview questions it covers (length, format, pacing, structure, visual and motion direction), so ask only what it cannot: the user's brand (their saved brand kit), product, offer, story and claims. Pick the installed style closest to the reference's look. Rebuild its structure, pacing, layout and motion techniques with the user's own brand, copy and assets; never reuse the original's logo, footage, characters, music, voice or claims. Note in `production/brief.md` which reference the film follows.
|
|
52
|
+
|
|
49
53
|
## Production contract
|
|
50
54
|
|
|
51
55
|
- Save the interview and creative decisions in `production/brief.md`; update `animaguru.project.json` with interview completion and the returned remote project ID. Do not store credentials there.
|
|
@@ -96,7 +100,7 @@ Run `npm run build`. Review the actual browser at desktop and mobile sizes, play
|
|
|
96
100
|
|
|
97
101
|
Deliver the playable HTML film first unless the user asked for MP4 too. For an embeddable HTML5 deliverable, use the download menu or `npm run export:html`. Unzip and test the static package over HTTP(S), including its iframe example; it should play without the authoring workspace or an AnimaGuru account. `npm run build` includes the same download in the static player. Preserve embed mode and hide authoring controls there.
|
|
98
102
|
|
|
99
|
-
For MP4, run `npm run export:mp4 -- --headless`; it captures in a local headless Chrome (installed Chrome or Edge, `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`, or `npx playwright install chromium`). Without `--headless`, open the printed local capture URL in a browser and keep it open until completion. The script verifies H.264/AAC, dimensions, duration and exact frame count and writes a JSON report next to the MP4. Captions stay out of the picture; the `captions` resource track becomes `.srt` (YouTube, desktop players) and `.vtt` (HTML5) subtitle files with the MP4's name, so keep that track's clips equal to the spoken lines. For social feeds that need open captions, add `-- --burn-captions`; it saves `SLUG-LANGUAGE-captioned.mp4` with captions in the picture and no subtitle files. Set `"poster"` in `film.config.json` to the seconds of the film's most representative frame (title, product or end card, never a fade or a blank moment); the export saves it beside the MP4 as a 1280-pixel JPEG thumbnail, which `animaguru youtube upload` and video pages can use. Exporting existing local assets spends no generation credits. If browser control is unavailable, give the user the printed URL and explain the one manual step.
|
|
103
|
+
For MP4 on the user's own computer, run `npm run export:mp4 -- --headless`; it captures in a local headless Chrome (installed Chrome or Edge, `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`, or `npx playwright install chromium`). Without `--headless`, open the printed local capture URL in a browser and keep it open until completion. The script verifies H.264/AAC, dimensions, duration and exact frame count and writes a JSON report next to the MP4. Captions stay out of the picture; the `captions` resource track becomes `.srt` (YouTube, desktop players) and `.vtt` (HTML5) subtitle files with the MP4's name, so keep that track's clips equal to the spoken lines. For social feeds that need open captions, add `-- --burn-captions`; it saves `SLUG-LANGUAGE-captioned.mp4` with captions in the picture and no subtitle files. Set `"poster"` in `film.config.json` to the seconds of the film's most representative frame (title, product or end card, never a fade or a blank moment); the export saves it beside the MP4 as a 1280-pixel JPEG thumbnail, which `animaguru youtube upload` and video pages can use. Exporting existing local assets spends no generation credits. If browser control is unavailable, give the user the printed URL and explain the one manual step. The cloud Animator makes no MP4s and `export:mp4` refuses to run there: the user applies the draft and exports the MP4 in their browser.
|
|
100
104
|
|
|
101
105
|
`npm run dev` starts the preview. The capture API and file map are documented in the generated project README. Publishing a website, package or campaign is separate from creating the local video.
|
|
102
106
|
|
|
@@ -108,6 +112,8 @@ Use `src/studies/timing.ts` for analytic easing, measured stroke reveals and cue
|
|
|
108
112
|
|
|
109
113
|
`npm run review -- --headless` collects actual text bounds every 0.2 s, representative frames and repeated cold-seek comparisons into `production/review/` and prints one line per finding (without `--headless`, open the printed URL). Besides seek, frame-bound, reading-time and cue checks, it flags overlapping text, text outside `safeArea`, text below `minTextSize` (default 2.2% of the shorter side) and film text under a burned-in caption, with time ranges. Fix those before looking at frames. Optional cues with `audioTime` are checked to an output-frame tolerance. Reading-time notes are design guidance, not a substitute for watching/listening; partial type reveals and occlusion still need visual review. Do not report a check as passed when metadata or media was unavailable.
|
|
110
114
|
|
|
115
|
+
Then run `animaguru review check --animation NAME --yes`. It sends the review frames, with the film's title, format and brief, to AnimaGuru's frame checker (Cloudflare's Clef-omni) and returns each frame's probability of an empty frame, text or a logo cut off by the edge, overlapping text, broken letters and text too hard to read on a phone, plus a polish score from 0 (broken) to 3 (polished). Flagged frames are listed first and the full answers are saved to `production/review/check.json`. It charges a fraction of a credit per frame; `--dry-run` returns the most it can cost. Open the flagged frames before the others. `--source frames` checks the full-size frames of `npm run frames -- --full` instead (up to 12), and `--questions FILE` adds up to 16 questions of your own, such as whether the logo is visible on the end card. The checker estimates probabilities: it does not judge motion, timing or taste, so still watch the film before calling it finished.
|
|
116
|
+
|
|
111
117
|
For visual review, run `npm run frames -- --animation NAME --at 1.3,4.9,…` (or `--every 3`, `--count 12`; `--no-captions`, `--full` for full-size JPEGs). It saves labelled contact sheets of up to 12 frames to `production/review/frames/` and prints the layout notes for those times. Look at one sheet instead of many single frames, and re-run only the times you changed.
|
|
112
118
|
|
|
113
119
|
## Sharing a finished film
|
|
@@ -49,6 +49,7 @@ A read at about 2 words per second, sped up 1.12×, fits 85–95 words into 45 s
|
|
|
49
49
|
## 7. Review in two or three rounds
|
|
50
50
|
|
|
51
51
|
- `npm run review -- --animation NAME --headless`, then fix every overlap, safe-area, small-text and caption line it prints.
|
|
52
|
+
- `animaguru review check --animation NAME --yes` checks the review frames for an empty hook frame, cut-off or overlapping text and broken letters. Open the frames it flags first.
|
|
52
53
|
- `npm run frames -- --animation NAME --at TIMES`, with each beat's midpoint and each transition, gives one contact sheet per 12 frames. Fix what it shows, then re-run only the changed times.
|
|
53
54
|
|
|
54
55
|
## 8. Export
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<meta name="theme-color" content="#f5f3ed" />
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="./brand/logo-mark.svg" />
|
|
8
8
|
<title>AnimaGuru · Motion studio</title>
|
|
@@ -12,6 +12,9 @@ import { workspacePlugin } from "./workspace-plugin.mjs";
|
|
|
12
12
|
import { subtitleCues, toSrt, toVtt } from "./subtitles.mjs";
|
|
13
13
|
import { captureHeadless, takeHeadlessFlag } from "./headless-browser.mjs";
|
|
14
14
|
|
|
15
|
+
// The cloud Animator makes no MP4s: once the user applies the draft, they export it in their own browser.
|
|
16
|
+
if (process.env.ANIMAGURU_CLOUD_WORKSPACE) throw new Error("MP4 export is not available in the cloud Animator. After the user applies the draft, they export the MP4 in their browser from the draft page.");
|
|
17
|
+
|
|
15
18
|
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
16
19
|
const selection = projectArguments(process.argv.slice(2));
|
|
17
20
|
const args = selection.rest;
|
|
@@ -48,6 +48,11 @@ export async function mountPlayer({ config, Film, chapters, branding, exportEndp
|
|
|
48
48
|
if (id !== "download-menu") element(id).title = label;
|
|
49
49
|
}
|
|
50
50
|
const format = (time: number) => `${Math.floor(time / 60)}:${String(Math.floor(time % 60)).padStart(2, "0")}`;
|
|
51
|
+
// Safari before 16.4 has only the prefixed Fullscreen API, and an iPhone has none for a page element: the player then
|
|
52
|
+
// fills the window instead.
|
|
53
|
+
type WebkitDocument = Document & { webkitFullscreenElement?: Element | null; webkitExitFullscreen?: () => void };
|
|
54
|
+
type WebkitElement = HTMLElement & { webkitRequestFullscreen?: () => void };
|
|
55
|
+
const fullscreenElement = () => document.fullscreenElement ?? (document as WebkitDocument).webkitFullscreenElement ?? null;
|
|
51
56
|
|
|
52
57
|
async function start() {
|
|
53
58
|
await document.fonts.ready;
|
|
@@ -55,7 +60,6 @@ export async function mountPlayer({ config, Film, chapters, branding, exportEndp
|
|
|
55
60
|
await app.init({ width: config.width, height: config.height, resolution: 1, autoStart: false, antialias: true, background: "#F4F1E9", preference: "webgl", preserveDrawingBuffer: true });
|
|
56
61
|
app.canvas.setAttribute("aria-label", config.title);
|
|
57
62
|
element("canvas-host").append(app.canvas);
|
|
58
|
-
element("loading").remove();
|
|
59
63
|
// Variable brand fonts are registered with their weight range, so bold weights are real rather than synthesized.
|
|
60
64
|
if (branding) await loadFonts(Object.values(branding.fonts).filter((font) => font.file).map((font) => ({ family: font.family.split(",")[0].trim().replace(/^["']|["']$/g, ""), url: publicAsset(font.file!) })));
|
|
61
65
|
const logo = branding?.logo ? await Assets.load(publicAsset(branding.logo)) : null;
|
|
@@ -114,9 +118,31 @@ export async function mountPlayer({ config, Film, chapters, branding, exportEndp
|
|
|
114
118
|
}
|
|
115
119
|
function toggleSound() { setMuted(!muted); }
|
|
116
120
|
function setCaptions(value: boolean) { film.captions = value; void render(time); update(); }
|
|
121
|
+
const fullscreenButton = element<HTMLButtonElement>("fullscreen");
|
|
122
|
+
function showFullscreen() {
|
|
123
|
+
const on = Boolean(fullscreenElement()) || theater.classList.contains("expanded");
|
|
124
|
+
fullscreenButton.setAttribute("aria-pressed", String(on));
|
|
125
|
+
fullscreenButton.setAttribute("aria-label", on ? copy.exitFullscreen : copy.fullscreen); fullscreenButton.title = fullscreenButton.getAttribute("aria-label")!;
|
|
126
|
+
}
|
|
127
|
+
function expand(on: boolean) { theater.classList.toggle("expanded", on); document.body.classList.toggle("theater-expanded", on); showFullscreen(); }
|
|
117
128
|
async function fullscreen() {
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
if (theater.classList.contains("expanded")) { expand(false); return; }
|
|
130
|
+
const page = document as WebkitDocument; const target = theater as WebkitElement;
|
|
131
|
+
try {
|
|
132
|
+
if (fullscreenElement()) { if (document.exitFullscreen) await document.exitFullscreen(); else page.webkitExitFullscreen?.(); }
|
|
133
|
+
else if (target.requestFullscreen) await target.requestFullscreen();
|
|
134
|
+
else if (target.webkitRequestFullscreen) target.webkitRequestFullscreen();
|
|
135
|
+
else expand(true);
|
|
136
|
+
} catch { expand(true); }
|
|
137
|
+
}
|
|
138
|
+
document.addEventListener("fullscreenchange", showFullscreen);
|
|
139
|
+
document.addEventListener("webkitfullscreenchange", showFullscreen);
|
|
140
|
+
/** A seek from the bar or the keys keeps a playing film playing; the API's seek pauses, for capture. */
|
|
141
|
+
let resumeAfterScrub = false;
|
|
142
|
+
async function scrub(seconds: number, done = true) {
|
|
143
|
+
resumeAfterScrub ||= playing;
|
|
144
|
+
await seek(seconds);
|
|
145
|
+
if (done && resumeAfterScrub) { resumeAfterScrub = false; await play(); }
|
|
120
146
|
}
|
|
121
147
|
async function saveFrame() {
|
|
122
148
|
pause(); await render(time);
|
|
@@ -178,15 +204,22 @@ export async function mountPlayer({ config, Film, chapters, branding, exportEndp
|
|
|
178
204
|
playButton.addEventListener("click", () => { if (playing) pause(); else void play(); });
|
|
179
205
|
soundButton.addEventListener("click", toggleSound);
|
|
180
206
|
captionsButton.addEventListener("click", () => setCaptions(!film.captions));
|
|
181
|
-
|
|
207
|
+
fullscreenButton.addEventListener("click", () => { void fullscreen(); });
|
|
208
|
+
// The picture plays or pauses on a click and goes full screen on a double click, as a video does.
|
|
209
|
+
if (!capture && !exporting) {
|
|
210
|
+
app.canvas.addEventListener("click", () => { if (playing) pause(); else void play(); });
|
|
211
|
+
app.canvas.addEventListener("dblclick", () => { void fullscreen(); });
|
|
212
|
+
}
|
|
182
213
|
element("save").addEventListener("click", () => { closeDownloads(true); void saveFrame(); });
|
|
183
|
-
timeline.addEventListener("input", () => { void
|
|
214
|
+
timeline.addEventListener("input", () => { void scrub(Number(timeline.value), false); });
|
|
215
|
+
timeline.addEventListener("change", () => { void scrub(Number(timeline.value)); });
|
|
184
216
|
document.addEventListener("visibilitychange", () => { if (document.hidden && !exporting) pause(); });
|
|
185
217
|
document.addEventListener("keydown", (event) => {
|
|
186
218
|
if (exporting || event.target instanceof HTMLElement && event.target.closest("button,input,a,summary,textarea,select,[contenteditable]")) return;
|
|
187
219
|
if (event.code === "Space") { event.preventDefault(); if (playing) pause(); else void play(); }
|
|
188
|
-
if (event.code === "ArrowRight") { event.preventDefault(); void
|
|
189
|
-
if (event.code === "ArrowLeft") { event.preventDefault(); void
|
|
220
|
+
if (event.code === "ArrowRight") { event.preventDefault(); void scrub(time + 5); }
|
|
221
|
+
if (event.code === "ArrowLeft") { event.preventDefault(); void scrub(time - 5); }
|
|
222
|
+
if (event.key === "Escape" && theater.classList.contains("expanded")) expand(false);
|
|
190
223
|
if (event.key.toLowerCase() === "m") toggleSound();
|
|
191
224
|
if (event.key.toLowerCase() === "c") setCaptions(!film.captions);
|
|
192
225
|
if (event.key.toLowerCase() === "f") void fullscreen();
|
|
@@ -203,7 +236,9 @@ export async function mountPlayer({ config, Film, chapters, branding, exportEndp
|
|
|
203
236
|
requestAnimationFrame((next) => { void tick(next); });
|
|
204
237
|
} catch (error) { pause(); status.textContent = copy.failed; console.error(error); }
|
|
205
238
|
}
|
|
206
|
-
await render(time); update();
|
|
239
|
+
await render(time); update(); showFullscreen();
|
|
240
|
+
// The first frame is drawn: the loading line gives way to it.
|
|
241
|
+
element("loading")?.remove();
|
|
207
242
|
const api = {
|
|
208
243
|
ready: true, width: config.width, height: config.height, duration: config.duration, fps: config.fps, language: config.language, languages: [{ code: config.language, name: config.language }], canvas: app.canvas,
|
|
209
244
|
get state() { return { time, playing, muted, captions: film.captions, audio: { configured: Boolean(audio), readyState: audio?.readyState ?? 0, error: audio?.error?.code ?? null } }; },
|
|
@@ -87,12 +87,17 @@ footer { max-width: 1380px; margin: auto; padding: 25px 48px; display: flex; jus
|
|
|
87
87
|
footer span:last-child { font-size: 8px; letter-spacing: 1.5px; }
|
|
88
88
|
#theater:fullscreen { border: none; border-radius: 0; display: flex; flex-direction: column; justify-content: center; background: #15221c; }
|
|
89
89
|
#theater:fullscreen #canvas-host { min-height: 0; flex: 1; aspect-ratio: auto; }
|
|
90
|
+
/* Full screen where the browser has none for the player (an iPhone): the player fills the window. */
|
|
91
|
+
#theater.expanded { position: fixed; inset: 0; z-index: 100; height: 100vh; height: 100dvh; display: flex; flex-direction: column; justify-content: center; border: 0; border-radius: 0; background: #15221c; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
|
|
92
|
+
#theater.expanded #canvas-host { min-height: 0; flex: 1; aspect-ratio: auto !important; }
|
|
93
|
+
.theater-expanded { overflow: hidden; }
|
|
94
|
+
body:not(.capture) #canvas-host canvas { cursor: pointer; }
|
|
90
95
|
.capture .masthead, .capture .transport, .capture .player-note, .capture #resource-timeline, .capture details, .capture footer { display: none; }
|
|
91
96
|
.capture main { padding: 0; max-width: none; }
|
|
92
97
|
.capture #theater { border: 0; border-radius: 0; box-shadow: none; }
|
|
93
98
|
.capture #canvas-host { height: 100vh; aspect-ratio: auto; }
|
|
94
99
|
.capture #status { display: none; }
|
|
95
|
-
.embed { height: 100vh; overflow: hidden; }
|
|
100
|
+
.embed { height: 100vh; height: 100dvh; overflow: hidden; }
|
|
96
101
|
.embed #app, .embed main { height: 100%; }
|
|
97
102
|
.embed main { padding: 0; max-width: none; }
|
|
98
103
|
.embed .masthead, .embed .player-note, .embed #resource-timeline, .embed details, .embed footer, .embed .download-control, .embed #status { display: none; }
|