pixi-effects 0.7.0 → 0.8.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -6
  3. package/ai/reference/cheatsheet.md +5 -1
  4. package/ai/template.html +3 -3
  5. package/dist/Composition-CM62XJKT.cjs +14 -0
  6. package/dist/{Composition-3KHQPRBA.cjs.map → Composition-CM62XJKT.cjs.map} +1 -1
  7. package/dist/Composition-RJDSQYTV.js +5 -0
  8. package/dist/{Composition-DT657FO5.js.map → Composition-RJDSQYTV.js.map} +1 -1
  9. package/dist/Controller.cjs +55 -20
  10. package/dist/Controller.cjs.map +1 -1
  11. package/dist/Controller.d.cts +31 -2
  12. package/dist/Controller.d.ts +31 -2
  13. package/dist/Controller.js +55 -21
  14. package/dist/Controller.js.map +1 -1
  15. package/dist/{Movie-BK9daiTt.d.cts → Movie-CQOhUmko.d.cts} +24 -1
  16. package/dist/{Movie-B0OUKlZI.d.ts → Movie-HF3pSGjQ.d.ts} +24 -1
  17. package/dist/{chunk-6I55T4OH.cjs → chunk-AGMNSJJH.cjs} +13 -4
  18. package/dist/chunk-AGMNSJJH.cjs.map +1 -0
  19. package/dist/{chunk-4O2NYDJG.js → chunk-FNKSXBKB.js} +13 -4
  20. package/dist/chunk-FNKSXBKB.js.map +1 -0
  21. package/dist/{chunk-36Z4RIRK.js → chunk-O4AJYUPK.js} +3 -3
  22. package/dist/{chunk-36Z4RIRK.js.map → chunk-O4AJYUPK.js.map} +1 -1
  23. package/dist/{chunk-4VSIJLU3.cjs → chunk-PDBDDGG5.cjs} +21 -21
  24. package/dist/{chunk-4VSIJLU3.cjs.map → chunk-PDBDDGG5.cjs.map} +1 -1
  25. package/dist/index.cjs +82 -23
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +71 -12
  30. package/dist/index.js.map +1 -1
  31. package/dist/three.cjs +5 -5
  32. package/dist/three.js +1 -1
  33. package/docs/api.md +51 -0
  34. package/docs/dsl.md +14 -2
  35. package/llms-full.txt +70 -3
  36. package/package.json +1 -1
  37. package/dist/Composition-3KHQPRBA.cjs +0 -14
  38. package/dist/Composition-DT657FO5.js +0 -5
  39. package/dist/chunk-4O2NYDJG.js.map +0 -1
  40. package/dist/chunk-6I55T4OH.cjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ **Added**
6
+
7
+ - Movie events with the names an HTML5 `<video>` uses: `ended` (playback ran off the end, after `pause`), `seeking` / `seeked` (a `gotoFrame()` to another frame; not for playback ticks, `render()`, `snapshot()` or `contactSheet()`), `volumechange` (`{ volume, muted }`) and `error` (`{ where: 'init' | 'render' | 'playback', message, error }`; the call still rejects). A player of your own needs nothing else: `examples/15-custom-player.html` is a complete one, and `docs/api.md` has a section on it.
8
+ - `Controller` theme: the bar's colours and thickness are CSS custom properties (`--mc-accent`, `--mc-fg`, `--mc-track`, `--mc-bar-bg`, `--mc-track-height`, `--mc-font`) with the old values as defaults, set by `new Controller(movie, { canvas, theme: { accent: '#ff4d6d' } })`, by `controller.setTheme(…)` (`null` = default) or by your page CSS. `CONTROLLER_CSS` is exported.
9
+ - `{ type: 'colorGradient', gradientType: 'linear' | 'radial' | 'conic' }`: the filter's own `type` option clashes with the DSL's `type`. 40 of the named filters are now rendered and exported in a real browser (`examples/_checks/named-filters-2.html` is the second half); the table in `docs/dsl.md` lists each one's options, and which ones are driven by `time` or need a texture.
10
+ - CI (`.github/workflows/ci.yml`): typecheck, build and unit tests on every push and pull request (Node 22), and the real-browser tool tests in headless Chrome as a non-blocking job.
11
+
12
+ **Fixed**
13
+
14
+ - The player showed a fullscreen button on browsers without the Fullscreen API for elements (iPhone Safari, which can only fullscreen a `<video>`): a button that did nothing. It is not shown there now, and the F key does nothing.
15
+ - Exporting a movie WITH SOUND to mp4 / mov failed on browsers without an AAC encoder (Chrome on Linux, which is also what a Linux server or CI runs) with "This specific encoder configuration (mp4a.40.2 …) is not supported". The audio codec is now the first one the browser can encode (aac, then opus, mp3, flac for mp4; opus, vorbis for webm), with a warning that says so; a codec you name with `audio.codec` is never swapped, and its failure says which one would work. Found by the real-browser CI job; checked by making `AudioEncoder.isConfigSupported` refuse AAC: the file was h264 + opus.
16
+
3
17
  ## 0.7.0
4
18
 
5
19
  **Fixed**
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pixi-effects
2
2
 
3
- > **Status**: experimental — current release `0.7.0`. The API may still change between minor versions (see [CHANGELOG](./CHANGELOG.md): `0.7.0` adds filters by name, the `pixi-effects-render` and `pixi-effects-view` commands and the timeline chart, `0.6.0` adds draw-on strokes (`trimEnd`) and text that changes over time, `0.5.0` adds synthesised sound effects (`sfx`, no files) and the `check` tool, `0.4.0` makes a mask share its layer's lifetime, `0.3.0` made keyframe `at` relative to the layer).
3
+ > **Status**: experimental — current release `0.8.0`. The API may still change between minor versions (see [CHANGELOG](./CHANGELOG.md): `0.8.0` adds events and a theme for building your own player, `0.7.0` adds filters by name, the `pixi-effects-render` and `pixi-effects-view` commands and the timeline chart, `0.6.0` adds draw-on strokes (`trimEnd`) and text that changes over time, `0.5.0` adds synthesised sound effects (`sfx`, no files) and the `check` tool, `0.4.0` makes a mask share its layer's lifetime, `0.3.0` made keyframe `at` relative to the layer).
4
4
 
5
5
  **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 33 portfolio pieces written as plain data by AI models · **[Live demos →](https://yjmtmtk.github.io/pixi-effects/)** · 12 numbered examples + an in-browser playground.
6
6
 
@@ -15,7 +15,7 @@ Declarative composition and video rendering for the web. After Effects-style tim
15
15
  - **Presets** — `kenBurns` for stills, `withFade` for fade-in/out.
16
16
  - **2.5D layers & camera** — add `threeD: true`, `z`, `rotationX/Y` and a `{ type: 'camera' }` layer for parallax, flips and dolly zooms; no three.js needed.
17
17
  - **three.js layer** *(optional)* — drop a real three.js scene in as a layer via `pixi-effects/three`; keyframes drive its objects (`three.cube.rotation.y`).
18
- - **Built-in player UI** — drop-in HTML5-`<video>`-style overlay controller (play, scrub, mute, volume, fullscreen, export-to-file).
18
+ - **Built-in player UI — or your own** — a drop-in HTML5-`<video>`-style overlay controller (play, scrub, mute, volume, fullscreen, export-to-file) whose colours are one option (`theme: { accent: '#ff4d6d' }`); or build a player yourself on the movie's `<video>`-named events (`play pause ended seeking seeked volumechange error`).
19
19
  - **MP4 / WebM / MOV export** — pick container and quality from the controller, call `movie.render()` from code, or render headless from the command line (`pixi-effects-render`).
20
20
  - **A timeline you can read and scrub** — `movie.timelineChart()` draws every layer as a bar on a time axis; `pixi-effects-view` opens the page with that timeline under it, zoomable, with a playhead you can drag.
21
21
  - **Tiny dependency surface** — only `mediabunny` (runtime) plus PixiJS and GSAP (peer deps). three.js is an *optional* peer, needed only if you import `pixi-effects/three`.
@@ -40,8 +40,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
40
40
  "gsap": "https://esm.sh/gsap@3.12.5",
41
41
  "gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
42
42
  "mediabunny": "https://esm.sh/mediabunny",
43
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.7.0/dist/index.js",
44
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.7.0/dist/Controller.js"
43
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.8.0/dist/index.js",
44
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.8.0/dist/Controller.js"
45
45
  }
46
46
  }
47
47
  </script>
@@ -52,7 +52,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
52
52
  </script>
53
53
  ```
54
54
 
55
- Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.7.0/dist/index.js`) rather than a CDN-rebundled build such as `esm.sh/pixi-effects` or jsDelivr's `+esm`: the entries (`pixi-effects`, `…/controller`, `…/three`) share internal chunks, which only works when each file is served untouched.
55
+ Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.8.0/dist/index.js`) rather than a CDN-rebundled build such as `esm.sh/pixi-effects` or jsDelivr's `+esm`: the entries (`pixi-effects`, `…/controller`, `…/three`) share internal chunks, which only works when each file is served untouched.
56
56
 
57
57
  > **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
58
58
  >
@@ -68,7 +68,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
68
68
  "imports": {
69
69
  "...": "(everything from the importmap above)",
70
70
  "three": "https://esm.sh/three@0.178.0",
71
- "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.7.0/dist/three.js"
71
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.8.0/dist/three.js"
72
72
  }
73
73
  }
74
74
  </script>
@@ -189,6 +189,7 @@ They ship in the npm package (`node_modules/pixi-effects/ai/`).
189
189
  - `11-depth.html` — 2.5D layers and camera: parallax, spin, dolly zoom
190
190
  - `12-title-motion.html` — a staggered 2.5D title: letters fly in from depth, particles, orbiting camera
191
191
  - `13-sfx.html` — synthesised sound effects (no audio files), a riser into a hit, a custom voice
192
+ - `15-custom-player.html` — a complete player of your own in about 40 lines (play, seek, volume, replay) using the movie's events
192
193
  - `14-draw-on.html` — draw-on strokes (`trimStart` / `trimEnd`) and text that changes over time (`visibleChars`, `set: { text }`)
193
194
  - `playground.html` — in-browser editor with preset dropdown
194
195
 
@@ -83,9 +83,13 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
83
83
 
84
84
  Add `threeD: true` to any visual layer, then use `z`, `rotationX`, `rotationY` (centre rotation with `anchorX/Y: 0.5` or `pivotX/Y`). Add a `{ type: 'camera' }` layer for a view. A `threeD` layer at `z: 0` with the default camera looks identical to a 2D one. Consecutive `threeD` layers are drawn farthest-first (equal `z` keeps array order); non-`threeD` layers ignore the camera and keep array order. Layers at/behind the camera plane are hidden. Not supported on `threeD` layers: masks, transitions, `filterArea`.
85
85
 
86
+ ## Player
87
+
88
+ `new Controller(movie, { canvas, theme: { accent: '#ff4d6d' } })` (import from `pixi-effects/controller`; theme keys `accent foreground track barBackground trackHeight font`, or CSS `--mc-accent` …; `controller.setTheme(…)`). Your own player: `movie.play() pause() gotoFrame(f) volume muted toggleMute()`, state `currentFrame totalFrames frameRate duration isPlaying`, events `ready frame play pause ended seeking seeked volumechange error progress` (`<video>` names). `examples/15-custom-player.html` is a complete one.
89
+
86
90
  ## Filters by name (no import)
87
91
 
88
- `filters: [{ type: 'glow', name: 'halo', outerStrength: 3, color: '#ffd166' }]` — `type` = the pixi / pixi-filters class name without `Filter`, camelCase (`glow dropShadow outline blur noise alpha adjustment hslAdjustment grayscale pixelate crt rgbSplit oldFilm glitch bulgePinch twist zoomBlur kawaseBlur radialBlur motionBlur emboss ascii colorOverlay advancedBloom bevel dot crossHatch tiltShift …`), the other keys are its options. `colorMatrix` takes `preset: 'sepia'|'grayscale'|'negative'|'polaroid'|'technicolor'|'vintage'|'kodachrome'|'browni'`. Name it to animate: `'filters.halo.outerStrength'` (scalars only; `pixelate` via `sizeX`/`sizeY`). Options: `glow` distance outerStrength innerStrength color alpha · `dropShadow` offset{x,y} blur alpha color · `outline` thickness color · `blur` strength · `adjustment` gamma contrast saturation brightness · `hslAdjustment` hue lightness saturation · `pixelate` size · `crt` curvature lineWidth noise vignetting · `rgbSplit` red/green/blue {x,y} · `oldFilm` sepia noise scratch vignetting · `glitch` slices offset · `twist` radius angle offset · `bulgePinch` center radius strength · `zoomBlur` strength center innerRadius · `colorOverlay` color alpha. **Centres/offsets are CANVAS pixels** (default top-left corner): put them on the layer. Glow / shadow / blur draw outside the layer: widen `filterArea` (not for `grayscale` / `oldFilm`, which fill the margin black). Full table: `docs/dsl.md` → Named filters.
92
+ `filters: [{ type: 'glow', name: 'halo', outerStrength: 3, color: '#ffd166' }]` — `type` = the pixi / pixi-filters class name without `Filter`, camelCase (`glow dropShadow outline blur noise alpha adjustment hslAdjustment grayscale pixelate crt rgbSplit oldFilm glitch bulgePinch twist zoomBlur kawaseBlur radialBlur motionBlur emboss ascii colorOverlay advancedBloom bevel dot crossHatch tiltShift …`), the other keys are its options. `colorMatrix` takes `preset: 'sepia'|'grayscale'|'negative'|'polaroid'|'technicolor'|'vintage'|'kodachrome'|'browni'`. Name it to animate: `'filters.halo.outerStrength'` (scalars only; `pixelate` via `sizeX`/`sizeY`). Options: `glow` distance outerStrength innerStrength color alpha · `dropShadow` offset{x,y} blur alpha color · `outline` thickness color · `blur` strength · `adjustment` gamma contrast saturation brightness · `hslAdjustment` hue lightness saturation · `pixelate` size · `crt` curvature lineWidth noise vignetting · `rgbSplit` red/green/blue {x,y} · `oldFilm` sepia noise scratch vignetting · `glitch` slices offset · `twist` radius angle offset · `bulgePinch` center radius strength · `zoomBlur` strength center innerRadius · `colorOverlay` color alpha · `colorGradient` gradientType('linear'|'radial'|'conic') stops[{offset,color,alpha}] angle · `colorReplace` originalColor targetColor tolerance · `convolution` matrix(9) · `shockwave` / `godray` are driven by `time` (animate it) · `colorMap` / `simpleLightmap` need a texture (use `custom`). **Centres/offsets are CANVAS pixels** (default top-left corner): put them on the layer. Glow / shadow / blur draw outside the layer: widen `filterArea` (not for `grayscale` / `oldFilm`, which fill the margin black). Full table: `docs/dsl.md` → Named filters.
89
93
 
90
94
  ## Transitions (on the parent composition)
91
95
 
package/ai/template.html CHANGED
@@ -36,9 +36,9 @@
36
36
  "gsap": "https://esm.sh/gsap@3.12.5",
37
37
  "gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
38
38
  "mediabunny": "https://esm.sh/mediabunny",
39
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.7.0/dist/index.js",
40
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.7.0/dist/Controller.js",
41
- "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.7.0/dist/three.js",
39
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.8.0/dist/index.js",
40
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.8.0/dist/Controller.js",
41
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.8.0/dist/three.js",
42
42
  "pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
43
43
  "three": "https://esm.sh/three@0.178.0"
44
44
  }
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ var chunkPDBDDGG5_cjs = require('./chunk-PDBDDGG5.cjs');
4
+ require('./chunk-AGMNSJJH.cjs');
5
+ require('./chunk-UQBGW5DO.cjs');
6
+
7
+
8
+
9
+ Object.defineProperty(exports, "CompositionSequence", {
10
+ enumerable: true,
11
+ get: function () { return chunkPDBDDGG5_cjs.CompositionSequence; }
12
+ });
13
+ //# sourceMappingURL=Composition-CM62XJKT.cjs.map
14
+ //# sourceMappingURL=Composition-CM62XJKT.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-3KHQPRBA.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-CM62XJKT.cjs"}
@@ -0,0 +1,5 @@
1
+ export { CompositionSequence } from './chunk-O4AJYUPK.js';
2
+ import './chunk-FNKSXBKB.js';
3
+ import './chunk-DKGG7WD4.js';
4
+ //# sourceMappingURL=Composition-RJDSQYTV.js.map
5
+ //# sourceMappingURL=Composition-RJDSQYTV.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-DT657FO5.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-RJDSQYTV.js"}
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
4
+
3
5
  // src/Controller.ts
4
6
  var ICONS = {
5
7
  play: '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3 2 L13 8 L3 14 Z"/></svg>',
@@ -11,12 +13,12 @@ var ICONS = {
11
13
  fullscreenExit: '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 V6 H2 M10 2 V6 H14 M6 14 V10 H2 M10 14 V10 H14"/></svg>'
12
14
  };
13
15
  var STYLE_ATTR = "data-movie-controller";
14
- var STYLE_CSS = `
16
+ var CONTROLLER_CSS = `
15
17
  .movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }
16
18
  .movie-controller {
17
19
  position: absolute;
18
20
  pointer-events: none;
19
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
21
+ font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
20
22
  display: flex;
21
23
  flex-direction: column;
22
24
  justify-content: flex-end;
@@ -36,31 +38,31 @@ var STYLE_CSS = `
36
38
  outline: none;
37
39
  }
38
40
  .mc-progress:focus-visible {
39
- outline: 2px solid #007AFF;
41
+ outline: 2px solid var(--mc-accent, #007AFF);
40
42
  outline-offset: 2px;
41
43
  }
42
44
  .mc-progress::before {
43
45
  content: ""; position: absolute; left: 12px; right: 12px;
44
- height: 3px; background: rgba(255,255,255,0.25);
46
+ height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));
45
47
  transition: height 120ms ease;
46
48
  }
47
- .mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: 5px; }
49
+ .mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }
48
50
  .mc-progress-fill {
49
51
  position: absolute; left: 12px; top: 50%;
50
- height: 3px;
52
+ height: var(--mc-track-height, 3px);
51
53
  width: calc((100% - 24px) * var(--mc-fill, 0));
52
- background: #007AFF;
54
+ background: var(--mc-accent, #007AFF);
53
55
  transform: translateY(-50%);
54
56
  transition: height 120ms ease;
55
57
  pointer-events: none;
56
58
  }
57
59
  .mc-progress:hover .mc-progress-fill,
58
- .mc-progress.mc-scrubbing .mc-progress-fill { height: 5px; }
60
+ .mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }
59
61
  .mc-progress-thumb {
60
62
  position: absolute; top: 50%;
61
63
  left: calc(12px + (100% - 24px) * var(--mc-fill, 0));
62
64
  width: 12px; height: 12px; border-radius: 50%;
63
- background: #007AFF;
65
+ background: var(--mc-accent, #007AFF);
64
66
  transform: translate(-50%, -50%) scale(0);
65
67
  transition: transform 120ms ease;
66
68
  pointer-events: none;
@@ -74,22 +76,22 @@ var STYLE_CSS = `
74
76
  align-items: center;
75
77
  gap: 12px;
76
78
  padding: 6px 12px 8px 12px;
77
- background: linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%);
78
- color: #fff;
79
+ background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));
80
+ color: var(--mc-fg, #fff);
79
81
  line-height: 1;
80
82
  }
81
83
  .mc-btn {
82
84
  background: none; border: 0; padding: 0; margin: 0;
83
85
  width: 28px; height: 28px;
84
86
  display: inline-flex; align-items: center; justify-content: center;
85
- color: #fff; opacity: 0.85; cursor: pointer;
87
+ color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;
86
88
  transition: opacity 120ms ease;
87
89
  }
88
90
  .mc-btn:hover { opacity: 1; }
89
91
  .mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }
90
92
  .mc-time {
91
93
  font-size: 12px; font-family: Menlo, Monaco, monospace;
92
- color: #fff; opacity: 0.85;
94
+ color: var(--mc-fg, #fff); opacity: 0.85;
93
95
  min-width: 90px;
94
96
  }
95
97
  .mc-spacer { flex: 1; }
@@ -113,16 +115,16 @@ var STYLE_CSS = `
113
115
  width: 70px;
114
116
  margin-left: 6px;
115
117
  }
116
- .mc-vol-slider:focus-visible { outline: 2px solid #007AFF; outline-offset: 2px; }
118
+ .mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }
117
119
  .mc-vol-slider::before {
118
120
  content: ""; position: absolute; left: 4px; right: 4px;
119
- height: 3px; background: rgba(255,255,255,0.25); border-radius: 2px;
121
+ height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;
120
122
  }
121
123
  .mc-vol-fill {
122
124
  position: absolute; left: 4px; top: 50%;
123
125
  height: 3px;
124
126
  width: calc((100% - 8px) * var(--mc-volume, 1));
125
- background: #fff;
127
+ background: var(--mc-fg, #fff);
126
128
  transform: translateY(-50%);
127
129
  border-radius: 2px;
128
130
  pointer-events: none;
@@ -131,7 +133,7 @@ var STYLE_CSS = `
131
133
  position: absolute; top: 50%;
132
134
  left: calc(4px + (100% - 8px) * var(--mc-volume, 1));
133
135
  width: 10px; height: 10px; border-radius: 50%;
134
- background: #fff;
136
+ background: var(--mc-fg, #fff);
135
137
  transform: translate(-50%, -50%);
136
138
  pointer-events: none;
137
139
  }
@@ -185,7 +187,7 @@ var STYLE_CSS = `
185
187
  .mc-export-confirm {
186
188
  margin-top: 4px;
187
189
  width: 100%;
188
- background: #007AFF;
190
+ background: var(--mc-accent, #007AFF);
189
191
  color: #fff;
190
192
  border: 0;
191
193
  border-radius: 4px;
@@ -218,7 +220,7 @@ var STYLE_CSS = `
218
220
  }
219
221
  .mc-export-fill {
220
222
  height: 100%; width: 0%;
221
- background: linear-gradient(90deg, #007AFF, #0056CC);
223
+ background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));
222
224
  transition: width 0.3s ease; border-radius: 3px;
223
225
  }
224
226
  .mc-export-text {
@@ -231,7 +233,7 @@ function installStyles() {
231
233
  if (styleRefCount === 0) {
232
234
  const el = document.createElement("style");
233
235
  el.setAttribute(STYLE_ATTR, "");
234
- el.textContent = STYLE_CSS;
236
+ el.textContent = CONTROLLER_CSS;
235
237
  document.head.appendChild(el);
236
238
  }
237
239
  styleRefCount++;
@@ -273,6 +275,14 @@ function extensionForMimeType(mime) {
273
275
  if (t.includes("matroska") || t.includes("mkv")) return "mkv";
274
276
  return "mp4";
275
277
  }
278
+ var THEME_PROPERTIES = {
279
+ accent: "--mc-accent",
280
+ foreground: "--mc-fg",
281
+ track: "--mc-track",
282
+ barBackground: "--mc-bar-bg",
283
+ trackHeight: "--mc-track-height",
284
+ font: "--mc-font"
285
+ };
276
286
  var Controller = class _Controller {
277
287
  movie;
278
288
  options;
@@ -345,6 +355,7 @@ var Controller = class _Controller {
345
355
  this.root.className = this.options.className;
346
356
  this.wrapper.appendChild(this.root);
347
357
  this.buildBar();
358
+ this.setTheme(options.theme ?? {});
348
359
  this.bindFitWrap();
349
360
  this.syncRootToCanvas();
350
361
  if (typeof ResizeObserver !== "undefined") {
@@ -424,6 +435,17 @@ var Controller = class _Controller {
424
435
  if (this.settingsFormatSelect) this.settingsFormatSelect.value = this.exportFormat;
425
436
  if (this.settingsQualitySelect) this.settingsQualitySelect.value = this.exportQuality;
426
437
  }
438
+ /** Change the look of the bar (see {@link ControllerTheme}): only the keys you give change, `null` restores the default. */
439
+ setTheme(theme) {
440
+ chunkUQBGW5DO_cjs.warnUnknownOptions("Controller theme", theme, Object.keys(THEME_PROPERTIES));
441
+ for (const key of Object.keys(THEME_PROPERTIES)) {
442
+ if (!(key in theme)) continue;
443
+ const value = theme[key];
444
+ const prop = THEME_PROPERTIES[key];
445
+ if (value === null || value === void 0 || value === "") this.root.style.removeProperty(prop);
446
+ else this.root.style.setProperty(prop, typeof value === "number" ? `${value}px` : String(value));
447
+ }
448
+ }
427
449
  ensurePositioningContext(canvas) {
428
450
  const parent = canvas.parentElement;
429
451
  if (!parent) {
@@ -605,8 +627,20 @@ var Controller = class _Controller {
605
627
  };
606
628
  container.addEventListener("wheel", this.wheelHandler, { passive: false });
607
629
  }
630
+ /**
631
+ * Does this browser have the Fullscreen API for an element? iPhone Safari does not (it can only put a <video> in fullscreen),
632
+ * and a button that does nothing is worse than none.
633
+ */
634
+ supportsFullscreen() {
635
+ return typeof this.wrapper.requestFullscreen === "function" && document.fullscreenEnabled !== false;
636
+ }
608
637
  bindFullscreen() {
609
638
  if (!this.fullscreenBtn) return;
639
+ if (!this.supportsFullscreen()) {
640
+ this.fullscreenBtn.remove();
641
+ this.fullscreenBtn = null;
642
+ return;
643
+ }
610
644
  this.fullscreenBtn.addEventListener("click", () => {
611
645
  void this.toggleFullscreen();
612
646
  });
@@ -1029,6 +1063,7 @@ var Controller = class _Controller {
1029
1063
  }
1030
1064
  };
1031
1065
 
1066
+ exports.CONTROLLER_CSS = CONTROLLER_CSS;
1032
1067
  exports.Controller = Controller;
1033
1068
  exports.extensionForMimeType = extensionForMimeType;
1034
1069
  exports.formatTime = formatTime;