UNPKG

@remotion/shapes

Version:

Generate SVG shapes

1,038 lines (1,023 loc) 25.6 kB
// src/utils/make-arrow.ts import { serializeInstructions } from "@remotion/paths"; var unitDir = (from, to) => { const dx = to[0] - from[0]; const dy = to[1] - from[1]; const len = Math.sqrt(dx * dx + dy * dy); return len === 0 ? [0, 0] : [dx / len, dy / len]; }; var buildArrowPath = (points, roundedIndices, cornerRadius) => { const n = points.length; if (cornerRadius === 0) { return [ { type: "M", x: points[0][0], y: points[0][1] }, ...points.slice(1).map(([x, y]) => ({ type: "L", x, y })), { type: "Z" } ]; } const d0 = unitDir(points[0], points[1]); const startX = points[0][0] + d0[0] * cornerRadius; const startY = points[0][1] + d0[1] * cornerRadius; const instrs = [{ type: "M", x: startX, y: startY }]; for (let i = 1;i < n; i++) { const curr = points[i]; if (roundedIndices.has(i)) { const prev = points[i - 1]; const next = points[(i + 1) % n]; const dIn = unitDir(prev, curr); const dOut = unitDir(curr, next); instrs.push({ type: "L", x: curr[0] - dIn[0] * cornerRadius, y: curr[1] - dIn[1] * cornerRadius }, { type: "C", cp1x: curr[0], cp1y: curr[1], cp2x: curr[0], cp2y: curr[1], x: curr[0] + dOut[0] * cornerRadius, y: curr[1] + dOut[1] * cornerRadius }); } else { instrs.push({ type: "L", x: curr[0], y: curr[1] }); } } const dIn0 = unitDir(points[n - 1], points[0]); instrs.push({ type: "L", x: points[0][0] - dIn0[0] * cornerRadius, y: points[0][1] - dIn0[1] * cornerRadius }, { type: "C", cp1x: points[0][0], cp1y: points[0][1], cp2x: points[0][0], cp2y: points[0][1], x: startX, y: startY }, { type: "Z" }); return instrs; }; var makeArrow = ({ length = 300, headWidth = 185, headLength = 120, shaftWidth = 80, direction = "right", cornerRadius = 0 }) => { if (length <= 0 || headWidth <= 0 || headLength <= 0 || shaftWidth <= 0) { throw new Error('All dimension parameters ("length", "headWidth", "headLength", "shaftWidth") must be positive numbers'); } if (headWidth < shaftWidth) { throw new Error(`"headWidth" must be greater than or equal to "shaftWidth", got headWidth=${headWidth} and shaftWidth=${shaftWidth}`); } if (headLength > length) { throw new Error(`"headLength" must be less than or equal to "length", got headLength=${headLength} and length=${length}`); } const shaftTop = (headWidth - shaftWidth) / 2; const shaftBottom = shaftTop + shaftWidth; const shaftEnd = length - headLength; const rightPoints = [ [0, shaftTop], [shaftEnd, shaftTop], [shaftEnd, 0], [length, headWidth / 2], [shaftEnd, headWidth], [shaftEnd, shaftBottom], [0, shaftBottom] ]; let points; let width; let height; if (direction === "right") { points = rightPoints; width = length; height = headWidth; } else if (direction === "left") { points = rightPoints.map(([x, y]) => [length - x, y]); width = length; height = headWidth; } else if (direction === "down") { points = [ [shaftTop, 0], [shaftBottom, 0], [shaftBottom, shaftEnd], [headWidth, shaftEnd], [headWidth / 2, length], [0, shaftEnd], [shaftTop, shaftEnd] ]; width = headWidth; height = length; } else { points = [ [shaftTop, length], [shaftBottom, length], [shaftBottom, headLength], [headWidth, headLength], [headWidth / 2, 0], [0, headLength], [shaftTop, headLength] ]; width = headWidth; height = length; } const roundedIndices = direction === "right" || direction === "left" ? new Set([0, 2, 3, 4, 6]) : new Set([0, 1, 3, 4, 5]); const instructions = buildArrowPath(points, roundedIndices, cornerRadius); const path = serializeInstructions(instructions); return { path, instructions, width, height, transformOrigin: `${width / 2} ${height / 2}` }; }; // src/components/render-svg.tsx import React, { useMemo } from "react"; import { version } from "react-dom"; // src/utils/does-react-support-canary.ts var doesReactSupportTransformOriginProperty = (version) => { if (version.includes("canary") || version.includes("experimental")) { const last8Chars = parseInt(version.slice(-8), 10); return last8Chars > 20230209; } const [major] = version.split(".").map(Number); return major > 18; }; // src/components/render-svg.tsx import { jsx, jsxs } from "react/jsx-runtime"; var RenderSvg = ({ width, height, path, style, pathStyle, transformOrigin, debug, instructions, ...props }) => { const actualStyle = useMemo(() => { return { overflow: "visible", ...style ?? {} }; }, [style]); const actualPathStyle = useMemo(() => { return { transformBox: "fill-box", ...pathStyle ?? {} }; }, [pathStyle]); const reactSupportsTransformOrigin = doesReactSupportTransformOriginProperty(version); return /* @__PURE__ */ jsxs("svg", { width, height, viewBox: `0 0 ${width} ${height}`, xmlns: "http://www.w3.org/2000/svg", style: actualStyle, children: [ /* @__PURE__ */ jsx("path", { ...reactSupportsTransformOrigin ? { transformOrigin } : { "transform-origin": transformOrigin }, d: path, style: actualPathStyle, ...props }), debug ? instructions.map((i, index) => { if (i.type === "C") { const prevInstruction = index === 0 ? instructions[instructions.length - 1] : instructions[index - 1]; if (prevInstruction.type === "V" || prevInstruction.type === "H" || prevInstruction.type === "a" || prevInstruction.type === "Z" || prevInstruction.type === "t" || prevInstruction.type === "q" || prevInstruction.type === "l" || prevInstruction.type === "c" || prevInstruction.type === "m" || prevInstruction.type === "h" || prevInstruction.type === "s" || prevInstruction.type === "v") { return null; } const prevX = prevInstruction.x; const prevY = prevInstruction.y; return /* @__PURE__ */ jsxs(React.Fragment, { children: [ /* @__PURE__ */ jsx("path", { d: `M ${prevX} ${prevY} ${i.cp1x} ${i.cp1y}`, strokeWidth: 2, stroke: "rgba(0, 0, 0, 0.4)" }), /* @__PURE__ */ jsx("path", { d: `M ${i.x} ${i.y} ${i.cp2x} ${i.cp2y}`, strokeWidth: 2, stroke: "rgba(0, 0, 0, 0.4)" }), /* @__PURE__ */ jsx("circle", { cx: i.cp1x, cy: i.cp1y, r: 3, fill: "white", strokeWidth: 2, stroke: "black" }), /* @__PURE__ */ jsx("circle", { cx: i.cp2x, cy: i.cp2y, r: 3, strokeWidth: 2, fill: "white", stroke: "black" }) ] }, index); } return null; }) : null ] }); }; // src/components/arrow.tsx import { jsx as jsx2 } from "react/jsx-runtime"; var Arrow = ({ length, headWidth, headLength, shaftWidth, direction, cornerRadius, ...props }) => { return /* @__PURE__ */ jsx2(RenderSvg, { ...makeArrow({ length, headWidth, headLength, shaftWidth, direction, cornerRadius }), ...props }); }; // src/utils/make-circle.ts import { serializeInstructions as serializeInstructions2 } from "@remotion/paths"; var makeCircle = ({ radius }) => { const instructions = [ { type: "M", x: radius, y: 0 }, { type: "a", rx: radius, ry: radius, xAxisRotation: 0, largeArcFlag: true, sweepFlag: true, dx: 0, dy: radius * 2 }, { type: "a", rx: radius, ry: radius, xAxisRotation: 0, largeArcFlag: true, sweepFlag: true, dx: 0, dy: -radius * 2 }, { type: "Z" } ]; const path = serializeInstructions2(instructions); return { height: radius * 2, width: radius * 2, path, instructions, transformOrigin: `${radius} ${radius}` }; }; // src/components/circle.tsx import { jsx as jsx3 } from "react/jsx-runtime"; var Circle = ({ radius, ...props }) => { return /* @__PURE__ */ jsx3(RenderSvg, { ...makeCircle({ radius }), ...props }); }; // src/utils/make-ellipse.ts import { serializeInstructions as serializeInstructions3 } from "@remotion/paths"; var makeEllipse = ({ rx, ry }) => { const instructions = [ { type: "M", x: rx, y: 0 }, { type: "a", rx, ry, xAxisRotation: 0, largeArcFlag: true, sweepFlag: false, dx: 1, dy: 0 }, { type: "Z" } ]; const path = serializeInstructions3(instructions); return { width: rx * 2, height: ry * 2, path, instructions, transformOrigin: `${rx} ${ry}` }; }; // src/components/ellipse.tsx import { jsx as jsx4 } from "react/jsx-runtime"; var Ellipse = ({ rx, ry, ...props }) => { return /* @__PURE__ */ jsx4(RenderSvg, { ...makeEllipse({ rx, ry }), ...props }); }; // src/utils/make-heart.ts import { serializeInstructions as serializeInstructions4 } from "@remotion/paths"; var makeHeart = ({ height, aspectRatio = 1.1, bottomRoundnessAdjustment = 0, depthAdjustment = 0 }) => { const width = height * aspectRatio; const bottomControlPointX = 23 / 110 * width + bottomRoundnessAdjustment * width; const bottomControlPointY = 69 / 100 * height; const bottomLeftControlPointY = 60 / 100 * height; const topLeftControlPoint = 13 / 100 * height; const topBezierWidth = 29 / 110 * width; const topRightControlPointX = 15 / 110 * width; const innerControlPointX = 5 / 110 * width; const innerControlPointY = 7 / 100 * height; const depth = 17 / 100 * height + depthAdjustment * height; const instructions = [ { type: "M", x: width / 2, y: height }, { type: "C", cp1x: width / 2 - bottomControlPointX, cp1y: bottomControlPointY, cp2x: 0, cp2y: bottomLeftControlPointY, x: 0, y: height / 4 }, { type: "C", cp1x: 0, cp1y: topLeftControlPoint, cp2x: width / 4 - topBezierWidth / 2, cp2y: 0, x: width / 4, y: 0 }, { type: "C", cp1x: width / 4 + topBezierWidth / 2, cp1y: 0, cp2x: width / 2 - innerControlPointX, cp2y: innerControlPointY, x: width / 2, y: depth }, { type: "C", cp1x: width / 2 + innerControlPointX, cp1y: innerControlPointY, cp2x: width / 2 + topRightControlPointX, cp2y: 0, x: width / 4 * 3, y: 0 }, { type: "C", cp1x: width / 4 * 3 + topBezierWidth / 2, cp1y: 0, cp2x: width, cp2y: topLeftControlPoint, x: width, y: height / 4 }, { type: "C", x: width / 2, y: height, cp1x: width, cp1y: bottomLeftControlPointY, cp2x: width / 2 + bottomControlPointX, cp2y: bottomControlPointY }, { type: "Z" } ]; const path = serializeInstructions4(instructions); return { path, width, height, transformOrigin: `${width / 2} ${height / 2}`, instructions }; }; // src/components/heart.tsx import { jsx as jsx5 } from "react/jsx-runtime"; var Heart = ({ aspectRatio, height, bottomRoundnessAdjustment = 0, depthAdjustment = 0, ...props }) => { return /* @__PURE__ */ jsx5(RenderSvg, { ...makeHeart({ aspectRatio, height, bottomRoundnessAdjustment, depthAdjustment }), ...props }); }; // src/utils/make-pie.ts import { serializeInstructions as serializeInstructions5 } from "@remotion/paths"; var getCoord = ({ counterClockwise, actualProgress, rotation, radius, coord }) => { const factor = counterClockwise ? -1 : 1; const val = Math[coord === "x" ? "cos" : "sin"](factor * actualProgress * Math.PI * 2 + Math.PI * 1.5 + rotation) * radius + radius; const rounded = Math.round(val * 1e5) / 1e5; return rounded; }; var makePie = ({ progress, radius, closePath = true, counterClockwise = false, rotation = 0 }) => { const actualProgress = Math.min(Math.max(progress, 0), 1); const endAngleX = getCoord({ actualProgress, coord: "x", counterClockwise, radius, rotation }); const endAngleY = getCoord({ actualProgress, coord: "y", counterClockwise, radius, rotation }); const start = { x: getCoord({ actualProgress: 0, coord: "x", counterClockwise, radius, rotation }), y: getCoord({ actualProgress: 0, coord: "y", counterClockwise, radius, rotation }) }; const end = { x: endAngleX, y: endAngleY }; const instructions = [ { type: "M", ...start }, { type: "A", rx: radius, ry: radius, xAxisRotation: 0, largeArcFlag: false, sweepFlag: !counterClockwise, x: actualProgress <= 0.5 ? endAngleX : getCoord({ actualProgress: 0.5, coord: "x", counterClockwise, radius, rotation }), y: actualProgress <= 0.5 ? endAngleY : getCoord({ actualProgress: 0.5, coord: "y", counterClockwise, radius, rotation }) }, actualProgress > 0.5 ? { type: "A", rx: radius, ry: radius, xAxisRotation: 0, largeArcFlag: false, sweepFlag: !counterClockwise, ...end } : null, actualProgress > 0 && actualProgress < 1 && closePath ? { type: "L", x: radius, y: radius } : null, closePath ? { type: "Z" } : null ].filter(Boolean); const path = serializeInstructions5(instructions); return { height: radius * 2, width: radius * 2, path, instructions, transformOrigin: `${radius} ${radius}` }; }; // src/components/pie.tsx import { jsx as jsx6 } from "react/jsx-runtime"; var Pie = ({ radius, progress, closePath, counterClockwise, rotation, ...props }) => { return /* @__PURE__ */ jsx6(RenderSvg, { ...makePie({ radius, progress, closePath, counterClockwise, rotation }), ...props }); }; // src/utils/make-polygon.ts import { PathInternals, reduceInstructions, resetPath, serializeInstructions as serializeInstructions6 } from "@remotion/paths"; // src/utils/join-points.ts var shortenVector = (vector, radius) => { const [x, y] = vector; const currentLength = Math.sqrt(x * x + y * y); const scalingFactor = (currentLength - radius) / currentLength; return [x * scalingFactor, y * scalingFactor]; }; var scaleVectorToLength = (vector, length) => { const [x, y] = vector; const currentLength = Math.sqrt(x * x + y * y); const scalingFactor = length / currentLength; return [x * scalingFactor, y * scalingFactor]; }; var joinPoints = (points, { edgeRoundness, cornerRadius, roundCornerStrategy }) => { return points.map(([x, y], i) => { const prevPointIndex = i === 0 ? points.length - 2 : i - 1; const prevPoint = points[prevPointIndex]; const nextPointIndex = i === points.length - 1 ? 1 : i + 1; const nextPoint = points[nextPointIndex]; const middleOfLine = [(x + nextPoint[0]) / 2, (y + nextPoint[1]) / 2]; const prevPointMiddleOfLine = [ (x + prevPoint[0]) / 2, (y + prevPoint[1]) / 2 ]; const prevVector = [x - prevPoint[0], y - prevPoint[1]]; const nextVector = [nextPoint[0] - x, nextPoint[1] - y]; if (i === 0) { if (edgeRoundness !== null) { return [ { type: "M", x: middleOfLine[0], y: middleOfLine[1] } ]; } if (cornerRadius !== 0) { const computeRadius = shortenVector(nextVector, cornerRadius); return [ { type: "M", x: computeRadius[0] + x, y: computeRadius[1] + y } ]; } return [ { type: "M", x, y } ]; } if (cornerRadius && edgeRoundness !== null) { throw new Error(`"cornerRadius" and "edgeRoundness" cannot be specified at the same time.`); } if (edgeRoundness === null) { if (cornerRadius === 0) { return [ { type: "L", x, y } ]; } const prevVectorMinusRadius = shortenVector(prevVector, cornerRadius); const prevVectorLength = scaleVectorToLength(prevVector, cornerRadius); const nextVectorMinusRadius = scaleVectorToLength(nextVector, cornerRadius); const firstDraw = [ prevPoint[0] + prevVectorMinusRadius[0], prevPoint[1] + prevVectorMinusRadius[1] ]; return [ { type: "L", x: firstDraw[0], y: firstDraw[1] }, roundCornerStrategy === "arc" ? { type: "a", rx: cornerRadius, ry: cornerRadius, xAxisRotation: 0, dx: prevVectorLength[0] + nextVectorMinusRadius[0], dy: prevVectorLength[1] + nextVectorMinusRadius[1], largeArcFlag: false, sweepFlag: true } : { type: "C", x: firstDraw[0] + prevVectorLength[0] + nextVectorMinusRadius[0], y: firstDraw[1] + prevVectorLength[1] + nextVectorMinusRadius[1], cp1x: x, cp1y: y, cp2x: x, cp2y: y } ]; } const controlPoint1 = [ prevPointMiddleOfLine[0] + prevVector[0] * edgeRoundness * 0.5, prevPointMiddleOfLine[1] + prevVector[1] * edgeRoundness * 0.5 ]; const controlPoint2 = [ middleOfLine[0] - nextVector[0] * edgeRoundness * 0.5, middleOfLine[1] - nextVector[1] * edgeRoundness * 0.5 ]; return [ { type: "C", cp1x: controlPoint1[0], cp1y: controlPoint1[1], cp2x: controlPoint2[0], cp2y: controlPoint2[1], x: middleOfLine[0], y: middleOfLine[1] } ]; }).flat(1); }; // src/utils/make-polygon.ts function polygon({ points, radius, centerX, centerY, cornerRadius, edgeRoundness }) { const degreeIncrement = Math.PI * 2 / points; const d = new Array(points).fill(0).map((_, i) => { const angle = degreeIncrement * i - Math.PI / 2; const point = { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) }; return [point.x, point.y]; }); return joinPoints([...d, d[0]], { edgeRoundness, cornerRadius, roundCornerStrategy: cornerRadius > 0 ? "bezier" : "arc" }); } var makePolygon = ({ points, radius, cornerRadius = 0, edgeRoundness = null }) => { if (points < 3) { throw new Error(`"points" should be minimum 3, got ${points}`); } const width = 2 * radius; const height = 2 * radius; const centerX = width / 2; const centerY = height / 2; const polygonPathInstructions = polygon({ points, radius, centerX, centerY, cornerRadius, edgeRoundness }); const reduced = reduceInstructions(polygonPathInstructions); const path = resetPath(serializeInstructions6(reduced)); const boundingBox = PathInternals.getBoundingBoxFromInstructions(reduced); return { path, width: boundingBox.width, height: boundingBox.height, transformOrigin: `${centerX} ${centerY}`, instructions: polygonPathInstructions }; }; // src/components/polygon.tsx import { jsx as jsx7 } from "react/jsx-runtime"; var Polygon = ({ points, radius, cornerRadius, edgeRoundness, ...props }) => { return /* @__PURE__ */ jsx7(RenderSvg, { ...makePolygon({ points, cornerRadius, edgeRoundness, radius }), ...props }); }; // src/utils/make-rect.ts import { serializeInstructions as serializeInstructions7 } from "@remotion/paths"; var makeRect = ({ width, height, edgeRoundness = null, cornerRadius = 0 }) => { const transformOrigin = [width / 2, height / 2]; const instructions = [ ...joinPoints([ [cornerRadius, 0], [width, 0], [width, height], [0, height], [0, 0] ], { edgeRoundness, cornerRadius, roundCornerStrategy: "arc" }), { type: "Z" } ]; const path = serializeInstructions7(instructions); return { width, height, instructions, path, transformOrigin: transformOrigin.join(" ") }; }; // src/components/rect.tsx import { jsx as jsx8 } from "react/jsx-runtime"; var Rect = ({ width, edgeRoundness, height, cornerRadius, ...props }) => { return /* @__PURE__ */ jsx8(RenderSvg, { ...makeRect({ height, width, edgeRoundness, cornerRadius }), ...props }); }; // src/utils/make-star.ts import { PathInternals as PathInternals2, reduceInstructions as reduceInstructions2, resetPath as resetPath2, serializeInstructions as serializeInstructions8 } from "@remotion/paths"; var star = ({ centerX, centerY, points, innerRadius, outerRadius, cornerRadius, edgeRoundness }) => { const degreeIncrement = Math.PI * 2 / (points * 2); const d = new Array(points * 2).fill(true).map((_p, i) => { const radius = i % 2 === 0 ? outerRadius : innerRadius; const angle = degreeIncrement * i - Math.PI / 2; const point = { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) }; return [point.x, point.y]; }); return [ ...joinPoints([...d, d[0]], { edgeRoundness, cornerRadius, roundCornerStrategy: cornerRadius > 0 ? "bezier" : "arc" }), { type: "Z" } ]; }; var makeStar = ({ points, innerRadius, outerRadius, cornerRadius = 0, edgeRoundness = null }) => { const width = outerRadius * 2; const height = outerRadius * 2; const centerX = width / 2; const centerY = height / 2; const starPathInstructions = star({ centerX, centerY, points, innerRadius, outerRadius, cornerRadius, edgeRoundness }); const reduced = reduceInstructions2(starPathInstructions); const path = resetPath2(serializeInstructions8(reduced)); const boundingBox = PathInternals2.getBoundingBoxFromInstructions(reduced); return { path, width: boundingBox.width, height: boundingBox.height, transformOrigin: `${centerX} ${centerY}`, instructions: starPathInstructions }; }; // src/components/star.tsx import { jsx as jsx9 } from "react/jsx-runtime"; var Star = ({ innerRadius, outerRadius, points, cornerRadius, edgeRoundness, ...props }) => { return /* @__PURE__ */ jsx9(RenderSvg, { ...makeStar({ innerRadius, outerRadius, points, cornerRadius, edgeRoundness }), ...props }); }; // src/utils/make-triangle.ts import { serializeInstructions as serializeInstructions9 } from "@remotion/paths"; var makeTriangle = ({ length, direction = "right", edgeRoundness = null, cornerRadius = 0 }) => { if (typeof length !== "number") { throw new Error(`"length" of a triangle must be a number, got ${JSON.stringify(length)}`); } const longerDimension = length; const shorterSize = Math.sqrt(length ** 2 * 0.75); const points = { up: [ [longerDimension / 2, 0], [0, shorterSize], [longerDimension, shorterSize], [longerDimension / 2, 0] ], right: [ [0, 0], [0, longerDimension], [shorterSize, longerDimension / 2], [0, 0] ], down: [ [0, 0], [longerDimension, 0], [longerDimension / 2, shorterSize], [0, 0] ], left: [ [shorterSize, 0], [shorterSize, longerDimension], [0, longerDimension / 2], [shorterSize, 0] ] }; const transformOriginX = { left: shorterSize / 3 * 2, right: shorterSize / 3, up: longerDimension / 2, down: longerDimension / 2 }[direction]; const transformOriginY = { up: shorterSize / 3 * 2, down: shorterSize / 3, left: longerDimension / 2, right: longerDimension / 2 }[direction]; const instructions = [ ...joinPoints(points[direction], { edgeRoundness, cornerRadius, roundCornerStrategy: "bezier" }), { type: "Z" } ]; const path = serializeInstructions9(instructions); return { path, instructions, width: direction === "up" || direction === "down" ? length : shorterSize, height: direction === "up" || direction === "down" ? shorterSize : length, transformOrigin: `${transformOriginX} ${transformOriginY}` }; }; // src/components/triangle.tsx import { jsx as jsx10 } from "react/jsx-runtime"; var Triangle = ({ length, direction, edgeRoundness, cornerRadius, ...props }) => { return /* @__PURE__ */ jsx10(RenderSvg, { ...makeTriangle({ length, direction, edgeRoundness, cornerRadius }), ...props }); }; export { makeTriangle, makeStar, makeRect, makePolygon, makePie, makeHeart, makeEllipse, makeCircle, makeArrow, Triangle, Star, Rect, Polygon, Pie, Heart, Ellipse, Circle, Arrow };