From 73a156c002b9f97b133d89b1e07f11eec7641576 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 31 Aug 2025 14:35:24 +0200 Subject: [PATCH] gradient backgrounds now work --- .../editor/media-panel/views/settings.tsx | 4 +- apps/web/src/lib/canvas-gradients.ts | 169 ++++++++++++++++++ apps/web/src/lib/timeline-renderer.ts | 16 +- 3 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/lib/canvas-gradients.ts diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx index 6cdd1b65..d2d21f36 100644 --- a/apps/web/src/components/editor/media-panel/views/settings.tsx +++ b/apps/web/src/components/editor/media-panel/views/settings.tsx @@ -178,9 +178,9 @@ const BackgroundPreviews = memo( }) => { return useMemo( () => - backgrounds.map((bg) => ( + backgrounds.map((bg, index) => (
{ + 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); + } + } +} diff --git a/apps/web/src/lib/timeline-renderer.ts b/apps/web/src/lib/timeline-renderer.ts index ddc361b5..4d00129c 100644 --- a/apps/web/src/lib/timeline-renderer.ts +++ b/apps/web/src/lib/timeline-renderer.ts @@ -2,6 +2,7 @@ import type { TimelineTrack } from "@/types/timeline"; import type { MediaFile } from "@/types/media"; import type { BlurIntensity } from "@/types/project"; import { videoCache } from "./video-cache"; +import { drawCssBackground } from "./canvas-gradients"; export interface RenderContext { ctx: CanvasRenderingContext2D; @@ -48,11 +49,24 @@ export async function renderTimelineFrame({ }: RenderContext): Promise { // Background ctx.clearRect(0, 0, canvasWidth, canvasHeight); - if (backgroundColor && backgroundColor !== "transparent") { + if ( + backgroundColor && + backgroundColor !== "transparent" && + !backgroundColor.includes("gradient") + ) { ctx.fillStyle = backgroundColor; 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 scaleY = projectCanvasSize ? canvasHeight / projectCanvasSize.height