mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
bunch of small improvements
This commit is contained in:
@@ -1,169 +1,219 @@
|
||||
function splitBackgroundLayers(input: string): string[] {
|
||||
const layers: string[] = [];
|
||||
let depth = 0;
|
||||
let start = 0;
|
||||
for (let i = 0; i < input.length; i += 1) {
|
||||
const ch = input[i] as string;
|
||||
if (ch === "(") depth += 1;
|
||||
else if (ch === ")") depth -= 1;
|
||||
else if (ch === "," && depth === 0) {
|
||||
layers.push(input.slice(start, i).trim());
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
layers.push(input.slice(start).trim());
|
||||
return layers;
|
||||
}
|
||||
|
||||
function extractColorFromStop(stop: string): string {
|
||||
const s = stop.trim();
|
||||
const funcs = ["rgba(", "rgb(", "hsla(", "hsl("] as const;
|
||||
for (const fn of funcs) {
|
||||
if (s.startsWith(fn)) {
|
||||
let depth = 0;
|
||||
for (let i = 0; i < s.length; i += 1) {
|
||||
const ch = s[i] as string;
|
||||
if (ch === "(") depth += 1;
|
||||
else if (ch === ")") {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return s.slice(0, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
const firstToken = s.split(" ")[0] as string;
|
||||
return firstToken;
|
||||
}
|
||||
|
||||
function drawLinearGradient(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
layer: string
|
||||
): void {
|
||||
const inside = layer.slice(layer.indexOf("(") + 1, layer.lastIndexOf(")"));
|
||||
const parts = splitBackgroundLayers(inside);
|
||||
const dir = (parts.shift() || "").trim();
|
||||
let x0 = 0,
|
||||
y0 = 0,
|
||||
x1 = width,
|
||||
y1 = 0;
|
||||
if (dir.endsWith("deg")) {
|
||||
const deg = parseFloat(dir);
|
||||
const rad = ((90 - deg) * Math.PI) / 180;
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const r = Math.hypot(width, height) / 2;
|
||||
x0 = cx - Math.cos(rad) * r;
|
||||
y0 = cy - Math.sin(rad) * r;
|
||||
x1 = cx + Math.cos(rad) * r;
|
||||
y1 = cy + Math.sin(rad) * r;
|
||||
} else if (dir.startsWith("to ")) {
|
||||
const d = dir.slice(3).trim();
|
||||
if (d === "right") {
|
||||
x0 = 0;
|
||||
y0 = 0;
|
||||
x1 = width;
|
||||
y1 = 0;
|
||||
} else if (d === "left") {
|
||||
x0 = width;
|
||||
y0 = 0;
|
||||
x1 = 0;
|
||||
y1 = 0;
|
||||
} else if (d === "bottom") {
|
||||
x0 = 0;
|
||||
y0 = 0;
|
||||
x1 = 0;
|
||||
y1 = height;
|
||||
} else if (d === "top") {
|
||||
x0 = 0;
|
||||
y0 = height;
|
||||
x1 = 0;
|
||||
y1 = 0;
|
||||
}
|
||||
} else {
|
||||
parts.unshift(dir);
|
||||
}
|
||||
const grad = ctx.createLinearGradient(x0, y0, x1, y1);
|
||||
const colorStops = parts;
|
||||
const n = Math.max(1, colorStops.length - 1);
|
||||
for (let i = 0; i < colorStops.length; i += 1) {
|
||||
const color = extractColorFromStop(colorStops[i] as string);
|
||||
grad.addColorStop(Math.min(1, Math.max(0, i / n)), color);
|
||||
}
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
function drawRadialGradient(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
layer: string
|
||||
): void {
|
||||
const inside = layer.slice(layer.indexOf("(") + 1, layer.lastIndexOf(")"));
|
||||
const parts = splitBackgroundLayers(inside);
|
||||
const first = (parts.shift() || "").trim();
|
||||
let cx = width / 2;
|
||||
let cy = height / 2;
|
||||
if (first.startsWith("circle at")) {
|
||||
const pos = first.replace("circle at", "").trim();
|
||||
const [px, py] = pos.split(" ");
|
||||
const parsePos = (p?: string, full?: number): number => {
|
||||
if (!p) return (full || 0) / 2;
|
||||
if (p.endsWith("%")) return (parseFloat(p) / 100) * (full || 0);
|
||||
if (p === "left") return 0;
|
||||
if (p === "right") return full || 0;
|
||||
if (p === "top") return 0;
|
||||
if (p === "bottom") return full || 0;
|
||||
if (p === "center") return (full || 0) / 2;
|
||||
return (full || 0) / 2;
|
||||
};
|
||||
if (px && py) {
|
||||
cx = parsePos(px, width);
|
||||
cy = parsePos(py, height);
|
||||
} else if (px) {
|
||||
cx = parsePos(px, width);
|
||||
cy = parsePos(px, height);
|
||||
}
|
||||
} else {
|
||||
parts.unshift(first);
|
||||
}
|
||||
const r = Math.hypot(width, height);
|
||||
const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
|
||||
const colorStops = parts;
|
||||
const n = Math.max(1, colorStops.length - 1);
|
||||
for (let i = 0; i < colorStops.length; i += 1) {
|
||||
const color = extractColorFromStop(colorStops[i] as string);
|
||||
grad.addColorStop(Math.min(1, Math.max(0, i / n)), color);
|
||||
}
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
export function drawCssBackground(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
css: string
|
||||
): void {
|
||||
const layers = splitBackgroundLayers(css).filter(Boolean);
|
||||
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
||||
const layer = layers[i] as string;
|
||||
if (layer.startsWith("linear-gradient(")) {
|
||||
drawLinearGradient(ctx, width, height, layer);
|
||||
} else if (layer.startsWith("radial-gradient(")) {
|
||||
drawRadialGradient(ctx, width, height, layer);
|
||||
} else if (
|
||||
layer.startsWith("#") ||
|
||||
layer.startsWith("rgb") ||
|
||||
layer.startsWith("hsl")
|
||||
) {
|
||||
ctx.fillStyle = layer;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
function splitBackgroundLayers(input: string): string[] {
|
||||
const layers: string[] = [];
|
||||
let depth = 0;
|
||||
let start = 0;
|
||||
for (let i = 0; i < input.length; i += 1) {
|
||||
const ch = input[i] as string;
|
||||
if (ch === "(") depth += 1;
|
||||
else if (ch === ")") depth -= 1;
|
||||
else if (ch === "," && depth === 0) {
|
||||
layers.push(input.slice(start, i).trim());
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
layers.push(input.slice(start).trim());
|
||||
return layers;
|
||||
}
|
||||
|
||||
function parseColorStop(stop: string): { color: string; position?: number } {
|
||||
const s = stop.trim();
|
||||
|
||||
// Handle functional colors like rgba(), rgb(), hsla(), hsl()
|
||||
const colorFunctions = ["rgba(", "rgb(", "hsla(", "hsl("];
|
||||
let color = "";
|
||||
let remaining = "";
|
||||
|
||||
for (const fn of colorFunctions) {
|
||||
if (s.startsWith(fn)) {
|
||||
let depth = 0;
|
||||
for (let i = 0; i < s.length; i += 1) {
|
||||
const ch = s[i] as string;
|
||||
if (ch === "(") depth += 1;
|
||||
else if (ch === ")") {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
color = s.slice(0, i + 1);
|
||||
remaining = s.slice(i + 1).trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!color) {
|
||||
const parts = s.split(/\s+/);
|
||||
color = parts[0] as string;
|
||||
remaining = parts.slice(1).join(" ");
|
||||
}
|
||||
|
||||
// Convert transparent to transparent white for better blending
|
||||
if (color === "transparent") {
|
||||
color = "rgba(255, 255, 255, 0)";
|
||||
}
|
||||
|
||||
// Parse position if present
|
||||
let position: number | undefined;
|
||||
if (remaining) {
|
||||
const posMatch = remaining.match(/(\d+(?:\.\d+)?)%/);
|
||||
if (posMatch) {
|
||||
position = parseFloat(posMatch[1] as string) / 100;
|
||||
}
|
||||
}
|
||||
|
||||
return { color, position };
|
||||
}
|
||||
|
||||
function parseLinearGradient(layer: string, width: number, height: number) {
|
||||
const inside = layer.slice(layer.indexOf("(") + 1, layer.lastIndexOf(")"));
|
||||
const parts = splitBackgroundLayers(inside);
|
||||
const dir = (parts.shift() || "").trim();
|
||||
|
||||
let x0 = 0,
|
||||
y0 = 0,
|
||||
x1 = width,
|
||||
y1 = 0; // default: to right
|
||||
|
||||
if (dir.endsWith("deg")) {
|
||||
const deg = parseFloat(dir);
|
||||
const rad = (deg * Math.PI) / 180;
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const r = Math.hypot(width, height) / 2;
|
||||
x0 = cx - Math.cos(rad) * r;
|
||||
y0 = cy - Math.sin(rad) * r;
|
||||
x1 = cx + Math.cos(rad) * r;
|
||||
y1 = cy + Math.sin(rad) * r;
|
||||
} else if (dir.startsWith("to ")) {
|
||||
const d = dir.slice(3).trim();
|
||||
if (d === "right") {
|
||||
x0 = 0;
|
||||
y0 = 0;
|
||||
x1 = width;
|
||||
y1 = 0;
|
||||
} else if (d === "left") {
|
||||
x0 = width;
|
||||
y0 = 0;
|
||||
x1 = 0;
|
||||
y1 = 0;
|
||||
} else if (d === "bottom") {
|
||||
x0 = 0;
|
||||
y0 = 0;
|
||||
x1 = 0;
|
||||
y1 = height;
|
||||
} else if (d === "top") {
|
||||
x0 = 0;
|
||||
y0 = height;
|
||||
x1 = 0;
|
||||
y1 = 0;
|
||||
}
|
||||
} else {
|
||||
// No direction specified, treat as first color
|
||||
parts.unshift(dir);
|
||||
}
|
||||
|
||||
return { x0, y0, x1, y1, colors: parts };
|
||||
}
|
||||
|
||||
function parseRadialGradient(layer: string, width: number, height: number) {
|
||||
const inside = layer.slice(layer.indexOf("(") + 1, layer.lastIndexOf(")"));
|
||||
const parts = splitBackgroundLayers(inside);
|
||||
const first = (parts.shift() || "").trim();
|
||||
|
||||
let cx = width / 2;
|
||||
let cy = height / 2;
|
||||
|
||||
if (first.startsWith("circle at")) {
|
||||
const pos = first.replace("circle at", "").trim();
|
||||
const coords = pos.split(/\s+/);
|
||||
|
||||
for (let i = 0; i < coords.length; i += 1) {
|
||||
const coord = coords[i] as string;
|
||||
if (coord.endsWith("%")) {
|
||||
const val = parseFloat(coord) / 100;
|
||||
if (i === 0) cx = val * width;
|
||||
else if (i === 1) cy = val * height;
|
||||
} else if (coord === "left") cx = 0;
|
||||
else if (coord === "right") cx = width;
|
||||
else if (coord === "top") cy = 0;
|
||||
else if (coord === "bottom") cy = height;
|
||||
else if (coord === "center") {
|
||||
if (i === 0) cx = width / 2;
|
||||
else if (i === 1) cy = height / 2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parts.unshift(first);
|
||||
}
|
||||
|
||||
// Use farthest-corner for radius
|
||||
const r = Math.max(
|
||||
Math.hypot(cx, cy),
|
||||
Math.hypot(width - cx, cy),
|
||||
Math.hypot(cx, height - cy),
|
||||
Math.hypot(width - cx, height - cy)
|
||||
);
|
||||
|
||||
return { cx, cy, r, colors: parts };
|
||||
}
|
||||
|
||||
export function drawCssBackground(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
css: string
|
||||
): void {
|
||||
const layers = splitBackgroundLayers(css).filter(Boolean);
|
||||
|
||||
// Draw layers from last to first (CSS background order)
|
||||
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
||||
const layer = layers[i] as string;
|
||||
|
||||
if (layer.startsWith("linear-gradient(")) {
|
||||
const { x0, y0, x1, y1, colors } = parseLinearGradient(
|
||||
layer,
|
||||
width,
|
||||
height
|
||||
);
|
||||
const grad = ctx.createLinearGradient(x0, y0, x1, y1);
|
||||
const colorStops = colors.map((c) => parseColorStop(c as string));
|
||||
|
||||
// Handle positions properly
|
||||
for (let j = 0; j < colorStops.length; j += 1) {
|
||||
const stop = colorStops[j] as { color: string; position?: number };
|
||||
const pos = stop.position ?? j / Math.max(1, colorStops.length - 1);
|
||||
grad.addColorStop(Math.max(0, Math.min(1, pos)), stop.color);
|
||||
}
|
||||
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
} else if (layer.startsWith("radial-gradient(")) {
|
||||
const { cx, cy, r, colors } = parseRadialGradient(layer, width, height);
|
||||
const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
|
||||
const colorStops = colors.map((c) => parseColorStop(c as string));
|
||||
|
||||
// Handle positions properly
|
||||
for (let j = 0; j < colorStops.length; j += 1) {
|
||||
const stop = colorStops[j] as { color: string; position?: number };
|
||||
const pos = stop.position ?? j / Math.max(1, colorStops.length - 1);
|
||||
grad.addColorStop(Math.max(0, Math.min(1, pos)), stop.color);
|
||||
}
|
||||
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
} else if (
|
||||
layer.startsWith("#") ||
|
||||
layer.startsWith("rgb") ||
|
||||
layer.startsWith("hsl") ||
|
||||
layer === "transparent" ||
|
||||
layer === "white" ||
|
||||
layer === "black"
|
||||
) {
|
||||
if (layer !== "transparent") {
|
||||
ctx.fillStyle = layer;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,13 +58,9 @@ export async function renderTimelineFrame({
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
// If backgroundColor is a CSS gradient string, draw it using JS (no foreignObject) to avoid CORS tainting
|
||||
// If backgroundColor is a CSS gradient string, draw it
|
||||
if (backgroundColor && backgroundColor.includes("gradient")) {
|
||||
try {
|
||||
drawCssBackground(ctx, canvasWidth, canvasHeight, backgroundColor);
|
||||
} catch {
|
||||
// best-effort; ignore failures
|
||||
}
|
||||
drawCssBackground(ctx, canvasWidth, canvasHeight, backgroundColor);
|
||||
}
|
||||
|
||||
const scaleX = projectCanvasSize ? canvasWidth / projectCanvasSize.width : 1;
|
||||
|
||||
Reference in New Issue
Block a user