4164 lines
131 KiB
JavaScript
4164 lines
131 KiB
JavaScript
var __dispose = Symbol.dispose || /* @__PURE__ */ Symbol.for("Symbol.dispose");
|
|
var __asyncDispose = Symbol.asyncDispose || /* @__PURE__ */ Symbol.for("Symbol.asyncDispose");
|
|
var __using = (stack, value, async) => {
|
|
if (value != null) {
|
|
if (typeof value !== "object" && typeof value !== "function")
|
|
throw TypeError('Object expected to be assigned to "using" declaration');
|
|
var dispose;
|
|
if (async)
|
|
dispose = value[__asyncDispose];
|
|
if (dispose === undefined)
|
|
dispose = value[__dispose];
|
|
if (typeof dispose !== "function")
|
|
throw TypeError("Object not disposable");
|
|
stack.push([async, dispose, value]);
|
|
} else if (async) {
|
|
stack.push([async]);
|
|
}
|
|
return value;
|
|
};
|
|
var __callDispose = (stack, error, hasError) => {
|
|
var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
|
|
return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
|
|
}, fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e), next = (it) => {
|
|
while (it = stack.pop()) {
|
|
try {
|
|
var result = it[1] && it[1].call(it[2]);
|
|
if (it[0])
|
|
return Promise.resolve(result).then(next, (e) => (fail(e), next()));
|
|
} catch (e) {
|
|
fail(e);
|
|
}
|
|
}
|
|
if (hasError)
|
|
throw error;
|
|
};
|
|
return next();
|
|
};
|
|
|
|
// src/can-render-media-on-web.ts
|
|
import { canEncodeVideo } from "mediabunny";
|
|
|
|
// src/can-use-webfs-target.ts
|
|
var canUseWebFsWriter = async () => {
|
|
if (!("storage" in navigator)) {
|
|
return false;
|
|
}
|
|
if (!("getDirectory" in navigator.storage)) {
|
|
return false;
|
|
}
|
|
try {
|
|
const directoryHandle = await navigator.storage.getDirectory();
|
|
const fileHandle = await directoryHandle.getFileHandle("remotion-probe-web-fs-support", {
|
|
create: true
|
|
});
|
|
const canUse = fileHandle.createWritable !== undefined;
|
|
return canUse;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// src/check-webgl-support.ts
|
|
var checkWebGLSupport = () => {
|
|
try {
|
|
const canvas = new OffscreenCanvas(1, 1);
|
|
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
|
if (!gl) {
|
|
return {
|
|
type: "webgl-unsupported",
|
|
message: "WebGL is not supported. 3D CSS transforms will fail.",
|
|
severity: "error"
|
|
};
|
|
}
|
|
return null;
|
|
} catch {
|
|
return {
|
|
type: "webgl-unsupported",
|
|
message: "WebGL is not supported. 3D CSS transforms will fail.",
|
|
severity: "error"
|
|
};
|
|
}
|
|
};
|
|
|
|
// src/mediabunny-mappings.ts
|
|
import {
|
|
Mp4OutputFormat,
|
|
QUALITY_HIGH,
|
|
QUALITY_LOW,
|
|
QUALITY_MEDIUM,
|
|
QUALITY_VERY_HIGH,
|
|
QUALITY_VERY_LOW,
|
|
WebMOutputFormat
|
|
} from "mediabunny";
|
|
var codecToMediabunnyCodec = (codec) => {
|
|
switch (codec) {
|
|
case "h264":
|
|
return "avc";
|
|
case "h265":
|
|
return "hevc";
|
|
case "vp8":
|
|
return "vp8";
|
|
case "vp9":
|
|
return "vp9";
|
|
case "av1":
|
|
return "av1";
|
|
default:
|
|
throw new Error(`Unsupported codec: ${codec}`);
|
|
}
|
|
};
|
|
var containerToMediabunnyContainer = (container) => {
|
|
switch (container) {
|
|
case "mp4":
|
|
return new Mp4OutputFormat;
|
|
case "webm":
|
|
return new WebMOutputFormat;
|
|
default:
|
|
throw new Error(`Unsupported container: ${container}`);
|
|
}
|
|
};
|
|
var getDefaultVideoCodecForContainer = (container) => {
|
|
switch (container) {
|
|
case "mp4":
|
|
return "h264";
|
|
case "webm":
|
|
return "vp8";
|
|
default:
|
|
throw new Error(`Unsupported container: ${container}`);
|
|
}
|
|
};
|
|
var getQualityForWebRendererQuality = (quality) => {
|
|
switch (quality) {
|
|
case "very-low":
|
|
return QUALITY_VERY_LOW;
|
|
case "low":
|
|
return QUALITY_LOW;
|
|
case "medium":
|
|
return QUALITY_MEDIUM;
|
|
case "high":
|
|
return QUALITY_HIGH;
|
|
case "very-high":
|
|
return QUALITY_VERY_HIGH;
|
|
default:
|
|
throw new Error(`Unsupported quality: ${quality}`);
|
|
}
|
|
};
|
|
var getMimeType = (container) => {
|
|
switch (container) {
|
|
case "mp4":
|
|
return "video/mp4";
|
|
case "webm":
|
|
return "video/webm";
|
|
default:
|
|
throw new Error(`Unsupported container: ${container}`);
|
|
}
|
|
};
|
|
var getDefaultAudioCodecForContainer = (container) => {
|
|
switch (container) {
|
|
case "mp4":
|
|
return "aac";
|
|
case "webm":
|
|
return "opus";
|
|
default:
|
|
throw new Error(`Unsupported container: ${container}`);
|
|
}
|
|
};
|
|
var WEB_RENDERER_VIDEO_CODECS = [
|
|
"h264",
|
|
"h265",
|
|
"vp8",
|
|
"vp9",
|
|
"av1"
|
|
];
|
|
var getSupportedVideoCodecsForContainer = (container) => {
|
|
const format = containerToMediabunnyContainer(container);
|
|
const allSupported = format.getSupportedVideoCodecs();
|
|
return WEB_RENDERER_VIDEO_CODECS.filter((codec) => allSupported.includes(codecToMediabunnyCodec(codec)));
|
|
};
|
|
var WEB_RENDERER_AUDIO_CODECS = ["aac", "opus"];
|
|
var getSupportedAudioCodecsForContainer = (container) => {
|
|
const format = containerToMediabunnyContainer(container);
|
|
const allSupported = format.getSupportedAudioCodecs();
|
|
return WEB_RENDERER_AUDIO_CODECS.filter((codec) => allSupported.includes(codec));
|
|
};
|
|
var audioCodecToMediabunnyAudioCodec = (audioCodec) => {
|
|
return audioCodec;
|
|
};
|
|
|
|
// src/resolve-audio-codec.ts
|
|
import { canEncodeAudio } from "mediabunny";
|
|
var resolveAudioCodec = async (options) => {
|
|
const issues = [];
|
|
const { container, requestedCodec, userSpecifiedAudioCodec, bitrate } = options;
|
|
const audioCodec = requestedCodec ?? getDefaultAudioCodecForContainer(container);
|
|
const supportedAudioCodecs = getSupportedAudioCodecsForContainer(container);
|
|
if (!supportedAudioCodecs.includes(audioCodec)) {
|
|
issues.push({
|
|
type: "audio-codec-unsupported",
|
|
message: `Audio codec "${audioCodec}" is not supported for container "${container}". Supported: ${supportedAudioCodecs.join(", ")}`,
|
|
severity: "error"
|
|
});
|
|
return { codec: null, issues };
|
|
}
|
|
const mediabunnyAudioCodec = audioCodecToMediabunnyAudioCodec(audioCodec);
|
|
const canEncode = await canEncodeAudio(mediabunnyAudioCodec, { bitrate });
|
|
if (canEncode) {
|
|
return { codec: audioCodec, issues };
|
|
}
|
|
if (userSpecifiedAudioCodec) {
|
|
issues.push({
|
|
type: "audio-codec-unsupported",
|
|
message: `Audio codec "${audioCodec}" cannot be encoded by this browser. This is common for AAC on Firefox. Try using "opus" instead.`,
|
|
severity: "error"
|
|
});
|
|
return { codec: null, issues };
|
|
}
|
|
for (const fallbackCodec of supportedAudioCodecs) {
|
|
if (fallbackCodec !== audioCodec) {
|
|
const fallbackMediabunnyCodec = audioCodecToMediabunnyAudioCodec(fallbackCodec);
|
|
const canEncodeFallback = await canEncodeAudio(fallbackMediabunnyCodec, {
|
|
bitrate
|
|
});
|
|
if (canEncodeFallback) {
|
|
issues.push({
|
|
type: "audio-codec-unsupported",
|
|
message: `Falling back from audio codec "${audioCodec}" to "${fallbackCodec}" because the original codec cannot be encoded by this browser.`,
|
|
severity: "warning"
|
|
});
|
|
return { codec: fallbackCodec, issues };
|
|
}
|
|
}
|
|
}
|
|
issues.push({
|
|
type: "audio-codec-unsupported",
|
|
message: `No audio codec can be encoded by this browser for container "${container}".`,
|
|
severity: "error"
|
|
});
|
|
return { codec: null, issues };
|
|
};
|
|
|
|
// src/validate-dimensions.ts
|
|
var validateDimensions = (options) => {
|
|
const { width, height, codec } = options;
|
|
if (codec === "h264" || codec === "h265") {
|
|
if (width % 2 !== 0 || height % 2 !== 0) {
|
|
return {
|
|
type: "invalid-dimensions",
|
|
message: `${codec.toUpperCase()} codec requires width and height to be multiples of 2. Got ${width}x${height}`,
|
|
severity: "error"
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// src/can-render-media-on-web.ts
|
|
var canRenderMediaOnWeb = async (options) => {
|
|
const issues = [];
|
|
if (typeof VideoEncoder === "undefined") {
|
|
issues.push({
|
|
type: "webcodecs-unavailable",
|
|
message: "WebCodecs API is not available in this browser. A modern browser with WebCodecs support is required.",
|
|
severity: "error"
|
|
});
|
|
}
|
|
const container = options.container ?? "mp4";
|
|
const videoCodec = options.videoCodec ?? getDefaultVideoCodecForContainer(container);
|
|
const transparent = options.transparent ?? false;
|
|
const muted = options.muted ?? false;
|
|
const { width, height } = options;
|
|
const resolvedVideoBitrate = typeof options.videoBitrate === "number" ? options.videoBitrate : getQualityForWebRendererQuality(options.videoBitrate ?? "medium");
|
|
const resolvedAudioBitrate = typeof options.audioBitrate === "number" ? options.audioBitrate : getQualityForWebRendererQuality(options.audioBitrate ?? "medium");
|
|
const format = containerToMediabunnyContainer(container);
|
|
if (!format.getSupportedCodecs().includes(codecToMediabunnyCodec(videoCodec))) {
|
|
issues.push({
|
|
type: "container-codec-mismatch",
|
|
message: `Codec ${videoCodec} is not supported for container ${container}`,
|
|
severity: "error"
|
|
});
|
|
}
|
|
const dimensionIssue = validateDimensions({ width, height, codec: videoCodec });
|
|
if (dimensionIssue) {
|
|
issues.push(dimensionIssue);
|
|
}
|
|
const canEncodeVideoResult = await canEncodeVideo(codecToMediabunnyCodec(videoCodec), { bitrate: resolvedVideoBitrate });
|
|
if (!canEncodeVideoResult) {
|
|
issues.push({
|
|
type: "video-codec-unsupported",
|
|
message: `Video codec "${videoCodec}" cannot be encoded by this browser`,
|
|
severity: "error"
|
|
});
|
|
}
|
|
if (transparent && !["vp8", "vp9"].includes(videoCodec)) {
|
|
issues.push({
|
|
type: "transparent-video-unsupported",
|
|
message: `Transparent video requires VP8 or VP9 codec with WebM container. ${videoCodec} does not support alpha channel.`,
|
|
severity: "error"
|
|
});
|
|
}
|
|
let resolvedAudioCodec = null;
|
|
if (!muted) {
|
|
const audioResult = await resolveAudioCodec({
|
|
container,
|
|
requestedCodec: options.audioCodec,
|
|
userSpecifiedAudioCodec: options.audioCodec !== undefined && options.audioCodec !== null,
|
|
bitrate: resolvedAudioBitrate
|
|
});
|
|
resolvedAudioCodec = audioResult.codec;
|
|
issues.push(...audioResult.issues);
|
|
}
|
|
const webglIssue = checkWebGLSupport();
|
|
if (webglIssue) {
|
|
issues.push(webglIssue);
|
|
}
|
|
const canUseWebFs = await canUseWebFsWriter();
|
|
let resolvedOutputTarget;
|
|
if (options.outputTarget === "web-fs") {
|
|
if (!canUseWebFs) {
|
|
issues.push({
|
|
type: "output-target-unsupported",
|
|
message: 'The "web-fs" output target is not supported in this browser. The File System Access API is required.',
|
|
severity: "error"
|
|
});
|
|
}
|
|
resolvedOutputTarget = "web-fs";
|
|
} else if (options.outputTarget === "arraybuffer") {
|
|
resolvedOutputTarget = "arraybuffer";
|
|
} else {
|
|
resolvedOutputTarget = canUseWebFs ? "web-fs" : "arraybuffer";
|
|
}
|
|
return {
|
|
canRender: issues.filter((i) => i.severity === "error").length === 0,
|
|
issues,
|
|
resolvedVideoCodec: videoCodec,
|
|
resolvedAudioCodec,
|
|
resolvedOutputTarget
|
|
};
|
|
};
|
|
// src/get-encodable-codecs.ts
|
|
import {
|
|
getEncodableAudioCodecs as mediabunnyGetEncodableAudioCodecs,
|
|
getEncodableVideoCodecs as mediabunnyGetEncodableVideoCodecs
|
|
} from "mediabunny";
|
|
var getEncodableVideoCodecs = async (container, options) => {
|
|
const supported = getSupportedVideoCodecsForContainer(container);
|
|
const mediabunnyCodecs = supported.map(codecToMediabunnyCodec);
|
|
const resolvedBitrate = options?.videoBitrate ? typeof options.videoBitrate === "number" ? options.videoBitrate : getQualityForWebRendererQuality(options.videoBitrate) : undefined;
|
|
const encodable = await mediabunnyGetEncodableVideoCodecs(mediabunnyCodecs, {
|
|
bitrate: resolvedBitrate
|
|
});
|
|
return supported.filter((c) => encodable.includes(codecToMediabunnyCodec(c)));
|
|
};
|
|
var getEncodableAudioCodecs = async (container, options) => {
|
|
const supported = getSupportedAudioCodecsForContainer(container);
|
|
const resolvedBitrate = options?.audioBitrate ? typeof options.audioBitrate === "number" ? options.audioBitrate : getQualityForWebRendererQuality(options.audioBitrate) : undefined;
|
|
const encodable = await mediabunnyGetEncodableAudioCodecs(supported, {
|
|
bitrate: resolvedBitrate
|
|
});
|
|
return supported.filter((c) => encodable.includes(c));
|
|
};
|
|
// src/render-media-on-web.tsx
|
|
import { BufferTarget, StreamTarget } from "mediabunny";
|
|
import { Internals as Internals8 } from "remotion";
|
|
|
|
// src/add-sample.ts
|
|
import { AudioSample, VideoSample } from "mediabunny";
|
|
var addVideoSampleAndCloseFrame = async (frameToEncode, videoSampleSource) => {
|
|
const sample = new VideoSample(frameToEncode);
|
|
try {
|
|
await videoSampleSource.add(sample);
|
|
} finally {
|
|
sample.close();
|
|
frameToEncode.close();
|
|
}
|
|
};
|
|
var addAudioSample = async (audio, audioSampleSource) => {
|
|
const sample = new AudioSample(audio);
|
|
try {
|
|
await audioSampleSource.add(sample);
|
|
} finally {
|
|
sample.close();
|
|
}
|
|
};
|
|
|
|
// src/artifact.ts
|
|
import { NoReactInternals } from "remotion/no-react";
|
|
var onlyArtifact = async ({
|
|
assets,
|
|
frameBuffer
|
|
}) => {
|
|
const artifacts = assets.filter((asset) => asset.type === "artifact");
|
|
let frameBufferUint8 = null;
|
|
const result = [];
|
|
for (const artifact of artifacts) {
|
|
if (artifact.contentType === "binary" || artifact.contentType === "text") {
|
|
result.push({
|
|
frame: artifact.frame,
|
|
content: artifact.content,
|
|
filename: artifact.filename,
|
|
downloadBehavior: artifact.downloadBehavior
|
|
});
|
|
continue;
|
|
}
|
|
if (artifact.contentType === "thumbnail") {
|
|
if (frameBuffer === null) {
|
|
continue;
|
|
}
|
|
const ab = frameBuffer instanceof Blob ? await frameBuffer.arrayBuffer() : new Uint8Array(await (await frameBuffer.convertToBlob({ type: "image/png" })).arrayBuffer());
|
|
frameBufferUint8 = new Uint8Array(ab);
|
|
result.push({
|
|
frame: artifact.frame,
|
|
content: frameBufferUint8,
|
|
filename: artifact.filename,
|
|
downloadBehavior: artifact.downloadBehavior
|
|
});
|
|
continue;
|
|
}
|
|
throw new Error("Unknown artifact type: " + artifact);
|
|
}
|
|
return result.filter(NoReactInternals.truthy);
|
|
};
|
|
var handleArtifacts = () => {
|
|
const previousArtifacts = [];
|
|
const handle = async ({
|
|
imageData,
|
|
frame,
|
|
assets: artifactAssets,
|
|
onArtifact
|
|
}) => {
|
|
const artifacts = await onlyArtifact({
|
|
assets: artifactAssets,
|
|
frameBuffer: imageData
|
|
});
|
|
for (const artifact of artifacts) {
|
|
const previousArtifact = previousArtifacts.find((a) => a.filename === artifact.filename);
|
|
if (previousArtifact) {
|
|
throw new Error(`An artifact with output "${artifact.filename}" was already registered at frame ${previousArtifact.frame}, but now registered again at frame ${frame}. Artifacts must have unique names. https://remotion.dev/docs/artifacts`);
|
|
}
|
|
onArtifact(artifact);
|
|
previousArtifacts.push({ frame, filename: artifact.filename });
|
|
}
|
|
};
|
|
return { handle };
|
|
};
|
|
|
|
// src/audio.ts
|
|
var TARGET_NUMBER_OF_CHANNELS = 2;
|
|
var TARGET_SAMPLE_RATE = 48000;
|
|
function mixAudio(waves, length) {
|
|
if (waves.length === 1 && waves[0].length === length) {
|
|
return waves[0];
|
|
}
|
|
const mixed = new Int16Array(length);
|
|
if (waves.length === 1) {
|
|
mixed.set(waves[0].subarray(0, length));
|
|
return mixed;
|
|
}
|
|
for (let i = 0;i < length; i++) {
|
|
const sum = waves.reduce((acc, wave) => {
|
|
return acc + (wave[i] ?? 0);
|
|
}, 0);
|
|
mixed[i] = Math.max(-32768, Math.min(32767, sum));
|
|
}
|
|
return mixed;
|
|
}
|
|
var onlyInlineAudio = ({
|
|
assets,
|
|
fps,
|
|
timestamp
|
|
}) => {
|
|
const inlineAudio = assets.filter((asset) => asset.type === "inline-audio");
|
|
if (inlineAudio.length === 0) {
|
|
return null;
|
|
}
|
|
const expectedLength = Math.round(TARGET_NUMBER_OF_CHANNELS * TARGET_SAMPLE_RATE / fps);
|
|
for (const asset of inlineAudio) {
|
|
if (asset.toneFrequency !== 1) {
|
|
throw new Error("Setting the toneFrequency is not supported yet in web rendering.");
|
|
}
|
|
}
|
|
const mixedAudio = mixAudio(inlineAudio.map((asset) => asset.audio), expectedLength);
|
|
return new AudioData({
|
|
data: mixedAudio,
|
|
format: "s16",
|
|
numberOfChannels: TARGET_NUMBER_OF_CHANNELS,
|
|
numberOfFrames: expectedLength / TARGET_NUMBER_OF_CHANNELS,
|
|
sampleRate: TARGET_SAMPLE_RATE,
|
|
timestamp
|
|
});
|
|
};
|
|
|
|
// src/background-keepalive.ts
|
|
import { Internals } from "remotion";
|
|
var WORKER_CODE = `
|
|
let intervalId = null;
|
|
self.onmessage = (e) => {
|
|
if (e.data.type === 'start') {
|
|
if (intervalId !== null) {
|
|
clearInterval(intervalId);
|
|
}
|
|
intervalId = setInterval(() => self.postMessage('tick'), e.data.intervalMs);
|
|
} else if (e.data.type === 'stop') {
|
|
if (intervalId !== null) {
|
|
clearInterval(intervalId);
|
|
intervalId = null;
|
|
}
|
|
}
|
|
};
|
|
`;
|
|
function createBackgroundKeepalive({
|
|
fps,
|
|
logLevel
|
|
}) {
|
|
const intervalMs = Math.round(1000 / fps);
|
|
let pendingResolvers = [];
|
|
let worker = null;
|
|
let disposed = false;
|
|
if (typeof Worker === "undefined") {
|
|
Internals.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, "Web Workers not available. Rendering may pause when tab is backgrounded.");
|
|
return {
|
|
waitForTick: () => {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, intervalMs);
|
|
});
|
|
},
|
|
[Symbol.dispose]: () => {}
|
|
};
|
|
}
|
|
const blob = new Blob([WORKER_CODE], { type: "application/javascript" });
|
|
const workerUrl = URL.createObjectURL(blob);
|
|
worker = new Worker(workerUrl);
|
|
worker.onmessage = () => {
|
|
const resolvers = pendingResolvers;
|
|
pendingResolvers = [];
|
|
for (const resolve of resolvers) {
|
|
resolve();
|
|
}
|
|
};
|
|
worker.onerror = (event) => {
|
|
Internals.Log.error({ logLevel, tag: "@remotion/web-renderer" }, "Background keepalive worker encountered an error and will be terminated.", event);
|
|
const resolvers = pendingResolvers;
|
|
pendingResolvers = [];
|
|
for (const resolve of resolvers) {
|
|
resolve();
|
|
}
|
|
if (!disposed) {
|
|
disposed = true;
|
|
worker?.terminate();
|
|
worker = null;
|
|
URL.revokeObjectURL(workerUrl);
|
|
}
|
|
};
|
|
worker.postMessage({ type: "start", intervalMs });
|
|
return {
|
|
waitForTick: () => {
|
|
return new Promise((resolve) => {
|
|
pendingResolvers.push(resolve);
|
|
});
|
|
},
|
|
[Symbol.dispose]: () => {
|
|
if (disposed) {
|
|
return;
|
|
}
|
|
disposed = true;
|
|
worker?.postMessage({ type: "stop" });
|
|
worker?.terminate();
|
|
worker = null;
|
|
URL.revokeObjectURL(workerUrl);
|
|
const resolvers = pendingResolvers;
|
|
pendingResolvers = [];
|
|
for (const resolve of resolvers) {
|
|
resolve();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// src/create-audio-sample-source.ts
|
|
import { AudioSampleSource } from "mediabunny";
|
|
var createAudioSampleSource = ({
|
|
muted,
|
|
codec,
|
|
bitrate
|
|
}) => {
|
|
if (muted || codec === null) {
|
|
return null;
|
|
}
|
|
const audioSampleSource = new AudioSampleSource({
|
|
codec,
|
|
bitrate
|
|
});
|
|
return { audioSampleSource, [Symbol.dispose]: () => audioSampleSource.close() };
|
|
};
|
|
|
|
// src/create-scaffold.tsx
|
|
import { createRef } from "react";
|
|
import { flushSync as flushSync2 } from "react-dom";
|
|
import ReactDOM from "react-dom/client";
|
|
import { Internals as Internals3 } from "remotion";
|
|
|
|
// src/update-time.tsx
|
|
import { useImperativeHandle, useState } from "react";
|
|
import { flushSync } from "react-dom";
|
|
import { Internals as Internals2 } from "remotion";
|
|
import { jsx } from "react/jsx-runtime";
|
|
var UpdateTime = ({
|
|
children,
|
|
audioEnabled,
|
|
videoEnabled,
|
|
logLevel,
|
|
compId,
|
|
initialFrame,
|
|
timeUpdater
|
|
}) => {
|
|
const [frame, setFrame] = useState(initialFrame);
|
|
useImperativeHandle(timeUpdater, () => ({
|
|
update: (f) => {
|
|
flushSync(() => {
|
|
setFrame(f);
|
|
});
|
|
}
|
|
}));
|
|
return /* @__PURE__ */ jsx(Internals2.RemotionRootContexts, {
|
|
audioEnabled,
|
|
videoEnabled,
|
|
logLevel,
|
|
numberOfAudioTags: 0,
|
|
audioLatencyHint: "interactive",
|
|
frameState: {
|
|
[compId]: frame
|
|
},
|
|
nonceContextSeed: 0,
|
|
children
|
|
});
|
|
};
|
|
|
|
// src/create-scaffold.tsx
|
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
function checkForError(errorHolder) {
|
|
if (errorHolder.error) {
|
|
throw errorHolder.error;
|
|
}
|
|
}
|
|
function createScaffold({
|
|
width,
|
|
height,
|
|
delayRenderTimeoutInMilliseconds,
|
|
logLevel,
|
|
resolvedProps,
|
|
id,
|
|
mediaCacheSizeInBytes,
|
|
durationInFrames,
|
|
fps,
|
|
initialFrame,
|
|
schema,
|
|
Component,
|
|
audioEnabled,
|
|
videoEnabled,
|
|
defaultCodec,
|
|
defaultOutName
|
|
}) {
|
|
if (!ReactDOM.createRoot) {
|
|
throw new Error("@remotion/web-renderer requires React 18 or higher");
|
|
}
|
|
const div = document.createElement("div");
|
|
div.style.position = "fixed";
|
|
div.style.display = "flex";
|
|
div.style.flexDirection = "column";
|
|
div.style.backgroundColor = "transparent";
|
|
div.style.width = `${width}px`;
|
|
div.style.height = `${height}px`;
|
|
div.style.zIndex = "-9999";
|
|
div.style.top = "0";
|
|
div.style.left = "0";
|
|
div.style.right = "0";
|
|
div.style.bottom = "0";
|
|
div.style.visibility = "hidden";
|
|
div.style.pointerEvents = "none";
|
|
const scaffoldClassName = `remotion-scaffold-${Math.random().toString(36).substring(2, 15)}`;
|
|
div.className = scaffoldClassName;
|
|
const cleanupCSS = Internals3.CSSUtils.injectCSS(Internals3.CSSUtils.makeDefaultPreviewCSS(`.${scaffoldClassName}`, "white"));
|
|
document.body.appendChild(div);
|
|
const errorHolder = { error: null };
|
|
const root = ReactDOM.createRoot(div, {
|
|
onUncaughtError: (err) => {
|
|
errorHolder.error = err instanceof Error ? err : new Error(String(err));
|
|
}
|
|
});
|
|
const delayRenderScope = {
|
|
remotion_renderReady: true,
|
|
remotion_delayRenderTimeouts: {},
|
|
remotion_puppeteerTimeout: delayRenderTimeoutInMilliseconds,
|
|
remotion_attempt: 0,
|
|
remotion_delayRenderHandles: []
|
|
};
|
|
const timeUpdater = createRef();
|
|
const collectAssets = createRef();
|
|
flushSync2(() => {
|
|
root.render(/* @__PURE__ */ jsx2(Internals3.MaxMediaCacheSizeContext.Provider, {
|
|
value: mediaCacheSizeInBytes,
|
|
children: /* @__PURE__ */ jsx2(Internals3.RemotionEnvironmentContext.Provider, {
|
|
value: {
|
|
isStudio: false,
|
|
isRendering: true,
|
|
isPlayer: false,
|
|
isReadOnlyStudio: false,
|
|
isClientSideRendering: true
|
|
},
|
|
children: /* @__PURE__ */ jsx2(Internals3.DelayRenderContextType.Provider, {
|
|
value: delayRenderScope,
|
|
children: /* @__PURE__ */ jsx2(Internals3.CompositionManager.Provider, {
|
|
value: {
|
|
compositions: [
|
|
{
|
|
id,
|
|
component: Component,
|
|
nonce: 0,
|
|
defaultProps: {},
|
|
folderName: null,
|
|
parentFolderName: null,
|
|
schema: schema ?? null,
|
|
calculateMetadata: null,
|
|
durationInFrames,
|
|
fps,
|
|
height,
|
|
width
|
|
}
|
|
],
|
|
canvasContent: {
|
|
type: "composition",
|
|
compositionId: id
|
|
},
|
|
currentCompositionMetadata: {
|
|
props: resolvedProps,
|
|
durationInFrames,
|
|
fps,
|
|
height,
|
|
width,
|
|
defaultCodec: defaultCodec ?? null,
|
|
defaultOutName: defaultOutName ?? null,
|
|
defaultVideoImageFormat: null,
|
|
defaultPixelFormat: null,
|
|
defaultProResProfile: null
|
|
},
|
|
folders: []
|
|
},
|
|
children: /* @__PURE__ */ jsx2(Internals3.RenderAssetManagerProvider, {
|
|
collectAssets,
|
|
children: /* @__PURE__ */ jsx2(UpdateTime, {
|
|
audioEnabled,
|
|
videoEnabled,
|
|
logLevel,
|
|
compId: id,
|
|
initialFrame,
|
|
timeUpdater,
|
|
children: /* @__PURE__ */ jsx2(Internals3.CanUseRemotionHooks.Provider, {
|
|
value: true,
|
|
children: /* @__PURE__ */ jsx2(Component, {
|
|
...resolvedProps
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}));
|
|
});
|
|
return {
|
|
delayRenderScope,
|
|
div,
|
|
errorHolder,
|
|
[Symbol.dispose]: () => {
|
|
root.unmount();
|
|
div.remove();
|
|
cleanupCSS();
|
|
},
|
|
timeUpdater,
|
|
collectAssets
|
|
};
|
|
}
|
|
|
|
// src/frame-range.ts
|
|
var getRealFrameRange = (durationInFrames, frameRange) => {
|
|
if (frameRange === null) {
|
|
return [0, durationInFrames - 1];
|
|
}
|
|
if (typeof frameRange === "number") {
|
|
if (frameRange < 0 || frameRange >= durationInFrames) {
|
|
throw new Error(`Frame number is out of range, must be between 0 and ${durationInFrames - 1} but got ${frameRange}`);
|
|
}
|
|
return [frameRange, frameRange];
|
|
}
|
|
if (frameRange[1] >= durationInFrames || frameRange[0] < 0) {
|
|
throw new Error(`The "durationInFrames" of the composition was evaluated to be ${durationInFrames}, but frame range ${frameRange.join("-")} is not inbetween 0-${durationInFrames - 1}`);
|
|
}
|
|
return frameRange;
|
|
};
|
|
|
|
// src/internal-state.ts
|
|
var makeInternalState = () => {
|
|
let drawnPrecomposedPixels = 0;
|
|
let precomposedTextures = 0;
|
|
let waitForReadyTime = 0;
|
|
let addSampleTime = 0;
|
|
let createFrameTime = 0;
|
|
let audioMixingTime = 0;
|
|
const helperCanvasState = {
|
|
current: null
|
|
};
|
|
return {
|
|
getDrawn3dPixels: () => drawnPrecomposedPixels,
|
|
getPrecomposedTiles: () => precomposedTextures,
|
|
addPrecompose: ({
|
|
canvasWidth,
|
|
canvasHeight
|
|
}) => {
|
|
drawnPrecomposedPixels += canvasWidth * canvasHeight;
|
|
precomposedTextures++;
|
|
},
|
|
helperCanvasState,
|
|
[Symbol.dispose]: () => {
|
|
if (helperCanvasState.current) {
|
|
helperCanvasState.current.cleanup();
|
|
}
|
|
},
|
|
getWaitForReadyTime: () => waitForReadyTime,
|
|
addWaitForReadyTime: (time) => {
|
|
waitForReadyTime += time;
|
|
},
|
|
getAddSampleTime: () => addSampleTime,
|
|
addAddSampleTime: (time) => {
|
|
addSampleTime += time;
|
|
},
|
|
getCreateFrameTime: () => createFrameTime,
|
|
addCreateFrameTime: (time) => {
|
|
createFrameTime += time;
|
|
},
|
|
getAudioMixingTime: () => audioMixingTime,
|
|
addAudioMixingTime: (time) => {
|
|
audioMixingTime += time;
|
|
}
|
|
};
|
|
};
|
|
|
|
// src/mediabunny-cleanups.ts
|
|
import { Output, VideoSampleSource } from "mediabunny";
|
|
var makeOutputWithCleanup = (options) => {
|
|
const output = new Output(options);
|
|
return {
|
|
output,
|
|
[Symbol.dispose]: () => {
|
|
if (output.state === "finalized" || output.state === "canceled") {
|
|
return;
|
|
}
|
|
output.cancel();
|
|
}
|
|
};
|
|
};
|
|
var makeVideoSampleSourceCleanup = (encodingConfig) => {
|
|
const videoSampleSource = new VideoSampleSource(encodingConfig);
|
|
return {
|
|
videoSampleSource,
|
|
[Symbol.dispose]: () => {
|
|
videoSampleSource.close();
|
|
}
|
|
};
|
|
};
|
|
|
|
// src/render-operations-queue.ts
|
|
var onlyOneRenderAtATimeQueue = {
|
|
ref: Promise.resolve()
|
|
};
|
|
|
|
// ../licensing/dist/esm/index.mjs
|
|
function isNetworkError(error) {
|
|
if (error.message.includes("Failed to fetch") || error.message.includes("Load failed") || error.message.includes("NetworkError when attempting to fetch resource")) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
var HOST = "https://www.remotion.pro";
|
|
var DEFAULT_MAX_RETRIES = 3;
|
|
var exponentialBackoffMs = (attempt) => {
|
|
return 1000 * 2 ** (attempt - 1);
|
|
};
|
|
var sleep = (ms) => {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
};
|
|
var internalRegisterUsageEvent = async ({
|
|
host,
|
|
succeeded,
|
|
event,
|
|
isStill,
|
|
isProduction,
|
|
licenseKey
|
|
}) => {
|
|
let lastError;
|
|
const totalAttempts = DEFAULT_MAX_RETRIES + 1;
|
|
for (let attempt = 1;attempt <= totalAttempts; attempt++) {
|
|
const abortController = new AbortController;
|
|
const timeout = setTimeout(() => {
|
|
abortController.abort();
|
|
}, 1e4);
|
|
try {
|
|
const res = await fetch(`${HOST}/api/track/register-usage-point`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
event,
|
|
apiKey: licenseKey,
|
|
host,
|
|
succeeded,
|
|
isStill,
|
|
isProduction
|
|
}),
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
signal: abortController.signal
|
|
});
|
|
clearTimeout(timeout);
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
return {
|
|
billable: json.billable,
|
|
classification: json.classification
|
|
};
|
|
}
|
|
if (!res.ok) {
|
|
throw new Error(json.error);
|
|
}
|
|
throw new Error(`Unexpected response from server: ${JSON.stringify(json)}`);
|
|
} catch (err) {
|
|
clearTimeout(timeout);
|
|
const error = err;
|
|
const isTimeout = error.name === "AbortError";
|
|
const isRetryable = isNetworkError(error) || isTimeout;
|
|
if (!isRetryable) {
|
|
throw err;
|
|
}
|
|
lastError = isTimeout ? new Error("Request timed out after 10 seconds") : error;
|
|
if (attempt < totalAttempts) {
|
|
const backoffMs = exponentialBackoffMs(attempt);
|
|
console.log(`Failed to send usage event (attempt ${attempt}/${totalAttempts}), retrying in ${backoffMs}ms...`, err);
|
|
await sleep(backoffMs);
|
|
}
|
|
}
|
|
}
|
|
throw lastError;
|
|
};
|
|
var LicensingInternals = {
|
|
internalRegisterUsageEvent
|
|
};
|
|
|
|
// src/send-telemetry-event.ts
|
|
import { Internals as Internals4 } from "remotion";
|
|
var sendUsageEvent = async ({
|
|
licenseKey,
|
|
succeeded,
|
|
apiName,
|
|
isStill,
|
|
isProduction
|
|
}) => {
|
|
const host = typeof window === "undefined" ? null : typeof window.location === "undefined" ? null : window.location.origin ?? null;
|
|
if (host === null) {
|
|
return;
|
|
}
|
|
if (licenseKey === null) {
|
|
Internals4.Log.warn({ logLevel: "warn", tag: "web-renderer" }, `Pass "licenseKey" to ${apiName}(). If you qualify for the Free License (https://remotion.dev/license), pass "free-license" instead.`);
|
|
}
|
|
await LicensingInternals.internalRegisterUsageEvent({
|
|
licenseKey: licenseKey === "free-license" ? null : licenseKey,
|
|
event: "webcodec-conversion",
|
|
host,
|
|
succeeded,
|
|
isStill,
|
|
isProduction
|
|
});
|
|
};
|
|
|
|
// src/tree-walker-cleanup-after-children.ts
|
|
var createTreeWalkerCleanupAfterChildren = (treeWalker) => {
|
|
const cleanupAfterChildren = [];
|
|
const checkCleanUpAtBeginningOfIteration = () => {
|
|
for (let i = 0;i < cleanupAfterChildren.length; ) {
|
|
const cleanup = cleanupAfterChildren[i];
|
|
if (!(cleanup.element === treeWalker.currentNode || cleanup.element.contains(treeWalker.currentNode))) {
|
|
cleanup.cleanupFn();
|
|
cleanupAfterChildren.splice(i, 1);
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
};
|
|
const addCleanup = (element, cleanupFn) => {
|
|
cleanupAfterChildren.unshift({
|
|
element,
|
|
cleanupFn
|
|
});
|
|
};
|
|
const cleanupInTheEndOfTheIteration = () => {
|
|
for (const cleanup of cleanupAfterChildren) {
|
|
cleanup.cleanupFn();
|
|
}
|
|
};
|
|
return {
|
|
checkCleanUpAtBeginningOfIteration,
|
|
addCleanup,
|
|
[Symbol.dispose]: cleanupInTheEndOfTheIteration
|
|
};
|
|
};
|
|
|
|
// src/drawing/calculate-object-fit.ts
|
|
var calculateFill = ({
|
|
containerSize,
|
|
intrinsicSize
|
|
}) => {
|
|
return {
|
|
sourceX: 0,
|
|
sourceY: 0,
|
|
sourceWidth: intrinsicSize.width,
|
|
sourceHeight: intrinsicSize.height,
|
|
destX: containerSize.left,
|
|
destY: containerSize.top,
|
|
destWidth: containerSize.width,
|
|
destHeight: containerSize.height
|
|
};
|
|
};
|
|
var calculateContain = ({
|
|
containerSize,
|
|
intrinsicSize
|
|
}) => {
|
|
const containerAspect = containerSize.width / containerSize.height;
|
|
const imageAspect = intrinsicSize.width / intrinsicSize.height;
|
|
let destWidth;
|
|
let destHeight;
|
|
if (imageAspect > containerAspect) {
|
|
destWidth = containerSize.width;
|
|
destHeight = containerSize.width / imageAspect;
|
|
} else {
|
|
destHeight = containerSize.height;
|
|
destWidth = containerSize.height * imageAspect;
|
|
}
|
|
const destX = containerSize.left + (containerSize.width - destWidth) / 2;
|
|
const destY = containerSize.top + (containerSize.height - destHeight) / 2;
|
|
return {
|
|
sourceX: 0,
|
|
sourceY: 0,
|
|
sourceWidth: intrinsicSize.width,
|
|
sourceHeight: intrinsicSize.height,
|
|
destX,
|
|
destY,
|
|
destWidth,
|
|
destHeight
|
|
};
|
|
};
|
|
var calculateCover = ({
|
|
containerSize,
|
|
intrinsicSize
|
|
}) => {
|
|
if (containerSize.height <= 0 || intrinsicSize.height <= 0) {
|
|
return {
|
|
sourceX: 0,
|
|
sourceY: 0,
|
|
sourceWidth: 0,
|
|
sourceHeight: 0,
|
|
destX: containerSize.left,
|
|
destY: containerSize.top,
|
|
destWidth: 0,
|
|
destHeight: 0
|
|
};
|
|
}
|
|
const containerAspect = containerSize.width / containerSize.height;
|
|
const imageAspect = intrinsicSize.width / intrinsicSize.height;
|
|
let sourceX = 0;
|
|
let sourceY = 0;
|
|
let sourceWidth = intrinsicSize.width;
|
|
let sourceHeight = intrinsicSize.height;
|
|
if (imageAspect > containerAspect) {
|
|
sourceWidth = intrinsicSize.height * containerAspect;
|
|
sourceX = (intrinsicSize.width - sourceWidth) / 2;
|
|
} else {
|
|
sourceHeight = intrinsicSize.width / containerAspect;
|
|
sourceY = (intrinsicSize.height - sourceHeight) / 2;
|
|
}
|
|
return {
|
|
sourceX,
|
|
sourceY,
|
|
sourceWidth,
|
|
sourceHeight,
|
|
destX: containerSize.left,
|
|
destY: containerSize.top,
|
|
destWidth: containerSize.width,
|
|
destHeight: containerSize.height
|
|
};
|
|
};
|
|
var calculateNone = ({
|
|
containerSize,
|
|
intrinsicSize
|
|
}) => {
|
|
const centeredX = containerSize.left + (containerSize.width - intrinsicSize.width) / 2;
|
|
const centeredY = containerSize.top + (containerSize.height - intrinsicSize.height) / 2;
|
|
let sourceX = 0;
|
|
let sourceY = 0;
|
|
let sourceWidth = intrinsicSize.width;
|
|
let sourceHeight = intrinsicSize.height;
|
|
let destX = centeredX;
|
|
let destY = centeredY;
|
|
let destWidth = intrinsicSize.width;
|
|
let destHeight = intrinsicSize.height;
|
|
if (destX < containerSize.left) {
|
|
const clipAmount = containerSize.left - destX;
|
|
sourceX = clipAmount;
|
|
sourceWidth -= clipAmount;
|
|
destX = containerSize.left;
|
|
destWidth -= clipAmount;
|
|
}
|
|
if (destY < containerSize.top) {
|
|
const clipAmount = containerSize.top - destY;
|
|
sourceY = clipAmount;
|
|
sourceHeight -= clipAmount;
|
|
destY = containerSize.top;
|
|
destHeight -= clipAmount;
|
|
}
|
|
const containerRight = containerSize.left + containerSize.width;
|
|
if (destX + destWidth > containerRight) {
|
|
const clipAmount = destX + destWidth - containerRight;
|
|
sourceWidth -= clipAmount;
|
|
destWidth -= clipAmount;
|
|
}
|
|
const containerBottom = containerSize.top + containerSize.height;
|
|
if (destY + destHeight > containerBottom) {
|
|
const clipAmount = destY + destHeight - containerBottom;
|
|
sourceHeight -= clipAmount;
|
|
destHeight -= clipAmount;
|
|
}
|
|
return {
|
|
sourceX,
|
|
sourceY,
|
|
sourceWidth,
|
|
sourceHeight,
|
|
destX,
|
|
destY,
|
|
destWidth,
|
|
destHeight
|
|
};
|
|
};
|
|
var calculateObjectFit = ({
|
|
objectFit,
|
|
containerSize,
|
|
intrinsicSize
|
|
}) => {
|
|
switch (objectFit) {
|
|
case "fill":
|
|
return calculateFill({ containerSize, intrinsicSize });
|
|
case "contain":
|
|
return calculateContain({ containerSize, intrinsicSize });
|
|
case "cover":
|
|
return calculateCover({ containerSize, intrinsicSize });
|
|
case "none":
|
|
return calculateNone({ containerSize, intrinsicSize });
|
|
case "scale-down": {
|
|
const containResult = calculateContain({ containerSize, intrinsicSize });
|
|
const noneResult = calculateNone({ containerSize, intrinsicSize });
|
|
const containArea = containResult.destWidth * containResult.destHeight;
|
|
const noneArea = noneResult.destWidth * noneResult.destHeight;
|
|
return containArea < noneArea ? containResult : noneResult;
|
|
}
|
|
default: {
|
|
const exhaustiveCheck = objectFit;
|
|
throw new Error(`Unknown object-fit value: ${exhaustiveCheck}`);
|
|
}
|
|
}
|
|
};
|
|
var parseObjectFit = (value) => {
|
|
if (!value) {
|
|
return "fill";
|
|
}
|
|
const normalized = value.trim().toLowerCase();
|
|
switch (normalized) {
|
|
case "fill":
|
|
case "contain":
|
|
case "cover":
|
|
case "none":
|
|
case "scale-down":
|
|
return normalized;
|
|
default:
|
|
return "fill";
|
|
}
|
|
};
|
|
|
|
// src/drawing/fit-svg-into-its-dimensions.ts
|
|
var fitSvgIntoItsContainer = ({
|
|
containerSize,
|
|
elementSize
|
|
}) => {
|
|
if (Math.round(containerSize.width) === Math.round(elementSize.width) && Math.round(containerSize.height) === Math.round(elementSize.height)) {
|
|
return {
|
|
width: containerSize.width,
|
|
height: containerSize.height,
|
|
top: containerSize.top,
|
|
left: containerSize.left
|
|
};
|
|
}
|
|
if (containerSize.width <= 0 || containerSize.height <= 0) {
|
|
throw new Error(`Container must have positive dimensions, but got ${containerSize.width}x${containerSize.height}`);
|
|
}
|
|
if (elementSize.width <= 0 || elementSize.height <= 0) {
|
|
throw new Error(`Element must have positive dimensions, but got ${elementSize.width}x${elementSize.height}`);
|
|
}
|
|
const heightRatio = containerSize.height / elementSize.height;
|
|
const widthRatio = containerSize.width / elementSize.width;
|
|
const ratio = Math.min(heightRatio, widthRatio);
|
|
const newWidth = elementSize.width * ratio;
|
|
const newHeight = elementSize.height * ratio;
|
|
if (newWidth > containerSize.width + 0.000001 || newHeight > containerSize.height + 0.000001) {
|
|
throw new Error(`Element is too big to fit into the container. Max size: ${containerSize.width}x${containerSize.height}, element size: ${newWidth}x${newHeight}`);
|
|
}
|
|
return {
|
|
width: newWidth,
|
|
height: newHeight,
|
|
top: (containerSize.height - newHeight) / 2 + containerSize.top,
|
|
left: (containerSize.width - newWidth) / 2 + containerSize.left
|
|
};
|
|
};
|
|
|
|
// src/drawing/turn-svg-into-drawable.ts
|
|
var turnSvgIntoDrawable = (svg) => {
|
|
const { fill, color } = getComputedStyle(svg);
|
|
const originalTransform = svg.style.transform;
|
|
const originalTransformOrigin = svg.style.transformOrigin;
|
|
const originalMarginLeft = svg.style.marginLeft;
|
|
const originalMarginRight = svg.style.marginRight;
|
|
const originalMarginTop = svg.style.marginTop;
|
|
const originalMarginBottom = svg.style.marginBottom;
|
|
const originalFill = svg.style.fill;
|
|
const originalColor = svg.style.color;
|
|
svg.style.transform = "none";
|
|
svg.style.transformOrigin = "";
|
|
svg.style.marginLeft = "0";
|
|
svg.style.marginRight = "0";
|
|
svg.style.marginTop = "0";
|
|
svg.style.marginBottom = "0";
|
|
svg.style.fill = fill;
|
|
svg.style.color = color;
|
|
const svgData = new XMLSerializer().serializeToString(svg);
|
|
svg.style.marginLeft = originalMarginLeft;
|
|
svg.style.marginRight = originalMarginRight;
|
|
svg.style.marginTop = originalMarginTop;
|
|
svg.style.marginBottom = originalMarginBottom;
|
|
svg.style.transform = originalTransform;
|
|
svg.style.transformOrigin = originalTransformOrigin;
|
|
svg.style.fill = originalFill;
|
|
svg.style.color = originalColor;
|
|
return new Promise((resolve, reject) => {
|
|
const image = new Image;
|
|
const url = `data:image/svg+xml;base64,${btoa(svgData)}`;
|
|
image.onload = function() {
|
|
resolve(image);
|
|
};
|
|
image.onerror = () => {
|
|
reject(new Error("Failed to convert SVG to image"));
|
|
};
|
|
image.src = url;
|
|
});
|
|
};
|
|
|
|
// src/drawing/draw-dom-element.ts
|
|
var getReadableImageError = (err, node) => {
|
|
if (!(err instanceof DOMException)) {
|
|
return null;
|
|
}
|
|
if (err.name === "SecurityError") {
|
|
return new Error(`Could not draw image with src="${node.src}" to canvas: ` + `The image is tainted due to CORS restrictions. ` + `The server hosting this image must respond with the "Access-Control-Allow-Origin" header. ` + `See: https://remotion.dev/docs/client-side-rendering/migration`);
|
|
}
|
|
if (err.name === "InvalidStateError") {
|
|
return new Error(`Could not draw image with src="${node.src}" to canvas: ` + `The image is in a broken state. ` + `This usually means the image failed to load - check that the URL is valid and accessible.`);
|
|
}
|
|
return null;
|
|
};
|
|
var drawSvg = ({
|
|
drawable,
|
|
dimensions,
|
|
contextToDraw
|
|
}) => {
|
|
const fitted = fitSvgIntoItsContainer({
|
|
containerSize: dimensions,
|
|
elementSize: {
|
|
width: drawable.width,
|
|
height: drawable.height
|
|
}
|
|
});
|
|
contextToDraw.drawImage(drawable, fitted.left, fitted.top, fitted.width, fitted.height);
|
|
};
|
|
var drawReplacedElement = ({
|
|
drawable,
|
|
dimensions,
|
|
computedStyle,
|
|
contextToDraw
|
|
}) => {
|
|
const objectFit = parseObjectFit(computedStyle.objectFit);
|
|
const intrinsicSize = drawable instanceof HTMLImageElement ? { width: drawable.naturalWidth, height: drawable.naturalHeight } : { width: drawable.width, height: drawable.height };
|
|
const result = calculateObjectFit({
|
|
objectFit,
|
|
containerSize: {
|
|
width: dimensions.width,
|
|
height: dimensions.height,
|
|
left: dimensions.left,
|
|
top: dimensions.top
|
|
},
|
|
intrinsicSize
|
|
});
|
|
contextToDraw.drawImage(drawable, result.sourceX, result.sourceY, result.sourceWidth, result.sourceHeight, result.destX, result.destY, result.destWidth, result.destHeight);
|
|
};
|
|
var drawDomElement = (node) => {
|
|
const domDrawFn = async ({
|
|
dimensions,
|
|
contextToDraw,
|
|
computedStyle
|
|
}) => {
|
|
if (node instanceof SVGSVGElement) {
|
|
const drawable = await turnSvgIntoDrawable(node);
|
|
drawSvg({ drawable, dimensions, contextToDraw });
|
|
return;
|
|
}
|
|
if (node instanceof HTMLImageElement || node instanceof HTMLCanvasElement) {
|
|
try {
|
|
drawReplacedElement({
|
|
drawable: node,
|
|
dimensions,
|
|
computedStyle,
|
|
contextToDraw
|
|
});
|
|
} catch (err) {
|
|
if (node instanceof HTMLImageElement) {
|
|
const readableError = getReadableImageError(err, node);
|
|
if (readableError) {
|
|
throw readableError;
|
|
}
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
};
|
|
return domDrawFn;
|
|
};
|
|
|
|
// src/drawing/process-node.ts
|
|
import { Internals as Internals6 } from "remotion";
|
|
|
|
// src/drawing/has-transform.ts
|
|
var hasTransformCssValue = (style) => {
|
|
return style.transform !== "none" && style.transform !== "";
|
|
};
|
|
var hasRotateCssValue = (style) => {
|
|
return style.rotate !== "none" && style.rotate !== "";
|
|
};
|
|
var hasScaleCssValue = (style) => {
|
|
return style.scale !== "none" && style.scale !== "";
|
|
};
|
|
var hasAnyTransformCssValue = (style) => {
|
|
return hasTransformCssValue(style) || hasRotateCssValue(style) || hasScaleCssValue(style);
|
|
};
|
|
|
|
// src/drawing/parse-linear-gradient.ts
|
|
import { NoReactInternals as NoReactInternals2 } from "remotion/no-react";
|
|
var isValidColor = (color) => {
|
|
try {
|
|
const result = NoReactInternals2.processColor(color);
|
|
return result !== null && result !== undefined;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
var parseDirection = (directionStr) => {
|
|
const trimmed = directionStr.trim().toLowerCase();
|
|
if (trimmed.startsWith("to ")) {
|
|
const direction = trimmed.substring(3).trim();
|
|
switch (direction) {
|
|
case "top":
|
|
return 0;
|
|
case "right":
|
|
return 90;
|
|
case "bottom":
|
|
return 180;
|
|
case "left":
|
|
return 270;
|
|
case "top right":
|
|
case "right top":
|
|
return 45;
|
|
case "bottom right":
|
|
case "right bottom":
|
|
return 135;
|
|
case "bottom left":
|
|
case "left bottom":
|
|
return 225;
|
|
case "top left":
|
|
case "left top":
|
|
return 315;
|
|
default:
|
|
return 180;
|
|
}
|
|
}
|
|
const angleMatch = trimmed.match(/^(-?\d+\.?\d*)(deg|rad|grad|turn)$/);
|
|
if (angleMatch) {
|
|
const value = parseFloat(angleMatch[1]);
|
|
const unit = angleMatch[2];
|
|
switch (unit) {
|
|
case "deg":
|
|
return value;
|
|
case "rad":
|
|
return value * 180 / Math.PI;
|
|
case "grad":
|
|
return value * 360 / 400;
|
|
case "turn":
|
|
return value * 360;
|
|
default:
|
|
return value;
|
|
}
|
|
}
|
|
return 180;
|
|
};
|
|
var parseColorStops = (colorStopsStr) => {
|
|
const parts = colorStopsStr.split(/,(?![^(]*\))/);
|
|
const stops = [];
|
|
for (const part of parts) {
|
|
const trimmed = part.trim();
|
|
if (!trimmed)
|
|
continue;
|
|
const colorMatch = trimmed.match(/(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i);
|
|
if (!colorMatch) {
|
|
continue;
|
|
}
|
|
const colorStr = colorMatch[0];
|
|
if (!isValidColor(colorStr)) {
|
|
continue;
|
|
}
|
|
const remaining = trimmed.substring(colorMatch.index + colorStr.length).trim();
|
|
const normalizedColor = colorStr;
|
|
let position = null;
|
|
if (remaining) {
|
|
const posMatch = remaining.match(/(-?\d+\.?\d*)(%|px)?/);
|
|
if (posMatch) {
|
|
const value = parseFloat(posMatch[1]);
|
|
const unit = posMatch[2];
|
|
if (unit === "%") {
|
|
position = value / 100;
|
|
} else if (unit === "px") {
|
|
position = null;
|
|
} else {
|
|
position = value / 100;
|
|
}
|
|
}
|
|
}
|
|
stops.push({
|
|
color: normalizedColor,
|
|
position: position !== null ? position : -1
|
|
});
|
|
}
|
|
if (stops.length === 0) {
|
|
return null;
|
|
}
|
|
let lastExplicitIndex = -1;
|
|
let lastExplicitPosition = 0;
|
|
for (let i = 0;i < stops.length; i++) {
|
|
if (stops[i].position !== -1) {
|
|
if (lastExplicitIndex >= 0) {
|
|
const numImplicit = i - lastExplicitIndex - 1;
|
|
if (numImplicit > 0) {
|
|
const step = (stops[i].position - lastExplicitPosition) / (numImplicit + 1);
|
|
for (let j = lastExplicitIndex + 1;j < i; j++) {
|
|
stops[j].position = lastExplicitPosition + step * (j - lastExplicitIndex);
|
|
}
|
|
}
|
|
} else {
|
|
const numImplicit = i;
|
|
if (numImplicit > 0) {
|
|
const step = stops[i].position / (numImplicit + 1);
|
|
for (let j = 0;j < i; j++) {
|
|
stops[j].position = step * (j + 1);
|
|
}
|
|
}
|
|
}
|
|
lastExplicitIndex = i;
|
|
lastExplicitPosition = stops[i].position;
|
|
}
|
|
}
|
|
if (stops.every((s) => s.position === -1)) {
|
|
if (stops.length === 1) {
|
|
stops[0].position = 0.5;
|
|
} else {
|
|
for (let i = 0;i < stops.length; i++) {
|
|
stops[i].position = i / (stops.length - 1);
|
|
}
|
|
}
|
|
} else if (lastExplicitIndex < stops.length - 1) {
|
|
const numImplicit = stops.length - 1 - lastExplicitIndex;
|
|
const step = (1 - lastExplicitPosition) / (numImplicit + 1);
|
|
for (let i = lastExplicitIndex + 1;i < stops.length; i++) {
|
|
stops[i].position = lastExplicitPosition + step * (i - lastExplicitIndex);
|
|
}
|
|
}
|
|
for (const stop of stops) {
|
|
stop.position = Math.max(0, Math.min(1, stop.position));
|
|
}
|
|
return stops;
|
|
};
|
|
var extractGradientContent = (backgroundImage) => {
|
|
const prefix = "linear-gradient(";
|
|
const startIndex = backgroundImage.toLowerCase().indexOf(prefix);
|
|
if (startIndex === -1) {
|
|
return null;
|
|
}
|
|
let depth = 0;
|
|
const contentStart = startIndex + prefix.length;
|
|
for (let i = contentStart;i < backgroundImage.length; i++) {
|
|
const char = backgroundImage[i];
|
|
if (char === "(") {
|
|
depth++;
|
|
} else if (char === ")") {
|
|
if (depth === 0) {
|
|
return backgroundImage.substring(contentStart, i).trim();
|
|
}
|
|
depth--;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
var parseLinearGradient = (backgroundImage) => {
|
|
if (!backgroundImage || backgroundImage === "none") {
|
|
return null;
|
|
}
|
|
const content = extractGradientContent(backgroundImage);
|
|
if (!content) {
|
|
return null;
|
|
}
|
|
const parts = content.split(/,(?![^(]*\))/);
|
|
let angle = 180;
|
|
let colorStopsStart = 0;
|
|
if (parts.length > 0) {
|
|
const firstPart = parts[0].trim();
|
|
const isDirection = firstPart.startsWith("to ") || /^-?\d+\.?\d*(deg|rad|grad|turn)$/.test(firstPart);
|
|
if (isDirection) {
|
|
angle = parseDirection(firstPart);
|
|
colorStopsStart = 1;
|
|
}
|
|
}
|
|
const colorStopsStr = parts.slice(colorStopsStart).join(",");
|
|
const colorStops = parseColorStops(colorStopsStr);
|
|
if (!colorStops || colorStops.length === 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
angle,
|
|
colorStops
|
|
};
|
|
};
|
|
var createCanvasGradient = ({
|
|
ctx,
|
|
rect,
|
|
gradientInfo,
|
|
offsetLeft,
|
|
offsetTop
|
|
}) => {
|
|
const angleRad = (gradientInfo.angle - 90) * Math.PI / 180;
|
|
const centerX = rect.left - offsetLeft + rect.width / 2;
|
|
const centerY = rect.top - offsetTop + rect.height / 2;
|
|
const cos = Math.cos(angleRad);
|
|
const sin = Math.sin(angleRad);
|
|
const halfWidth = rect.width / 2;
|
|
const halfHeight = rect.height / 2;
|
|
let length = Math.abs(cos) * halfWidth + Math.abs(sin) * halfHeight;
|
|
if (!Number.isFinite(length) || length === 0) {
|
|
length = Math.sqrt(halfWidth ** 2 + halfHeight ** 2);
|
|
}
|
|
const x0 = centerX - cos * length;
|
|
const y0 = centerY - sin * length;
|
|
const x1 = centerX + cos * length;
|
|
const y1 = centerY + sin * length;
|
|
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
for (const stop of gradientInfo.colorStops) {
|
|
gradient.addColorStop(stop.position, stop.color);
|
|
}
|
|
return gradient;
|
|
};
|
|
|
|
// src/drawing/mask-image.ts
|
|
var getMaskImageValue = (computedStyle) => {
|
|
const { maskImage, webkitMaskImage } = computedStyle;
|
|
const value = maskImage || webkitMaskImage;
|
|
if (!value || value === "none") {
|
|
return null;
|
|
}
|
|
return value;
|
|
};
|
|
var parseMaskImage = (maskImageValue) => {
|
|
return parseLinearGradient(maskImageValue);
|
|
};
|
|
|
|
// src/drawing/parse-transform-origin.ts
|
|
var parseTransformOrigin = (transformOrigin) => {
|
|
if (transformOrigin.trim() === "") {
|
|
return null;
|
|
}
|
|
const [x, y] = transformOrigin.split(" ");
|
|
return { x: parseFloat(x), y: parseFloat(y) };
|
|
};
|
|
|
|
// src/drawing/calculate-transforms.ts
|
|
var getInternalTransformOrigin = (transform) => {
|
|
const centerX = transform.boundingClientRect.width / 2;
|
|
const centerY = transform.boundingClientRect.height / 2;
|
|
const origin = parseTransformOrigin(transform.transformOrigin) ?? {
|
|
x: centerX,
|
|
y: centerY
|
|
};
|
|
return origin;
|
|
};
|
|
var getGlobalTransformOrigin = ({ transform }) => {
|
|
const { x: originX, y: originY } = getInternalTransformOrigin(transform);
|
|
return {
|
|
x: originX + transform.boundingClientRect.left,
|
|
y: originY + transform.boundingClientRect.top
|
|
};
|
|
};
|
|
var calculateTransforms = ({
|
|
element,
|
|
rootElement
|
|
}) => {
|
|
let parent = element;
|
|
const transforms = [];
|
|
const toReset = [];
|
|
let opacity = 1;
|
|
let elementComputedStyle = null;
|
|
let maskImageInfo = null;
|
|
while (parent) {
|
|
const computedStyle = getComputedStyle(parent);
|
|
if (parent === element) {
|
|
elementComputedStyle = computedStyle;
|
|
opacity = parseFloat(computedStyle.opacity);
|
|
const maskImageValue = getMaskImageValue(computedStyle);
|
|
maskImageInfo = maskImageValue ? parseMaskImage(maskImageValue) : null;
|
|
const originalMaskImage = parent.style.maskImage;
|
|
const originalWebkitMaskImage = parent.style.webkitMaskImage;
|
|
parent.style.maskImage = "none";
|
|
parent.style.webkitMaskImage = "none";
|
|
const parentRef = parent;
|
|
toReset.push(() => {
|
|
parentRef.style.maskImage = originalMaskImage;
|
|
parentRef.style.webkitMaskImage = originalWebkitMaskImage;
|
|
});
|
|
}
|
|
if (hasAnyTransformCssValue(computedStyle) || parent === element) {
|
|
const toParse = hasTransformCssValue(computedStyle) ? computedStyle.transform : undefined;
|
|
const matrix = new DOMMatrix(toParse);
|
|
const { transform, scale, rotate } = parent.style;
|
|
const additionalMatrices = [];
|
|
if (rotate !== "" && rotate !== "none") {
|
|
additionalMatrices.push(new DOMMatrix(`rotate(${rotate})`));
|
|
}
|
|
if (scale !== "" && scale !== "none") {
|
|
additionalMatrices.push(new DOMMatrix(`scale(${scale})`));
|
|
}
|
|
additionalMatrices.push(matrix);
|
|
parent.style.transform = "none";
|
|
parent.style.scale = "none";
|
|
parent.style.rotate = "none";
|
|
transforms.push({
|
|
element: parent,
|
|
transformOrigin: computedStyle.transformOrigin,
|
|
boundingClientRect: null,
|
|
matrices: additionalMatrices
|
|
});
|
|
const parentRef = parent;
|
|
toReset.push(() => {
|
|
parentRef.style.transform = transform;
|
|
parentRef.style.scale = scale;
|
|
parentRef.style.rotate = rotate;
|
|
});
|
|
}
|
|
if (parent === rootElement) {
|
|
break;
|
|
}
|
|
parent = parent.parentElement;
|
|
}
|
|
for (const transform of transforms) {
|
|
transform.boundingClientRect = transform.element.getBoundingClientRect();
|
|
}
|
|
const dimensions = transforms[0].boundingClientRect;
|
|
const nativeTransformOrigin = getInternalTransformOrigin(transforms[0]);
|
|
const totalMatrix = new DOMMatrix;
|
|
for (const transform of transforms.slice().reverse()) {
|
|
for (const matrix of transform.matrices) {
|
|
const globalTransformOrigin = getGlobalTransformOrigin({
|
|
transform
|
|
});
|
|
const transformMatrix = new DOMMatrix().translate(globalTransformOrigin.x, globalTransformOrigin.y).multiply(matrix).translate(-globalTransformOrigin.x, -globalTransformOrigin.y);
|
|
totalMatrix.multiplySelf(transformMatrix);
|
|
}
|
|
}
|
|
if (!elementComputedStyle) {
|
|
throw new Error("Element computed style not found");
|
|
}
|
|
const needs3DTransformViaWebGL = !totalMatrix.is2D;
|
|
const needsMaskImage = maskImageInfo !== null;
|
|
return {
|
|
dimensions,
|
|
totalMatrix,
|
|
[Symbol.dispose]: () => {
|
|
for (const reset of toReset) {
|
|
reset();
|
|
}
|
|
},
|
|
nativeTransformOrigin,
|
|
computedStyle: elementComputedStyle,
|
|
opacity,
|
|
maskImageInfo,
|
|
precompositing: {
|
|
needs3DTransformViaWebGL,
|
|
needsMaskImage: maskImageInfo,
|
|
needsPrecompositing: Boolean(needs3DTransformViaWebGL || needsMaskImage)
|
|
}
|
|
};
|
|
};
|
|
|
|
// src/drawing/round-to-expand-rect.ts
|
|
var roundToExpandRect = (rect) => {
|
|
const left = Math.floor(rect.left);
|
|
const top = Math.floor(rect.top);
|
|
const right = Math.ceil(rect.right);
|
|
const bottom = Math.ceil(rect.bottom);
|
|
return new DOMRect(left, top, right - left, bottom - top);
|
|
};
|
|
|
|
// src/drawing/clamp-rect-to-parent-bounds.ts
|
|
var getNarrowerRect = ({
|
|
firstRect,
|
|
secondRect
|
|
}) => {
|
|
const left = Math.max(firstRect.left, secondRect.left);
|
|
const top = Math.max(firstRect.top, secondRect.top);
|
|
const bottom = Math.min(firstRect.bottom, secondRect.bottom);
|
|
const right = Math.min(firstRect.right, secondRect.right);
|
|
return new DOMRect(left, top, right - left, bottom - top);
|
|
};
|
|
var getWiderRectAndExpand = ({
|
|
firstRect,
|
|
secondRect
|
|
}) => {
|
|
if (firstRect === null) {
|
|
return roundToExpandRect(secondRect);
|
|
}
|
|
const left = Math.min(firstRect.left, secondRect.left);
|
|
const top = Math.min(firstRect.top, secondRect.top);
|
|
const bottom = Math.max(firstRect.bottom, secondRect.bottom);
|
|
const right = Math.max(firstRect.right, secondRect.right);
|
|
return roundToExpandRect(new DOMRect(left, top, right - left, bottom - top));
|
|
};
|
|
|
|
// src/drawing/do-rects-intersect.ts
|
|
function doRectsIntersect(rect1, rect2) {
|
|
return !(rect1.right <= rect2.left || rect1.left >= rect2.right || rect1.bottom <= rect2.top || rect1.top >= rect2.bottom);
|
|
}
|
|
|
|
// src/drawing/draw-rounded.ts
|
|
var drawRoundedRectPath = ({
|
|
ctx,
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
borderRadius
|
|
}) => {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + borderRadius.topLeft.horizontal, y);
|
|
ctx.lineTo(x + width - borderRadius.topRight.horizontal, y);
|
|
if (borderRadius.topRight.horizontal > 0 || borderRadius.topRight.vertical > 0) {
|
|
ctx.ellipse(x + width - borderRadius.topRight.horizontal, y + borderRadius.topRight.vertical, borderRadius.topRight.horizontal, borderRadius.topRight.vertical, 0, -Math.PI / 2, 0);
|
|
}
|
|
ctx.lineTo(x + width, y + height - borderRadius.bottomRight.vertical);
|
|
if (borderRadius.bottomRight.horizontal > 0 || borderRadius.bottomRight.vertical > 0) {
|
|
ctx.ellipse(x + width - borderRadius.bottomRight.horizontal, y + height - borderRadius.bottomRight.vertical, borderRadius.bottomRight.horizontal, borderRadius.bottomRight.vertical, 0, 0, Math.PI / 2);
|
|
}
|
|
ctx.lineTo(x + borderRadius.bottomLeft.horizontal, y + height);
|
|
if (borderRadius.bottomLeft.horizontal > 0 || borderRadius.bottomLeft.vertical > 0) {
|
|
ctx.ellipse(x + borderRadius.bottomLeft.horizontal, y + height - borderRadius.bottomLeft.vertical, borderRadius.bottomLeft.horizontal, borderRadius.bottomLeft.vertical, 0, Math.PI / 2, Math.PI);
|
|
}
|
|
ctx.lineTo(x, y + borderRadius.topLeft.vertical);
|
|
if (borderRadius.topLeft.horizontal > 0 || borderRadius.topLeft.vertical > 0) {
|
|
ctx.ellipse(x + borderRadius.topLeft.horizontal, y + borderRadius.topLeft.vertical, borderRadius.topLeft.horizontal, borderRadius.topLeft.vertical, 0, Math.PI, Math.PI * 3 / 2);
|
|
}
|
|
ctx.closePath();
|
|
};
|
|
|
|
// src/drawing/get-padding-box.ts
|
|
var getPaddingBox = (rect, computedStyle) => {
|
|
const borderLeft = parseFloat(computedStyle.borderLeftWidth);
|
|
const borderRight = parseFloat(computedStyle.borderRightWidth);
|
|
const borderTop = parseFloat(computedStyle.borderTopWidth);
|
|
const borderBottom = parseFloat(computedStyle.borderBottomWidth);
|
|
return new DOMRect(rect.left + borderLeft, rect.top + borderTop, rect.width - borderLeft - borderRight, rect.height - borderTop - borderBottom);
|
|
};
|
|
var getContentBox = (rect, computedStyle) => {
|
|
const paddingBox = getPaddingBox(rect, computedStyle);
|
|
const paddingLeft = parseFloat(computedStyle.paddingLeft);
|
|
const paddingRight = parseFloat(computedStyle.paddingRight);
|
|
const paddingTop = parseFloat(computedStyle.paddingTop);
|
|
const paddingBottom = parseFloat(computedStyle.paddingBottom);
|
|
return new DOMRect(paddingBox.left + paddingLeft, paddingBox.top + paddingTop, paddingBox.width - paddingLeft - paddingRight, paddingBox.height - paddingTop - paddingBottom);
|
|
};
|
|
var getBoxBasedOnBackgroundClip = (rect, computedStyle, backgroundClip) => {
|
|
if (!backgroundClip) {
|
|
return rect;
|
|
}
|
|
if (backgroundClip.includes("text")) {
|
|
return rect;
|
|
}
|
|
if (backgroundClip.includes("padding-box")) {
|
|
return getPaddingBox(rect, computedStyle);
|
|
}
|
|
if (backgroundClip.includes("content-box")) {
|
|
return getContentBox(rect, computedStyle);
|
|
}
|
|
return rect;
|
|
};
|
|
|
|
// src/drawing/border-radius.ts
|
|
function parseValue({
|
|
value,
|
|
reference
|
|
}) {
|
|
value = value.trim();
|
|
if (value.endsWith("%")) {
|
|
const percentage = parseFloat(value);
|
|
return percentage / 100 * reference;
|
|
}
|
|
if (value.endsWith("px")) {
|
|
return parseFloat(value);
|
|
}
|
|
return parseFloat(value);
|
|
}
|
|
function expandShorthand(values) {
|
|
if (values.length === 1) {
|
|
return [values[0], values[0], values[0], values[0]];
|
|
}
|
|
if (values.length === 2) {
|
|
return [values[0], values[1], values[0], values[1]];
|
|
}
|
|
if (values.length === 3) {
|
|
return [values[0], values[1], values[2], values[1]];
|
|
}
|
|
return [values[0], values[1], values[2], values[3]];
|
|
}
|
|
function clampBorderRadius({
|
|
borderRadius,
|
|
width,
|
|
height
|
|
}) {
|
|
const clamped = {
|
|
topLeft: { ...borderRadius.topLeft },
|
|
topRight: { ...borderRadius.topRight },
|
|
bottomRight: { ...borderRadius.bottomRight },
|
|
bottomLeft: { ...borderRadius.bottomLeft }
|
|
};
|
|
const topSum = clamped.topLeft.horizontal + clamped.topRight.horizontal;
|
|
if (topSum > width) {
|
|
const factor = width / topSum;
|
|
clamped.topLeft.horizontal *= factor;
|
|
clamped.topRight.horizontal *= factor;
|
|
}
|
|
const rightSum = clamped.topRight.vertical + clamped.bottomRight.vertical;
|
|
if (rightSum > height) {
|
|
const factor = height / rightSum;
|
|
clamped.topRight.vertical *= factor;
|
|
clamped.bottomRight.vertical *= factor;
|
|
}
|
|
const bottomSum = clamped.bottomRight.horizontal + clamped.bottomLeft.horizontal;
|
|
if (bottomSum > width) {
|
|
const factor = width / bottomSum;
|
|
clamped.bottomRight.horizontal *= factor;
|
|
clamped.bottomLeft.horizontal *= factor;
|
|
}
|
|
const leftSum = clamped.bottomLeft.vertical + clamped.topLeft.vertical;
|
|
if (leftSum > height) {
|
|
const factor = height / leftSum;
|
|
clamped.bottomLeft.vertical *= factor;
|
|
clamped.topLeft.vertical *= factor;
|
|
}
|
|
return clamped;
|
|
}
|
|
function parseBorderRadius({
|
|
borderRadius,
|
|
width,
|
|
height
|
|
}) {
|
|
const parts = borderRadius.split("/").map((part) => part.trim());
|
|
const horizontalPart = parts[0];
|
|
const verticalPart = parts[1];
|
|
const horizontalValues = horizontalPart.split(/\s+/).filter((v) => v);
|
|
const verticalValues = verticalPart ? verticalPart.split(/\s+/).filter((v) => v) : horizontalValues;
|
|
const [hTopLeft, hTopRight, hBottomRight, hBottomLeft] = expandShorthand(horizontalValues);
|
|
const [vTopLeft, vTopRight, vBottomRight, vBottomLeft] = expandShorthand(verticalValues);
|
|
return clampBorderRadius({
|
|
borderRadius: {
|
|
topLeft: {
|
|
horizontal: parseValue({ value: hTopLeft, reference: width }),
|
|
vertical: parseValue({ value: vTopLeft, reference: height })
|
|
},
|
|
topRight: {
|
|
horizontal: parseValue({ value: hTopRight, reference: width }),
|
|
vertical: parseValue({ value: vTopRight, reference: height })
|
|
},
|
|
bottomRight: {
|
|
horizontal: parseValue({ value: hBottomRight, reference: width }),
|
|
vertical: parseValue({ value: vBottomRight, reference: height })
|
|
},
|
|
bottomLeft: {
|
|
horizontal: parseValue({ value: hBottomLeft, reference: width }),
|
|
vertical: parseValue({ value: vBottomLeft, reference: height })
|
|
}
|
|
},
|
|
width,
|
|
height
|
|
});
|
|
}
|
|
function setBorderRadius({
|
|
ctx,
|
|
rect,
|
|
borderRadius,
|
|
forceClipEvenWhenZero = false,
|
|
computedStyle,
|
|
backgroundClip
|
|
}) {
|
|
if (borderRadius.topLeft.horizontal === 0 && borderRadius.topLeft.vertical === 0 && borderRadius.topRight.horizontal === 0 && borderRadius.topRight.vertical === 0 && borderRadius.bottomRight.horizontal === 0 && borderRadius.bottomRight.vertical === 0 && borderRadius.bottomLeft.horizontal === 0 && borderRadius.bottomLeft.vertical === 0 && !forceClipEvenWhenZero) {
|
|
return () => {};
|
|
}
|
|
ctx.save();
|
|
const boundingRect = getBoxBasedOnBackgroundClip(rect, computedStyle, backgroundClip);
|
|
const actualBorderRadius = {
|
|
topLeft: {
|
|
horizontal: Math.max(0, borderRadius.topLeft.horizontal - (boundingRect.left - rect.left)),
|
|
vertical: Math.max(0, borderRadius.topLeft.vertical - (boundingRect.top - rect.top))
|
|
},
|
|
topRight: {
|
|
horizontal: Math.max(0, borderRadius.topRight.horizontal - (rect.right - boundingRect.right)),
|
|
vertical: Math.max(0, borderRadius.topRight.vertical - (boundingRect.top - rect.top))
|
|
},
|
|
bottomRight: {
|
|
horizontal: Math.max(0, borderRadius.bottomRight.horizontal - (rect.right - boundingRect.right)),
|
|
vertical: Math.max(0, borderRadius.bottomRight.vertical - (rect.bottom - boundingRect.bottom))
|
|
},
|
|
bottomLeft: {
|
|
horizontal: Math.max(0, borderRadius.bottomLeft.horizontal - (boundingRect.left - rect.left)),
|
|
vertical: Math.max(0, borderRadius.bottomLeft.vertical - (rect.bottom - boundingRect.bottom))
|
|
}
|
|
};
|
|
drawRoundedRectPath({
|
|
ctx,
|
|
x: boundingRect.left,
|
|
y: boundingRect.top,
|
|
width: boundingRect.width,
|
|
height: boundingRect.height,
|
|
borderRadius: actualBorderRadius
|
|
});
|
|
ctx.clip();
|
|
return () => {
|
|
ctx.restore();
|
|
};
|
|
}
|
|
|
|
// src/drawing/get-background-fill.ts
|
|
var isColorTransparent = (color) => {
|
|
return color === "transparent" || color.startsWith("rgba") && (color.endsWith(", 0)") || color.endsWith(",0"));
|
|
};
|
|
var getBackgroundFill = ({
|
|
backgroundColor,
|
|
backgroundImage,
|
|
contextToDraw,
|
|
boundingRect,
|
|
offsetLeft,
|
|
offsetTop
|
|
}) => {
|
|
if (backgroundImage && backgroundImage !== "none") {
|
|
const gradientInfo = parseLinearGradient(backgroundImage);
|
|
if (gradientInfo) {
|
|
const gradient = createCanvasGradient({
|
|
ctx: contextToDraw,
|
|
rect: boundingRect,
|
|
gradientInfo,
|
|
offsetLeft,
|
|
offsetTop
|
|
});
|
|
return gradient;
|
|
}
|
|
}
|
|
if (backgroundColor && backgroundColor !== "transparent" && !isColorTransparent(backgroundColor)) {
|
|
return backgroundColor;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// src/drawing/draw-background.ts
|
|
var drawBackground = async ({
|
|
backgroundImage,
|
|
context,
|
|
rect,
|
|
backgroundColor,
|
|
backgroundClip,
|
|
element,
|
|
logLevel,
|
|
internalState,
|
|
computedStyle,
|
|
offsetLeft: parentOffsetLeft,
|
|
offsetTop: parentOffsetTop,
|
|
scale
|
|
}) => {
|
|
let __stack = [];
|
|
try {
|
|
let contextToDraw = context;
|
|
const originalCompositeOperation = context.globalCompositeOperation;
|
|
let offsetLeft = 0;
|
|
let offsetTop = 0;
|
|
const _ = __using(__stack, {
|
|
[Symbol.dispose]: () => {
|
|
context.globalCompositeOperation = originalCompositeOperation;
|
|
if (context !== contextToDraw) {
|
|
context.drawImage(contextToDraw.canvas, offsetLeft, offsetTop, contextToDraw.canvas.width / scale, contextToDraw.canvas.height / scale);
|
|
}
|
|
}
|
|
}, 0);
|
|
const boundingRect = getBoxBasedOnBackgroundClip(rect, computedStyle, backgroundClip);
|
|
if (backgroundClip.includes("text")) {
|
|
offsetLeft = boundingRect.left;
|
|
offsetTop = boundingRect.top;
|
|
const originalBackgroundClip = element.style.backgroundClip;
|
|
const originalWebkitBackgroundClip = element.style.webkitBackgroundClip;
|
|
element.style.backgroundClip = "initial";
|
|
element.style.webkitBackgroundClip = "initial";
|
|
const onlyBackgroundClipText = await createLayer({
|
|
element,
|
|
cutout: new DOMRect(boundingRect.left + parentOffsetLeft, boundingRect.top + parentOffsetTop, boundingRect.width, boundingRect.height),
|
|
logLevel,
|
|
internalState,
|
|
scale,
|
|
onlyBackgroundClipText: true
|
|
});
|
|
onlyBackgroundClipText.setTransform(new DOMMatrix().scale(scale, scale));
|
|
element.style.backgroundClip = originalBackgroundClip;
|
|
element.style.webkitBackgroundClip = originalWebkitBackgroundClip;
|
|
contextToDraw = onlyBackgroundClipText;
|
|
contextToDraw.globalCompositeOperation = "source-in";
|
|
}
|
|
const backgroundFill = getBackgroundFill({
|
|
backgroundImage,
|
|
backgroundColor,
|
|
contextToDraw,
|
|
boundingRect,
|
|
offsetLeft,
|
|
offsetTop
|
|
});
|
|
if (!backgroundFill) {
|
|
return;
|
|
}
|
|
const originalFillStyle = contextToDraw.fillStyle;
|
|
contextToDraw.fillStyle = backgroundFill;
|
|
contextToDraw.fillRect(boundingRect.left - offsetLeft, boundingRect.top - offsetTop, boundingRect.width, boundingRect.height);
|
|
contextToDraw.fillStyle = originalFillStyle;
|
|
} catch (_catch) {
|
|
var _err = _catch, _hasErr = 1;
|
|
} finally {
|
|
__callDispose(__stack, _err, _hasErr);
|
|
}
|
|
};
|
|
|
|
// src/drawing/draw-border.ts
|
|
var parseBorderWidth = (value) => {
|
|
return parseFloat(value) || 0;
|
|
};
|
|
var getBorderSideProperties = (computedStyle) => {
|
|
return {
|
|
top: {
|
|
width: parseBorderWidth(computedStyle.borderTopWidth),
|
|
color: computedStyle.borderTopColor || computedStyle.borderColor || "black",
|
|
style: computedStyle.borderTopStyle || computedStyle.borderStyle || "solid"
|
|
},
|
|
right: {
|
|
width: parseBorderWidth(computedStyle.borderRightWidth),
|
|
color: computedStyle.borderRightColor || computedStyle.borderColor || "black",
|
|
style: computedStyle.borderRightStyle || computedStyle.borderStyle || "solid"
|
|
},
|
|
bottom: {
|
|
width: parseBorderWidth(computedStyle.borderBottomWidth),
|
|
color: computedStyle.borderBottomColor || computedStyle.borderColor || "black",
|
|
style: computedStyle.borderBottomStyle || computedStyle.borderStyle || "solid"
|
|
},
|
|
left: {
|
|
width: parseBorderWidth(computedStyle.borderLeftWidth),
|
|
color: computedStyle.borderLeftColor || computedStyle.borderColor || "black",
|
|
style: computedStyle.borderLeftStyle || computedStyle.borderStyle || "solid"
|
|
}
|
|
};
|
|
};
|
|
var getLineDashPattern = (style, width) => {
|
|
if (style === "dashed") {
|
|
return [width * 2, width];
|
|
}
|
|
if (style === "dotted") {
|
|
return [width, width];
|
|
}
|
|
return [];
|
|
};
|
|
var drawBorderSide = ({
|
|
ctx,
|
|
side,
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
borderRadius,
|
|
borderProperties
|
|
}) => {
|
|
const { width: borderWidth, color, style } = borderProperties;
|
|
if (borderWidth <= 0 || style === "none" || style === "hidden") {
|
|
return;
|
|
}
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = borderWidth;
|
|
ctx.setLineDash(getLineDashPattern(style, borderWidth));
|
|
const halfWidth = borderWidth / 2;
|
|
if (side === "top") {
|
|
const startX = x + borderRadius.topLeft.horizontal;
|
|
const startY = y + halfWidth;
|
|
const endX = x + width - borderRadius.topRight.horizontal;
|
|
const endY = y + halfWidth;
|
|
ctx.moveTo(startX, startY);
|
|
ctx.lineTo(endX, endY);
|
|
} else if (side === "right") {
|
|
const startX = x + width - halfWidth;
|
|
const startY = y + borderRadius.topRight.vertical;
|
|
const endX = x + width - halfWidth;
|
|
const endY = y + height - borderRadius.bottomRight.vertical;
|
|
ctx.moveTo(startX, startY);
|
|
ctx.lineTo(endX, endY);
|
|
} else if (side === "bottom") {
|
|
const startX = x + borderRadius.bottomLeft.horizontal;
|
|
const startY = y + height - halfWidth;
|
|
const endX = x + width - borderRadius.bottomRight.horizontal;
|
|
const endY = y + height - halfWidth;
|
|
ctx.moveTo(startX, startY);
|
|
ctx.lineTo(endX, endY);
|
|
} else if (side === "left") {
|
|
const startX = x + halfWidth;
|
|
const startY = y + borderRadius.topLeft.vertical;
|
|
const endX = x + halfWidth;
|
|
const endY = y + height - borderRadius.bottomLeft.vertical;
|
|
ctx.moveTo(startX, startY);
|
|
ctx.lineTo(endX, endY);
|
|
}
|
|
ctx.stroke();
|
|
};
|
|
var drawCorner = ({
|
|
ctx,
|
|
corner,
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
borderRadius,
|
|
topBorder,
|
|
rightBorder,
|
|
bottomBorder,
|
|
leftBorder
|
|
}) => {
|
|
const radius = borderRadius[corner];
|
|
if (radius.horizontal <= 0 && radius.vertical <= 0) {
|
|
return;
|
|
}
|
|
let border1;
|
|
let border2;
|
|
let centerX;
|
|
let centerY;
|
|
let startAngle;
|
|
let endAngle;
|
|
if (corner === "topLeft") {
|
|
border1 = leftBorder;
|
|
border2 = topBorder;
|
|
centerX = x + radius.horizontal;
|
|
centerY = y + radius.vertical;
|
|
startAngle = Math.PI;
|
|
endAngle = Math.PI * 3 / 2;
|
|
} else if (corner === "topRight") {
|
|
border1 = topBorder;
|
|
border2 = rightBorder;
|
|
centerX = x + width - radius.horizontal;
|
|
centerY = y + radius.vertical;
|
|
startAngle = -Math.PI / 2;
|
|
endAngle = 0;
|
|
} else if (corner === "bottomRight") {
|
|
border1 = rightBorder;
|
|
border2 = bottomBorder;
|
|
centerX = x + width - radius.horizontal;
|
|
centerY = y + height - radius.vertical;
|
|
startAngle = 0;
|
|
endAngle = Math.PI / 2;
|
|
} else {
|
|
border1 = bottomBorder;
|
|
border2 = leftBorder;
|
|
centerX = x + radius.horizontal;
|
|
centerY = y + height - radius.vertical;
|
|
startAngle = Math.PI / 2;
|
|
endAngle = Math.PI;
|
|
}
|
|
const avgWidth = (border1.width + border2.width) / 2;
|
|
const useColor = border1.width >= border2.width ? border1.color : border2.color;
|
|
const useStyle = border1.width >= border2.width ? border1.style : border2.style;
|
|
if (avgWidth > 0 && useStyle !== "none" && useStyle !== "hidden") {
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = useColor;
|
|
ctx.lineWidth = avgWidth;
|
|
ctx.setLineDash(getLineDashPattern(useStyle, avgWidth));
|
|
const adjustedRadiusH = Math.max(0, radius.horizontal - avgWidth / 2);
|
|
const adjustedRadiusV = Math.max(0, radius.vertical - avgWidth / 2);
|
|
ctx.ellipse(centerX, centerY, adjustedRadiusH, adjustedRadiusV, 0, startAngle, endAngle);
|
|
ctx.stroke();
|
|
}
|
|
};
|
|
var drawUniformBorder = ({
|
|
ctx,
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
borderRadius,
|
|
borderWidth,
|
|
borderColor,
|
|
borderStyle
|
|
}) => {
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = borderColor;
|
|
ctx.lineWidth = borderWidth;
|
|
ctx.setLineDash(getLineDashPattern(borderStyle, borderWidth));
|
|
const halfWidth = borderWidth / 2;
|
|
const borderX = x + halfWidth;
|
|
const borderY = y + halfWidth;
|
|
const borderW = width - borderWidth;
|
|
const borderH = height - borderWidth;
|
|
const adjustedBorderRadius = {
|
|
topLeft: {
|
|
horizontal: Math.max(0, borderRadius.topLeft.horizontal - halfWidth),
|
|
vertical: Math.max(0, borderRadius.topLeft.vertical - halfWidth)
|
|
},
|
|
topRight: {
|
|
horizontal: Math.max(0, borderRadius.topRight.horizontal - halfWidth),
|
|
vertical: Math.max(0, borderRadius.topRight.vertical - halfWidth)
|
|
},
|
|
bottomRight: {
|
|
horizontal: Math.max(0, borderRadius.bottomRight.horizontal - halfWidth),
|
|
vertical: Math.max(0, borderRadius.bottomRight.vertical - halfWidth)
|
|
},
|
|
bottomLeft: {
|
|
horizontal: Math.max(0, borderRadius.bottomLeft.horizontal - halfWidth),
|
|
vertical: Math.max(0, borderRadius.bottomLeft.vertical - halfWidth)
|
|
}
|
|
};
|
|
ctx.moveTo(borderX + adjustedBorderRadius.topLeft.horizontal, borderY);
|
|
ctx.lineTo(borderX + borderW - adjustedBorderRadius.topRight.horizontal, borderY);
|
|
if (adjustedBorderRadius.topRight.horizontal > 0 || adjustedBorderRadius.topRight.vertical > 0) {
|
|
ctx.ellipse(borderX + borderW - adjustedBorderRadius.topRight.horizontal, borderY + adjustedBorderRadius.topRight.vertical, adjustedBorderRadius.topRight.horizontal, adjustedBorderRadius.topRight.vertical, 0, -Math.PI / 2, 0);
|
|
}
|
|
ctx.lineTo(borderX + borderW, borderY + borderH - adjustedBorderRadius.bottomRight.vertical);
|
|
if (adjustedBorderRadius.bottomRight.horizontal > 0 || adjustedBorderRadius.bottomRight.vertical > 0) {
|
|
ctx.ellipse(borderX + borderW - adjustedBorderRadius.bottomRight.horizontal, borderY + borderH - adjustedBorderRadius.bottomRight.vertical, adjustedBorderRadius.bottomRight.horizontal, adjustedBorderRadius.bottomRight.vertical, 0, 0, Math.PI / 2);
|
|
}
|
|
ctx.lineTo(borderX + adjustedBorderRadius.bottomLeft.horizontal, borderY + borderH);
|
|
if (adjustedBorderRadius.bottomLeft.horizontal > 0 || adjustedBorderRadius.bottomLeft.vertical > 0) {
|
|
ctx.ellipse(borderX + adjustedBorderRadius.bottomLeft.horizontal, borderY + borderH - adjustedBorderRadius.bottomLeft.vertical, adjustedBorderRadius.bottomLeft.horizontal, adjustedBorderRadius.bottomLeft.vertical, 0, Math.PI / 2, Math.PI);
|
|
}
|
|
ctx.lineTo(borderX, borderY + adjustedBorderRadius.topLeft.vertical);
|
|
if (adjustedBorderRadius.topLeft.horizontal > 0 || adjustedBorderRadius.topLeft.vertical > 0) {
|
|
ctx.ellipse(borderX + adjustedBorderRadius.topLeft.horizontal, borderY + adjustedBorderRadius.topLeft.vertical, adjustedBorderRadius.topLeft.horizontal, adjustedBorderRadius.topLeft.vertical, 0, Math.PI, Math.PI * 3 / 2);
|
|
}
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
};
|
|
var drawBorder = ({
|
|
ctx,
|
|
rect,
|
|
borderRadius,
|
|
computedStyle
|
|
}) => {
|
|
const borders = getBorderSideProperties(computedStyle);
|
|
const hasBorder = borders.top.width > 0 || borders.right.width > 0 || borders.bottom.width > 0 || borders.left.width > 0;
|
|
if (!hasBorder) {
|
|
return;
|
|
}
|
|
const originalStrokeStyle = ctx.strokeStyle;
|
|
const originalLineWidth = ctx.lineWidth;
|
|
const originalLineDash = ctx.getLineDash();
|
|
const allSidesEqual = borders.top.width === borders.right.width && borders.top.width === borders.bottom.width && borders.top.width === borders.left.width && borders.top.color === borders.right.color && borders.top.color === borders.bottom.color && borders.top.color === borders.left.color && borders.top.style === borders.right.style && borders.top.style === borders.bottom.style && borders.top.style === borders.left.style && borders.top.width > 0;
|
|
if (allSidesEqual) {
|
|
drawUniformBorder({
|
|
ctx,
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius,
|
|
borderWidth: borders.top.width,
|
|
borderColor: borders.top.color,
|
|
borderStyle: borders.top.style
|
|
});
|
|
} else {
|
|
drawCorner({
|
|
ctx,
|
|
corner: "topLeft",
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius,
|
|
topBorder: borders.top,
|
|
rightBorder: borders.right,
|
|
bottomBorder: borders.bottom,
|
|
leftBorder: borders.left
|
|
});
|
|
drawCorner({
|
|
ctx,
|
|
corner: "topRight",
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius,
|
|
topBorder: borders.top,
|
|
rightBorder: borders.right,
|
|
bottomBorder: borders.bottom,
|
|
leftBorder: borders.left
|
|
});
|
|
drawCorner({
|
|
ctx,
|
|
corner: "bottomRight",
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius,
|
|
topBorder: borders.top,
|
|
rightBorder: borders.right,
|
|
bottomBorder: borders.bottom,
|
|
leftBorder: borders.left
|
|
});
|
|
drawCorner({
|
|
ctx,
|
|
corner: "bottomLeft",
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius,
|
|
topBorder: borders.top,
|
|
rightBorder: borders.right,
|
|
bottomBorder: borders.bottom,
|
|
leftBorder: borders.left
|
|
});
|
|
drawBorderSide({
|
|
ctx,
|
|
side: "top",
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius,
|
|
borderProperties: borders.top
|
|
});
|
|
drawBorderSide({
|
|
ctx,
|
|
side: "right",
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius,
|
|
borderProperties: borders.right
|
|
});
|
|
drawBorderSide({
|
|
ctx,
|
|
side: "bottom",
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius,
|
|
borderProperties: borders.bottom
|
|
});
|
|
drawBorderSide({
|
|
ctx,
|
|
side: "left",
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius,
|
|
borderProperties: borders.left
|
|
});
|
|
}
|
|
ctx.strokeStyle = originalStrokeStyle;
|
|
ctx.lineWidth = originalLineWidth;
|
|
ctx.setLineDash(originalLineDash);
|
|
};
|
|
|
|
// src/drawing/draw-box-shadow.ts
|
|
import { Internals as Internals5 } from "remotion";
|
|
var parseBoxShadow = (boxShadowValue) => {
|
|
if (!boxShadowValue || boxShadowValue === "none") {
|
|
return [];
|
|
}
|
|
const shadows = [];
|
|
const shadowStrings = boxShadowValue.split(/,(?![^(]*\))/);
|
|
for (const shadowStr of shadowStrings) {
|
|
const trimmed = shadowStr.trim();
|
|
if (!trimmed || trimmed === "none") {
|
|
continue;
|
|
}
|
|
const shadow = {
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
blurRadius: 0,
|
|
color: "rgba(0, 0, 0, 0.5)",
|
|
inset: false
|
|
};
|
|
shadow.inset = /\binset\b/i.test(trimmed);
|
|
let remaining = trimmed.replace(/\binset\b/gi, "").trim();
|
|
const colorMatch = remaining.match(/(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i);
|
|
if (colorMatch) {
|
|
shadow.color = colorMatch[0];
|
|
remaining = remaining.replace(colorMatch[0], "").trim();
|
|
}
|
|
const numbers = remaining.match(/[+-]?\d*\.?\d+(?:px|em|rem|%)?/gi) || [];
|
|
const values = numbers.map((n) => parseFloat(n) || 0);
|
|
if (values.length >= 2) {
|
|
shadow.offsetX = values[0];
|
|
shadow.offsetY = values[1];
|
|
if (values.length >= 3) {
|
|
shadow.blurRadius = Math.max(0, values[2]);
|
|
}
|
|
}
|
|
shadows.push(shadow);
|
|
}
|
|
return shadows;
|
|
};
|
|
var drawBorderRadius = ({
|
|
ctx,
|
|
rect,
|
|
borderRadius,
|
|
computedStyle,
|
|
logLevel
|
|
}) => {
|
|
const shadows = parseBoxShadow(computedStyle.boxShadow);
|
|
if (shadows.length === 0) {
|
|
return;
|
|
}
|
|
for (let i = shadows.length - 1;i >= 0; i--) {
|
|
const shadow = shadows[i];
|
|
const newLeft = rect.left + Math.min(shadow.offsetX, 0) - shadow.blurRadius;
|
|
const newRight = rect.right + Math.max(shadow.offsetX, 0) + shadow.blurRadius;
|
|
const newTop = rect.top + Math.min(shadow.offsetY, 0) - shadow.blurRadius;
|
|
const newBottom = rect.bottom + Math.max(shadow.offsetY, 0) + shadow.blurRadius;
|
|
const newRect = new DOMRect(newLeft, newTop, newRight - newLeft, newBottom - newTop);
|
|
const leftOffset = rect.left - newLeft;
|
|
const topOffset = rect.top - newTop;
|
|
const newCanvas = new OffscreenCanvas(newRect.width, newRect.height);
|
|
const newCtx = newCanvas.getContext("2d");
|
|
if (!newCtx) {
|
|
throw new Error("Failed to get context");
|
|
}
|
|
if (shadow.inset) {
|
|
Internals5.Log.warn({
|
|
logLevel,
|
|
tag: "@remotion/web-renderer"
|
|
}, 'Detected "box-shadow" with "inset". This is not yet supported in @remotion/web-renderer');
|
|
continue;
|
|
}
|
|
newCtx.shadowBlur = shadow.blurRadius;
|
|
newCtx.shadowColor = shadow.color;
|
|
newCtx.shadowOffsetX = shadow.offsetX;
|
|
newCtx.shadowOffsetY = shadow.offsetY;
|
|
newCtx.fillStyle = "black";
|
|
drawRoundedRectPath({
|
|
ctx: newCtx,
|
|
x: leftOffset,
|
|
y: topOffset,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius
|
|
});
|
|
newCtx.fill();
|
|
newCtx.shadowColor = "transparent";
|
|
newCtx.globalCompositeOperation = "destination-out";
|
|
drawRoundedRectPath({
|
|
ctx: newCtx,
|
|
x: leftOffset,
|
|
y: topOffset,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
borderRadius
|
|
});
|
|
newCtx.fill();
|
|
ctx.drawImage(newCanvas, rect.left - leftOffset, rect.top - topOffset);
|
|
}
|
|
};
|
|
|
|
// src/drawing/draw-outline.ts
|
|
var parseOutlineWidth = (value) => {
|
|
return parseFloat(value) || 0;
|
|
};
|
|
var parseOutlineOffset = (value) => {
|
|
return parseFloat(value) || 0;
|
|
};
|
|
var getLineDashPattern2 = (style, width) => {
|
|
if (style === "dashed") {
|
|
return [width * 2, width];
|
|
}
|
|
if (style === "dotted") {
|
|
return [width, width];
|
|
}
|
|
return [];
|
|
};
|
|
var drawOutline = ({
|
|
ctx,
|
|
rect,
|
|
borderRadius,
|
|
computedStyle
|
|
}) => {
|
|
const outlineWidth = parseOutlineWidth(computedStyle.outlineWidth);
|
|
const { outlineStyle } = computedStyle;
|
|
const outlineColor = computedStyle.outlineColor || "black";
|
|
const outlineOffset = parseOutlineOffset(computedStyle.outlineOffset);
|
|
if (outlineWidth <= 0 || outlineStyle === "none" || outlineStyle === "hidden") {
|
|
return;
|
|
}
|
|
const originalStrokeStyle = ctx.strokeStyle;
|
|
const originalLineWidth = ctx.lineWidth;
|
|
const originalLineDash = ctx.getLineDash();
|
|
ctx.strokeStyle = outlineColor;
|
|
ctx.lineWidth = outlineWidth;
|
|
ctx.setLineDash(getLineDashPattern2(outlineStyle, outlineWidth));
|
|
const halfWidth = outlineWidth / 2;
|
|
const offset = outlineOffset + halfWidth;
|
|
const outlineX = rect.left - offset;
|
|
const outlineY = rect.top - offset;
|
|
const outlineW = rect.width + offset * 2;
|
|
const outlineH = rect.height + offset * 2;
|
|
const adjustedBorderRadius = {
|
|
topLeft: {
|
|
horizontal: borderRadius.topLeft.horizontal === 0 ? 0 : Math.max(0, borderRadius.topLeft.horizontal + offset),
|
|
vertical: borderRadius.topLeft.vertical === 0 ? 0 : Math.max(0, borderRadius.topLeft.vertical + offset)
|
|
},
|
|
topRight: {
|
|
horizontal: borderRadius.topRight.horizontal === 0 ? 0 : Math.max(0, borderRadius.topRight.horizontal + offset),
|
|
vertical: borderRadius.topRight.vertical === 0 ? 0 : Math.max(0, borderRadius.topRight.vertical + offset)
|
|
},
|
|
bottomRight: {
|
|
horizontal: borderRadius.bottomRight.horizontal === 0 ? 0 : Math.max(0, borderRadius.bottomRight.horizontal + offset),
|
|
vertical: borderRadius.bottomRight.vertical === 0 ? 0 : Math.max(0, borderRadius.bottomRight.vertical + offset)
|
|
},
|
|
bottomLeft: {
|
|
horizontal: borderRadius.bottomLeft.horizontal === 0 ? 0 : Math.max(0, borderRadius.bottomLeft.horizontal + offset),
|
|
vertical: borderRadius.bottomLeft.vertical === 0 ? 0 : Math.max(0, borderRadius.bottomLeft.vertical + offset)
|
|
}
|
|
};
|
|
drawRoundedRectPath({
|
|
ctx,
|
|
x: outlineX,
|
|
y: outlineY,
|
|
width: outlineW,
|
|
height: outlineH,
|
|
borderRadius: adjustedBorderRadius
|
|
});
|
|
ctx.stroke();
|
|
ctx.strokeStyle = originalStrokeStyle;
|
|
ctx.lineWidth = originalLineWidth;
|
|
ctx.setLineDash(originalLineDash);
|
|
};
|
|
|
|
// src/drawing/opacity.ts
|
|
var setOpacity = ({
|
|
ctx,
|
|
opacity
|
|
}) => {
|
|
const previousAlpha = ctx.globalAlpha;
|
|
ctx.globalAlpha = previousAlpha * opacity;
|
|
return () => {
|
|
ctx.globalAlpha = previousAlpha;
|
|
};
|
|
};
|
|
|
|
// src/drawing/overflow.ts
|
|
var setOverflowHidden = ({
|
|
ctx,
|
|
rect,
|
|
borderRadius,
|
|
overflowHidden,
|
|
computedStyle,
|
|
backgroundClip
|
|
}) => {
|
|
if (!overflowHidden) {
|
|
return () => {};
|
|
}
|
|
return setBorderRadius({
|
|
ctx,
|
|
rect,
|
|
borderRadius,
|
|
forceClipEvenWhenZero: true,
|
|
computedStyle,
|
|
backgroundClip
|
|
});
|
|
};
|
|
|
|
// src/drawing/transform.ts
|
|
var setTransform = ({
|
|
ctx,
|
|
transform,
|
|
parentRect,
|
|
scale
|
|
}) => {
|
|
const offsetMatrix = new DOMMatrix().scale(scale, scale).translate(-parentRect.x, -parentRect.y).multiply(transform).translate(parentRect.x, parentRect.y);
|
|
ctx.setTransform(offsetMatrix);
|
|
return () => {
|
|
ctx.setTransform(new DOMMatrix);
|
|
};
|
|
};
|
|
|
|
// src/drawing/draw-element.ts
|
|
var drawElement = async ({
|
|
rect,
|
|
computedStyle,
|
|
context,
|
|
draw,
|
|
opacity,
|
|
totalMatrix,
|
|
parentRect,
|
|
logLevel,
|
|
element,
|
|
internalState,
|
|
scale
|
|
}) => {
|
|
const { backgroundImage, backgroundColor, backgroundClip } = computedStyle;
|
|
const borderRadius = parseBorderRadius({
|
|
borderRadius: computedStyle.borderRadius,
|
|
width: rect.width,
|
|
height: rect.height
|
|
});
|
|
const finishTransform = setTransform({
|
|
ctx: context,
|
|
transform: totalMatrix,
|
|
parentRect,
|
|
scale
|
|
});
|
|
const finishOpacity = setOpacity({
|
|
ctx: context,
|
|
opacity
|
|
});
|
|
drawBorderRadius({
|
|
ctx: context,
|
|
computedStyle,
|
|
rect,
|
|
borderRadius,
|
|
logLevel
|
|
});
|
|
const finishBorderRadius = setBorderRadius({
|
|
ctx: context,
|
|
rect,
|
|
borderRadius,
|
|
forceClipEvenWhenZero: false,
|
|
computedStyle,
|
|
backgroundClip
|
|
});
|
|
await drawBackground({
|
|
backgroundImage,
|
|
context,
|
|
rect,
|
|
backgroundColor,
|
|
backgroundClip,
|
|
element,
|
|
logLevel,
|
|
internalState,
|
|
computedStyle,
|
|
offsetLeft: parentRect.left,
|
|
offsetTop: parentRect.top,
|
|
scale
|
|
});
|
|
await draw({ dimensions: rect, computedStyle, contextToDraw: context });
|
|
finishBorderRadius();
|
|
drawBorder({
|
|
ctx: context,
|
|
rect,
|
|
borderRadius,
|
|
computedStyle
|
|
});
|
|
drawOutline({
|
|
ctx: context,
|
|
rect,
|
|
borderRadius,
|
|
computedStyle
|
|
});
|
|
const finishOverflowHidden = setOverflowHidden({
|
|
ctx: context,
|
|
rect,
|
|
borderRadius,
|
|
overflowHidden: computedStyle.overflow === "hidden",
|
|
computedStyle,
|
|
backgroundClip
|
|
});
|
|
finishTransform();
|
|
return {
|
|
cleanupAfterChildren: () => {
|
|
finishOpacity();
|
|
finishOverflowHidden();
|
|
}
|
|
};
|
|
};
|
|
|
|
// src/walk-tree.ts
|
|
function skipToNextNonDescendant(treeWalker) {
|
|
if (treeWalker.nextSibling()) {
|
|
return true;
|
|
}
|
|
while (treeWalker.parentNode()) {
|
|
if (treeWalker.nextSibling()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// src/get-biggest-bounding-client-rect.ts
|
|
var getBiggestBoundingClientRect = (element) => {
|
|
const treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT);
|
|
let mostLeft = Infinity;
|
|
let mostTop = Infinity;
|
|
let mostRight = -Infinity;
|
|
let mostBottom = -Infinity;
|
|
while (true) {
|
|
const computedStyle = getComputedStyle(treeWalker.currentNode);
|
|
const outlineWidth = parseOutlineWidth(computedStyle.outlineWidth);
|
|
const outlineOffset = parseOutlineOffset(computedStyle.outlineOffset);
|
|
const rect = treeWalker.currentNode.getBoundingClientRect();
|
|
const shadows = parseBoxShadow(computedStyle.boxShadow);
|
|
let shadowLeft = 0;
|
|
let shadowRight = 0;
|
|
let shadowTop = 0;
|
|
let shadowBottom = 0;
|
|
for (const shadow of shadows) {
|
|
if (!shadow.inset) {
|
|
shadowLeft = Math.max(shadowLeft, Math.abs(Math.min(shadow.offsetX, 0)) + shadow.blurRadius);
|
|
shadowRight = Math.max(shadowRight, Math.max(shadow.offsetX, 0) + shadow.blurRadius);
|
|
shadowTop = Math.max(shadowTop, Math.abs(Math.min(shadow.offsetY, 0)) + shadow.blurRadius);
|
|
shadowBottom = Math.max(shadowBottom, Math.max(shadow.offsetY, 0) + shadow.blurRadius);
|
|
}
|
|
}
|
|
mostLeft = Math.min(mostLeft, rect.left - outlineOffset - outlineWidth - shadowLeft);
|
|
mostTop = Math.min(mostTop, rect.top - outlineOffset - outlineWidth - shadowTop);
|
|
mostRight = Math.max(mostRight, rect.right + outlineOffset + outlineWidth + shadowRight);
|
|
mostBottom = Math.max(mostBottom, rect.bottom + outlineOffset + outlineWidth + shadowBottom);
|
|
if (computedStyle.overflow === "hidden") {
|
|
if (!skipToNextNonDescendant(treeWalker)) {
|
|
break;
|
|
}
|
|
}
|
|
if (!treeWalker.nextNode()) {
|
|
break;
|
|
}
|
|
}
|
|
return new DOMRect(mostLeft, mostTop, mostRight - mostLeft, mostBottom - mostTop);
|
|
};
|
|
|
|
// src/drawing/get-pretransform-rect.ts
|
|
var MAX_SCALE_FACTOR = 100;
|
|
var isScaleTooBig = (matrix) => {
|
|
const origin = new DOMPoint(0, 0).matrixTransform(matrix);
|
|
const unitX = new DOMPoint(1, 0).matrixTransform(matrix);
|
|
const unitY = new DOMPoint(0, 1).matrixTransform(matrix);
|
|
const basisX = { x: unitX.x - origin.x, y: unitX.y - origin.y };
|
|
const basisY = { x: unitY.x - origin.x, y: unitY.y - origin.y };
|
|
const scaleX = 1 / Math.hypot(basisX.x, basisX.y);
|
|
const scaleY = 1 / Math.hypot(basisY.x, basisY.y);
|
|
const maxScale = Math.max(scaleX, scaleY);
|
|
if (maxScale > MAX_SCALE_FACTOR) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
function invertProjectivePoint(xp, yp, matrix) {
|
|
const A = matrix.m11 - xp * matrix.m14;
|
|
const B = matrix.m21 - xp * matrix.m24;
|
|
const C = xp * matrix.m44 - matrix.m41;
|
|
const D = matrix.m12 - yp * matrix.m14;
|
|
const E = matrix.m22 - yp * matrix.m24;
|
|
const F = yp * matrix.m44 - matrix.m42;
|
|
const det = A * E - B * D;
|
|
if (Math.abs(det) < 0.0000000001) {
|
|
return null;
|
|
}
|
|
const x = (C * E - B * F) / det;
|
|
const y = (A * F - C * D) / det;
|
|
return { x, y };
|
|
}
|
|
function getPreTransformRect(targetRect, matrix) {
|
|
if (isScaleTooBig(matrix)) {
|
|
return null;
|
|
}
|
|
const corners = [
|
|
{ x: targetRect.x, y: targetRect.y },
|
|
{ x: targetRect.x + targetRect.width, y: targetRect.y },
|
|
{ x: targetRect.x + targetRect.width, y: targetRect.y + targetRect.height },
|
|
{ x: targetRect.x, y: targetRect.y + targetRect.height }
|
|
];
|
|
const invertedCorners = [];
|
|
for (const corner of corners) {
|
|
const inverted = invertProjectivePoint(corner.x, corner.y, matrix);
|
|
if (inverted === null) {
|
|
return null;
|
|
}
|
|
invertedCorners.push(inverted);
|
|
}
|
|
const xCoords = invertedCorners.map((p) => p.x);
|
|
const yCoords = invertedCorners.map((p) => p.y);
|
|
return new DOMRect(Math.min(...xCoords), Math.min(...yCoords), Math.max(...xCoords) - Math.min(...xCoords), Math.max(...yCoords) - Math.min(...yCoords));
|
|
}
|
|
|
|
// src/drawing/transform-in-3d.ts
|
|
var vsSource = `
|
|
attribute vec2 aPosition;
|
|
attribute vec2 aTexCoord;
|
|
uniform mat4 uTransform;
|
|
uniform vec2 uResolution;
|
|
uniform vec2 uOffset;
|
|
varying vec2 vTexCoord;
|
|
|
|
void main() {
|
|
vec4 pos = uTransform * vec4(aPosition, 0.0, 1.0);
|
|
pos.xy = pos.xy + uOffset * pos.w;
|
|
|
|
// Convert homogeneous coords to clip space
|
|
gl_Position = vec4(
|
|
(pos.x / uResolution.x) * 2.0 - pos.w, // x
|
|
pos.w - (pos.y / uResolution.y) * 2.0, // y (flipped)
|
|
0.0,
|
|
pos.w
|
|
);
|
|
|
|
vTexCoord = aTexCoord;
|
|
}
|
|
`;
|
|
var fsSource = `
|
|
precision mediump float;
|
|
uniform sampler2D uTexture;
|
|
varying vec2 vTexCoord;
|
|
|
|
void main() {
|
|
gl_FragColor = texture2D(uTexture, vTexCoord);
|
|
}
|
|
`;
|
|
function compileShader(shaderGl, source, type) {
|
|
const shader = shaderGl.createShader(type);
|
|
if (!shader) {
|
|
throw new Error("Could not create shader");
|
|
}
|
|
shaderGl.shaderSource(shader, source);
|
|
shaderGl.compileShader(shader);
|
|
if (!shaderGl.getShaderParameter(shader, shaderGl.COMPILE_STATUS)) {
|
|
const log = shaderGl.getShaderInfoLog(shader);
|
|
shaderGl.deleteShader(shader);
|
|
throw new Error("Shader compile error: " + log);
|
|
}
|
|
return shader;
|
|
}
|
|
var createHelperCanvas = ({
|
|
canvasWidth,
|
|
canvasHeight,
|
|
helperCanvasState
|
|
}) => {
|
|
if (helperCanvasState.current) {
|
|
if (helperCanvasState.current.canvas.width !== canvasWidth || helperCanvasState.current.canvas.height !== canvasHeight) {
|
|
helperCanvasState.current.canvas.width = canvasWidth;
|
|
helperCanvasState.current.canvas.height = canvasHeight;
|
|
}
|
|
helperCanvasState.current.gl.viewport(0, 0, canvasWidth, canvasHeight);
|
|
helperCanvasState.current.gl.clearColor(0, 0, 0, 0);
|
|
helperCanvasState.current.gl.clear(helperCanvasState.current.gl.COLOR_BUFFER_BIT);
|
|
return helperCanvasState.current;
|
|
}
|
|
const canvas = new OffscreenCanvas(canvasWidth, canvasHeight);
|
|
const gl = canvas.getContext("webgl", {
|
|
premultipliedAlpha: true
|
|
}) ?? undefined;
|
|
if (!gl) {
|
|
throw new Error("WebGL not supported");
|
|
}
|
|
const vertexShader = compileShader(gl, vsSource, gl.VERTEX_SHADER);
|
|
const fragmentShader = compileShader(gl, fsSource, gl.FRAGMENT_SHADER);
|
|
const program = gl.createProgram();
|
|
if (!program) {
|
|
throw new Error("Could not create program");
|
|
}
|
|
gl.attachShader(program, vertexShader);
|
|
gl.attachShader(program, fragmentShader);
|
|
gl.linkProgram(program);
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
throw new Error("Program link error: " + gl.getProgramInfoLog(program));
|
|
}
|
|
const locations = {
|
|
aPosition: gl.getAttribLocation(program, "aPosition"),
|
|
aTexCoord: gl.getAttribLocation(program, "aTexCoord"),
|
|
uTransform: gl.getUniformLocation(program, "uTransform"),
|
|
uResolution: gl.getUniformLocation(program, "uResolution"),
|
|
uOffset: gl.getUniformLocation(program, "uOffset"),
|
|
uTexture: gl.getUniformLocation(program, "uTexture")
|
|
};
|
|
gl.deleteShader(vertexShader);
|
|
gl.deleteShader(fragmentShader);
|
|
const cleanup = () => {
|
|
gl.deleteProgram(program);
|
|
const loseContext = gl.getExtension("WEBGL_lose_context");
|
|
if (loseContext) {
|
|
loseContext.loseContext();
|
|
}
|
|
};
|
|
helperCanvasState.current = { canvas, gl, program, locations, cleanup };
|
|
return helperCanvasState.current;
|
|
};
|
|
var transformIn3d = ({
|
|
matrix,
|
|
sourceCanvas,
|
|
sourceRect,
|
|
destRect,
|
|
internalState,
|
|
scale
|
|
}) => {
|
|
const { canvas, gl, program, locations } = createHelperCanvas({
|
|
canvasWidth: destRect.width,
|
|
canvasHeight: destRect.height,
|
|
helperCanvasState: internalState.helperCanvasState
|
|
});
|
|
gl.useProgram(program);
|
|
gl.viewport(0, 0, destRect.width, destRect.height);
|
|
gl.clearColor(0, 0, 0, 0);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
gl.enable(gl.BLEND);
|
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
const positionBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
const positions = new Float32Array([
|
|
sourceRect.x,
|
|
sourceRect.y,
|
|
sourceRect.x + sourceRect.width,
|
|
sourceRect.y,
|
|
sourceRect.x,
|
|
sourceRect.y + sourceRect.height,
|
|
sourceRect.x,
|
|
sourceRect.y + sourceRect.height,
|
|
sourceRect.x + sourceRect.width,
|
|
sourceRect.y,
|
|
sourceRect.x + sourceRect.width,
|
|
sourceRect.y + sourceRect.height
|
|
]);
|
|
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
|
|
gl.enableVertexAttribArray(locations.aPosition);
|
|
gl.vertexAttribPointer(locations.aPosition, 2, gl.FLOAT, false, 0, 0);
|
|
const texCoordBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
|
|
const texCoords = new Float32Array([
|
|
0,
|
|
0,
|
|
1,
|
|
0,
|
|
0,
|
|
1,
|
|
0,
|
|
1,
|
|
1,
|
|
0,
|
|
1,
|
|
1
|
|
]);
|
|
gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);
|
|
gl.enableVertexAttribArray(locations.aTexCoord);
|
|
gl.vertexAttribPointer(locations.aTexCoord, 2, gl.FLOAT, false, 0, 0);
|
|
const texture = gl.createTexture();
|
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, sourceCanvas);
|
|
const actualMatrix = scale !== 1 ? new DOMMatrix().scale(scale, scale).multiply(matrix) : matrix;
|
|
const transformMatrix = actualMatrix.toFloat32Array();
|
|
gl.uniformMatrix4fv(locations.uTransform, false, transformMatrix);
|
|
gl.uniform2f(locations.uResolution, destRect.width, destRect.height);
|
|
gl.uniform2f(locations.uOffset, -destRect.x, -destRect.y);
|
|
gl.uniform1i(locations.uTexture, 0);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
gl.disableVertexAttribArray(locations.aPosition);
|
|
gl.disableVertexAttribArray(locations.aTexCoord);
|
|
gl.deleteTexture(texture);
|
|
gl.deleteBuffer(positionBuffer);
|
|
gl.deleteBuffer(texCoordBuffer);
|
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
gl.disable(gl.BLEND);
|
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
return canvas;
|
|
};
|
|
|
|
// src/drawing/handle-3d-transform.ts
|
|
var getPrecomposeRectFor3DTransform = ({
|
|
element,
|
|
parentRect,
|
|
matrix
|
|
}) => {
|
|
const unclampedBiggestBoundingClientRect = getBiggestBoundingClientRect(element);
|
|
const biggestPossiblePretransformRect = getPreTransformRect(parentRect, matrix);
|
|
if (!biggestPossiblePretransformRect) {
|
|
return null;
|
|
}
|
|
const preTransformRect = getNarrowerRect({
|
|
firstRect: unclampedBiggestBoundingClientRect,
|
|
secondRect: biggestPossiblePretransformRect
|
|
});
|
|
return preTransformRect;
|
|
};
|
|
var handle3dTransform = ({
|
|
matrix,
|
|
sourceRect,
|
|
tempCanvas,
|
|
rectAfterTransforms,
|
|
internalState,
|
|
scale
|
|
}) => {
|
|
if (rectAfterTransforms.width <= 0 || rectAfterTransforms.height <= 0) {
|
|
return null;
|
|
}
|
|
const transformed = transformIn3d({
|
|
sourceRect,
|
|
matrix,
|
|
sourceCanvas: tempCanvas,
|
|
destRect: rectAfterTransforms,
|
|
internalState,
|
|
scale
|
|
});
|
|
return transformed;
|
|
};
|
|
|
|
// src/drawing/handle-mask.ts
|
|
var getPrecomposeRectForMask = (element) => {
|
|
const boundingRect = getBiggestBoundingClientRect(element);
|
|
return boundingRect;
|
|
};
|
|
var handleMask = ({
|
|
gradientInfo,
|
|
rect,
|
|
precomposeRect,
|
|
tempContext,
|
|
scale
|
|
}) => {
|
|
const rectToFill = new DOMRect((rect.left - precomposeRect.left) * scale, (rect.top - precomposeRect.top) * scale, rect.width * scale, rect.height * scale);
|
|
const gradient = createCanvasGradient({
|
|
ctx: tempContext,
|
|
rect: rectToFill,
|
|
gradientInfo,
|
|
offsetLeft: 0,
|
|
offsetTop: 0
|
|
});
|
|
tempContext.globalCompositeOperation = "destination-in";
|
|
tempContext.fillStyle = gradient;
|
|
tempContext.fillRect(rectToFill.left, rectToFill.top, rectToFill.width, rectToFill.height);
|
|
};
|
|
|
|
// src/drawing/scale-rect.ts
|
|
var scaleRect = ({
|
|
rect,
|
|
scale
|
|
}) => {
|
|
return new DOMRect(rect.x * scale, rect.y * scale, rect.width * scale, rect.height * scale);
|
|
};
|
|
|
|
// src/drawing/transform-rect-with-matrix.ts
|
|
function transformDOMRect({
|
|
rect,
|
|
matrix
|
|
}) {
|
|
const topLeft = new DOMPointReadOnly(rect.left, rect.top);
|
|
const topRight = new DOMPointReadOnly(rect.right, rect.top);
|
|
const bottomLeft = new DOMPointReadOnly(rect.left, rect.bottom);
|
|
const bottomRight = new DOMPointReadOnly(rect.right, rect.bottom);
|
|
const transformedTopLeft = topLeft.matrixTransform(matrix);
|
|
const transformedTopRight = topRight.matrixTransform(matrix);
|
|
const transformedBottomLeft = bottomLeft.matrixTransform(matrix);
|
|
const transformedBottomRight = bottomRight.matrixTransform(matrix);
|
|
const minX = Math.min(transformedTopLeft.x / transformedTopLeft.w, transformedTopRight.x / transformedTopRight.w, transformedBottomLeft.x / transformedBottomLeft.w, transformedBottomRight.x / transformedBottomRight.w);
|
|
const maxX = Math.max(transformedTopLeft.x / transformedTopLeft.w, transformedTopRight.x / transformedTopRight.w, transformedBottomLeft.x / transformedBottomLeft.w, transformedBottomRight.x / transformedBottomRight.w);
|
|
const minY = Math.min(transformedTopLeft.y / transformedTopLeft.w, transformedTopRight.y / transformedTopRight.w, transformedBottomLeft.y / transformedBottomLeft.w, transformedBottomRight.y / transformedBottomRight.w);
|
|
const maxY = Math.max(transformedTopLeft.y / transformedTopLeft.w, transformedTopRight.y / transformedTopRight.w, transformedBottomLeft.y / transformedBottomLeft.w, transformedBottomRight.y / transformedBottomRight.w);
|
|
return new DOMRect(minX, minY, maxX - minX, maxY - minY);
|
|
}
|
|
|
|
// src/drawing/process-node.ts
|
|
var processNode = async ({
|
|
element,
|
|
context,
|
|
draw,
|
|
logLevel,
|
|
parentRect,
|
|
internalState,
|
|
rootElement,
|
|
scale
|
|
}) => {
|
|
let __stack = [];
|
|
try {
|
|
const transforms = __using(__stack, calculateTransforms({
|
|
element,
|
|
rootElement
|
|
}), 0);
|
|
const { opacity, computedStyle, totalMatrix, dimensions, precompositing } = transforms;
|
|
if (opacity === 0) {
|
|
return { type: "skip-children" };
|
|
}
|
|
if (computedStyle.backfaceVisibility === "hidden" && totalMatrix.m33 < 0) {
|
|
return { type: "skip-children" };
|
|
}
|
|
if (dimensions.width <= 0 || dimensions.height <= 0) {
|
|
return { type: "continue", cleanupAfterChildren: null };
|
|
}
|
|
const rect = new DOMRect(dimensions.left - parentRect.x, dimensions.top - parentRect.y, dimensions.width, dimensions.height);
|
|
if (precompositing.needsPrecompositing) {
|
|
const start = Date.now();
|
|
let precomposeRect = null;
|
|
if (precompositing.needsMaskImage) {
|
|
precomposeRect = roundToExpandRect(getPrecomposeRectForMask(element));
|
|
}
|
|
if (precompositing.needs3DTransformViaWebGL) {
|
|
const tentativePrecomposeRect = getPrecomposeRectFor3DTransform({
|
|
element,
|
|
parentRect,
|
|
matrix: totalMatrix
|
|
});
|
|
if (!tentativePrecomposeRect) {
|
|
return { type: "continue", cleanupAfterChildren: null };
|
|
}
|
|
precomposeRect = roundToExpandRect(getWiderRectAndExpand({
|
|
firstRect: precomposeRect,
|
|
secondRect: tentativePrecomposeRect
|
|
}));
|
|
}
|
|
if (!precomposeRect) {
|
|
throw new Error("Precompose rect not found");
|
|
}
|
|
if (precomposeRect.width <= 0 || precomposeRect.height <= 0) {
|
|
return { type: "continue", cleanupAfterChildren: null };
|
|
}
|
|
if (!doRectsIntersect(precomposeRect, parentRect)) {
|
|
return { type: "continue", cleanupAfterChildren: null };
|
|
}
|
|
const tempContext = await createLayer({
|
|
cutout: precomposeRect,
|
|
element,
|
|
logLevel,
|
|
internalState,
|
|
scale,
|
|
onlyBackgroundClipText: false
|
|
});
|
|
let drawable = tempContext.canvas;
|
|
const rectAfterTransforms = roundToExpandRect(scaleRect({
|
|
scale,
|
|
rect: transformDOMRect({
|
|
rect: precomposeRect,
|
|
matrix: totalMatrix
|
|
})
|
|
}));
|
|
if (precompositing.needsMaskImage) {
|
|
handleMask({
|
|
gradientInfo: precompositing.needsMaskImage,
|
|
rect,
|
|
precomposeRect,
|
|
tempContext,
|
|
scale
|
|
});
|
|
}
|
|
if (precompositing.needs3DTransformViaWebGL) {
|
|
const t = handle3dTransform({
|
|
matrix: totalMatrix,
|
|
sourceRect: precomposeRect,
|
|
tempCanvas: drawable,
|
|
rectAfterTransforms,
|
|
internalState,
|
|
scale
|
|
});
|
|
if (t) {
|
|
drawable = t;
|
|
}
|
|
}
|
|
const previousTransform = context.getTransform();
|
|
context.setTransform(new DOMMatrix);
|
|
context.drawImage(drawable, 0, drawable.height - rectAfterTransforms.height, rectAfterTransforms.width, rectAfterTransforms.height, rectAfterTransforms.left - parentRect.x, rectAfterTransforms.top - parentRect.y, rectAfterTransforms.width, rectAfterTransforms.height);
|
|
context.setTransform(previousTransform);
|
|
Internals6.Log.trace({
|
|
logLevel,
|
|
tag: "@remotion/web-renderer"
|
|
}, `Transforming element in 3D - canvas size: ${precomposeRect.width}x${precomposeRect.height} - compose: ${Date.now() - start}ms - helper canvas: ${drawable.width}x${drawable.height}`);
|
|
internalState.addPrecompose({
|
|
canvasWidth: precomposeRect.width,
|
|
canvasHeight: precomposeRect.height
|
|
});
|
|
return { type: "skip-children" };
|
|
}
|
|
const { cleanupAfterChildren } = await drawElement({
|
|
rect,
|
|
computedStyle,
|
|
context,
|
|
draw,
|
|
opacity,
|
|
totalMatrix,
|
|
parentRect,
|
|
logLevel,
|
|
element,
|
|
internalState,
|
|
scale
|
|
});
|
|
return { type: "continue", cleanupAfterChildren };
|
|
} catch (_catch) {
|
|
var _err = _catch, _hasErr = 1;
|
|
} finally {
|
|
__callDispose(__stack, _err, _hasErr);
|
|
}
|
|
};
|
|
|
|
// src/drawing/text/draw-text.ts
|
|
import { Internals as Internals7 } from "remotion";
|
|
|
|
// src/drawing/text/apply-text-transform.ts
|
|
var applyTextTransform = (text, transform) => {
|
|
if (transform === "uppercase") {
|
|
return text.toUpperCase();
|
|
}
|
|
if (transform === "lowercase") {
|
|
return text.toLowerCase();
|
|
}
|
|
if (transform === "capitalize") {
|
|
return text.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
}
|
|
return text;
|
|
};
|
|
|
|
// src/drawing/text/find-line-breaks.text.ts
|
|
var findWords = (span) => {
|
|
const originalText = span.textContent;
|
|
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
|
|
const segments = segmenter.segment(span.textContent);
|
|
const words = Array.from(segments).map((s) => s.segment);
|
|
const tokens = [];
|
|
for (let i = 0;i < words.length; i++) {
|
|
const wordsBefore = words.slice(0, i);
|
|
const wordsAfter = words.slice(i + 1);
|
|
const word = words[i];
|
|
const wordsBeforeText = wordsBefore.join("");
|
|
const wordsAfterText = wordsAfter.join("");
|
|
const beforeNode = document.createTextNode(wordsBeforeText);
|
|
const afterNode = document.createTextNode(wordsAfterText);
|
|
const interstitialNode = document.createElement("span");
|
|
interstitialNode.textContent = word;
|
|
span.textContent = "";
|
|
span.appendChild(beforeNode);
|
|
span.appendChild(interstitialNode);
|
|
span.appendChild(afterNode);
|
|
const rect = interstitialNode.getBoundingClientRect();
|
|
span.textContent = originalText;
|
|
tokens.push({ text: word, rect });
|
|
}
|
|
return tokens;
|
|
};
|
|
|
|
// src/drawing/text/draw-text.ts
|
|
var drawText = ({
|
|
span,
|
|
logLevel,
|
|
onlyBackgroundClipText,
|
|
parentRect
|
|
}) => {
|
|
const drawFn = ({ computedStyle, contextToDraw }) => {
|
|
const {
|
|
fontFamily,
|
|
fontSize,
|
|
fontWeight,
|
|
direction,
|
|
writingMode,
|
|
letterSpacing,
|
|
textTransform,
|
|
webkitTextFillColor
|
|
} = computedStyle;
|
|
const isVertical = writingMode !== "horizontal-tb";
|
|
if (isVertical) {
|
|
Internals7.Log.warn({
|
|
logLevel,
|
|
tag: "@remotion/web-renderer"
|
|
}, 'Detected "writing-mode" CSS property. Vertical text is not yet supported in @remotion/web-renderer');
|
|
return;
|
|
}
|
|
contextToDraw.save();
|
|
const fontSizePx = parseFloat(fontSize);
|
|
contextToDraw.font = `${fontWeight} ${fontSizePx}px ${fontFamily}`;
|
|
contextToDraw.fillStyle = onlyBackgroundClipText ? "black" : webkitTextFillColor;
|
|
contextToDraw.letterSpacing = letterSpacing;
|
|
const isRTL = direction === "rtl";
|
|
contextToDraw.textAlign = isRTL ? "right" : "left";
|
|
contextToDraw.textBaseline = "alphabetic";
|
|
const originalText = span.textContent;
|
|
const transformedText = applyTextTransform(originalText, textTransform);
|
|
span.textContent = transformedText;
|
|
const tokens = findWords(span);
|
|
for (const token of tokens) {
|
|
const measurements = contextToDraw.measureText(originalText);
|
|
const { fontBoundingBoxDescent, fontBoundingBoxAscent } = measurements;
|
|
const fontHeight = fontBoundingBoxAscent + fontBoundingBoxDescent;
|
|
const leading = token.rect.height - fontHeight;
|
|
const halfLeading = leading / 2;
|
|
contextToDraw.fillText(token.text, (isRTL ? token.rect.right : token.rect.left) - parentRect.x, token.rect.top + fontBoundingBoxAscent + halfLeading - parentRect.y);
|
|
}
|
|
span.textContent = originalText;
|
|
contextToDraw.restore();
|
|
};
|
|
return drawFn;
|
|
};
|
|
|
|
// src/drawing/text/handle-text-node.ts
|
|
var handleTextNode = async ({
|
|
node,
|
|
context,
|
|
logLevel,
|
|
parentRect,
|
|
internalState,
|
|
rootElement,
|
|
onlyBackgroundClipText,
|
|
scale
|
|
}) => {
|
|
const span = document.createElement("span");
|
|
const parent = node.parentNode;
|
|
if (!parent) {
|
|
throw new Error("Text node has no parent");
|
|
}
|
|
parent.insertBefore(span, node);
|
|
span.appendChild(node);
|
|
const value = await processNode({
|
|
context,
|
|
element: span,
|
|
draw: drawText({ span, logLevel, onlyBackgroundClipText, parentRect }),
|
|
logLevel,
|
|
parentRect,
|
|
internalState,
|
|
rootElement,
|
|
scale
|
|
});
|
|
parent.insertBefore(node, span);
|
|
parent.removeChild(span);
|
|
return value;
|
|
};
|
|
|
|
// src/walk-over-node.ts
|
|
var walkOverNode = ({
|
|
node,
|
|
context,
|
|
logLevel,
|
|
parentRect,
|
|
internalState,
|
|
rootElement,
|
|
onlyBackgroundClipText,
|
|
scale
|
|
}) => {
|
|
if (node instanceof HTMLElement || node instanceof SVGElement) {
|
|
return processNode({
|
|
element: node,
|
|
context,
|
|
draw: drawDomElement(node),
|
|
logLevel,
|
|
parentRect,
|
|
internalState,
|
|
rootElement,
|
|
scale
|
|
});
|
|
}
|
|
if (node instanceof Text) {
|
|
return handleTextNode({
|
|
node,
|
|
context,
|
|
logLevel,
|
|
parentRect,
|
|
internalState,
|
|
rootElement,
|
|
onlyBackgroundClipText,
|
|
scale
|
|
});
|
|
}
|
|
throw new Error("Unknown node type");
|
|
};
|
|
|
|
// src/compose.ts
|
|
var getFilterFunction = (node) => {
|
|
if (!(node instanceof Element)) {
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
}
|
|
if (node.parentElement instanceof SVGSVGElement) {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
const computedStyle = getComputedStyle(node);
|
|
if (computedStyle.display === "none") {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
};
|
|
var compose = async ({
|
|
element,
|
|
context,
|
|
logLevel,
|
|
parentRect,
|
|
internalState,
|
|
onlyBackgroundClipText,
|
|
scale
|
|
}) => {
|
|
let __stack = [];
|
|
try {
|
|
const treeWalker = document.createTreeWalker(element, onlyBackgroundClipText ? NodeFilter.SHOW_TEXT : NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, getFilterFunction);
|
|
if (onlyBackgroundClipText) {
|
|
treeWalker.nextNode();
|
|
if (!treeWalker.currentNode) {
|
|
return;
|
|
}
|
|
}
|
|
const treeWalkerClean = __using(__stack, createTreeWalkerCleanupAfterChildren(treeWalker), 0);
|
|
const { checkCleanUpAtBeginningOfIteration, addCleanup } = treeWalkerClean;
|
|
while (true) {
|
|
checkCleanUpAtBeginningOfIteration();
|
|
const val = await walkOverNode({
|
|
node: treeWalker.currentNode,
|
|
context,
|
|
logLevel,
|
|
parentRect,
|
|
internalState,
|
|
rootElement: element,
|
|
onlyBackgroundClipText,
|
|
scale
|
|
});
|
|
if (val.type === "skip-children") {
|
|
if (!skipToNextNonDescendant(treeWalker)) {
|
|
break;
|
|
}
|
|
} else {
|
|
if (val.cleanupAfterChildren) {
|
|
addCleanup(treeWalker.currentNode, val.cleanupAfterChildren);
|
|
}
|
|
if (!treeWalker.nextNode()) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} catch (_catch) {
|
|
var _err = _catch, _hasErr = 1;
|
|
} finally {
|
|
__callDispose(__stack, _err, _hasErr);
|
|
}
|
|
};
|
|
|
|
// src/take-screenshot.ts
|
|
var createLayer = async ({
|
|
element,
|
|
scale,
|
|
logLevel,
|
|
internalState,
|
|
onlyBackgroundClipText,
|
|
cutout
|
|
}) => {
|
|
const scaledWidth = Math.ceil(cutout.width * scale);
|
|
const scaledHeight = Math.ceil(cutout.height * scale);
|
|
const canvas = new OffscreenCanvas(scaledWidth, scaledHeight);
|
|
const context = canvas.getContext("2d");
|
|
if (!context) {
|
|
throw new Error("Could not get context");
|
|
}
|
|
await compose({
|
|
element,
|
|
context,
|
|
logLevel,
|
|
parentRect: cutout,
|
|
internalState,
|
|
onlyBackgroundClipText,
|
|
scale
|
|
});
|
|
return context;
|
|
};
|
|
|
|
// src/throttle-progress.ts
|
|
var DEFAULT_THROTTLE_MS = 250;
|
|
var createThrottledProgressCallback = (callback, throttleMs = DEFAULT_THROTTLE_MS) => {
|
|
if (!callback) {
|
|
return null;
|
|
}
|
|
let lastCallTime = 0;
|
|
let pendingUpdate = null;
|
|
let timeoutId = null;
|
|
const throttled = (progress) => {
|
|
const now = Date.now();
|
|
const timeSinceLastCall = now - lastCallTime;
|
|
pendingUpdate = progress;
|
|
if (timeSinceLastCall >= throttleMs) {
|
|
lastCallTime = now;
|
|
callback(progress);
|
|
pendingUpdate = null;
|
|
if (timeoutId !== null) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
}
|
|
} else if (timeoutId === null) {
|
|
const remainingTime = throttleMs - timeSinceLastCall;
|
|
timeoutId = setTimeout(() => {
|
|
if (pendingUpdate !== null) {
|
|
lastCallTime = Date.now();
|
|
callback(pendingUpdate);
|
|
pendingUpdate = null;
|
|
}
|
|
timeoutId = null;
|
|
}, remainingTime);
|
|
}
|
|
};
|
|
const cleanup = () => {
|
|
if (timeoutId !== null) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
}
|
|
pendingUpdate = null;
|
|
};
|
|
return { throttled, [Symbol.dispose]: cleanup };
|
|
};
|
|
|
|
// src/validate-scale.ts
|
|
var validateScale = (scale) => {
|
|
if (typeof scale === "undefined") {
|
|
return;
|
|
}
|
|
if (typeof scale !== "number") {
|
|
throw new Error('Scale should be a number or undefined, but is "' + JSON.stringify(scale) + '"');
|
|
}
|
|
if (Number.isNaN(scale)) {
|
|
throw new Error("`scale` should not be NaN, but is NaN");
|
|
}
|
|
if (!Number.isFinite(scale)) {
|
|
throw new Error(`"scale" must be finite, but is ${scale}`);
|
|
}
|
|
if (scale <= 0) {
|
|
throw new Error(`"scale" must be bigger than 0, but is ${scale}`);
|
|
}
|
|
if (scale > 16) {
|
|
throw new Error(`"scale" must be smaller or equal than 16, but is ${scale}`);
|
|
}
|
|
};
|
|
|
|
// src/validate-video-frame.ts
|
|
var validateVideoFrame = ({
|
|
originalFrame,
|
|
returnedFrame,
|
|
expectedWidth,
|
|
expectedHeight,
|
|
expectedTimestamp
|
|
}) => {
|
|
if (!(returnedFrame instanceof VideoFrame)) {
|
|
originalFrame.close();
|
|
throw new Error("onFrame callback must return a VideoFrame or void");
|
|
}
|
|
if (returnedFrame === originalFrame) {
|
|
return returnedFrame;
|
|
}
|
|
if (returnedFrame.displayWidth !== expectedWidth || returnedFrame.displayHeight !== expectedHeight) {
|
|
originalFrame.close();
|
|
returnedFrame.close();
|
|
throw new Error(`VideoFrame dimensions mismatch: expected ${expectedWidth}x${expectedHeight}, got ${returnedFrame.displayWidth}x${returnedFrame.displayHeight}`);
|
|
}
|
|
if (returnedFrame.timestamp !== expectedTimestamp) {
|
|
originalFrame.close();
|
|
returnedFrame.close();
|
|
throw new Error(`VideoFrame timestamp mismatch: expected ${expectedTimestamp}, got ${returnedFrame.timestamp}`);
|
|
}
|
|
originalFrame.close();
|
|
return returnedFrame;
|
|
};
|
|
|
|
// src/with-resolvers.ts
|
|
var withResolvers = function() {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
return { promise, resolve, reject };
|
|
};
|
|
|
|
// src/wait-for-ready.ts
|
|
var waitForReady = ({
|
|
timeoutInMilliseconds,
|
|
scope,
|
|
signal,
|
|
apiName,
|
|
internalState,
|
|
keepalive
|
|
}) => {
|
|
const start = performance.now();
|
|
const { promise, resolve, reject } = withResolvers();
|
|
let cancelled = false;
|
|
const check = () => {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
if (signal?.aborted) {
|
|
cancelled = true;
|
|
internalState?.addWaitForReadyTime(performance.now() - start);
|
|
reject(new Error(`${apiName}() was cancelled`));
|
|
return;
|
|
}
|
|
if (scope.remotion_renderReady === true) {
|
|
internalState?.addWaitForReadyTime(performance.now() - start);
|
|
resolve();
|
|
return;
|
|
}
|
|
if (scope.remotion_cancelledError !== undefined) {
|
|
cancelled = true;
|
|
internalState?.addWaitForReadyTime(performance.now() - start);
|
|
const stack = scope.remotion_cancelledError;
|
|
const message = stack.split(`
|
|
`)[0].replace(/^Error: /, "");
|
|
const error = new Error(message);
|
|
error.stack = stack;
|
|
reject(error);
|
|
return;
|
|
}
|
|
if (performance.now() - start > timeoutInMilliseconds + 3000) {
|
|
cancelled = true;
|
|
internalState?.addWaitForReadyTime(performance.now() - start);
|
|
reject(new Error(Object.values(scope.remotion_delayRenderTimeouts).map((d) => d.label).join(", ")));
|
|
return;
|
|
}
|
|
scheduleNextCheck();
|
|
};
|
|
const scheduleNextCheck = () => {
|
|
const rafTick = new Promise((res) => {
|
|
requestAnimationFrame(() => res());
|
|
});
|
|
const backgroundSafeTick = keepalive ? Promise.race([rafTick, keepalive.waitForTick()]) : rafTick;
|
|
backgroundSafeTick.then(check);
|
|
};
|
|
check();
|
|
return promise;
|
|
};
|
|
|
|
// src/web-fs-target.ts
|
|
var sessionId = null;
|
|
var getPrefix = () => {
|
|
if (!sessionId) {
|
|
sessionId = crypto.randomUUID();
|
|
}
|
|
return `__remotion_render:${sessionId}:`;
|
|
};
|
|
var cleanupStaleOpfsFiles = async () => {
|
|
try {
|
|
const root = await navigator.storage.getDirectory();
|
|
for await (const [name] of root.entries()) {
|
|
if (name.startsWith("__remotion_render:") && !name.startsWith(getPrefix())) {
|
|
await root.removeEntry(name);
|
|
}
|
|
}
|
|
} catch {}
|
|
};
|
|
var createWebFsTarget = async () => {
|
|
const directoryHandle = await navigator.storage.getDirectory();
|
|
const filename = `${getPrefix()}${crypto.randomUUID()}`;
|
|
const fileHandle = await directoryHandle.getFileHandle(filename, {
|
|
create: true
|
|
});
|
|
const writable = await fileHandle.createWritable();
|
|
const stream = new WritableStream({
|
|
async write(chunk) {
|
|
await writable.seek(chunk.position);
|
|
await writable.write(chunk);
|
|
}
|
|
});
|
|
const getBlob = async () => {
|
|
const handle = await directoryHandle.getFileHandle(filename);
|
|
return handle.getFile();
|
|
};
|
|
const close = () => writable.close();
|
|
return { stream, getBlob, close };
|
|
};
|
|
|
|
// src/render-media-on-web.tsx
|
|
var internalRenderMediaOnWeb = async ({
|
|
composition,
|
|
inputProps,
|
|
delayRenderTimeoutInMilliseconds,
|
|
logLevel,
|
|
mediaCacheSizeInBytes,
|
|
schema,
|
|
videoCodec: codec,
|
|
audioCodec: unresolvedAudioCodec,
|
|
audioBitrate,
|
|
container,
|
|
signal,
|
|
onProgress,
|
|
hardwareAcceleration,
|
|
keyframeIntervalInSeconds,
|
|
videoBitrate,
|
|
frameRange,
|
|
transparent,
|
|
onArtifact,
|
|
onFrame,
|
|
outputTarget: userDesiredOutputTarget,
|
|
licenseKey,
|
|
muted,
|
|
scale,
|
|
isProduction
|
|
}) => {
|
|
let __stack2 = [];
|
|
try {
|
|
validateScale(scale);
|
|
const outputTarget = userDesiredOutputTarget === null ? await canUseWebFsWriter() ? "web-fs" : "arraybuffer" : userDesiredOutputTarget;
|
|
if (outputTarget === "web-fs") {
|
|
await cleanupStaleOpfsFiles();
|
|
}
|
|
const format = containerToMediabunnyContainer(container);
|
|
if (codec && !format.getSupportedCodecs().includes(codecToMediabunnyCodec(codec))) {
|
|
return Promise.reject(new Error(`Codec ${codec} is not supported for container ${container}`));
|
|
}
|
|
const resolvedAudioBitrate = typeof audioBitrate === "number" ? audioBitrate : getQualityForWebRendererQuality(audioBitrate);
|
|
let finalAudioCodec = null;
|
|
if (!muted) {
|
|
const audioResult = await resolveAudioCodec({
|
|
container,
|
|
requestedCodec: unresolvedAudioCodec,
|
|
userSpecifiedAudioCodec: unresolvedAudioCodec !== undefined && unresolvedAudioCodec !== null,
|
|
bitrate: resolvedAudioBitrate
|
|
});
|
|
for (const issue of audioResult.issues) {
|
|
if (issue.severity === "error") {
|
|
return Promise.reject(new Error(issue.message));
|
|
}
|
|
Internals8.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, issue.message);
|
|
}
|
|
finalAudioCodec = audioResult.codec;
|
|
}
|
|
const resolved = await Internals8.resolveVideoConfig({
|
|
calculateMetadata: composition.calculateMetadata ?? null,
|
|
signal: signal ?? new AbortController().signal,
|
|
defaultProps: composition.defaultProps ?? {},
|
|
inputProps: inputProps ?? {},
|
|
compositionId: composition.id,
|
|
compositionDurationInFrames: composition.durationInFrames ?? null,
|
|
compositionFps: composition.fps ?? null,
|
|
compositionHeight: composition.height ?? null,
|
|
compositionWidth: composition.width ?? null
|
|
});
|
|
const realFrameRange = getRealFrameRange(resolved.durationInFrames, frameRange);
|
|
if (signal?.aborted) {
|
|
return Promise.reject(new Error("renderMediaOnWeb() was cancelled"));
|
|
}
|
|
const scaffold = __using(__stack2, createScaffold({
|
|
width: resolved.width,
|
|
height: resolved.height,
|
|
fps: resolved.fps,
|
|
durationInFrames: resolved.durationInFrames,
|
|
Component: composition.component,
|
|
resolvedProps: resolved.props,
|
|
id: resolved.id,
|
|
delayRenderTimeoutInMilliseconds,
|
|
logLevel,
|
|
mediaCacheSizeInBytes,
|
|
schema: schema ?? null,
|
|
audioEnabled: !muted,
|
|
videoEnabled: true,
|
|
initialFrame: 0,
|
|
defaultCodec: resolved.defaultCodec,
|
|
defaultOutName: resolved.defaultOutName
|
|
}), 0);
|
|
const { delayRenderScope, div, timeUpdater, collectAssets, errorHolder } = scaffold;
|
|
const internalState = __using(__stack2, makeInternalState(), 0);
|
|
const keepalive = __using(__stack2, createBackgroundKeepalive({
|
|
fps: resolved.fps,
|
|
logLevel
|
|
}), 0);
|
|
const artifactsHandler = handleArtifacts();
|
|
const webFsTarget = outputTarget === "web-fs" ? await createWebFsTarget() : null;
|
|
const target = webFsTarget ? new StreamTarget(webFsTarget.stream) : new BufferTarget;
|
|
const outputWithCleanup = __using(__stack2, makeOutputWithCleanup({
|
|
format,
|
|
target
|
|
}), 0);
|
|
const throttledProgress = __using(__stack2, createThrottledProgressCallback(onProgress), 0);
|
|
const throttledOnProgress = throttledProgress?.throttled ?? null;
|
|
try {
|
|
let __stack = [];
|
|
try {
|
|
if (signal?.aborted) {
|
|
throw new Error("renderMediaOnWeb() was cancelled");
|
|
}
|
|
await waitForReady({
|
|
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
|
|
scope: delayRenderScope,
|
|
signal,
|
|
apiName: "renderMediaOnWeb",
|
|
internalState,
|
|
keepalive
|
|
});
|
|
checkForError(errorHolder);
|
|
if (signal?.aborted) {
|
|
throw new Error("renderMediaOnWeb() was cancelled");
|
|
}
|
|
const videoSampleSource = __using(__stack, makeVideoSampleSourceCleanup({
|
|
codec: codecToMediabunnyCodec(codec),
|
|
bitrate: typeof videoBitrate === "number" ? videoBitrate : getQualityForWebRendererQuality(videoBitrate),
|
|
sizeChangeBehavior: "deny",
|
|
hardwareAcceleration,
|
|
latencyMode: "quality",
|
|
keyFrameInterval: keyframeIntervalInSeconds,
|
|
alpha: transparent ? "keep" : "discard"
|
|
}), 0);
|
|
outputWithCleanup.output.addVideoTrack(videoSampleSource.videoSampleSource);
|
|
const audioSampleSource = __using(__stack, createAudioSampleSource({
|
|
muted,
|
|
codec: finalAudioCodec ? audioCodecToMediabunnyAudioCodec(finalAudioCodec) : null,
|
|
bitrate: resolvedAudioBitrate
|
|
}), 0);
|
|
if (audioSampleSource) {
|
|
outputWithCleanup.output.addAudioTrack(audioSampleSource.audioSampleSource);
|
|
}
|
|
await outputWithCleanup.output.start();
|
|
if (signal?.aborted) {
|
|
throw new Error("renderMediaOnWeb() was cancelled");
|
|
}
|
|
const progress = {
|
|
renderedFrames: 0,
|
|
encodedFrames: 0
|
|
};
|
|
for (let frame = realFrameRange[0];frame <= realFrameRange[1]; frame++) {
|
|
if (signal?.aborted) {
|
|
throw new Error("renderMediaOnWeb() was cancelled");
|
|
}
|
|
timeUpdater.current?.update(frame);
|
|
await waitForReady({
|
|
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
|
|
scope: delayRenderScope,
|
|
signal,
|
|
apiName: "renderMediaOnWeb",
|
|
keepalive,
|
|
internalState
|
|
});
|
|
checkForError(errorHolder);
|
|
if (signal?.aborted) {
|
|
throw new Error("renderMediaOnWeb() was cancelled");
|
|
}
|
|
const createFrameStart = performance.now();
|
|
const layer = await createLayer({
|
|
element: div,
|
|
scale,
|
|
logLevel,
|
|
internalState,
|
|
onlyBackgroundClipText: false,
|
|
cutout: new DOMRect(0, 0, resolved.width, resolved.height)
|
|
});
|
|
internalState.addCreateFrameTime(performance.now() - createFrameStart);
|
|
if (signal?.aborted) {
|
|
throw new Error("renderMediaOnWeb() was cancelled");
|
|
}
|
|
const timestamp = Math.round((frame - realFrameRange[0]) / resolved.fps * 1e6);
|
|
const videoFrame = new VideoFrame(layer.canvas, {
|
|
timestamp
|
|
});
|
|
progress.renderedFrames++;
|
|
throttledOnProgress?.({ ...progress });
|
|
let frameToEncode = videoFrame;
|
|
if (onFrame) {
|
|
const returnedFrame = await onFrame(videoFrame);
|
|
if (signal?.aborted) {
|
|
throw new Error("renderMediaOnWeb() was cancelled");
|
|
}
|
|
frameToEncode = validateVideoFrame({
|
|
originalFrame: videoFrame,
|
|
returnedFrame,
|
|
expectedWidth: Math.round(resolved.width * scale),
|
|
expectedHeight: Math.round(resolved.height * scale),
|
|
expectedTimestamp: timestamp
|
|
});
|
|
}
|
|
const audioCombineStart = performance.now();
|
|
const assets = collectAssets.current.collectAssets();
|
|
if (onArtifact) {
|
|
await artifactsHandler.handle({
|
|
imageData: layer.canvas,
|
|
frame,
|
|
assets,
|
|
onArtifact
|
|
});
|
|
}
|
|
if (signal?.aborted) {
|
|
throw new Error("renderMediaOnWeb() was cancelled");
|
|
}
|
|
const audio = muted ? null : onlyInlineAudio({ assets, fps: resolved.fps, timestamp });
|
|
internalState.addAudioMixingTime(performance.now() - audioCombineStart);
|
|
const addSampleStart = performance.now();
|
|
await Promise.all([
|
|
addVideoSampleAndCloseFrame(frameToEncode, videoSampleSource.videoSampleSource),
|
|
audio && audioSampleSource ? addAudioSample(audio, audioSampleSource.audioSampleSource) : Promise.resolve()
|
|
]);
|
|
internalState.addAddSampleTime(performance.now() - addSampleStart);
|
|
progress.encodedFrames++;
|
|
throttledOnProgress?.({ ...progress });
|
|
if (signal?.aborted) {
|
|
throw new Error("renderMediaOnWeb() was cancelled");
|
|
}
|
|
}
|
|
onProgress?.({ ...progress });
|
|
videoSampleSource.videoSampleSource.close();
|
|
audioSampleSource?.audioSampleSource.close();
|
|
await outputWithCleanup.output.finalize();
|
|
Internals8.Log.verbose({ logLevel, tag: "web-renderer" }, `Render timings: waitForReady=${internalState.getWaitForReadyTime().toFixed(2)}ms, createFrame=${internalState.getCreateFrameTime().toFixed(2)}ms, addSample=${internalState.getAddSampleTime().toFixed(2)}ms, audioMixing=${internalState.getAudioMixingTime().toFixed(2)}ms`);
|
|
if (webFsTarget) {
|
|
sendUsageEvent({
|
|
licenseKey: licenseKey ?? null,
|
|
succeeded: true,
|
|
apiName: "renderMediaOnWeb",
|
|
isStill: false,
|
|
isProduction: isProduction ?? true
|
|
});
|
|
await webFsTarget.close();
|
|
return {
|
|
getBlob: () => {
|
|
return webFsTarget.getBlob();
|
|
},
|
|
internalState
|
|
};
|
|
}
|
|
if (!(target instanceof BufferTarget)) {
|
|
throw new Error("Expected target to be a BufferTarget");
|
|
}
|
|
sendUsageEvent({
|
|
licenseKey: licenseKey ?? null,
|
|
succeeded: true,
|
|
apiName: "renderMediaOnWeb",
|
|
isStill: false,
|
|
isProduction: isProduction ?? true
|
|
});
|
|
return {
|
|
getBlob: () => {
|
|
if (!target.buffer) {
|
|
throw new Error("The resulting buffer is empty");
|
|
}
|
|
return Promise.resolve(new Blob([target.buffer], { type: getMimeType(container) }));
|
|
},
|
|
internalState
|
|
};
|
|
} catch (_catch) {
|
|
var _err = _catch, _hasErr = 1;
|
|
} finally {
|
|
__callDispose(__stack, _err, _hasErr);
|
|
}
|
|
} catch (err) {
|
|
if (!signal?.aborted) {
|
|
sendUsageEvent({
|
|
succeeded: false,
|
|
licenseKey: licenseKey ?? null,
|
|
apiName: "renderMediaOnWeb",
|
|
isStill: false,
|
|
isProduction: isProduction ?? true
|
|
}).catch((err2) => {
|
|
Internals8.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
|
|
});
|
|
}
|
|
throw err;
|
|
}
|
|
} catch (_catch2) {
|
|
var _err2 = _catch2, _hasErr2 = 1;
|
|
} finally {
|
|
__callDispose(__stack2, _err2, _hasErr2);
|
|
}
|
|
};
|
|
var renderMediaOnWeb = (options) => {
|
|
const container = options.container ?? "mp4";
|
|
const codec = options.videoCodec ?? getDefaultVideoCodecForContainer(container);
|
|
onlyOneRenderAtATimeQueue.ref = onlyOneRenderAtATimeQueue.ref.catch(() => Promise.resolve()).then(() => internalRenderMediaOnWeb({
|
|
...options,
|
|
delayRenderTimeoutInMilliseconds: options.delayRenderTimeoutInMilliseconds ?? 30000,
|
|
logLevel: options.logLevel ?? window.remotion_logLevel ?? "info",
|
|
schema: options.schema ?? undefined,
|
|
mediaCacheSizeInBytes: options.mediaCacheSizeInBytes ?? null,
|
|
videoCodec: codec,
|
|
audioCodec: options.audioCodec ?? null,
|
|
audioBitrate: options.audioBitrate ?? "medium",
|
|
container,
|
|
signal: options.signal ?? null,
|
|
onProgress: options.onProgress ?? null,
|
|
hardwareAcceleration: options.hardwareAcceleration ?? "no-preference",
|
|
keyframeIntervalInSeconds: options.keyframeIntervalInSeconds ?? 5,
|
|
videoBitrate: options.videoBitrate ?? "medium",
|
|
frameRange: options.frameRange ?? null,
|
|
transparent: options.transparent ?? false,
|
|
onArtifact: options.onArtifact ?? null,
|
|
onFrame: options.onFrame ?? null,
|
|
outputTarget: options.outputTarget ?? null,
|
|
licenseKey: options.licenseKey ?? null,
|
|
muted: options.muted ?? false,
|
|
scale: options.scale ?? 1,
|
|
isProduction: options.isProduction ?? true
|
|
}));
|
|
return onlyOneRenderAtATimeQueue.ref;
|
|
};
|
|
// src/render-still-on-web.tsx
|
|
import {
|
|
Internals as Internals9
|
|
} from "remotion";
|
|
async function internalRenderStillOnWeb({
|
|
frame,
|
|
delayRenderTimeoutInMilliseconds,
|
|
logLevel,
|
|
inputProps,
|
|
schema,
|
|
imageFormat,
|
|
mediaCacheSizeInBytes,
|
|
composition,
|
|
signal,
|
|
onArtifact,
|
|
licenseKey,
|
|
scale,
|
|
isProduction
|
|
}) {
|
|
let __stack = [];
|
|
try {
|
|
validateScale(scale);
|
|
const resolved = await Internals9.resolveVideoConfig({
|
|
calculateMetadata: composition.calculateMetadata ?? null,
|
|
signal: signal ?? new AbortController().signal,
|
|
defaultProps: composition.defaultProps ?? {},
|
|
inputProps: inputProps ?? {},
|
|
compositionId: composition.id,
|
|
compositionDurationInFrames: composition.durationInFrames ?? null,
|
|
compositionFps: composition.fps ?? null,
|
|
compositionHeight: composition.height ?? null,
|
|
compositionWidth: composition.width ?? null
|
|
});
|
|
if (signal?.aborted) {
|
|
return Promise.reject(new Error("renderStillOnWeb() was cancelled"));
|
|
}
|
|
const internalState = __using(__stack, makeInternalState(), 0);
|
|
const scaffold = __using(__stack, createScaffold({
|
|
width: resolved.width,
|
|
height: resolved.height,
|
|
delayRenderTimeoutInMilliseconds,
|
|
logLevel,
|
|
resolvedProps: resolved.props,
|
|
id: resolved.id,
|
|
mediaCacheSizeInBytes,
|
|
audioEnabled: false,
|
|
Component: composition.component,
|
|
videoEnabled: true,
|
|
durationInFrames: resolved.durationInFrames,
|
|
fps: resolved.fps,
|
|
schema: schema ?? null,
|
|
initialFrame: frame,
|
|
defaultCodec: resolved.defaultCodec,
|
|
defaultOutName: resolved.defaultOutName
|
|
}), 0);
|
|
const { delayRenderScope, div, collectAssets, errorHolder } = scaffold;
|
|
const artifactsHandler = handleArtifacts();
|
|
try {
|
|
if (signal?.aborted) {
|
|
throw new Error("renderStillOnWeb() was cancelled");
|
|
}
|
|
await waitForReady({
|
|
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
|
|
scope: delayRenderScope,
|
|
signal,
|
|
apiName: "renderStillOnWeb",
|
|
internalState: null,
|
|
keepalive: null
|
|
});
|
|
checkForError(errorHolder);
|
|
if (signal?.aborted) {
|
|
throw new Error("renderStillOnWeb() was cancelled");
|
|
}
|
|
const capturedFrame = await createLayer({
|
|
element: div,
|
|
scale,
|
|
logLevel,
|
|
internalState,
|
|
onlyBackgroundClipText: false,
|
|
cutout: new DOMRect(0, 0, resolved.width, resolved.height)
|
|
});
|
|
const imageData = await capturedFrame.canvas.convertToBlob({
|
|
type: `image/${imageFormat}`
|
|
});
|
|
const assets = collectAssets.current.collectAssets();
|
|
if (onArtifact) {
|
|
await artifactsHandler.handle({ imageData, frame, assets, onArtifact });
|
|
}
|
|
sendUsageEvent({
|
|
licenseKey: licenseKey ?? null,
|
|
succeeded: true,
|
|
apiName: "renderStillOnWeb",
|
|
isStill: true,
|
|
isProduction
|
|
});
|
|
return { blob: imageData, internalState };
|
|
} catch (err) {
|
|
if (!signal?.aborted) {
|
|
sendUsageEvent({
|
|
succeeded: false,
|
|
licenseKey: licenseKey ?? null,
|
|
apiName: "renderStillOnWeb",
|
|
isStill: true,
|
|
isProduction
|
|
}).catch((err2) => {
|
|
Internals9.Log.error({ logLevel: "error", tag: "web-renderer" }, "Failed to send usage event", err2);
|
|
});
|
|
}
|
|
throw err;
|
|
}
|
|
} catch (_catch) {
|
|
var _err = _catch, _hasErr = 1;
|
|
} finally {
|
|
__callDispose(__stack, _err, _hasErr);
|
|
}
|
|
}
|
|
var renderStillOnWeb = (options) => {
|
|
onlyOneRenderAtATimeQueue.ref = onlyOneRenderAtATimeQueue.ref.catch(() => Promise.resolve()).then(() => internalRenderStillOnWeb({
|
|
...options,
|
|
delayRenderTimeoutInMilliseconds: options.delayRenderTimeoutInMilliseconds ?? 30000,
|
|
logLevel: options.logLevel ?? window.remotion_logLevel ?? "info",
|
|
schema: options.schema ?? undefined,
|
|
mediaCacheSizeInBytes: options.mediaCacheSizeInBytes ?? null,
|
|
signal: options.signal ?? null,
|
|
onArtifact: options.onArtifact ?? null,
|
|
licenseKey: options.licenseKey ?? null,
|
|
scale: options.scale ?? 1,
|
|
isProduction: options.isProduction ?? true
|
|
}));
|
|
return onlyOneRenderAtATimeQueue.ref;
|
|
};
|
|
export {
|
|
renderStillOnWeb,
|
|
renderMediaOnWeb,
|
|
getSupportedVideoCodecsForContainer,
|
|
getSupportedAudioCodecsForContainer,
|
|
getEncodableVideoCodecs,
|
|
getEncodableAudioCodecs,
|
|
getDefaultVideoCodecForContainer,
|
|
getDefaultAudioCodecForContainer,
|
|
canRenderMediaOnWeb
|
|
};
|