Add .gitignore to exclude all node packages and lock files
This commit is contained in:
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import type { AudioSampleSource, VideoSampleSource } from 'mediabunny';
|
||||
export declare const addVideoSampleAndCloseFrame: (frameToEncode: VideoFrame, videoSampleSource: VideoSampleSource) => Promise<void>;
|
||||
export declare const addAudioSample: (audio: AudioData, audioSampleSource: AudioSampleSource) => Promise<void>;
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
import { AudioSample, VideoSample } from 'mediabunny';
|
||||
export const addVideoSampleAndCloseFrame = async (frameToEncode, videoSampleSource) => {
|
||||
const sample = new VideoSample(frameToEncode);
|
||||
try {
|
||||
await videoSampleSource.add(sample);
|
||||
}
|
||||
finally {
|
||||
sample.close();
|
||||
frameToEncode.close();
|
||||
}
|
||||
};
|
||||
export const addAudioSample = async (audio, audioSampleSource) => {
|
||||
const sample = new AudioSample(audio);
|
||||
try {
|
||||
await audioSampleSource.add(sample);
|
||||
}
|
||||
finally {
|
||||
sample.close();
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
import type { DownloadBehavior, TRenderAsset } from 'remotion';
|
||||
export type EmittedArtifact = {
|
||||
filename: string;
|
||||
content: string | Uint8Array;
|
||||
frame: number;
|
||||
downloadBehavior: DownloadBehavior | null;
|
||||
};
|
||||
export declare const onlyArtifact: ({ assets, frameBuffer, }: {
|
||||
assets: TRenderAsset[];
|
||||
frameBuffer: Blob | OffscreenCanvas | null;
|
||||
}) => Promise<EmittedArtifact[]>;
|
||||
export type WebRendererOnArtifact = (asset: EmittedArtifact) => void;
|
||||
export type ArtifactsRef = React.RefObject<{
|
||||
collectAssets: () => TRenderAsset[];
|
||||
} | null>;
|
||||
export declare const handleArtifacts: () => {
|
||||
handle: ({ imageData, frame, assets: artifactAssets, onArtifact, }: {
|
||||
imageData: Blob | OffscreenCanvas | null;
|
||||
frame: number;
|
||||
assets: TRenderAsset[];
|
||||
onArtifact: WebRendererOnArtifact;
|
||||
}) => Promise<void>;
|
||||
};
|
||||
Generated
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
import { NoReactInternals } from 'remotion/no-react';
|
||||
export const 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) {
|
||||
// A thumbnail artifact was defined to be emitted, but the output was not a video.
|
||||
// Also, in Lambda, there are extra frames which are not video frames.
|
||||
// This could happen if a thumbnail is unconditionally emitted.
|
||||
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);
|
||||
};
|
||||
export const 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 };
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import type { TRenderAsset } from 'remotion';
|
||||
export declare const onlyInlineAudio: ({ assets, fps, timestamp, }: {
|
||||
assets: TRenderAsset[];
|
||||
fps: number;
|
||||
timestamp: number;
|
||||
}) => AudioData | null;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
const TARGET_NUMBER_OF_CHANNELS = 2;
|
||||
const 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) => {
|
||||
var _a;
|
||||
return acc + ((_a = wave[i]) !== null && _a !== void 0 ? _a : 0);
|
||||
}, 0);
|
||||
// Clamp to Int16 range
|
||||
mixed[i] = Math.max(-32768, Math.min(32767, sum));
|
||||
}
|
||||
return mixed;
|
||||
}
|
||||
export const onlyInlineAudio = ({ assets, fps, frame, }) => {
|
||||
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: (frame / fps) * 1000000,
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import { type LogLevel } from 'remotion';
|
||||
export type BackgroundKeepalive = {
|
||||
waitForTick: () => Promise<void>;
|
||||
[Symbol.dispose]: () => void;
|
||||
};
|
||||
export declare function createBackgroundKeepalive({ fps, logLevel }: {
|
||||
fps: number;
|
||||
logLevel: LogLevel;
|
||||
}): BackgroundKeepalive;
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import type { CanRenderMediaOnWebOptions, CanRenderMediaOnWebResult } from './can-render-types';
|
||||
export type { CanRenderIssue, CanRenderMediaOnWebOptions, CanRenderMediaOnWebResult, } from './can-render-types';
|
||||
export declare const canRenderMediaOnWeb: (options: CanRenderMediaOnWebOptions) => Promise<CanRenderMediaOnWebResult>;
|
||||
Generated
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
import type { WebRendererAudioCodec, WebRendererContainer, WebRendererQuality, WebRendererVideoCodec } from './mediabunny-mappings';
|
||||
import type { WebRendererOutputTarget } from './output-target';
|
||||
export type CanRenderIssueType = 'video-codec-unsupported' | 'audio-codec-unsupported' | 'webgl-unsupported' | 'webcodecs-unavailable' | 'container-codec-mismatch' | 'transparent-video-unsupported' | 'invalid-dimensions' | 'output-target-unsupported';
|
||||
export type CanRenderIssue = {
|
||||
type: CanRenderIssueType;
|
||||
message: string;
|
||||
severity: 'error' | 'warning';
|
||||
};
|
||||
export type CanRenderMediaOnWebResult = {
|
||||
canRender: boolean;
|
||||
issues: CanRenderIssue[];
|
||||
resolvedVideoCodec: WebRendererVideoCodec;
|
||||
resolvedAudioCodec: WebRendererAudioCodec | null;
|
||||
resolvedOutputTarget: WebRendererOutputTarget;
|
||||
};
|
||||
export type CanRenderMediaOnWebOptions = {
|
||||
container?: WebRendererContainer;
|
||||
videoCodec?: WebRendererVideoCodec;
|
||||
audioCodec?: WebRendererAudioCodec | null;
|
||||
width: number;
|
||||
height: number;
|
||||
transparent?: boolean;
|
||||
muted?: boolean;
|
||||
videoBitrate?: number | WebRendererQuality;
|
||||
audioBitrate?: number | WebRendererQuality;
|
||||
outputTarget?: WebRendererOutputTarget | null;
|
||||
};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare const canUseWebFsWriter: () => Promise<boolean>;
|
||||
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
export const 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 (_a) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import type { CanRenderIssue } from './can-render-types';
|
||||
export declare const checkWebGLSupport: () => CanRenderIssue | null;
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
export declare const compose: ({ element, context, logLevel, parentRect, internalState, onlyBackgroundClipText, scale, }: {
|
||||
element: HTMLElement | SVGElement;
|
||||
context: OffscreenCanvasRenderingContext2D;
|
||||
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
|
||||
parentRect: DOMRect;
|
||||
internalState: {
|
||||
getDrawn3dPixels: () => number;
|
||||
getPrecomposedTiles: () => number;
|
||||
addPrecompose: ({ canvasWidth, canvasHeight, }: {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}) => void;
|
||||
helperCanvasState: import("./internal-state").HelperCanvasState;
|
||||
[Symbol.dispose]: () => void;
|
||||
getWaitForReadyTime: () => number;
|
||||
addWaitForReadyTime: (time: number) => void;
|
||||
getAddSampleTime: () => number;
|
||||
addAddSampleTime: (time: number) => void;
|
||||
getCreateFrameTime: () => number;
|
||||
addCreateFrameTime: (time: number) => void;
|
||||
getAudioMixingTime: () => number;
|
||||
addAudioMixingTime: (time: number) => void;
|
||||
};
|
||||
onlyBackgroundClipText: boolean;
|
||||
scale: number;
|
||||
}) => Promise<void>;
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { drawDomElement } from './drawing/draw-dom-element';
|
||||
import { processNode } from './drawing/process-node';
|
||||
import { handleTextNode } from './drawing/text/handle-text-node';
|
||||
import { createTreeWalkerCleanupAfterChildren } from './tree-walker-cleanup-after-children';
|
||||
import { skipToNextNonDescendant } from './walk-tree';
|
||||
const walkOverNode = ({ node, context, logLevel, parentRect, internalState, rootElement, onlyBackgroundClip, }) => {
|
||||
if (node instanceof HTMLElement || node instanceof SVGElement) {
|
||||
return processNode({
|
||||
element: node,
|
||||
context,
|
||||
draw: drawDomElement(node),
|
||||
logLevel,
|
||||
parentRect,
|
||||
internalState,
|
||||
rootElement,
|
||||
});
|
||||
}
|
||||
if (node instanceof Text) {
|
||||
return handleTextNode({
|
||||
node,
|
||||
context,
|
||||
logLevel,
|
||||
parentRect,
|
||||
internalState,
|
||||
rootElement,
|
||||
onlyBackgroundClip,
|
||||
});
|
||||
}
|
||||
throw new Error('Unknown node type');
|
||||
};
|
||||
const getFilterFunction = (node) => {
|
||||
if (!(node instanceof Element)) {
|
||||
// Must be a text node!
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
// SVG does have children, but we process SVG elements in its
|
||||
// entirety
|
||||
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;
|
||||
};
|
||||
export const compose = async ({ element, context, logLevel, parentRect, internalState, onlyBackgroundClip, }) => {
|
||||
const treeWalker = document.createTreeWalker(element, onlyBackgroundClip
|
||||
? NodeFilter.SHOW_TEXT
|
||||
: NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, getFilterFunction);
|
||||
// Skip to the first text node
|
||||
if (onlyBackgroundClip) {
|
||||
treeWalker.nextNode();
|
||||
if (!treeWalker.currentNode) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { checkCleanUpAtBeginningOfIteration, addCleanup, cleanupInTheEndOfTheIteration, } = createTreeWalkerCleanupAfterChildren(treeWalker);
|
||||
while (true) {
|
||||
checkCleanUpAtBeginningOfIteration();
|
||||
const val = await walkOverNode({
|
||||
node: treeWalker.currentNode,
|
||||
context,
|
||||
logLevel,
|
||||
parentRect,
|
||||
internalState,
|
||||
rootElement: element,
|
||||
onlyBackgroundClip,
|
||||
});
|
||||
if (val.type === 'skip-children') {
|
||||
if (!skipToNextNonDescendant(treeWalker)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (val.cleanupAfterChildren) {
|
||||
addCleanup(treeWalker.currentNode, val.cleanupAfterChildren);
|
||||
}
|
||||
if (!treeWalker.nextNode()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanupInTheEndOfTheIteration();
|
||||
};
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import { AudioSampleSource, type Quality } from 'mediabunny';
|
||||
export declare const createAudioSampleSource: ({ muted, codec, bitrate, }: {
|
||||
muted: boolean;
|
||||
codec: "aac" | "ac3" | "alaw" | "eac3" | "flac" | "mp3" | "opus" | "pcm-f32" | "pcm-f32be" | "pcm-f64" | "pcm-f64be" | "pcm-s16" | "pcm-s16be" | "pcm-s24" | "pcm-s24be" | "pcm-s32" | "pcm-s32be" | "pcm-s8" | "pcm-u8" | "ulaw" | "vorbis" | null;
|
||||
bitrate: number | Quality;
|
||||
}) => {
|
||||
audioSampleSource: AudioSampleSource;
|
||||
[Symbol.dispose]: () => void;
|
||||
} | null;
|
||||
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
import { type ComponentType } from 'react';
|
||||
import type { Codec, DelayRenderScope, LogLevel, TRenderAsset } from 'remotion';
|
||||
import type { AnyZodObject } from 'zod';
|
||||
import type { TimeUpdaterRef } from './update-time';
|
||||
export type ErrorHolder = {
|
||||
error: Error | null;
|
||||
};
|
||||
export declare function checkForError(errorHolder: ErrorHolder): void;
|
||||
export declare function createScaffold<Props extends Record<string, unknown>>({ width, height, delayRenderTimeoutInMilliseconds, logLevel, resolvedProps, id, mediaCacheSizeInBytes, durationInFrames, fps, initialFrame, schema, Component, audioEnabled, videoEnabled, defaultCodec, defaultOutName }: {
|
||||
width: number;
|
||||
height: number;
|
||||
delayRenderTimeoutInMilliseconds: number;
|
||||
logLevel: LogLevel;
|
||||
resolvedProps: Record<string, unknown>;
|
||||
id: string;
|
||||
mediaCacheSizeInBytes: number | null;
|
||||
initialFrame: number;
|
||||
durationInFrames: number;
|
||||
fps: number;
|
||||
schema: AnyZodObject | null;
|
||||
Component: ComponentType<Props>;
|
||||
audioEnabled: boolean;
|
||||
videoEnabled: boolean;
|
||||
defaultCodec: Codec | null;
|
||||
defaultOutName: string | null;
|
||||
}): {
|
||||
delayRenderScope: DelayRenderScope;
|
||||
div: HTMLDivElement;
|
||||
timeUpdater: React.RefObject<TimeUpdaterRef | null>;
|
||||
collectAssets: React.RefObject<{
|
||||
collectAssets: () => TRenderAsset[];
|
||||
} | null>;
|
||||
errorHolder: ErrorHolder;
|
||||
[Symbol.dispose]: () => void;
|
||||
};
|
||||
Generated
Vendored
+104
@@ -0,0 +1,104 @@
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { createRef } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Internals } from 'remotion';
|
||||
import { UpdateTime } from './update-time';
|
||||
import { withResolvers } from './with-resolvers';
|
||||
export async 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');
|
||||
// Match same behavior as in portal-node.ts
|
||||
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 = Internals.CSSUtils.injectCSS(Internals.CSSUtils.makeDefaultPreviewCSS(`.${scaffoldClassName}`, 'white'));
|
||||
document.body.appendChild(div);
|
||||
const { promise, resolve, reject } = withResolvers();
|
||||
// TODO: This might not work in React 18
|
||||
const root = ReactDOM.createRoot(div, {
|
||||
onUncaughtError: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
const delayRenderScope = {
|
||||
remotion_renderReady: true,
|
||||
remotion_delayRenderTimeouts: {},
|
||||
remotion_puppeteerTimeout: delayRenderTimeoutInMilliseconds,
|
||||
remotion_attempt: 0,
|
||||
remotion_delayRenderHandles: [],
|
||||
};
|
||||
const timeUpdater = createRef();
|
||||
const collectAssets = createRef();
|
||||
flushSync(() => {
|
||||
root.render(_jsx(Internals.MaxMediaCacheSizeContext.Provider, { value: mediaCacheSizeInBytes, children: _jsx(Internals.RemotionEnvironmentContext.Provider, { value: {
|
||||
isStudio: false,
|
||||
isRendering: true,
|
||||
isPlayer: false,
|
||||
isReadOnlyStudio: false,
|
||||
isClientSideRendering: true,
|
||||
}, children: _jsx(Internals.DelayRenderContextType.Provider, { value: delayRenderScope, children: _jsx(Internals.CompositionManager.Provider, { value: {
|
||||
compositions: [
|
||||
{
|
||||
id,
|
||||
// @ts-expect-error
|
||||
component: Component,
|
||||
nonce: 0,
|
||||
defaultProps: {},
|
||||
folderName: null,
|
||||
parentFolderName: null,
|
||||
schema: schema !== null && schema !== void 0 ? schema : null,
|
||||
calculateMetadata: null,
|
||||
durationInFrames,
|
||||
fps,
|
||||
height,
|
||||
width,
|
||||
},
|
||||
],
|
||||
canvasContent: {
|
||||
type: 'composition',
|
||||
compositionId: id,
|
||||
},
|
||||
currentCompositionMetadata: {
|
||||
props: resolvedProps,
|
||||
durationInFrames,
|
||||
fps,
|
||||
height,
|
||||
width,
|
||||
defaultCodec: defaultCodec !== null && defaultCodec !== void 0 ? defaultCodec : null,
|
||||
defaultOutName: defaultOutName !== null && defaultOutName !== void 0 ? defaultOutName : null,
|
||||
defaultVideoImageFormat: null,
|
||||
defaultPixelFormat: null,
|
||||
defaultProResProfile: null,
|
||||
},
|
||||
folders: [],
|
||||
}, children: _jsx(Internals.RenderAssetManagerProvider, { collectAssets: collectAssets, children: _jsx(UpdateTime, { audioEnabled: audioEnabled, videoEnabled: videoEnabled, logLevel: logLevel, compId: id, initialFrame: initialFrame, timeUpdater: timeUpdater, children: _jsx(Internals.CanUseRemotionHooks.Provider, { value: true, children: _jsx(Component, { ...resolvedProps }) }) }) }) }) }) }) }));
|
||||
});
|
||||
resolve();
|
||||
await promise;
|
||||
return {
|
||||
delayRenderScope,
|
||||
div,
|
||||
cleanupScaffold: () => {
|
||||
root.unmount();
|
||||
div.remove();
|
||||
cleanupCSS();
|
||||
},
|
||||
timeUpdater,
|
||||
collectAssets,
|
||||
};
|
||||
}
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
export type BorderRadiusCorners = {
|
||||
topLeft: {
|
||||
horizontal: number;
|
||||
vertical: number;
|
||||
};
|
||||
topRight: {
|
||||
horizontal: number;
|
||||
vertical: number;
|
||||
};
|
||||
bottomRight: {
|
||||
horizontal: number;
|
||||
vertical: number;
|
||||
};
|
||||
bottomLeft: {
|
||||
horizontal: number;
|
||||
vertical: number;
|
||||
};
|
||||
};
|
||||
export declare function parseBorderRadius({ borderRadius, width, height }: {
|
||||
borderRadius: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}): BorderRadiusCorners;
|
||||
export declare function setBorderRadius({ ctx, rect, borderRadius, forceClipEvenWhenZero, computedStyle, backgroundClip }: {
|
||||
ctx: OffscreenCanvasRenderingContext2D;
|
||||
rect: DOMRect;
|
||||
borderRadius: BorderRadiusCorners;
|
||||
forceClipEvenWhenZero: boolean;
|
||||
computedStyle: CSSStyleDeclaration;
|
||||
backgroundClip: string;
|
||||
}): () => void;
|
||||
Generated
Vendored
+151
@@ -0,0 +1,151 @@
|
||||
import { drawRoundedRectPath } from './draw-rounded';
|
||||
import { getBoxBasedOnBackgroundClip } from './get-padding-box';
|
||||
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);
|
||||
}
|
||||
// If no unit, assume pixels
|
||||
return parseFloat(value);
|
||||
}
|
||||
function expandShorthand(values) {
|
||||
if (values.length === 1) {
|
||||
// All corners the same
|
||||
return [values[0], values[0], values[0], values[0]];
|
||||
}
|
||||
if (values.length === 2) {
|
||||
// [0] = top-left & bottom-right, [1] = top-right & bottom-left
|
||||
return [values[0], values[1], values[0], values[1]];
|
||||
}
|
||||
if (values.length === 3) {
|
||||
// [0] = top-left, [1] = top-right & bottom-left, [2] = bottom-right
|
||||
return [values[0], values[1], values[2], values[1]];
|
||||
}
|
||||
// 4 values: top-left, top-right, bottom-right, bottom-left
|
||||
return [values[0], values[1], values[2], values[3]];
|
||||
}
|
||||
function clampBorderRadius({ borderRadius, width, height, }) {
|
||||
// According to CSS spec, if the sum of border radii on adjacent corners
|
||||
// exceeds the length of the edge, they should be proportionally reduced
|
||||
const clamped = {
|
||||
topLeft: { ...borderRadius.topLeft },
|
||||
topRight: { ...borderRadius.topRight },
|
||||
bottomRight: { ...borderRadius.bottomRight },
|
||||
bottomLeft: { ...borderRadius.bottomLeft },
|
||||
};
|
||||
// Check top edge
|
||||
const topSum = clamped.topLeft.horizontal + clamped.topRight.horizontal;
|
||||
if (topSum > width) {
|
||||
const factor = width / topSum;
|
||||
clamped.topLeft.horizontal *= factor;
|
||||
clamped.topRight.horizontal *= factor;
|
||||
}
|
||||
// Check right edge
|
||||
const rightSum = clamped.topRight.vertical + clamped.bottomRight.vertical;
|
||||
if (rightSum > height) {
|
||||
const factor = height / rightSum;
|
||||
clamped.topRight.vertical *= factor;
|
||||
clamped.bottomRight.vertical *= factor;
|
||||
}
|
||||
// Check bottom edge
|
||||
const bottomSum = clamped.bottomRight.horizontal + clamped.bottomLeft.horizontal;
|
||||
if (bottomSum > width) {
|
||||
const factor = width / bottomSum;
|
||||
clamped.bottomRight.horizontal *= factor;
|
||||
clamped.bottomLeft.horizontal *= factor;
|
||||
}
|
||||
// Check left edge
|
||||
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;
|
||||
}
|
||||
export function parseBorderRadius({ borderRadius, width, height, }) {
|
||||
// Split by '/' to separate horizontal and vertical radii
|
||||
const parts = borderRadius.split('/').map((part) => part.trim());
|
||||
const horizontalPart = parts[0];
|
||||
const verticalPart = parts[1];
|
||||
// Split each part into individual values
|
||||
const horizontalValues = horizontalPart.split(/\s+/).filter((v) => v);
|
||||
const verticalValues = verticalPart
|
||||
? verticalPart.split(/\s+/).filter((v) => v)
|
||||
: horizontalValues; // If no '/', use horizontal values for vertical
|
||||
// Expand shorthand to 4 values
|
||||
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,
|
||||
});
|
||||
}
|
||||
export 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);
|
||||
// See background-clip tests for why this logic matters!
|
||||
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();
|
||||
};
|
||||
}
|
||||
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
|
||||
export type ObjectFitResult = {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourceWidth: number;
|
||||
sourceHeight: number;
|
||||
destX: number;
|
||||
destY: number;
|
||||
destWidth: number;
|
||||
destHeight: number;
|
||||
};
|
||||
type ObjectFitParams = {
|
||||
containerSize: {
|
||||
width: number;
|
||||
height: number;
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
intrinsicSize: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Calculates how to draw an image based on object-fit CSS property.
|
||||
*
|
||||
* @param objectFit - The CSS object-fit value
|
||||
* @param containerSize - The container dimensions (where the image should be drawn)
|
||||
* @param intrinsicSize - The natural/intrinsic size of the image
|
||||
* @returns Source and destination rectangles for drawImage
|
||||
*/
|
||||
export declare const calculateObjectFit: ({ objectFit, containerSize, intrinsicSize, }: {
|
||||
objectFit: ObjectFit;
|
||||
} & ObjectFitParams) => ObjectFitResult;
|
||||
/**
|
||||
* Parse an object-fit CSS value string into our ObjectFit type.
|
||||
* Returns 'fill' as the default if the value is not recognized.
|
||||
*/
|
||||
export declare const parseObjectFit: (value: string | null | undefined) => ObjectFit;
|
||||
export {};
|
||||
Generated
Vendored
+208
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* fill: Stretch the image to fill the container, ignoring aspect ratio
|
||||
*/
|
||||
const 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,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* contain: Scale the image to fit inside the container while maintaining aspect ratio.
|
||||
* This may result in letterboxing (empty space on sides or top/bottom).
|
||||
*/
|
||||
const calculateContain = ({ containerSize, intrinsicSize, }) => {
|
||||
const containerAspect = containerSize.width / containerSize.height;
|
||||
const imageAspect = intrinsicSize.width / intrinsicSize.height;
|
||||
let destWidth;
|
||||
let destHeight;
|
||||
if (imageAspect > containerAspect) {
|
||||
// Image is wider than container (relative to their heights)
|
||||
// Fit by width, letterbox top/bottom
|
||||
destWidth = containerSize.width;
|
||||
destHeight = containerSize.width / imageAspect;
|
||||
}
|
||||
else {
|
||||
// Image is taller than container (relative to their widths)
|
||||
// Fit by height, letterbox left/right
|
||||
destHeight = containerSize.height;
|
||||
destWidth = containerSize.height * imageAspect;
|
||||
}
|
||||
// Center the image in the container
|
||||
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,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* cover: Scale the image to cover the container while maintaining aspect ratio.
|
||||
* Parts of the image may be cropped.
|
||||
*/
|
||||
const calculateCover = ({ containerSize, intrinsicSize, }) => {
|
||||
// Guard against zero or non-positive heights to avoid division by zero and NaN/Infinity.
|
||||
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) {
|
||||
// Image is wider than container - crop horizontally
|
||||
// Scale by height, then crop width
|
||||
sourceWidth = intrinsicSize.height * containerAspect;
|
||||
sourceX = (intrinsicSize.width - sourceWidth) / 2;
|
||||
}
|
||||
else {
|
||||
// Image is taller than container - crop vertically
|
||||
// Scale by width, then crop height
|
||||
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,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* none: Draw the image at its natural size, centered in the container.
|
||||
* Clips to the container bounds if the image overflows.
|
||||
*/
|
||||
const calculateNone = ({ containerSize, intrinsicSize, }) => {
|
||||
// Calculate centered position (can be negative if image is larger than container)
|
||||
const centeredX = containerSize.left + (containerSize.width - intrinsicSize.width) / 2;
|
||||
const centeredY = containerSize.top + (containerSize.height - intrinsicSize.height) / 2;
|
||||
// Calculate clipping bounds
|
||||
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;
|
||||
// Clip left edge
|
||||
if (destX < containerSize.left) {
|
||||
const clipAmount = containerSize.left - destX;
|
||||
sourceX = clipAmount;
|
||||
sourceWidth -= clipAmount;
|
||||
destX = containerSize.left;
|
||||
destWidth -= clipAmount;
|
||||
}
|
||||
// Clip top edge
|
||||
if (destY < containerSize.top) {
|
||||
const clipAmount = containerSize.top - destY;
|
||||
sourceY = clipAmount;
|
||||
sourceHeight -= clipAmount;
|
||||
destY = containerSize.top;
|
||||
destHeight -= clipAmount;
|
||||
}
|
||||
// Clip right edge
|
||||
const containerRight = containerSize.left + containerSize.width;
|
||||
if (destX + destWidth > containerRight) {
|
||||
const clipAmount = destX + destWidth - containerRight;
|
||||
sourceWidth -= clipAmount;
|
||||
destWidth -= clipAmount;
|
||||
}
|
||||
// Clip bottom edge
|
||||
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,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Calculates how to draw an image based on object-fit CSS property.
|
||||
*
|
||||
* @param objectFit - The CSS object-fit value
|
||||
* @param containerSize - The container dimensions (where the image should be drawn)
|
||||
* @param intrinsicSize - The natural/intrinsic size of the image
|
||||
* @returns Source and destination rectangles for drawImage
|
||||
*/
|
||||
export const 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': {
|
||||
// scale-down behaves like contain or none, whichever results in a smaller image
|
||||
const containResult = calculateContain({ containerSize, intrinsicSize });
|
||||
const noneResult = calculateNone({ containerSize, intrinsicSize });
|
||||
// Compare the rendered size - use whichever is smaller
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Parse an object-fit CSS value string into our ObjectFit type.
|
||||
* Returns 'fill' as the default if the value is not recognized.
|
||||
*/
|
||||
export const 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';
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
import type { LinearGradientInfo } from './parse-linear-gradient';
|
||||
export declare const calculateTransforms: ({ element, rootElement, }: {
|
||||
element: HTMLElement | SVGElement;
|
||||
rootElement: HTMLElement | SVGElement;
|
||||
}) => {
|
||||
dimensions: DOMRect;
|
||||
totalMatrix: DOMMatrix;
|
||||
[Symbol.dispose]: () => void;
|
||||
nativeTransformOrigin: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
computedStyle: CSSStyleDeclaration;
|
||||
opacity: number;
|
||||
maskImageInfo: LinearGradientInfo | null;
|
||||
precompositing: {
|
||||
needs3DTransformViaWebGL: boolean;
|
||||
needsMaskImage: LinearGradientInfo | null;
|
||||
needsPrecompositing: boolean;
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+127
@@ -0,0 +1,127 @@
|
||||
import { hasAnyTransformCssValue, hasTransformCssValue } from './has-transform';
|
||||
import { getMaskImageValue, parseMaskImage } from './mask-image';
|
||||
import { parseTransformOrigin } from './parse-transform-origin';
|
||||
const getInternalTransformOrigin = (transform) => {
|
||||
var _a;
|
||||
const centerX = transform.boundingClientRect.width / 2;
|
||||
const centerY = transform.boundingClientRect.height / 2;
|
||||
const origin = (_a = parseTransformOrigin(transform.transformOrigin)) !== null && _a !== void 0 ? _a : {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
};
|
||||
return origin;
|
||||
};
|
||||
const getGlobalTransformOrigin = ({ transform }) => {
|
||||
const { x: originX, y: originY } = getInternalTransformOrigin(transform);
|
||||
return {
|
||||
x: originX + transform.boundingClientRect.left,
|
||||
y: originY + transform.boundingClientRect.top,
|
||||
};
|
||||
};
|
||||
export const calculateTransforms = ({ element, rootElement, }) => {
|
||||
// Compute the cumulative transform by traversing parent nodes
|
||||
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 = [];
|
||||
// The order of transformations is:
|
||||
// 1. Translate --> We do not have to consider it since it changes getClientBoundingRect()
|
||||
// 2. Rotate
|
||||
// 3. Scale
|
||||
// 4. CSS "transform"
|
||||
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,
|
||||
reset: () => {
|
||||
for (const reset of toReset) {
|
||||
reset();
|
||||
}
|
||||
},
|
||||
nativeTransformOrigin,
|
||||
computedStyle: elementComputedStyle,
|
||||
opacity,
|
||||
maskImageInfo,
|
||||
precompositing: {
|
||||
needs3DTransformViaWebGL,
|
||||
needsMaskImage: maskImageInfo,
|
||||
needsPrecompositing: Boolean(needs3DTransformViaWebGL || needsMaskImage),
|
||||
},
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export declare const getNarrowerRect: ({ firstRect, secondRect, }: {
|
||||
firstRect: DOMRect;
|
||||
secondRect: DOMRect;
|
||||
}) => DOMRect;
|
||||
export declare const getWiderRectAndExpand: ({ firstRect, secondRect, }: {
|
||||
firstRect: DOMRect | null;
|
||||
secondRect: DOMRect;
|
||||
}) => DOMRect;
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
import { roundToExpandRect } from './round-to-expand-rect';
|
||||
export const 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);
|
||||
};
|
||||
export const 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));
|
||||
};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function doRectsIntersect(rect1: DOMRect, rect2: DOMRect): boolean;
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export function doRectsIntersect(rect1, rect2) {
|
||||
return !(rect1.right <= rect2.left ||
|
||||
rect1.left >= rect2.right ||
|
||||
rect1.bottom <= rect2.top ||
|
||||
rect1.top >= rect2.bottom);
|
||||
}
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
export declare const drawBackground: ({ backgroundImage, context, rect, backgroundColor, backgroundClip, element, logLevel, internalState, computedStyle, offsetLeft: parentOffsetLeft, offsetTop: parentOffsetTop, scale, }: {
|
||||
backgroundImage: string;
|
||||
context: OffscreenCanvasRenderingContext2D;
|
||||
rect: DOMRect;
|
||||
backgroundColor: string;
|
||||
backgroundClip: string;
|
||||
element: HTMLElement | SVGElement;
|
||||
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
|
||||
internalState: {
|
||||
getDrawn3dPixels: () => number;
|
||||
getPrecomposedTiles: () => number;
|
||||
addPrecompose: ({ canvasWidth, canvasHeight, }: {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}) => void;
|
||||
helperCanvasState: import("../internal-state").HelperCanvasState;
|
||||
[Symbol.dispose]: () => void;
|
||||
getWaitForReadyTime: () => number;
|
||||
addWaitForReadyTime: (time: number) => void;
|
||||
getAddSampleTime: () => number;
|
||||
addAddSampleTime: (time: number) => void;
|
||||
getCreateFrameTime: () => number;
|
||||
addCreateFrameTime: (time: number) => void;
|
||||
getAudioMixingTime: () => number;
|
||||
addAudioMixingTime: (time: number) => void;
|
||||
};
|
||||
computedStyle: CSSStyleDeclaration;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
scale: number;
|
||||
}) => Promise<void>;
|
||||
Generated
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
import { getClippedBackground } from './get-clipped-background';
|
||||
import { getBoxBasedOnBackgroundClip } from './get-padding-box';
|
||||
import { createCanvasGradient, parseLinearGradient, } from './parse-linear-gradient';
|
||||
export const drawBackground = async ({ backgroundImage, context, rect, backgroundColor, backgroundClip, element, logLevel, internalState, computedStyle, offsetLeft: parentOffsetLeft, offsetTop: parentOffsetTop, }) => {
|
||||
let contextToDraw = context;
|
||||
const originalCompositeOperation = context.globalCompositeOperation;
|
||||
let offsetLeft = 0;
|
||||
let offsetTop = 0;
|
||||
const finish = () => {
|
||||
context.globalCompositeOperation = originalCompositeOperation;
|
||||
if (context !== contextToDraw) {
|
||||
context.drawImage(contextToDraw.canvas, offsetLeft, offsetTop, contextToDraw.canvas.width, contextToDraw.canvas.height);
|
||||
}
|
||||
};
|
||||
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 drawn = await getClippedBackground({
|
||||
element,
|
||||
boundingRect: new DOMRect(boundingRect.left + parentOffsetLeft, boundingRect.top + parentOffsetTop, boundingRect.width, boundingRect.height),
|
||||
logLevel,
|
||||
internalState,
|
||||
});
|
||||
element.style.backgroundClip = originalBackgroundClip;
|
||||
element.style.webkitBackgroundClip = originalWebkitBackgroundClip;
|
||||
contextToDraw = drawn;
|
||||
contextToDraw.globalCompositeOperation = 'source-in';
|
||||
}
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
const gradientInfo = parseLinearGradient(backgroundImage);
|
||||
if (gradientInfo) {
|
||||
const gradient = createCanvasGradient({
|
||||
ctx: contextToDraw,
|
||||
rect: boundingRect,
|
||||
gradientInfo,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
});
|
||||
const originalFillStyle = contextToDraw.fillStyle;
|
||||
contextToDraw.fillStyle = gradient;
|
||||
contextToDraw.fillRect(boundingRect.left - offsetLeft, boundingRect.top - offsetTop, boundingRect.width, boundingRect.height);
|
||||
contextToDraw.fillStyle = originalFillStyle;
|
||||
return finish();
|
||||
}
|
||||
}
|
||||
// Fallback to solid background color if no gradient was drawn
|
||||
if (backgroundColor &&
|
||||
backgroundColor !== 'transparent' &&
|
||||
!(backgroundColor.startsWith('rgba') &&
|
||||
(backgroundColor.endsWith(', 0)') || backgroundColor.endsWith(',0')))) {
|
||||
const originalFillStyle = contextToDraw.fillStyle;
|
||||
contextToDraw.fillStyle = backgroundColor;
|
||||
contextToDraw.fillRect(boundingRect.left - offsetLeft, boundingRect.top - offsetTop, boundingRect.width, boundingRect.height);
|
||||
contextToDraw.fillStyle = originalFillStyle;
|
||||
}
|
||||
finish();
|
||||
};
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import type { BorderRadiusCorners } from './border-radius';
|
||||
export declare const drawBorder: ({ ctx, rect, borderRadius, computedStyle, }: {
|
||||
ctx: OffscreenCanvasRenderingContext2D;
|
||||
rect: DOMRect;
|
||||
borderRadius: BorderRadiusCorners;
|
||||
computedStyle: CSSStyleDeclaration;
|
||||
}) => void;
|
||||
Generated
Vendored
+353
@@ -0,0 +1,353 @@
|
||||
const parseBorderWidth = (value) => {
|
||||
return parseFloat(value) || 0;
|
||||
};
|
||||
const getBorderSideProperties = (computedStyle) => {
|
||||
// Parse individual border properties for each side
|
||||
// This handles both shorthand (border: "1px solid red") and longhand (border-top-width: "1px") properties
|
||||
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',
|
||||
},
|
||||
};
|
||||
};
|
||||
const getLineDashPattern = (style, width) => {
|
||||
if (style === 'dashed') {
|
||||
return [width * 2, width];
|
||||
}
|
||||
if (style === 'dotted') {
|
||||
return [width, width];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const 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') {
|
||||
// Start point (accounting for left border and top-left radius)
|
||||
const startX = x + borderRadius.topLeft.horizontal;
|
||||
const startY = y + halfWidth;
|
||||
// End point (accounting for top-right radius)
|
||||
const endX = x + width - borderRadius.topRight.horizontal;
|
||||
const endY = y + halfWidth;
|
||||
ctx.moveTo(startX, startY);
|
||||
ctx.lineTo(endX, endY);
|
||||
}
|
||||
else if (side === 'right') {
|
||||
// Start point (accounting for top border and top-right radius)
|
||||
const startX = x + width - halfWidth;
|
||||
const startY = y + borderRadius.topRight.vertical;
|
||||
// End point (accounting for bottom-right radius)
|
||||
const endX = x + width - halfWidth;
|
||||
const endY = y + height - borderRadius.bottomRight.vertical;
|
||||
ctx.moveTo(startX, startY);
|
||||
ctx.lineTo(endX, endY);
|
||||
}
|
||||
else if (side === 'bottom') {
|
||||
// Start point (accounting for bottom-left radius)
|
||||
const startX = x + borderRadius.bottomLeft.horizontal;
|
||||
const startY = y + height - halfWidth;
|
||||
// End point (accounting for right border and bottom-right radius)
|
||||
const endX = x + width - borderRadius.bottomRight.horizontal;
|
||||
const endY = y + height - halfWidth;
|
||||
ctx.moveTo(startX, startY);
|
||||
ctx.lineTo(endX, endY);
|
||||
}
|
||||
else if (side === 'left') {
|
||||
// Start point (accounting for top-left radius)
|
||||
const startX = x + halfWidth;
|
||||
const startY = y + borderRadius.topLeft.vertical;
|
||||
// End point (accounting for bottom border and bottom-left radius)
|
||||
const endX = x + halfWidth;
|
||||
const endY = y + height - borderRadius.bottomLeft.vertical;
|
||||
ctx.moveTo(startX, startY);
|
||||
ctx.lineTo(endX, endY);
|
||||
}
|
||||
ctx.stroke();
|
||||
};
|
||||
const 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 {
|
||||
// bottomLeft
|
||||
border1 = bottomBorder;
|
||||
border2 = leftBorder;
|
||||
centerX = x + radius.horizontal;
|
||||
centerY = y + height - radius.vertical;
|
||||
startAngle = Math.PI / 2;
|
||||
endAngle = Math.PI;
|
||||
}
|
||||
// Draw corner arc - use the average of the two adjacent borders
|
||||
// In a more sophisticated implementation, we could blend the two borders
|
||||
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));
|
||||
// Adjust radius for the border width
|
||||
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();
|
||||
}
|
||||
};
|
||||
const 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;
|
||||
// Adjust border radius for the border width
|
||||
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),
|
||||
},
|
||||
};
|
||||
// Draw continuous path with border radius
|
||||
ctx.moveTo(borderX + adjustedBorderRadius.topLeft.horizontal, borderY);
|
||||
// Top edge
|
||||
ctx.lineTo(borderX + borderW - adjustedBorderRadius.topRight.horizontal, borderY);
|
||||
// Top-right corner
|
||||
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);
|
||||
}
|
||||
// Right edge
|
||||
ctx.lineTo(borderX + borderW, borderY + borderH - adjustedBorderRadius.bottomRight.vertical);
|
||||
// Bottom-right corner
|
||||
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);
|
||||
}
|
||||
// Bottom edge
|
||||
ctx.lineTo(borderX + adjustedBorderRadius.bottomLeft.horizontal, borderY + borderH);
|
||||
// Bottom-left corner
|
||||
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);
|
||||
}
|
||||
// Left edge
|
||||
ctx.lineTo(borderX, borderY + adjustedBorderRadius.topLeft.vertical);
|
||||
// Top-left corner
|
||||
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();
|
||||
};
|
||||
export const drawBorder = ({ ctx, rect, borderRadius, computedStyle, }) => {
|
||||
const borders = getBorderSideProperties(computedStyle);
|
||||
// Check if we have any visible border
|
||||
const hasBorder = borders.top.width > 0 ||
|
||||
borders.right.width > 0 ||
|
||||
borders.bottom.width > 0 ||
|
||||
borders.left.width > 0;
|
||||
if (!hasBorder) {
|
||||
return;
|
||||
}
|
||||
// Save original canvas state
|
||||
const originalStrokeStyle = ctx.strokeStyle;
|
||||
const originalLineWidth = ctx.lineWidth;
|
||||
const originalLineDash = ctx.getLineDash();
|
||||
// Check if all borders are uniform (same width, color, and style)
|
||||
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) {
|
||||
// Draw as a single continuous border for continuous dashing
|
||||
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 {
|
||||
// Draw corners first (they go underneath the straight edges)
|
||||
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,
|
||||
});
|
||||
// Draw each border side
|
||||
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,
|
||||
});
|
||||
}
|
||||
// Restore original canvas state
|
||||
ctx.strokeStyle = originalStrokeStyle;
|
||||
ctx.lineWidth = originalLineWidth;
|
||||
ctx.setLineDash(originalLineDash);
|
||||
};
|
||||
Generated
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
import type { BorderRadiusCorners } from './border-radius';
|
||||
interface BoxShadow {
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
blurRadius: number;
|
||||
color: string;
|
||||
inset: boolean;
|
||||
}
|
||||
export declare const parseBoxShadow: (boxShadowValue: string) => BoxShadow[];
|
||||
export declare const drawBorderRadius: ({ ctx, rect, borderRadius, computedStyle, logLevel, }: {
|
||||
ctx: OffscreenCanvasRenderingContext2D;
|
||||
rect: DOMRect;
|
||||
borderRadius: BorderRadiusCorners;
|
||||
computedStyle: CSSStyleDeclaration;
|
||||
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
|
||||
}) => void;
|
||||
export {};
|
||||
Generated
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
import { Internals } from 'remotion';
|
||||
import { drawRoundedRectPath } from './draw-rounded';
|
||||
export const parseBoxShadow = (boxShadowValue) => {
|
||||
if (!boxShadowValue || boxShadowValue === 'none') {
|
||||
return [];
|
||||
}
|
||||
const shadows = [];
|
||||
// Split by comma, but respect rgba() colors
|
||||
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,
|
||||
};
|
||||
// Check for inset
|
||||
shadow.inset = /\binset\b/i.test(trimmed);
|
||||
// Remove 'inset' keyword
|
||||
let remaining = trimmed.replace(/\binset\b/gi, '').trim();
|
||||
// Extract color (can be rgb(), rgba(), hsl(), hsla(), hex, or named color)
|
||||
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();
|
||||
}
|
||||
// Parse remaining numeric values (offset-x offset-y blur spread)
|
||||
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]); // Blur cannot be negative
|
||||
}
|
||||
}
|
||||
shadows.push(shadow);
|
||||
}
|
||||
return shadows;
|
||||
};
|
||||
export const drawBorderRadius = ({ ctx, rect, borderRadius, computedStyle, logLevel, }) => {
|
||||
const shadows = parseBoxShadow(computedStyle.boxShadow);
|
||||
if (shadows.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Draw shadows from last to first (so first shadow appears on top)
|
||||
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) {
|
||||
// TODO: Only warn once per render.
|
||||
Internals.Log.warn({
|
||||
logLevel,
|
||||
tag: '@remotion/web-renderer',
|
||||
}, 'Detected "box-shadow" with "inset". This is not yet supported in @remotion/web-renderer');
|
||||
continue;
|
||||
}
|
||||
// Apply shadow properties to canvas
|
||||
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();
|
||||
// Cut out the shape, leaving only shadow
|
||||
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);
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import type { DrawFn } from './drawn-fn';
|
||||
export declare const drawDomElement: (node: HTMLElement | SVGElement) => DrawFn;
|
||||
Generated
Vendored
+85
@@ -0,0 +1,85 @@
|
||||
import { calculateObjectFit, parseObjectFit } from './calculate-object-fit';
|
||||
import { fitSvgIntoItsContainer } from './fit-svg-into-its-dimensions';
|
||||
import { turnSvgIntoDrawable } from './turn-svg-into-drawable';
|
||||
const 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;
|
||||
};
|
||||
/**
|
||||
* Draw an SVG element using "contain" behavior (the default for SVGs).
|
||||
*/
|
||||
const 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);
|
||||
};
|
||||
/**
|
||||
* Draw an image or canvas element using the object-fit CSS property.
|
||||
*/
|
||||
const 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,
|
||||
});
|
||||
// Use the 9-argument drawImage to support source cropping (for cover mode)
|
||||
contextToDraw.drawImage(drawable, result.sourceX, result.sourceY, result.sourceWidth, result.sourceHeight, result.destX, result.destY, result.destWidth, result.destHeight);
|
||||
};
|
||||
export const drawDomElement = (node) => {
|
||||
const domDrawFn = async ({ dimensions, contextToDraw, computedStyle, }) => {
|
||||
// Handle SVG elements separately - they use "contain" behavior by default
|
||||
if (node instanceof SVGSVGElement) {
|
||||
const drawable = await turnSvgIntoDrawable(node);
|
||||
drawSvg({ drawable, dimensions, contextToDraw });
|
||||
return;
|
||||
}
|
||||
// Handle replaced elements (img, canvas) with object-fit support
|
||||
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;
|
||||
};
|
||||
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
import type { DrawFn } from './drawn-fn';
|
||||
export declare const drawElement: ({ rect, computedStyle, context, draw, opacity, totalMatrix, parentRect, logLevel, element, internalState, scale, }: {
|
||||
rect: DOMRect;
|
||||
computedStyle: CSSStyleDeclaration;
|
||||
context: OffscreenCanvasRenderingContext2D;
|
||||
opacity: number;
|
||||
totalMatrix: DOMMatrix;
|
||||
draw: DrawFn;
|
||||
parentRect: DOMRect;
|
||||
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
|
||||
element: HTMLElement | SVGElement;
|
||||
internalState: {
|
||||
getDrawn3dPixels: () => number;
|
||||
getPrecomposedTiles: () => number;
|
||||
addPrecompose: ({ canvasWidth, canvasHeight, }: {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}) => void;
|
||||
helperCanvasState: import("../internal-state").HelperCanvasState;
|
||||
[Symbol.dispose]: () => void;
|
||||
getWaitForReadyTime: () => number;
|
||||
addWaitForReadyTime: (time: number) => void;
|
||||
getAddSampleTime: () => number;
|
||||
addAddSampleTime: (time: number) => void;
|
||||
getCreateFrameTime: () => number;
|
||||
addCreateFrameTime: (time: number) => void;
|
||||
getAudioMixingTime: () => number;
|
||||
addAudioMixingTime: (time: number) => void;
|
||||
};
|
||||
scale: number;
|
||||
}) => Promise<{
|
||||
cleanupAfterChildren: () => void;
|
||||
}>;
|
||||
Generated
Vendored
+84
@@ -0,0 +1,84 @@
|
||||
import { parseBorderRadius, setBorderRadius } from './border-radius';
|
||||
import { drawBackground } from './draw-background';
|
||||
import { drawBorder } from './draw-border';
|
||||
import { drawBorderRadius } from './draw-box-shadow';
|
||||
import { drawOutline } from './draw-outline';
|
||||
import { setOpacity } from './opacity';
|
||||
import { setOverflowHidden } from './overflow';
|
||||
import { setTransform } from './transform';
|
||||
export const drawElement = async ({ rect, computedStyle, context, draw, opacity, totalMatrix, parentRect, logLevel, element, internalState, }) => {
|
||||
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,
|
||||
});
|
||||
const finishOpacity = setOpacity({
|
||||
ctx: context,
|
||||
opacity,
|
||||
});
|
||||
// Draw box shadow before border radius clip and background
|
||||
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,
|
||||
});
|
||||
await draw({ dimensions: rect, computedStyle, contextToDraw: context });
|
||||
finishBorderRadius();
|
||||
drawBorder({
|
||||
ctx: context,
|
||||
rect,
|
||||
borderRadius,
|
||||
computedStyle,
|
||||
});
|
||||
// Drawing outline ignores overflow: hidden, finishing it and starting a new one for the outline
|
||||
drawOutline({
|
||||
ctx: context,
|
||||
rect,
|
||||
borderRadius,
|
||||
computedStyle,
|
||||
});
|
||||
const finishOverflowHidden = setOverflowHidden({
|
||||
ctx: context,
|
||||
rect,
|
||||
borderRadius,
|
||||
overflowHidden: computedStyle.overflow === 'hidden',
|
||||
computedStyle,
|
||||
backgroundClip,
|
||||
});
|
||||
finishTransform();
|
||||
return {
|
||||
cleanupAfterChildren: () => {
|
||||
finishOpacity();
|
||||
finishOverflowHidden();
|
||||
},
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import type { BorderRadiusCorners } from './border-radius';
|
||||
export declare const parseOutlineWidth: (value: string) => number;
|
||||
export declare const parseOutlineOffset: (value: string) => number;
|
||||
export declare const drawOutline: ({ ctx, rect, borderRadius, computedStyle, }: {
|
||||
ctx: OffscreenCanvasRenderingContext2D;
|
||||
rect: DOMRect;
|
||||
borderRadius: BorderRadiusCorners;
|
||||
computedStyle: CSSStyleDeclaration;
|
||||
}) => void;
|
||||
Generated
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
import { drawRoundedRectPath } from './draw-rounded';
|
||||
export const parseOutlineWidth = (value) => {
|
||||
return parseFloat(value) || 0;
|
||||
};
|
||||
export const parseOutlineOffset = (value) => {
|
||||
return parseFloat(value) || 0;
|
||||
};
|
||||
const getLineDashPattern = (style, width) => {
|
||||
if (style === 'dashed') {
|
||||
return [width * 2, width];
|
||||
}
|
||||
if (style === 'dotted') {
|
||||
return [width, width];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
export const drawOutline = ({ ctx, rect, borderRadius, computedStyle, }) => {
|
||||
const outlineWidth = parseOutlineWidth(computedStyle.outlineWidth);
|
||||
const { outlineStyle } = computedStyle;
|
||||
const outlineColor = computedStyle.outlineColor || 'black';
|
||||
const outlineOffset = parseOutlineOffset(computedStyle.outlineOffset);
|
||||
// Check if we have a visible outline
|
||||
if (outlineWidth <= 0 ||
|
||||
outlineStyle === 'none' ||
|
||||
outlineStyle === 'hidden') {
|
||||
return;
|
||||
}
|
||||
// Save original canvas state
|
||||
const originalStrokeStyle = ctx.strokeStyle;
|
||||
const originalLineWidth = ctx.lineWidth;
|
||||
const originalLineDash = ctx.getLineDash();
|
||||
ctx.strokeStyle = outlineColor;
|
||||
ctx.lineWidth = outlineWidth;
|
||||
ctx.setLineDash(getLineDashPattern(outlineStyle, outlineWidth));
|
||||
// Calculate outline position
|
||||
// Outline is drawn outside the border edge, offset by outlineOffset
|
||||
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;
|
||||
// Adjust border radius for the outline offset
|
||||
// When outline-offset is positive, we need to expand the radius
|
||||
// When outline-offset is negative, we need to shrink the radius
|
||||
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();
|
||||
// Restore original canvas state
|
||||
ctx.strokeStyle = originalStrokeStyle;
|
||||
ctx.lineWidth = originalLineWidth;
|
||||
ctx.setLineDash(originalLineDash);
|
||||
};
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import type { BorderRadiusCorners } from './border-radius';
|
||||
export declare const drawRoundedRectPath: ({ ctx, x, y, width, height, borderRadius, }: {
|
||||
ctx: OffscreenCanvasRenderingContext2D;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
borderRadius: BorderRadiusCorners;
|
||||
}) => void;
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
export const drawRoundedRectPath = ({ ctx, x, y, width, height, borderRadius, }) => {
|
||||
ctx.beginPath();
|
||||
// Start from top-left corner, after the radius
|
||||
ctx.moveTo(x + borderRadius.topLeft.horizontal, y);
|
||||
// Top edge
|
||||
ctx.lineTo(x + width - borderRadius.topRight.horizontal, y);
|
||||
// Top-right corner
|
||||
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);
|
||||
}
|
||||
// Right edge
|
||||
ctx.lineTo(x + width, y + height - borderRadius.bottomRight.vertical);
|
||||
// Bottom-right corner
|
||||
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);
|
||||
}
|
||||
// Bottom edge
|
||||
ctx.lineTo(x + borderRadius.bottomLeft.horizontal, y + height);
|
||||
// Bottom-left corner
|
||||
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);
|
||||
}
|
||||
// Left edge
|
||||
ctx.lineTo(x, y + borderRadius.topLeft.vertical);
|
||||
// Top-left corner
|
||||
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();
|
||||
};
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export type DrawFn = ({ computedStyle, contextToDraw, dimensions }: {
|
||||
dimensions: DOMRect;
|
||||
computedStyle: CSSStyleDeclaration;
|
||||
contextToDraw: OffscreenCanvasRenderingContext2D;
|
||||
}) => Promise<void> | void;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
export declare const fitSvgIntoItsContainer: ({ containerSize, elementSize, }: {
|
||||
containerSize: DOMRect;
|
||||
elementSize: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}) => {
|
||||
width: number;
|
||||
height: number;
|
||||
top: number;
|
||||
left: number;
|
||||
};
|
||||
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
// The bitmap created from the SVG height and width might not be what we expect.
|
||||
// Adjust the dimensions
|
||||
export const fitSvgIntoItsContainer = ({ containerSize, elementSize, }) => {
|
||||
// If was already fitting, no need to calculate and lose precision
|
||||
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,
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export declare const getBackgroundFill: ({ backgroundColor, backgroundImage, contextToDraw, boundingRect, offsetLeft, offsetTop, }: {
|
||||
backgroundImage: string;
|
||||
backgroundColor: string;
|
||||
contextToDraw: OffscreenCanvasRenderingContext2D;
|
||||
boundingRect: DOMRect;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
}) => string | CanvasGradient | null;
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import type { LogLevel } from 'remotion';
|
||||
import type { InternalState } from '../internal-state';
|
||||
export declare const getClippedBackground: ({ element, boundingRect, logLevel, internalState, }: {
|
||||
element: HTMLElement | SVGElement;
|
||||
boundingRect: DOMRect;
|
||||
logLevel: LogLevel;
|
||||
internalState: InternalState;
|
||||
}) => Promise<OffscreenCanvasRenderingContext2D>;
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { compose } from '../compose';
|
||||
export const getClippedBackground = async ({ element, boundingRect, logLevel, internalState, }) => {
|
||||
const tempCanvas = new OffscreenCanvas(boundingRect.width, boundingRect.height);
|
||||
const tempContext = tempCanvas.getContext('2d');
|
||||
await compose({
|
||||
element,
|
||||
context: tempContext,
|
||||
logLevel,
|
||||
parentRect: boundingRect,
|
||||
internalState,
|
||||
onlyBackgroundClip: true,
|
||||
});
|
||||
return tempContext;
|
||||
};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare const getBoxBasedOnBackgroundClip: (rect: DOMRect, computedStyle: CSSStyleDeclaration, backgroundClip: string | undefined) => DOMRect;
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
const 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);
|
||||
};
|
||||
const 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);
|
||||
};
|
||||
export const 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;
|
||||
};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function getPreTransformRect(targetRect: DOMRect, matrix: DOMMatrix): DOMRect | null;
|
||||
Generated
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
// In some cases, we get a matrix that is too compressed:
|
||||
// e.g. https://github.com/remotion-dev/remotion/issues/6185
|
||||
// > You're rotating around the X-axis by ~89.96°, which means the Y-axis gets compressed to cos(89.96°) ≈ 0.000691 of its original size in the viewport.
|
||||
const MAX_SCALE_FACTOR = 100;
|
||||
export function getPreTransformRect(targetRect, matrix) {
|
||||
// 1. Determine the effective 2D transformation by transforming basis vectors
|
||||
const origin = new DOMPoint(0, 0).matrixTransform(matrix);
|
||||
const unitX = new DOMPoint(1, 0).matrixTransform(matrix);
|
||||
const unitY = new DOMPoint(0, 1).matrixTransform(matrix);
|
||||
// 2. Compute the 2D basis vectors after transformation
|
||||
const basisX = { x: unitX.x - origin.x, y: unitX.y - origin.y };
|
||||
const basisY = { x: unitY.x - origin.x, y: unitY.y - origin.y };
|
||||
// Check effective scale in each axis
|
||||
const scaleX = Math.hypot(basisX.x, basisX.y);
|
||||
const scaleY = Math.hypot(basisY.x, basisY.y);
|
||||
// If either axis is too compressed, the inverse will explode
|
||||
const minScale = Math.min(scaleX, scaleY);
|
||||
if (minScale < 1 / MAX_SCALE_FACTOR) {
|
||||
// Content is nearly invisible, e.g. 89.96deg X rotation (edge-on view)
|
||||
return new DOMRect(0, 0, 0, 0);
|
||||
}
|
||||
// 3. Build the effective 2D matrix and invert it
|
||||
const effective2D = new DOMMatrix([
|
||||
basisX.x,
|
||||
basisX.y, // a, b (first column)
|
||||
basisY.x,
|
||||
basisY.y, // c, d (second column)
|
||||
origin.x,
|
||||
origin.y, // e, f (translation)
|
||||
]);
|
||||
const inverse2D = effective2D.inverse();
|
||||
const wasNotInvertible = isNaN(inverse2D.m11);
|
||||
// For example, a 90 degree rotation, is not being rendered
|
||||
if (wasNotInvertible) {
|
||||
return new DOMRect(0, 0, 0, 0);
|
||||
}
|
||||
// 4. Transform target rect corners using the 2D inverse
|
||||
const corners = [
|
||||
new DOMPoint(targetRect.x, targetRect.y),
|
||||
new DOMPoint(targetRect.x + targetRect.width, targetRect.y),
|
||||
new DOMPoint(targetRect.x + targetRect.width, targetRect.y + targetRect.height),
|
||||
new DOMPoint(targetRect.x, targetRect.y + targetRect.height),
|
||||
];
|
||||
const transformedCorners = corners.map((c) => c.matrixTransform(inverse2D));
|
||||
// 5. Compute bounding box
|
||||
const xs = transformedCorners.map((p) => p.x);
|
||||
const ys = transformedCorners.map((p) => p.y);
|
||||
return new DOMRect(Math.min(...xs), Math.min(...ys), Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys));
|
||||
}
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
export declare const getPrecomposeRectFor3DTransform: ({ element, parentRect, matrix, }: {
|
||||
element: HTMLElement | SVGElement;
|
||||
parentRect: DOMRect;
|
||||
matrix: DOMMatrix;
|
||||
}) => DOMRect | null;
|
||||
export declare const handle3dTransform: ({ matrix, sourceRect, tempCanvas, rectAfterTransforms, internalState, scale, }: {
|
||||
matrix: DOMMatrix;
|
||||
sourceRect: DOMRect;
|
||||
tempCanvas: OffscreenCanvas;
|
||||
rectAfterTransforms: DOMRect;
|
||||
internalState: {
|
||||
getDrawn3dPixels: () => number;
|
||||
getPrecomposedTiles: () => number;
|
||||
addPrecompose: ({ canvasWidth, canvasHeight, }: {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}) => void;
|
||||
helperCanvasState: import("../internal-state").HelperCanvasState;
|
||||
[Symbol.dispose]: () => void;
|
||||
getWaitForReadyTime: () => number;
|
||||
addWaitForReadyTime: (time: number) => void;
|
||||
getAddSampleTime: () => number;
|
||||
addAddSampleTime: (time: number) => void;
|
||||
getCreateFrameTime: () => number;
|
||||
addCreateFrameTime: (time: number) => void;
|
||||
getAudioMixingTime: () => number;
|
||||
addAudioMixingTime: (time: number) => void;
|
||||
};
|
||||
scale: number;
|
||||
}) => OffscreenCanvas | null;
|
||||
skills/remotion-prompt-video/node_modules/@remotion/web-renderer/dist/drawing/handle-3d-transform.js
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
import { getBiggestBoundingClientRect } from '../get-biggest-bounding-client-rect';
|
||||
import { getNarrowerRect } from './clamp-rect-to-parent-bounds';
|
||||
import { getPreTransformRect } from './get-pretransform-rect';
|
||||
import { transformIn3d } from './transform-in-3d';
|
||||
export const getPrecomposeRectFor3DTransform = ({ element, parentRect, matrix, }) => {
|
||||
const unclampedBiggestBoundingClientRect = getBiggestBoundingClientRect(element);
|
||||
const biggestPossiblePretransformRect = getPreTransformRect(parentRect, matrix);
|
||||
const preTransformRect = getNarrowerRect({
|
||||
firstRect: unclampedBiggestBoundingClientRect,
|
||||
secondRect: biggestPossiblePretransformRect,
|
||||
});
|
||||
return preTransformRect;
|
||||
};
|
||||
export const handle3dTransform = ({ matrix, precomposeRect, tempCanvas, rectAfterTransforms, internalState, }) => {
|
||||
const { canvas: transformed, rect: transformedRect } = transformIn3d({
|
||||
untransformedRect: precomposeRect,
|
||||
matrix,
|
||||
sourceCanvas: tempCanvas,
|
||||
rectAfterTransforms,
|
||||
internalState,
|
||||
});
|
||||
if (transformedRect.width <= 0 || transformedRect.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
return transformed;
|
||||
};
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import type { LinearGradientInfo } from './parse-linear-gradient';
|
||||
export declare const getPrecomposeRectForMask: (element: HTMLElement | SVGElement) => DOMRect;
|
||||
export declare const handleMask: ({ gradientInfo, rect, precomposeRect, tempContext, scale, }: {
|
||||
gradientInfo: LinearGradientInfo;
|
||||
rect: DOMRect;
|
||||
precomposeRect: DOMRect;
|
||||
tempContext: OffscreenCanvasRenderingContext2D;
|
||||
scale: number;
|
||||
}) => void;
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
import { getBiggestBoundingClientRect } from '../get-biggest-bounding-client-rect';
|
||||
import { createCanvasGradient } from './parse-linear-gradient';
|
||||
export const getPrecomposeRectForMask = (element) => {
|
||||
const boundingRect = getBiggestBoundingClientRect(element);
|
||||
return boundingRect;
|
||||
};
|
||||
export const handleMask = ({ gradientInfo, rect, precomposeRect, tempContext, }) => {
|
||||
const rectOffsetX = rect.left - precomposeRect.left;
|
||||
const rectOffsetY = rect.top - precomposeRect.top;
|
||||
const rectToFill = new DOMRect(rectOffsetX, rectOffsetY, rect.width, rect.height);
|
||||
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);
|
||||
};
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export declare const hasTransformCssValue: (style: CSSStyleDeclaration) => boolean;
|
||||
export declare const hasRotateCssValue: (style: CSSStyleDeclaration) => boolean;
|
||||
export declare const hasScaleCssValue: (style: CSSStyleDeclaration) => boolean;
|
||||
export declare const hasAnyTransformCssValue: (style: CSSStyleDeclaration) => boolean;
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
export const hasTransformCssValue = (style) => {
|
||||
return style.transform !== 'none' && style.transform !== '';
|
||||
};
|
||||
export const hasRotateCssValue = (style) => {
|
||||
return style.rotate !== 'none' && style.rotate !== '';
|
||||
};
|
||||
export const hasScaleCssValue = (style) => {
|
||||
return style.scale !== 'none' && style.scale !== '';
|
||||
};
|
||||
export const hasAnyTransformCssValue = (style) => {
|
||||
return (hasTransformCssValue(style) ||
|
||||
hasRotateCssValue(style) ||
|
||||
hasScaleCssValue(style));
|
||||
};
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import type { LinearGradientInfo } from './parse-linear-gradient';
|
||||
export declare const getMaskImageValue: (computedStyle: CSSStyleDeclaration) => string | null;
|
||||
export declare const parseMaskImage: (maskImageValue: string) => LinearGradientInfo | null;
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { parseLinearGradient } from './parse-linear-gradient';
|
||||
export const getMaskImageValue = (computedStyle) => {
|
||||
// Check both standard and webkit-prefixed properties
|
||||
const { maskImage, webkitMaskImage } = computedStyle;
|
||||
const value = maskImage || webkitMaskImage;
|
||||
if (!value || value === 'none') {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
export const parseMaskImage = (maskImageValue) => {
|
||||
// Only linear gradients are supported for now
|
||||
return parseLinearGradient(maskImageValue);
|
||||
};
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export declare const setOpacity: ({ ctx, opacity, }: {
|
||||
ctx: OffscreenCanvasRenderingContext2D;
|
||||
opacity: number;
|
||||
}) => () => void;
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export const setOpacity = ({ ctx, opacity, }) => {
|
||||
const previousAlpha = ctx.globalAlpha;
|
||||
ctx.globalAlpha = previousAlpha * opacity;
|
||||
return () => {
|
||||
ctx.globalAlpha = previousAlpha;
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import type { BorderRadiusCorners } from './border-radius';
|
||||
export declare const setOverflowHidden: ({ ctx, rect, borderRadius, overflowHidden, computedStyle, backgroundClip, }: {
|
||||
ctx: OffscreenCanvasRenderingContext2D;
|
||||
rect: DOMRect;
|
||||
borderRadius: BorderRadiusCorners;
|
||||
overflowHidden: boolean;
|
||||
computedStyle: CSSStyleDeclaration;
|
||||
backgroundClip: string;
|
||||
}) => () => void;
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { setBorderRadius } from './border-radius';
|
||||
export const setOverflowHidden = ({ ctx, rect, borderRadius, overflowHidden, computedStyle, backgroundClip, }) => {
|
||||
if (!overflowHidden) {
|
||||
return () => { };
|
||||
}
|
||||
return setBorderRadius({
|
||||
ctx,
|
||||
rect,
|
||||
borderRadius,
|
||||
forceClipEvenWhenZero: true,
|
||||
computedStyle,
|
||||
backgroundClip,
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
export interface ColorStop {
|
||||
color: string;
|
||||
position: number;
|
||||
}
|
||||
export interface LinearGradientInfo {
|
||||
angle: number;
|
||||
colorStops: ColorStop[];
|
||||
}
|
||||
export declare const parseLinearGradient: (backgroundImage: string) => LinearGradientInfo | null;
|
||||
export declare const createCanvasGradient: ({ ctx, rect, gradientInfo, offsetLeft, offsetTop, }: {
|
||||
ctx: OffscreenCanvasRenderingContext2D;
|
||||
rect: DOMRect;
|
||||
gradientInfo: LinearGradientInfo;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
}) => CanvasGradient;
|
||||
Generated
Vendored
+260
@@ -0,0 +1,260 @@
|
||||
import { NoReactInternals } from 'remotion/no-react';
|
||||
const isValidColor = (color) => {
|
||||
try {
|
||||
const result = NoReactInternals.processColor(color);
|
||||
return result !== null && result !== undefined;
|
||||
}
|
||||
catch (_a) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const parseDirection = (directionStr) => {
|
||||
const trimmed = directionStr.trim().toLowerCase();
|
||||
// Handle keywords like "to right", "to bottom", etc.
|
||||
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; // Default to bottom
|
||||
}
|
||||
}
|
||||
// Handle angle values: deg, rad, grad, turn
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Default: to bottom
|
||||
return 180;
|
||||
};
|
||||
const parseColorStops = (colorStopsStr) => {
|
||||
// Split by comma, but respect parentheses in rgba(), rgb(), hsl(), hsla()
|
||||
const parts = colorStopsStr.split(/,(?![^(]*\))/);
|
||||
const stops = [];
|
||||
for (const part of parts) {
|
||||
const trimmed = part.trim();
|
||||
if (!trimmed)
|
||||
continue;
|
||||
// Extract color: can be rgb(), rgba(), hsl(), hsla(), hex, or named color
|
||||
const colorMatch = trimmed.match(/(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i);
|
||||
if (!colorMatch) {
|
||||
continue;
|
||||
}
|
||||
const colorStr = colorMatch[0];
|
||||
// Validate that this is actually a valid CSS color
|
||||
if (!isValidColor(colorStr)) {
|
||||
continue;
|
||||
}
|
||||
const remaining = trimmed
|
||||
.substring(colorMatch.index + colorStr.length)
|
||||
.trim();
|
||||
// Canvas API supports CSS colors directly, so we can use the color string as-is
|
||||
const normalizedColor = colorStr;
|
||||
// Parse position if provided
|
||||
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') {
|
||||
// px values need element dimensions, which we don't have here
|
||||
// We'll handle this as a percentage for now (not fully CSS-compliant but good enough)
|
||||
position = null;
|
||||
}
|
||||
else {
|
||||
position = value / 100; // Assume percentage if no unit
|
||||
}
|
||||
}
|
||||
}
|
||||
stops.push({
|
||||
color: normalizedColor,
|
||||
position: position !== null ? position : -1, // -1 means needs to be calculated
|
||||
});
|
||||
}
|
||||
if (stops.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// Distribute positions evenly for stops that don't have explicit positions
|
||||
let lastExplicitIndex = -1;
|
||||
let lastExplicitPosition = 0;
|
||||
for (let i = 0; i < stops.length; i++) {
|
||||
if (stops[i].position !== -1) {
|
||||
// Found an explicit position
|
||||
if (lastExplicitIndex >= 0) {
|
||||
// Interpolate between last explicit and current explicit
|
||||
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 {
|
||||
// Backfill from start to first explicit
|
||||
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 no explicit positions were provided at all, distribute evenly
|
||||
// Check this BEFORE handling trailing stops
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Clamp positions to 0-1
|
||||
for (const stop of stops) {
|
||||
stop.position = Math.max(0, Math.min(1, stop.position));
|
||||
}
|
||||
return stops;
|
||||
};
|
||||
const extractGradientContent = (backgroundImage) => {
|
||||
const prefix = 'linear-gradient(';
|
||||
const startIndex = backgroundImage.toLowerCase().indexOf(prefix);
|
||||
if (startIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
// Find matching closing parenthesis, handling nested parens from rgb(), rgba(), etc.
|
||||
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;
|
||||
};
|
||||
export const parseLinearGradient = (backgroundImage) => {
|
||||
if (!backgroundImage || backgroundImage === 'none') {
|
||||
return null;
|
||||
}
|
||||
const content = extractGradientContent(backgroundImage);
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
// Try to identify the direction/angle part vs color stops
|
||||
// Direction/angle is optional and comes first
|
||||
// It can be: "to right", "45deg", etc.
|
||||
// Split into parts, respecting parentheses
|
||||
const parts = content.split(/,(?![^(]*\))/);
|
||||
let angle = 180; // Default: to bottom
|
||||
let colorStopsStart = 0;
|
||||
// Check if first part is a direction/angle
|
||||
if (parts.length > 0) {
|
||||
const firstPart = parts[0].trim();
|
||||
// Check if it looks like a direction or angle (not a color)
|
||||
const isDirection = firstPart.startsWith('to ') ||
|
||||
/^-?\d+\.?\d*(deg|rad|grad|turn)$/.test(firstPart);
|
||||
if (isDirection) {
|
||||
angle = parseDirection(firstPart);
|
||||
colorStopsStart = 1;
|
||||
}
|
||||
}
|
||||
// Parse color stops
|
||||
const colorStopsStr = parts.slice(colorStopsStart).join(',');
|
||||
const colorStops = parseColorStops(colorStopsStr);
|
||||
if (!colorStops || colorStops.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
angle,
|
||||
colorStops,
|
||||
};
|
||||
};
|
||||
export const createCanvasGradient = ({ ctx, rect, gradientInfo, offsetLeft, offsetTop, }) => {
|
||||
// Convert angle to radians
|
||||
// CSS angles: 0deg = to top, 90deg = to right, 180deg = to bottom, 270deg = to left
|
||||
// We need to calculate the gradient line that spans the rectangle at the given angle
|
||||
const angleRad = ((gradientInfo.angle - 90) * Math.PI) / 180;
|
||||
const centerX = rect.left - offsetLeft + rect.width / 2;
|
||||
const centerY = rect.top - offsetTop + rect.height / 2;
|
||||
// Calculate gradient line endpoints
|
||||
// The gradient line passes through the center and has the specified angle
|
||||
const cos = Math.cos(angleRad);
|
||||
const sin = Math.sin(angleRad);
|
||||
// Find the intersection of the gradient line with the rectangle edges
|
||||
const halfWidth = rect.width / 2;
|
||||
const halfHeight = rect.height / 2;
|
||||
// Calculate the length from center to edge along the gradient line.
|
||||
// Primary formula should always be > 0 for valid angles and non-zero rects;
|
||||
// fall back to diagonal length only for degenerate or invalid cases.
|
||||
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);
|
||||
// Add color stops
|
||||
for (const stop of gradientInfo.colorStops) {
|
||||
gradient.addColorStop(stop.position, stop.color);
|
||||
}
|
||||
return gradient;
|
||||
};
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export declare const parseTransformOrigin: (transformOrigin: string) => {
|
||||
x: number;
|
||||
y: number;
|
||||
} | null;
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export const parseTransformOrigin = (transformOrigin) => {
|
||||
if (transformOrigin.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
const [x, y] = transformOrigin.split(' ');
|
||||
return { x: parseFloat(x), y: parseFloat(y) };
|
||||
};
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import type { LogLevel } from 'remotion';
|
||||
import type { InternalState } from '../internal-state';
|
||||
export declare const precomposeDOMElement: ({ boundingRect, element, logLevel, internalState, }: {
|
||||
boundingRect: DOMRect;
|
||||
element: HTMLElement | SVGElement;
|
||||
logLevel: LogLevel;
|
||||
internalState: InternalState;
|
||||
}) => Promise<{
|
||||
tempCanvas: OffscreenCanvas;
|
||||
tempContext: OffscreenCanvasRenderingContext2D;
|
||||
}>;
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { compose } from '../compose';
|
||||
export const precomposeDOMElement = async ({ boundingRect, element, logLevel, internalState, }) => {
|
||||
const tempCanvas = new OffscreenCanvas(boundingRect.width, boundingRect.height);
|
||||
const tempContext = tempCanvas.getContext('2d');
|
||||
await compose({
|
||||
element,
|
||||
context: tempContext,
|
||||
logLevel,
|
||||
parentRect: boundingRect,
|
||||
internalState,
|
||||
onlyBackgroundClip: false,
|
||||
});
|
||||
return { tempCanvas, tempContext };
|
||||
};
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
import type { DrawFn } from './drawn-fn';
|
||||
export type ProcessNodeReturnValue = {
|
||||
type: 'continue';
|
||||
cleanupAfterChildren: null | (() => void);
|
||||
} | {
|
||||
type: 'skip-children';
|
||||
};
|
||||
export declare const processNode: ({ element, context, draw, logLevel, parentRect, internalState, rootElement, scale, }: {
|
||||
element: HTMLElement | SVGElement;
|
||||
context: OffscreenCanvasRenderingContext2D;
|
||||
draw: DrawFn;
|
||||
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
|
||||
parentRect: DOMRect;
|
||||
internalState: {
|
||||
getDrawn3dPixels: () => number;
|
||||
getPrecomposedTiles: () => number;
|
||||
addPrecompose: ({ canvasWidth, canvasHeight, }: {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}) => void;
|
||||
helperCanvasState: import("../internal-state").HelperCanvasState;
|
||||
[Symbol.dispose]: () => void;
|
||||
getWaitForReadyTime: () => number;
|
||||
addWaitForReadyTime: (time: number) => void;
|
||||
getAddSampleTime: () => number;
|
||||
addAddSampleTime: (time: number) => void;
|
||||
getCreateFrameTime: () => number;
|
||||
addCreateFrameTime: (time: number) => void;
|
||||
getAudioMixingTime: () => number;
|
||||
addAudioMixingTime: (time: number) => void;
|
||||
};
|
||||
rootElement: HTMLElement | SVGElement;
|
||||
scale: number;
|
||||
}) => Promise<ProcessNodeReturnValue>;
|
||||
Generated
Vendored
+122
@@ -0,0 +1,122 @@
|
||||
import { Internals } from 'remotion';
|
||||
import { calculateTransforms } from './calculate-transforms';
|
||||
import { getWiderRectAndExpand } from './clamp-rect-to-parent-bounds';
|
||||
import { doRectsIntersect } from './do-rects-intersect';
|
||||
import { drawElement } from './draw-element';
|
||||
import { getPrecomposeRectFor3DTransform, handle3dTransform, } from './handle-3d-transform';
|
||||
import { getPrecomposeRectForMask, handleMask } from './handle-mask';
|
||||
import { precomposeDOMElement } from './precompose';
|
||||
import { roundToExpandRect } from './round-to-expand-rect';
|
||||
import { transformDOMRect } from './transform-rect-with-matrix';
|
||||
export const processNode = async ({ element, context, draw, logLevel, parentRect, internalState, rootElement, }) => {
|
||||
const { totalMatrix, reset, dimensions, opacity, computedStyle, precompositing, } = calculateTransforms({
|
||||
element,
|
||||
rootElement,
|
||||
});
|
||||
if (opacity === 0) {
|
||||
reset();
|
||||
return { type: 'skip-children' };
|
||||
}
|
||||
// When backfaceVisibility is 'hidden', don't render if the element is rotated
|
||||
// to show its backface. The backface is visible when the z-component of the
|
||||
// transformed normal vector (0, 0, 1) is negative, which corresponds to m33 < 0.
|
||||
if (computedStyle.backfaceVisibility === 'hidden' && totalMatrix.m33 < 0) {
|
||||
reset();
|
||||
return { type: 'skip-children' };
|
||||
}
|
||||
if (dimensions.width <= 0 || dimensions.height <= 0) {
|
||||
reset();
|
||||
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 = getWiderRectAndExpand({
|
||||
firstRect: precomposeRect,
|
||||
secondRect: getPrecomposeRectForMask(element),
|
||||
});
|
||||
}
|
||||
if (precompositing.needs3DTransformViaWebGL) {
|
||||
precomposeRect = getWiderRectAndExpand({
|
||||
firstRect: precomposeRect,
|
||||
secondRect: getPrecomposeRectFor3DTransform({
|
||||
element,
|
||||
parentRect,
|
||||
matrix: totalMatrix,
|
||||
}),
|
||||
});
|
||||
}
|
||||
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 { tempCanvas, tempContext } = await precomposeDOMElement({
|
||||
boundingRect: precomposeRect,
|
||||
element,
|
||||
logLevel,
|
||||
internalState,
|
||||
});
|
||||
let drawable = tempCanvas;
|
||||
const rectAfterTransforms = roundToExpandRect(transformDOMRect({
|
||||
rect: precomposeRect,
|
||||
matrix: totalMatrix,
|
||||
}));
|
||||
if (precompositing.needsMaskImage) {
|
||||
handleMask({
|
||||
gradientInfo: precompositing.needsMaskImage,
|
||||
rect,
|
||||
precomposeRect,
|
||||
tempContext,
|
||||
});
|
||||
}
|
||||
if (precompositing.needs3DTransformViaWebGL) {
|
||||
const t = handle3dTransform({
|
||||
matrix: totalMatrix,
|
||||
precomposeRect,
|
||||
tempCanvas: drawable,
|
||||
rectAfterTransforms,
|
||||
internalState,
|
||||
});
|
||||
if (t) {
|
||||
drawable = t;
|
||||
}
|
||||
}
|
||||
const previousTransform = context.getTransform();
|
||||
if (drawable) {
|
||||
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);
|
||||
Internals.Log.trace({
|
||||
logLevel,
|
||||
tag: '@remotion/web-renderer',
|
||||
}, `Transforming element in 3D - canvas size: ${precomposeRect.width}x${precomposeRect.height} - compose: ${Date.now() - start}ms`);
|
||||
internalState.addPrecompose({
|
||||
canvasWidth: precomposeRect.width,
|
||||
canvasHeight: precomposeRect.height,
|
||||
});
|
||||
}
|
||||
reset();
|
||||
return { type: 'skip-children' };
|
||||
}
|
||||
const { cleanupAfterChildren } = await drawElement({
|
||||
rect,
|
||||
computedStyle,
|
||||
context,
|
||||
draw,
|
||||
opacity,
|
||||
totalMatrix,
|
||||
parentRect,
|
||||
logLevel,
|
||||
element,
|
||||
internalState,
|
||||
});
|
||||
reset();
|
||||
return { type: 'continue', cleanupAfterChildren };
|
||||
};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare const roundToExpandRect: (rect: DOMRect) => DOMRect;
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export const 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);
|
||||
};
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export declare const scaleRect: ({ rect, scale, }: {
|
||||
rect: DOMRect;
|
||||
scale: number;
|
||||
}) => DOMRect;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare const applyTextTransform: (text: string, transform: string) => string;
|
||||
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
export const 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;
|
||||
};
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import type { DrawFn } from '../drawn-fn';
|
||||
export declare const drawText: ({ span, logLevel, onlyBackgroundClipText, parentRect, }: {
|
||||
span: HTMLSpanElement;
|
||||
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
|
||||
parentRect: DOMRect;
|
||||
onlyBackgroundClipText: boolean;
|
||||
}) => DrawFn;
|
||||
Generated
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
import { Internals } from 'remotion';
|
||||
import { applyTextTransform } from './apply-text-transform';
|
||||
import { findLineBreaks } from './find-line-breaks.text';
|
||||
import { getCollapsedText } from './get-collapsed-text';
|
||||
export const drawText = ({ span, logLevel, onlyBackgroundClip, }) => {
|
||||
const drawFn = ({ dimensions: rect, computedStyle, contextToDraw }) => {
|
||||
const { fontFamily, fontSize, fontWeight, direction, writingMode, letterSpacing, textTransform, webkitTextFillColor, } = computedStyle;
|
||||
const isVertical = writingMode !== 'horizontal-tb';
|
||||
if (isVertical) {
|
||||
// TODO: Only warn once per render.
|
||||
Internals.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 =
|
||||
// If text is being applied with backgroundClipText, we need to use a solid color otherwise it won't get
|
||||
// applied in canvas
|
||||
onlyBackgroundClip
|
||||
? 'black'
|
||||
: // -webkit-text-fill-color overrides color, and defaults to the value of `color`
|
||||
webkitTextFillColor;
|
||||
contextToDraw.letterSpacing = letterSpacing;
|
||||
const isRTL = direction === 'rtl';
|
||||
contextToDraw.textAlign = isRTL ? 'right' : 'left';
|
||||
contextToDraw.textBaseline = 'alphabetic';
|
||||
const originalText = span.textContent;
|
||||
const collapsedText = getCollapsedText(span);
|
||||
const transformedText = applyTextTransform(collapsedText, textTransform);
|
||||
span.textContent = transformedText;
|
||||
// For RTL text, fill from the right edge instead of left
|
||||
const xPosition = isRTL ? rect.right : rect.left;
|
||||
const lines = findLineBreaks(span, isRTL);
|
||||
let offsetTop = 0;
|
||||
const measurements = contextToDraw.measureText(lines[0].text);
|
||||
const { fontBoundingBoxDescent, fontBoundingBoxAscent } = measurements;
|
||||
const fontHeight = fontBoundingBoxAscent + fontBoundingBoxDescent;
|
||||
for (const line of lines) {
|
||||
// Calculate leading
|
||||
const leading = line.height - fontHeight;
|
||||
const halfLeading = leading / 2;
|
||||
contextToDraw.fillText(line.text, xPosition + line.offsetHorizontal, rect.top + halfLeading + fontBoundingBoxAscent + offsetTop);
|
||||
offsetTop += line.height;
|
||||
}
|
||||
span.textContent = originalText;
|
||||
contextToDraw.restore();
|
||||
};
|
||||
return drawFn;
|
||||
};
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
type Token = {
|
||||
text: string;
|
||||
rect: DOMRect;
|
||||
};
|
||||
export declare const findWords: (span: HTMLSpanElement) => Token[];
|
||||
export {};
|
||||
Generated
Vendored
+118
@@ -0,0 +1,118 @@
|
||||
import { getCollapsedText } from './get-collapsed-text';
|
||||
// Punctuation that cannot start a line according to Unicode line breaking rules
|
||||
// When these would start a line, the browser moves the preceding word to the new line
|
||||
const cannotStartLine = (segment) => {
|
||||
if (segment.length === 0)
|
||||
return false;
|
||||
const firstChar = segment[0];
|
||||
const forbiddenLineStarts = [
|
||||
'.',
|
||||
',',
|
||||
';',
|
||||
':',
|
||||
'!',
|
||||
'?',
|
||||
')',
|
||||
']',
|
||||
'}',
|
||||
'"',
|
||||
"'",
|
||||
'"',
|
||||
`'`,
|
||||
'»',
|
||||
'…',
|
||||
'‥',
|
||||
'·',
|
||||
'%',
|
||||
'‰',
|
||||
];
|
||||
return forbiddenLineStarts.includes(firstChar);
|
||||
};
|
||||
export function findLineBreaks(span, rtl) {
|
||||
const textNode = span.childNodes[0];
|
||||
const originalText = textNode.textContent;
|
||||
const originalRect = span.getBoundingClientRect();
|
||||
const computedStyle = getComputedStyle(span);
|
||||
const segmenter = new Intl.Segmenter('en', { granularity: 'word' });
|
||||
const segments = segmenter.segment(originalText);
|
||||
const words = Array.from(segments).map((s) => s.segment);
|
||||
// If the text would be centered in a flexbox container
|
||||
const lines = [];
|
||||
let currentLine = '';
|
||||
let testText = '';
|
||||
let previousRect = originalRect;
|
||||
textNode.textContent = '';
|
||||
for (let i = 0; i < words.length; i += 1) {
|
||||
const word = words[i];
|
||||
testText += word;
|
||||
let wordsToAdd = word;
|
||||
while (typeof words[i + 1] !== 'undefined' && words[i + 1].trim() === '') {
|
||||
testText += words[i + 1];
|
||||
wordsToAdd += words[i + 1];
|
||||
i++;
|
||||
}
|
||||
previousRect = span.getBoundingClientRect();
|
||||
textNode.textContent = testText;
|
||||
const collapsedText = getCollapsedText(span);
|
||||
textNode.textContent = collapsedText;
|
||||
const rect = span.getBoundingClientRect();
|
||||
const currentHeight = rect.height;
|
||||
// If height changed significantly, we had a line break
|
||||
if (previousRect &&
|
||||
previousRect.height !== 0 &&
|
||||
Math.abs(currentHeight - previousRect.height) > 2) {
|
||||
const offsetHorizontal = rtl
|
||||
? previousRect.right - originalRect.right
|
||||
: previousRect.left - originalRect.left;
|
||||
const shouldCollapse = !computedStyle.whiteSpaceCollapse.includes('preserve');
|
||||
let textForPreviousLine = currentLine;
|
||||
let textForNewLine = wordsToAdd;
|
||||
// If the segment that triggered the break can't start a line (e.g., punctuation),
|
||||
// the browser would have moved the preceding word to the new line as well
|
||||
if (cannotStartLine(word)) {
|
||||
const currentLineSegments = Array.from(segmenter.segment(currentLine)).map((s) => s.segment);
|
||||
// Find the last non-whitespace segment (the word to move)
|
||||
let lastWordIndex = currentLineSegments.length - 1;
|
||||
while (lastWordIndex >= 0 &&
|
||||
currentLineSegments[lastWordIndex].trim() === '') {
|
||||
lastWordIndex--;
|
||||
}
|
||||
if (lastWordIndex >= 0) {
|
||||
// Move the last word (and any trailing whitespace) to the new line
|
||||
textForPreviousLine = currentLineSegments
|
||||
.slice(0, lastWordIndex)
|
||||
.join('');
|
||||
textForNewLine =
|
||||
currentLineSegments.slice(lastWordIndex).join('') + wordsToAdd;
|
||||
}
|
||||
}
|
||||
lines.push({
|
||||
text: shouldCollapse ? textForPreviousLine.trim() : textForPreviousLine,
|
||||
height: currentHeight - previousRect.height,
|
||||
offsetHorizontal,
|
||||
});
|
||||
currentLine = textForNewLine;
|
||||
}
|
||||
else {
|
||||
currentLine += wordsToAdd;
|
||||
}
|
||||
}
|
||||
// Add the last line
|
||||
if (currentLine) {
|
||||
textNode.textContent = testText;
|
||||
const rects = span.getClientRects();
|
||||
const rect = span.getBoundingClientRect();
|
||||
const lastRect = rects[rects.length - 1];
|
||||
const offsetHorizontal = rtl
|
||||
? lastRect.right - originalRect.right
|
||||
: lastRect.left - originalRect.left;
|
||||
lines.push({
|
||||
text: currentLine,
|
||||
height: rect.height - lines.reduce((acc, curr) => acc + curr.height, 0),
|
||||
offsetHorizontal,
|
||||
});
|
||||
}
|
||||
// Reset to original text
|
||||
textNode.textContent = originalText;
|
||||
return lines;
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare const getCollapsedText: (span: HTMLSpanElement) => string;
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
export const getCollapsedText = (span) => {
|
||||
const textNode = span.firstChild;
|
||||
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
|
||||
throw new Error('Span must contain a single text node');
|
||||
}
|
||||
const originalText = textNode.textContent || '';
|
||||
let collapsedText = originalText;
|
||||
// Helper to measure width
|
||||
const measureWidth = (text) => {
|
||||
textNode.textContent = text;
|
||||
return span.getBoundingClientRect().width;
|
||||
};
|
||||
const originalWidth = measureWidth(originalText);
|
||||
// Test leading whitespace
|
||||
if (/^\s/.test(collapsedText)) {
|
||||
const trimmedLeading = collapsedText.replace(/^\s+/, '');
|
||||
const newWidth = measureWidth(trimmedLeading);
|
||||
if (newWidth === originalWidth) {
|
||||
// Whitespace was collapsed by the browser
|
||||
collapsedText = trimmedLeading;
|
||||
}
|
||||
}
|
||||
// Test trailing whitespace (on current collapsed text)
|
||||
if (/\s$/.test(collapsedText)) {
|
||||
const currentWidth = measureWidth(collapsedText);
|
||||
const trimmedTrailing = collapsedText.replace(/\s+$/, '');
|
||||
const newWidth = measureWidth(trimmedTrailing);
|
||||
if (newWidth === currentWidth) {
|
||||
// Whitespace was collapsed by the browser
|
||||
collapsedText = trimmedTrailing;
|
||||
}
|
||||
}
|
||||
// Test internal duplicate whitespace (on current collapsed text)
|
||||
if (/\s\s/.test(collapsedText)) {
|
||||
const currentWidth = measureWidth(collapsedText);
|
||||
const collapsedInternal = collapsedText.replace(/\s\s+/g, ' ');
|
||||
const newWidth = measureWidth(collapsedInternal);
|
||||
if (newWidth === currentWidth) {
|
||||
// Whitespace was collapsed by the browser
|
||||
collapsedText = collapsedInternal;
|
||||
}
|
||||
}
|
||||
// Restore original text
|
||||
textNode.textContent = originalText;
|
||||
return collapsedText;
|
||||
};
|
||||
Generated
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
import type { ProcessNodeReturnValue } from '../process-node';
|
||||
export declare const handleTextNode: ({ node, context, logLevel, parentRect, internalState, rootElement, onlyBackgroundClipText, scale, }: {
|
||||
node: Text;
|
||||
context: OffscreenCanvasRenderingContext2D;
|
||||
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
|
||||
parentRect: DOMRect;
|
||||
internalState: {
|
||||
getDrawn3dPixels: () => number;
|
||||
getPrecomposedTiles: () => number;
|
||||
addPrecompose: ({ canvasWidth, canvasHeight, }: {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}) => void;
|
||||
helperCanvasState: import("../../internal-state").HelperCanvasState;
|
||||
[Symbol.dispose]: () => void;
|
||||
getWaitForReadyTime: () => number;
|
||||
addWaitForReadyTime: (time: number) => void;
|
||||
getAddSampleTime: () => number;
|
||||
addAddSampleTime: (time: number) => void;
|
||||
getCreateFrameTime: () => number;
|
||||
addCreateFrameTime: (time: number) => void;
|
||||
getAudioMixingTime: () => number;
|
||||
addAudioMixingTime: (time: number) => void;
|
||||
};
|
||||
rootElement: HTMLElement | SVGElement;
|
||||
onlyBackgroundClipText: boolean;
|
||||
scale: number;
|
||||
}) => Promise<ProcessNodeReturnValue>;
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
import { processNode } from '../process-node';
|
||||
import { drawText } from './draw-text';
|
||||
export const handleTextNode = async ({ node, context, logLevel, parentRect, internalState, rootElement, onlyBackgroundClip, }) => {
|
||||
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, onlyBackgroundClip }),
|
||||
logLevel,
|
||||
parentRect,
|
||||
internalState,
|
||||
rootElement,
|
||||
});
|
||||
// Undo the layout manipulation
|
||||
parent.insertBefore(node, span);
|
||||
parent.removeChild(span);
|
||||
return value;
|
||||
};
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
import type { HelperCanvasState } from '../internal-state';
|
||||
export declare const transformIn3d: ({ matrix, sourceCanvas, sourceRect, destRect, internalState, scale, }: {
|
||||
sourceRect: DOMRect;
|
||||
matrix: DOMMatrix;
|
||||
sourceCanvas: OffscreenCanvas;
|
||||
destRect: DOMRect;
|
||||
internalState: {
|
||||
getDrawn3dPixels: () => number;
|
||||
getPrecomposedTiles: () => number;
|
||||
addPrecompose: ({ canvasWidth, canvasHeight, }: {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}) => void;
|
||||
helperCanvasState: HelperCanvasState;
|
||||
[Symbol.dispose]: () => void;
|
||||
getWaitForReadyTime: () => number;
|
||||
addWaitForReadyTime: (time: number) => void;
|
||||
getAddSampleTime: () => number;
|
||||
addAddSampleTime: (time: number) => void;
|
||||
getCreateFrameTime: () => number;
|
||||
addCreateFrameTime: (time: number) => void;
|
||||
getAudioMixingTime: () => number;
|
||||
addAudioMixingTime: (time: number) => void;
|
||||
};
|
||||
scale: number;
|
||||
}) => OffscreenCanvas;
|
||||
Generated
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
// Vertex shader - now includes texture coordinates
|
||||
const vsSource = `
|
||||
attribute vec2 aPosition;
|
||||
attribute vec2 aTexCoord;
|
||||
uniform mat4 uTransform;
|
||||
uniform mat4 uProjection;
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
gl_Position = uProjection * uTransform * vec4(aPosition, 0.0, 1.0);
|
||||
vTexCoord = aTexCoord;
|
||||
}
|
||||
`;
|
||||
// Fragment shader - samples from texture and unpremultiplies alpha
|
||||
const 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;
|
||||
}
|
||||
const createHelperCanvas = ({ canvasWidth, canvasHeight, helperCanvasState, }) => {
|
||||
var _a;
|
||||
if (helperCanvasState.current) {
|
||||
// Resize canvas if dimensions changed
|
||||
if (helperCanvasState.current.canvas.width !== canvasWidth ||
|
||||
helperCanvasState.current.canvas.height !== canvasHeight) {
|
||||
helperCanvasState.current.canvas.width = canvasWidth;
|
||||
helperCanvasState.current.canvas.height = canvasHeight;
|
||||
}
|
||||
// Always reset viewport and clear when reusing
|
||||
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 = (_a = canvas.getContext('webgl', {
|
||||
premultipliedAlpha: true,
|
||||
})) !== null && _a !== void 0 ? _a : undefined;
|
||||
if (!gl) {
|
||||
throw new Error('WebGL not supported');
|
||||
}
|
||||
// Compile shaders and create program once
|
||||
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));
|
||||
}
|
||||
// Get attribute and uniform locations once
|
||||
const locations = {
|
||||
aPosition: gl.getAttribLocation(program, 'aPosition'),
|
||||
aTexCoord: gl.getAttribLocation(program, 'aTexCoord'),
|
||||
uTransform: gl.getUniformLocation(program, 'uTransform'),
|
||||
uProjection: gl.getUniformLocation(program, 'uProjection'),
|
||||
uTexture: gl.getUniformLocation(program, 'uTexture'),
|
||||
};
|
||||
// Shaders can be deleted after linking
|
||||
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;
|
||||
};
|
||||
export const transformIn3d = ({ matrix, sourceCanvas, untransformedRect, rectAfterTransforms, internalState, }) => {
|
||||
const { canvas, gl, program, locations } = createHelperCanvas({
|
||||
canvasWidth: rectAfterTransforms.width,
|
||||
canvasHeight: rectAfterTransforms.height,
|
||||
helperCanvasState: internalState.helperCanvasState,
|
||||
});
|
||||
// Use the cached program
|
||||
gl.useProgram(program);
|
||||
// Setup viewport and clear (already done in createHelperCanvas, but ensure it's set)
|
||||
gl.viewport(0, 0, rectAfterTransforms.width, rectAfterTransforms.height);
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
// Enable blending
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
||||
// Create vertex buffer
|
||||
const vertexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
// prettier-ignore
|
||||
const vertices = new Float32Array([
|
||||
untransformedRect.x, untransformedRect.y, 0, 0,
|
||||
untransformedRect.x + untransformedRect.width, untransformedRect.y, 1, 0,
|
||||
untransformedRect.x, untransformedRect.y + untransformedRect.height, 0, 1,
|
||||
untransformedRect.x, untransformedRect.y + untransformedRect.height, 0, 1,
|
||||
untransformedRect.x + untransformedRect.width, untransformedRect.y, 1, 0,
|
||||
untransformedRect.x + untransformedRect.width, untransformedRect.y + untransformedRect.height, 1, 1,
|
||||
]);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
||||
// Setup attributes using cached locations
|
||||
gl.enableVertexAttribArray(locations.aPosition);
|
||||
gl.vertexAttribPointer(locations.aPosition, 2, gl.FLOAT, false, 4 * 4, 0);
|
||||
gl.enableVertexAttribArray(locations.aTexCoord);
|
||||
gl.vertexAttribPointer(locations.aTexCoord, 2, gl.FLOAT, false, 4 * 4, 2 * 4);
|
||||
// Create and configure texture
|
||||
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);
|
||||
// Set uniforms using cached locations
|
||||
const transformMatrix = matrix.toFloat32Array();
|
||||
const zScale = 1000000000;
|
||||
// Projection matrix accounts for the output canvas dimensions
|
||||
const projectionMatrix = new Float32Array([
|
||||
2 / rectAfterTransforms.width,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
-2 / rectAfterTransforms.height,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
-2 / zScale,
|
||||
0,
|
||||
-1 + (2 * -rectAfterTransforms.x) / rectAfterTransforms.width,
|
||||
1 - (2 * -rectAfterTransforms.y) / rectAfterTransforms.height,
|
||||
0,
|
||||
1,
|
||||
]);
|
||||
gl.uniformMatrix4fv(locations.uTransform, false, transformMatrix);
|
||||
gl.uniformMatrix4fv(locations.uProjection, false, projectionMatrix);
|
||||
gl.uniform1i(locations.uTexture, 0);
|
||||
// Draw
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
// Clean up per-frame resources only
|
||||
gl.disableVertexAttribArray(locations.aPosition);
|
||||
gl.disableVertexAttribArray(locations.aTexCoord);
|
||||
gl.deleteTexture(texture);
|
||||
gl.deleteBuffer(vertexBuffer);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||
// Reset state
|
||||
gl.disable(gl.BLEND);
|
||||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
||||
return {
|
||||
canvas,
|
||||
rect: rectAfterTransforms,
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export declare function transformDOMRect({ rect, matrix }: {
|
||||
rect: DOMRect;
|
||||
matrix: DOMMatrix;
|
||||
}): DOMRect;
|
||||
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
export function transformDOMRect({ rect, matrix, }) {
|
||||
// Get all four corners of the rectangle
|
||||
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);
|
||||
// Transform all corners
|
||||
const transformedTopLeft = topLeft.matrixTransform(matrix);
|
||||
const transformedTopRight = topRight.matrixTransform(matrix);
|
||||
const transformedBottomLeft = bottomLeft.matrixTransform(matrix);
|
||||
const transformedBottomRight = bottomRight.matrixTransform(matrix);
|
||||
// Find the bounding box of the transformed points
|
||||
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);
|
||||
// Create a new DOMRect from the bounding box
|
||||
return new DOMRect(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export declare const setTransform: ({ ctx, transform, parentRect, scale, }: {
|
||||
ctx: OffscreenCanvasRenderingContext2D;
|
||||
transform: DOMMatrix;
|
||||
parentRect: DOMRect;
|
||||
scale: number;
|
||||
}) => () => void;
|
||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
export const setTransform = ({ ctx, transform, parentRect, }) => {
|
||||
const offsetMatrix = new DOMMatrix()
|
||||
.translate(-parentRect.x, -parentRect.y)
|
||||
.multiply(transform)
|
||||
.translate(parentRect.x, parentRect.y);
|
||||
ctx.setTransform(offsetMatrix);
|
||||
return () => {
|
||||
ctx.setTransform(new DOMMatrix());
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare const turnSvgIntoDrawable: (svg: SVGSVGElement) => Promise<HTMLImageElement>;
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
export const 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 = '';
|
||||
// Margins were already included in the positioning calculation,
|
||||
// so we need to remove them to avoid double counting.
|
||||
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;
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+4171
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export type FrameRange = number | [number, number] | [number, null];
|
||||
export declare const getRealFrameRange: (durationInFrames: number, frameRange: FrameRange | null) => [number, number];
|
||||
Generated
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
export const 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;
|
||||
};
|
||||
skills/remotion-prompt-video/node_modules/@remotion/web-renderer/dist/get-audio-encoding-config.d.ts
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { type AudioEncodingConfig } from 'mediabunny';
|
||||
export declare const getDefaultAudioEncodingConfig: () => Promise<AudioEncodingConfig | null>;
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
import { canEncodeAudio, QUALITY_MEDIUM, } from 'mediabunny';
|
||||
export const getDefaultAudioEncodingConfig = async () => {
|
||||
const preferredDefaultAudioEncodingConfig = {
|
||||
codec: 'aac',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
};
|
||||
if (await canEncodeAudio(preferredDefaultAudioEncodingConfig.codec, preferredDefaultAudioEncodingConfig)) {
|
||||
return preferredDefaultAudioEncodingConfig;
|
||||
}
|
||||
const backupDefaultAudioEncodingConfig = {
|
||||
codec: 'opus',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
};
|
||||
if (await canEncodeAudio(backupDefaultAudioEncodingConfig.codec, backupDefaultAudioEncodingConfig)) {
|
||||
return backupDefaultAudioEncodingConfig;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare const getBiggestBoundingClientRect: (element: HTMLElement | SVGElement) => DOMRect;
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
import { parseBoxShadow } from './drawing/draw-box-shadow';
|
||||
import { parseOutlineOffset, parseOutlineWidth } from './drawing/draw-outline';
|
||||
import { skipToNextNonDescendant } from './walk-tree';
|
||||
export const 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();
|
||||
// Calculate box shadow extensions
|
||||
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);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user