mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
gradient backgrounds now work
This commit is contained in:
@@ -178,9 +178,9 @@ const BackgroundPreviews = memo(
|
|||||||
}) => {
|
}) => {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
backgrounds.map((bg) => (
|
backgrounds.map((bg, index) => (
|
||||||
<div
|
<div
|
||||||
key={bg}
|
key={`${index}-${bg}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary",
|
"w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary",
|
||||||
isColorBackground &&
|
isColorBackground &&
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import type { TimelineTrack } from "@/types/timeline";
|
|||||||
import type { MediaFile } from "@/types/media";
|
import type { MediaFile } from "@/types/media";
|
||||||
import type { BlurIntensity } from "@/types/project";
|
import type { BlurIntensity } from "@/types/project";
|
||||||
import { videoCache } from "./video-cache";
|
import { videoCache } from "./video-cache";
|
||||||
|
import { drawCssBackground } from "./canvas-gradients";
|
||||||
|
|
||||||
export interface RenderContext {
|
export interface RenderContext {
|
||||||
ctx: CanvasRenderingContext2D;
|
ctx: CanvasRenderingContext2D;
|
||||||
@@ -48,11 +49,24 @@ export async function renderTimelineFrame({
|
|||||||
}: RenderContext): Promise<void> {
|
}: RenderContext): Promise<void> {
|
||||||
// Background
|
// Background
|
||||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||||
if (backgroundColor && backgroundColor !== "transparent") {
|
if (
|
||||||
|
backgroundColor &&
|
||||||
|
backgroundColor !== "transparent" &&
|
||||||
|
!backgroundColor.includes("gradient")
|
||||||
|
) {
|
||||||
ctx.fillStyle = backgroundColor;
|
ctx.fillStyle = backgroundColor;
|
||||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
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 && backgroundColor.includes("gradient")) {
|
||||||
|
try {
|
||||||
|
drawCssBackground(ctx, canvasWidth, canvasHeight, backgroundColor);
|
||||||
|
} catch {
|
||||||
|
// best-effort; ignore failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const scaleX = projectCanvasSize ? canvasWidth / projectCanvasSize.width : 1;
|
const scaleX = projectCanvasSize ? canvasWidth / projectCanvasSize.width : 1;
|
||||||
const scaleY = projectCanvasSize
|
const scaleY = projectCanvasSize
|
||||||
? canvasHeight / projectCanvasSize.height
|
? canvasHeight / projectCanvasSize.height
|
||||||
|
|||||||
Reference in New Issue
Block a user