export type FillType = "solid" | "gradient"; export type LinearGradient = { from: string; to: string; angle: number; }; type Point = { x: number; y: number }; type GradientPointMode = "rect" | "centered"; const toRadians = (angle: number) => (angle * Math.PI) / 180; const normalizeAngle = (angle: number) => { if (!Number.isFinite(angle)) return 0; return ((angle % 360) + 360) % 360; }; const getGradientPoints = ( width: number, height: number, angle: number, mode: GradientPointMode, ): { start: Point; end: Point } => { const safeWidth = Math.max(1, width); const safeHeight = Math.max(1, height); // App/UI angle: 0° = top→bottom, 90° = left→right (clockwise). const rad = toRadians(normalizeAngle(angle)); const half = Math.sqrt(safeWidth * safeWidth + safeHeight * safeHeight) / 2; const halfX = Math.sin(rad) * half; const halfY = Math.cos(rad) * half; if (mode === "centered") { return { start: { x: -halfX, y: -halfY }, end: { x: halfX, y: halfY }, }; } const cx = safeWidth / 2; const cy = safeHeight / 2; return { start: { x: cx - halfX, y: cy - halfY }, end: { x: cx + halfX, y: cy + halfY }, }; }; export const getKonvaGradientProps = ( fillType: FillType | undefined, gradient: LinearGradient | undefined, width: number, height: number, mode: GradientPointMode, offset: Point = { x: 0, y: 0 }, ) => { if (fillType !== "gradient" || !gradient) return {}; const points = getGradientPoints(width, height, gradient.angle, mode); return { fillPriority: "linear-gradient" as const, fillLinearGradientStartPoint: { x: points.start.x + offset.x, y: points.start.y + offset.y, }, fillLinearGradientEndPoint: { x: points.end.x + offset.x, y: points.end.y + offset.y, }, fillLinearGradientColorStops: [0, gradient.from, 1, gradient.to] as [ number, string, number, string, ], }; }; /** SVG linearGradient endpoints matching the Konva gradient math. */ export const getSvgGradientEndpoints = ( gradient: LinearGradient, width: number, height: number, mode: GradientPointMode = "rect", offset: Point = { x: 0, y: 0 }, ) => { const points = getGradientPoints(width, height, gradient.angle, mode); return { x1: points.start.x + offset.x, y1: points.start.y + offset.y, x2: points.end.x + offset.x, y2: points.end.y + offset.y, }; }; /** Convert app/UI angle to CSS linear-gradient degrees (0deg = upward in CSS). */ export const toCssGradientAngle = (angle: number) => normalizeAngle(180 - normalizeAngle(angle)); export const toCssLinearGradient = (gradient: LinearGradient | undefined) => { if (!gradient) return undefined; return `linear-gradient(${toCssGradientAngle(gradient.angle)}deg, ${gradient.from} 0%, ${gradient.to} 100%)`; };