pixi-effects 0.26.1 → 0.26.3

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 (49) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -6
  3. package/ai/chat-template.html +3 -3
  4. package/ai/tools/render.mjs +16 -11
  5. package/dist/Composition-3O7L2VL5.js +7 -0
  6. package/dist/{Composition-Z2VY3KWZ.js.map → Composition-3O7L2VL5.js.map} +1 -1
  7. package/dist/Composition-EE3KEE2L.cjs +16 -0
  8. package/dist/{Composition-THQPG4E5.cjs.map → Composition-EE3KEE2L.cjs.map} +1 -1
  9. package/dist/Controller.d.cts +1 -1
  10. package/dist/Controller.d.ts +1 -1
  11. package/dist/{Movie-i05Oh9mJ.d.cts → Movie-BWLLWRRe.d.cts} +10 -3
  12. package/dist/{Movie-DVeDTdnc.d.ts → Movie-BkP82KXS.d.ts} +10 -3
  13. package/dist/Presenter.d.cts +1 -1
  14. package/dist/Presenter.d.ts +1 -1
  15. package/dist/{chunk-EABL3UKJ.cjs → chunk-A225HYSY.cjs} +351 -20
  16. package/dist/chunk-A225HYSY.cjs.map +1 -0
  17. package/dist/{chunk-JHJNEIDI.js → chunk-FXAHNPBP.js} +351 -20
  18. package/dist/chunk-FXAHNPBP.js.map +1 -0
  19. package/dist/{chunk-H3RHEH5W.cjs → chunk-IIDBEYQH.cjs} +59 -59
  20. package/dist/{chunk-H3RHEH5W.cjs.map → chunk-IIDBEYQH.cjs.map} +1 -1
  21. package/dist/{chunk-YEVAUQ6Y.js → chunk-WMMZDPWK.js} +3 -3
  22. package/dist/{chunk-YEVAUQ6Y.js.map → chunk-WMMZDPWK.js.map} +1 -1
  23. package/dist/index.cjs +144 -50
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +121 -27
  28. package/dist/index.js.map +1 -1
  29. package/dist/loader.css +1 -1
  30. package/dist/{music-AF4VTFAF.js → music-3O5MDQXY.js} +52 -6
  31. package/dist/music-3O5MDQXY.js.map +1 -0
  32. package/dist/{music-KB7YZLHP.cjs → music-4O367SPR.cjs} +52 -6
  33. package/dist/music-4O367SPR.cjs.map +1 -0
  34. package/dist/three.cjs +5 -5
  35. package/dist/three.js +1 -1
  36. package/docs/api.md +11 -5
  37. package/docs/dsl.md +4 -2
  38. package/llms-full.txt +21 -12
  39. package/package.json +1 -1
  40. package/skills/pixi-effects/SKILL.md +1 -1
  41. package/skills/pixi-effects/reference/cheatsheet.md +4 -4
  42. package/skills/pixi-effects/reference/pitfalls.md +1 -0
  43. package/skills/pixi-effects/template.html +6 -6
  44. package/dist/Composition-THQPG4E5.cjs +0 -16
  45. package/dist/Composition-Z2VY3KWZ.js +0 -7
  46. package/dist/chunk-EABL3UKJ.cjs.map +0 -1
  47. package/dist/chunk-JHJNEIDI.js.map +0 -1
  48. package/dist/music-AF4VTFAF.js.map +0 -1
  49. package/dist/music-KB7YZLHP.cjs.map +0 -1
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FontReport, c as FrameEvent, I as InspectOptions, d as InspectReport, e as IssueGroup, L as LayerInfo, f as LoadProgressState, g as LoadStage, h as LoaderOption, M as MotionBlurOptions, i as MotionBlurSpec, j as Movie, k as MovieErrorEvent, l as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, m as ReviewOptions, n as ReviewReport, S as SnapshotOptions, o as SoundMeasure, p as StopEvent, q as StopImage, r as StopImagesOptions, T as TimelineData, s as TimelineHtmlOptions, t as TimelineRow, u as TimelineSvgOptions, v as TimelineTransition, V as VolumeEvent, w as dismissLoader, x as failLoader, y as resolveLoader } from './Movie-i05Oh9mJ.cjs';
1
+ export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FontReport, c as FrameEvent, I as InspectOptions, d as InspectReport, e as IssueGroup, L as LayerInfo, f as LoadProgressState, g as LoadStage, h as LoaderOption, M as MotionBlurOptions, i as MotionBlurSpec, j as Movie, k as MovieErrorEvent, l as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, m as ReviewOptions, n as ReviewReport, S as SnapshotOptions, o as SoundMeasure, p as StopEvent, q as StopImage, r as StopImagesOptions, T as TimelineData, s as TimelineHtmlOptions, t as TimelineRow, u as TimelineSvgOptions, v as TimelineTransition, V as VolumeEvent, w as dismissLoader, x as failLoader, y as resolveLoader } from './Movie-BWLLWRRe.cjs';
2
2
  import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, K as Keyframe, T as TextSequenceSpec, M as MusicOptions, P as Props, b as TransitionSpec, c as CompositionSpec, d as SequenceCommon } from './types-DSGwN5w1.cjs';
3
3
  export { A as ArcShapeSpec, e as AssetSpec, f as AudioAssetSpec, g as AudioMusicSpec, h as AudioSequenceSpec, i as AudioSfxSpec, B as BlendModeName, j as ChromaKeyFilterSpec, k as CircleShapeSpec, l as CompositionSequenceSpec, m as CrossfadeTransition, n as CustomFilterSpec, D as DipTransition, o as DissolveTransition, E as EllipseShapeSpec, p as Expr, F as FilterSpec, G as GradientSpec, q as IrisTransition, L as LightSequenceSpec, r as LineShapeSpec, s as LumaTransition, t as MaskRefSpec, u as MatteRefSpec, v as MusicDrum, w as MusicDrums, x as MusicInstrument, y as MusicLevel, z as MusicTrack, N as NullSequenceSpec, H as PathShapeSpec, J as PolygonShapeSpec, O as PropValue, R as RectShapeSpec, Q as SfxKnobs, U as SfxOptions, V as SfxPreset, W as SfxVoice, X as ShaderSequenceSpec, Y as ShapeSequenceSpec, Z as SlideTransition, _ as Stop, $ as StopSpec, a0 as TransitionCommon, a1 as VideoSequenceSpec, a2 as WipeTransition, a3 as ZoomTransition, a4 as normalizeStops, a5 as pictureStops } from './types-DSGwN5w1.cjs';
4
4
  import { S as Sequence } from './Base-TbBhHPrG.cjs';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FontReport, c as FrameEvent, I as InspectOptions, d as InspectReport, e as IssueGroup, L as LayerInfo, f as LoadProgressState, g as LoadStage, h as LoaderOption, M as MotionBlurOptions, i as MotionBlurSpec, j as Movie, k as MovieErrorEvent, l as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, m as ReviewOptions, n as ReviewReport, S as SnapshotOptions, o as SoundMeasure, p as StopEvent, q as StopImage, r as StopImagesOptions, T as TimelineData, s as TimelineHtmlOptions, t as TimelineRow, u as TimelineSvgOptions, v as TimelineTransition, V as VolumeEvent, w as dismissLoader, x as failLoader, y as resolveLoader } from './Movie-DVeDTdnc.js';
1
+ export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FontReport, c as FrameEvent, I as InspectOptions, d as InspectReport, e as IssueGroup, L as LayerInfo, f as LoadProgressState, g as LoadStage, h as LoaderOption, M as MotionBlurOptions, i as MotionBlurSpec, j as Movie, k as MovieErrorEvent, l as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, m as ReviewOptions, n as ReviewReport, S as SnapshotOptions, o as SoundMeasure, p as StopEvent, q as StopImage, r as StopImagesOptions, T as TimelineData, s as TimelineHtmlOptions, t as TimelineRow, u as TimelineSvgOptions, v as TimelineTransition, V as VolumeEvent, w as dismissLoader, x as failLoader, y as resolveLoader } from './Movie-BkP82KXS.js';
2
2
  import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, K as Keyframe, T as TextSequenceSpec, M as MusicOptions, P as Props, b as TransitionSpec, c as CompositionSpec, d as SequenceCommon } from './types-DSGwN5w1.js';
3
3
  export { A as ArcShapeSpec, e as AssetSpec, f as AudioAssetSpec, g as AudioMusicSpec, h as AudioSequenceSpec, i as AudioSfxSpec, B as BlendModeName, j as ChromaKeyFilterSpec, k as CircleShapeSpec, l as CompositionSequenceSpec, m as CrossfadeTransition, n as CustomFilterSpec, D as DipTransition, o as DissolveTransition, E as EllipseShapeSpec, p as Expr, F as FilterSpec, G as GradientSpec, q as IrisTransition, L as LightSequenceSpec, r as LineShapeSpec, s as LumaTransition, t as MaskRefSpec, u as MatteRefSpec, v as MusicDrum, w as MusicDrums, x as MusicInstrument, y as MusicLevel, z as MusicTrack, N as NullSequenceSpec, H as PathShapeSpec, J as PolygonShapeSpec, O as PropValue, R as RectShapeSpec, Q as SfxKnobs, U as SfxOptions, V as SfxPreset, W as SfxVoice, X as ShaderSequenceSpec, Y as ShapeSequenceSpec, Z as SlideTransition, _ as Stop, $ as StopSpec, a0 as TransitionCommon, a1 as VideoSequenceSpec, a2 as WipeTransition, a3 as ZoomTransition, a4 as normalizeStops, a5 as pictureStops } from './types-DSGwN5w1.js';
4
4
  import { S as Sequence } from './Base-CpdDs4HT.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { CompositionSequence, maskSourceOf } from './chunk-YEVAUQ6Y.js';
2
- import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, timeRemapOf, contentLength, resolveAt, checkEase } from './chunk-JHJNEIDI.js';
3
- export { noise, rand, random, registerSequenceType } from './chunk-JHJNEIDI.js';
1
+ import { CompositionSequence, maskSourceOf } from './chunk-WMMZDPWK.js';
2
+ import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, timeRemapOf, contentLength, resolveAt, checkEase } from './chunk-FXAHNPBP.js';
3
+ export { noise, rand, random, registerSequenceType } from './chunk-FXAHNPBP.js';
4
4
  import { usesAdvancedBlend, enableAdvancedBlend } from './chunk-AXZUKNXF.js';
5
5
  import { collectPropKeys } from './chunk-TLYTYBWX.js';
6
6
  import { warnUnknownOptions, suggestName } from './chunk-BXOCEZ4U.js';
@@ -8,7 +8,7 @@ import * as PIXI from 'pixi.js';
8
8
  import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, CanvasTextMetrics, TextStyle, Assets, ExtensionType, Filter, Texture, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
9
9
  import { gsap } from 'gsap';
10
10
  import { PixiPlugin } from 'gsap/PixiPlugin';
11
- import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat, getFirstEncodableAudioCodec } from 'mediabunny';
11
+ import { Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, OggOutputFormat, QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat, canEncodeVideo, getFirstEncodableAudioCodec } from 'mediabunny';
12
12
 
13
13
  var registered = false;
14
14
  function ensureLoadersRegistered(audioContext) {
@@ -759,24 +759,70 @@ function resolveRenderOptions(o) {
759
759
  };
760
760
  }
761
761
 
762
- // src/core/Renderer.ts
763
- var VIDEO_CODEC_BY_FORMAT = {
764
- mp4: "avc",
765
- mov: "avc",
766
- webm: "vp9",
767
- mkv: "vp9"
762
+ // src/core/renderOptions.ts
763
+ var QUALITY_NAMES = ["very-low", "low", "medium", "high", "very-high"];
764
+ function parseBitrate(v) {
765
+ if (v === void 0 || v === null) return {};
766
+ if (typeof v === "string" && QUALITY_NAMES.includes(v)) return { quality: v };
767
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return { bps: Math.round(v) };
768
+ const m = typeof v === "string" ? /^\s*(\d+(?:\.\d+)?)\s*([kKmM]?)\s*$/.exec(v) : null;
769
+ if (m) {
770
+ const n = Number(m[1]) * { "": 1, k: 1e3, m: 1e6 }[m[2].toLowerCase()];
771
+ if (n > 0) return { bps: Math.round(n) };
772
+ }
773
+ throw new Error(`pixi-effects: bitrate ${JSON.stringify(v)}: use a quality (${QUALITY_NAMES.join(", ")}) or bits a second (8000000, '8M', '800k')`);
774
+ }
775
+ var AUDIO_FORMATS = ["wav", "ogg"];
776
+ var isAudioFormat = (f) => AUDIO_FORMATS.includes(f);
777
+ var VIDEO_CODEC_PREFERENCE = {
778
+ mp4: ["avc", "hevc", "vp9", "av1"],
779
+ mov: ["avc", "hevc"],
780
+ webm: ["vp9", "av1", "vp8"],
781
+ mkv: ["vp9", "avc", "hevc", "av1", "vp8"]
768
782
  };
769
- var AUDIO_CODEC_BY_FORMAT = {
770
- mp4: "aac",
771
- mov: "aac",
772
- webm: "opus",
773
- mkv: "opus"
783
+ var CODEC_ALIASES = {
784
+ h264: "avc",
785
+ "h.264": "avc",
786
+ x264: "avc",
787
+ avc1: "avc",
788
+ h265: "hevc",
789
+ "h.265": "hevc",
790
+ x265: "hevc",
791
+ hvc1: "hevc",
792
+ hev1: "hevc",
793
+ vp09: "vp9",
794
+ "vp-9": "vp9",
795
+ av01: "av1",
796
+ vp80: "vp8",
797
+ "vp-8": "vp8"
774
798
  };
799
+ async function chooseVideoCodec(a) {
800
+ const held = a.supported ?? VIDEO_CODEC_PREFERENCE[a.fmt];
801
+ const order = VIDEO_CODEC_PREFERENCE[a.fmt].filter((c) => held.includes(c));
802
+ const firstThatWorks = async (list) => {
803
+ for (const c of list) if (await a.canEncode(c)) return c;
804
+ return null;
805
+ };
806
+ if (a.requested) {
807
+ const alias = CODEC_ALIASES[a.requested.toLowerCase()];
808
+ if (alias) throw new Error(`pixi-effects: render({ video: { codec: "${a.requested}" } }): "${a.requested}" is not a codec name here; did you mean "${alias}"?`);
809
+ if (!order.includes(a.requested)) throw new Error(`pixi-effects: render(): "${a.requested}" cannot go in ${a.fmt} (${a.fmt} holds ${order.join(", ")})`);
810
+ if (await a.canEncode(a.requested)) return { codec: a.requested };
811
+ const alt = await firstThatWorks(order.filter((c) => c !== a.requested));
812
+ throw new Error(`pixi-effects: this browser cannot encode "${a.requested}" video for ${a.fmt}` + (alt ? `; "${alt}" would work: render({ format: '${a.fmt}', video: { codec: '${alt}' } }), or leave video.codec out and the best one is chosen.` : "; it can encode none of " + order.join(", ") + "."));
813
+ }
814
+ const chosen = await firstThatWorks(order);
815
+ if (!chosen) throw new Error(`pixi-effects: this browser has no video encoder for ${a.fmt} (it tried ${order.join(", ")}): try another browser, or another format`);
816
+ return chosen === order[0] ? { codec: chosen } : { codec: chosen, fellBackFrom: order[0] };
817
+ }
818
+
819
+ // src/core/Renderer.ts
775
820
  var AUDIO_PREFERENCE = {
776
821
  mp4: ["aac", "opus", "mp3", "flac"],
777
822
  mov: ["aac", "mp3"],
778
823
  webm: ["opus", "vorbis"],
779
- mkv: ["opus", "vorbis", "aac", "flac", "mp3"]
824
+ mkv: ["opus", "vorbis", "aac", "flac", "mp3"],
825
+ ogg: ["opus", "vorbis", "flac"]
780
826
  };
781
827
  async function chooseAudioCodec(fmt, requested, container, audio, bitrate) {
782
828
  const supported = container.getSupportedAudioCodecs?.();
@@ -801,6 +847,10 @@ var qualityMap = {
801
847
  "high": QUALITY_HIGH,
802
848
  "very-high": QUALITY_VERY_HIGH
803
849
  };
850
+ function bitrateOf(v, fallback) {
851
+ const b = parseBitrate(v);
852
+ return b.bps ?? (b.quality ? qualityMap[b.quality] : fallback);
853
+ }
804
854
  function makeOutputFormat(name) {
805
855
  switch (name) {
806
856
  case "mp4":
@@ -820,19 +870,47 @@ function sliceAudio(buffer, fromSec, toSec, fadeIn, fadeOut) {
820
870
  cut.forEach((data, c) => out.copyToChannel(data, c));
821
871
  return out;
822
872
  }
873
+ async function exportAudioOnly(movie, options, fmt, range2) {
874
+ if (!movie.audioBuffer) {
875
+ throw new Error(`pixi-effects: render({ format: '${fmt}' }): this movie has no sound to export (add an audio layer, an sfx or a music score); for the picture use mp4, webm, mov or mkv`);
876
+ }
877
+ if (options.video) console.warn(`pixi-effects: render({ format: '${fmt}' }): video options are ignored, only the sound is written`);
878
+ const audio = range2.partial ? sliceAudio(movie.audioBuffer, range2.fromSec, range2.toSec, range2.fromFrame > 0, range2.toFrame < movie.totalFrames) : movie.audioBuffer;
879
+ const container = fmt === "wav" ? new WavOutputFormat() : new OggOutputFormat();
880
+ let codec;
881
+ if (fmt === "wav") {
882
+ codec = options.audio?.codec ?? "pcm-s16";
883
+ const held = container.getSupportedAudioCodecs();
884
+ if (!held.includes(codec)) throw new Error(`pixi-effects: render({ format: 'wav', audio: { codec: "${codec}" } }): wav holds ${held.join(", ")} (the usual is pcm-s16)`);
885
+ } else {
886
+ codec = await chooseAudioCodec("ogg", options.audio?.codec, container, audio, bitrateOf(options.audio?.bitrate, QUALITY_HIGH));
887
+ }
888
+ const output = new Output({ format: container, target: new BufferTarget() });
889
+ const source = new AudioBufferSource({ codec, ...fmt === "wav" ? {} : { bitrate: bitrateOf(options.audio?.bitrate, QUALITY_HIGH) } });
890
+ output.addAudioTrack(source);
891
+ movie.emit("progress", { progress: 0, frame: range2.fromFrame, totalFrames: movie.totalFrames });
892
+ await output.start();
893
+ await source.add(audio);
894
+ await source.close();
895
+ await output.finalize();
896
+ movie.emit("progress", { progress: 100, frame: range2.toFrame, totalFrames: movie.totalFrames });
897
+ return new Blob([output.target.buffer], { type: output.format.mimeType });
898
+ }
823
899
  async function exportFrames(movie, options = {}) {
824
900
  const fmt = options.format ?? "mp4";
825
901
  const eff = resolveRenderOptions(options);
826
902
  const range2 = resolveRange(options.range, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows: typeof options.range === "string" ? movie.timelineData().rows : void 0 });
903
+ if (isAudioFormat(fmt)) return exportAudioOnly(movie, options, fmt, range2);
904
+ if (!["mp4", "mov", "webm", "mkv"].includes(fmt)) throw new Error(`pixi-effects: render({ format: "${String(fmt)}" }): use mp4, mov, webm, mkv (a picture) or wav, ogg (only the sound)`);
905
+ warnUnknownOptions("movie.render({ video })", options.video, ["codec", "bitrate", "hardware", "keyFrameInterval"]);
906
+ warnUnknownOptions("movie.render({ audio })", options.audio, ["codec", "bitrate"]);
907
+ const videoBitrate = bitrateOf(eff.bitrate ?? "high", QUALITY_HIGH);
827
908
  const opts = {
828
909
  format: fmt,
829
- video: {
830
- codec: options.video?.codec ?? VIDEO_CODEC_BY_FORMAT[fmt],
831
- bitrate: qualityMap[eff.bitrate ?? "high"] ?? QUALITY_HIGH
832
- },
910
+ video: { codec: "", bitrate: videoBitrate },
833
911
  audio: {
834
- codec: options.audio?.codec ?? AUDIO_CODEC_BY_FORMAT[fmt],
835
- bitrate: qualityMap[options.audio?.bitrate ?? "high"] ?? QUALITY_HIGH
912
+ codec: options.audio?.codec ?? (fmt === "mp4" || fmt === "mov" ? "aac" : "opus"),
913
+ bitrate: bitrateOf(options.audio?.bitrate, QUALITY_HIGH)
836
914
  }
837
915
  };
838
916
  const container = makeOutputFormat(opts.format);
@@ -847,9 +925,23 @@ async function exportFrames(movie, options = {}) {
847
925
  const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
848
926
  const small = eff.scale < 1 ? Object.assign(document.createElement("canvas"), { width: Math.max(2, Math.round(stage.width * eff.scale / 2) * 2), height: Math.max(2, Math.round(stage.height * eff.scale / 2) * 2) }) : null;
849
927
  const smallCtx = small?.getContext("2d") ?? null;
850
- const canvasSource = new CanvasSource(small ?? blurCanvas ?? stage, {
928
+ const picture = small ?? blurCanvas ?? stage;
929
+ const chosen = await chooseVideoCodec({
930
+ fmt,
931
+ requested: options.video?.codec,
932
+ supported: container.getSupportedVideoCodecs(),
933
+ canEncode: (c) => canEncodeVideo(c, { width: picture.width, height: picture.height, bitrate: opts.video.bitrate })
934
+ });
935
+ if (chosen.fellBackFrom) console.warn(`pixi-effects: this browser cannot encode ${chosen.fellBackFrom.toUpperCase()} video, so the ${fmt} carries ${chosen.codec.toUpperCase()} instead (name a codec with render({ video: { codec } }) to choose yourself).`);
936
+ opts.video.codec = chosen.codec;
937
+ const hardware = options.video?.hardware;
938
+ if (hardware !== void 0 && !["no-preference", "prefer-hardware", "prefer-software"].includes(hardware)) {
939
+ throw new Error(`pixi-effects: render({ video: { hardware: "${String(hardware)}" } }): use 'prefer-hardware', 'prefer-software' or 'no-preference'`);
940
+ }
941
+ const canvasSource = new CanvasSource(picture, {
851
942
  codec: opts.video.codec,
852
- bitrate: opts.video.bitrate
943
+ bitrate: opts.video.bitrate,
944
+ ...hardware ? { hardwareAcceleration: hardware } : {}
853
945
  });
854
946
  output.addVideoTrack(canvasSource, { frameRate: movie.frameRate });
855
947
  if (audio) {
@@ -865,7 +957,9 @@ async function exportFrames(movie, options = {}) {
865
957
  await output.start();
866
958
  }
867
959
  movie.app.ticker.stop();
868
- const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
960
+ const interval = options.video?.keyFrameInterval ?? 2;
961
+ if (!(typeof interval === "number" && Number.isFinite(interval) && interval > 0)) throw new Error(`pixi-effects: render({ video: { keyFrameInterval: ${String(interval)} } }): seconds between keyframes, above 0 (default 2)`);
962
+ const keyframeIntervalFrames = Math.max(1, Math.round(interval * movie.frameRate));
869
963
  try {
870
964
  const count = Math.max(1, range2.toFrame - range2.fromFrame);
871
965
  for (let frame = range2.fromFrame; frame <= range2.toFrame; frame++) {
@@ -4223,7 +4317,7 @@ async function audioEnvelope(source, options = {}) {
4223
4317
 
4224
4318
  // src/audio/musicEnvelope.ts
4225
4319
  async function musicEnvelope(music, options = {}) {
4226
- const { resolveMusic, renderMusic, musicLength, musicBeatTimes, TempoMap } = await import('./music-AF4VTFAF.js');
4320
+ const { resolveMusic, renderMusic, musicLength, musicBeatTimes, TempoMap } = await import('./music-3O5MDQXY.js');
4227
4321
  const messages = [];
4228
4322
  const m = resolveMusic(music, "musicEnvelope()", (msg) => messages.push(msg));
4229
4323
  if (!m) throw new Error(`pixi-effects: musicEnvelope(): ${messages.join(" ") || "the music has nothing to play"}`);
@@ -4236,7 +4330,7 @@ async function musicEnvelope(music, options = {}) {
4236
4330
 
4237
4331
  // src/audio/musicBuffer.ts
4238
4332
  async function musicBuffer(music, options = {}) {
4239
- const { resolveMusic, renderMusic, musicLength } = await import('./music-AF4VTFAF.js');
4333
+ const { resolveMusic, renderMusic, musicLength } = await import('./music-3O5MDQXY.js');
4240
4334
  const messages = [];
4241
4335
  const m = resolveMusic(music, "musicBuffer()", (msg) => messages.push(msg));
4242
4336
  if (!m) throw new Error(`pixi-effects: musicBuffer(): ${messages.join(" ") || "the music has nothing to play"}`);