UNPKG

tldraw

Version:

A tiny little drawing editor.

338 lines (337 loc) • 15 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var getGeoShapePath_exports = {}; __export(getGeoShapePath_exports, { getGeoShapePath: () => getGeoShapePath }); module.exports = __toCommonJS(getGeoShapePath_exports); var import_editor = require("@tldraw/editor"); var import_shared = require("../arrow/shared"); var import_PathBuilder = require("../shared/PathBuilder"); const pathCache = new import_editor.WeakCache(); function getGeoShapePath(shape) { return pathCache.get(shape, _getGeoPath); } function _getGeoPath(shape) { const w = Math.max(1, shape.props.w); const h = Math.max(1, shape.props.h + shape.props.growY); const cx = w / 2; const cy = h / 2; const sw = import_shared.STROKE_SIZES[shape.props.size] * shape.props.scale; const isFilled = shape.props.fill !== "none"; switch (shape.props.geo) { case "arrow-down": { const ox = w * 0.16; const oy = Math.min(w, h) * 0.38; return new import_PathBuilder.PathBuilder().moveTo(ox, 0, { geometry: { isFilled } }).lineTo(w - ox, 0).lineTo(w - ox, h - oy).lineTo(w, h - oy).lineTo(w / 2, h).lineTo(0, h - oy).lineTo(ox, h - oy).close(); } case "arrow-left": { const ox = Math.min(w, h) * 0.38; const oy = h * 0.16; return new import_PathBuilder.PathBuilder().moveTo(ox, 0, { geometry: { isFilled } }).lineTo(ox, oy).lineTo(w, oy).lineTo(w, h - oy).lineTo(ox, h - oy).lineTo(ox, h).lineTo(0, h / 2).close(); } case "arrow-right": { const ox = Math.min(w, h) * 0.38; const oy = h * 0.16; return new import_PathBuilder.PathBuilder().moveTo(0, oy, { geometry: { isFilled } }).lineTo(w - ox, oy).lineTo(w - ox, 0).lineTo(w, h / 2).lineTo(w - ox, h).lineTo(w - ox, h - oy).lineTo(0, h - oy).close(); } case "arrow-up": { const ox = w * 0.16; const oy = Math.min(w, h) * 0.38; return new import_PathBuilder.PathBuilder().moveTo(w / 2, 0, { geometry: { isFilled } }).lineTo(w, oy).lineTo(w - ox, oy).lineTo(w - ox, h).lineTo(ox, h).lineTo(ox, oy).lineTo(0, oy).close(); } case "check-box": { const size = Math.min(w, h) * 0.82; const ox = (w - size) / 2; const oy = (h - size) / 2; return new import_PathBuilder.PathBuilder().moveTo(0, 0, { geometry: { isFilled } }).lineTo(w, 0).lineTo(w, h).lineTo(0, h).close().moveTo((0, import_editor.clamp)(ox + size * 0.25, 0, w), (0, import_editor.clamp)(oy + size * 0.52, 0, h), { geometry: { isInternal: true, isFilled: false }, offset: 0 }).lineTo((0, import_editor.clamp)(ox + size * 0.45, 0, w), (0, import_editor.clamp)(oy + size * 0.82, 0, h)).lineTo((0, import_editor.clamp)(ox + size * 0.82, 0, w), (0, import_editor.clamp)(oy + size * 0.22, 0, h), { offset: 0 }); } case "diamond": return new import_PathBuilder.PathBuilder().moveTo(cx, 0, { geometry: { isFilled } }).lineTo(w, cy).lineTo(cx, h).lineTo(0, cy).close(); case "ellipse": return new import_PathBuilder.PathBuilder().moveTo(0, cy, { geometry: { isFilled } }).arcTo(cx, cy, false, true, 0, w, cy).arcTo(cx, cy, false, true, 0, 0, cy).close(); case "heart": { const o = w / 4; const k = h / 4; return new import_PathBuilder.PathBuilder().moveTo(cx, h, { geometry: { isFilled } }).cubicBezierTo(0, k * 1.2, o * 1.5, k * 3, 0, k * 2.5).cubicBezierTo(cx, k * 0.9, 0, -k * 0.32, o * 1.85, -k * 0.32).cubicBezierTo(w, k * 1.2, o * 2.15, -k * 0.32, w, -k * 0.32).cubicBezierTo(cx, h, w, k * 2.5, o * 2.5, k * 3).close(); } case "hexagon": return import_PathBuilder.PathBuilder.lineThroughPoints((0, import_editor.getPolygonVertices)(w, h, 6), { geometry: { isFilled } }).close(); case "octagon": return import_PathBuilder.PathBuilder.lineThroughPoints((0, import_editor.getPolygonVertices)(w, h, 8), { geometry: { isFilled } }).close(); case "oval": return getStadiumPath(w, h, isFilled); case "pentagon": return import_PathBuilder.PathBuilder.lineThroughPoints((0, import_editor.getPolygonVertices)(w, h, 5), { geometry: { isFilled } }).close(); case "rectangle": return new import_PathBuilder.PathBuilder().moveTo(0, 0, { geometry: { isFilled } }).lineTo(w, 0).lineTo(w, h).lineTo(0, h).close(); case "rhombus": { const offset = Math.min(w * 0.38, h * 0.38); return new import_PathBuilder.PathBuilder().moveTo(offset, 0, { geometry: { isFilled } }).lineTo(w, 0).lineTo(w - offset, h).lineTo(0, h).close(); } case "rhombus-2": { const offset = Math.min(w * 0.38, h * 0.38); return new import_PathBuilder.PathBuilder().moveTo(0, 0, { geometry: { isFilled } }).lineTo(w - offset, 0).lineTo(w, h).lineTo(offset, h).close(); } case "star": return getStarPath(w, h, isFilled); case "trapezoid": { const offset = Math.min(w * 0.38, h * 0.38); return new import_PathBuilder.PathBuilder().moveTo(offset, 0, { geometry: { isFilled } }).lineTo(w - offset, 0).lineTo(w, h).lineTo(0, h).close(); } case "triangle": return new import_PathBuilder.PathBuilder().moveTo(cx, 0, { geometry: { isFilled } }).lineTo(w, h).lineTo(0, h).close(); case "x-box": return getXBoxPath(w, h, sw, shape.props.dash, isFilled); case "cloud": return getCloudPath(w, h, shape.id, shape.props.size, shape.props.scale, isFilled); default: (0, import_editor.exhaustiveSwitchError)(shape.props.geo); } } function getXBoxPath(w, h, sw, dash, isFilled) { const cx = w / 2; const cy = h / 2; const path = new import_PathBuilder.PathBuilder().moveTo(0, 0, { geometry: { isFilled } }).lineTo(w, 0).lineTo(w, h).lineTo(0, h).close(); if (dash === "dashed" || dash === "dotted") { return path.moveTo(0, 0, { geometry: { isInternal: true, isFilled: false }, dashStart: "skip", dashEnd: "outset" }).lineTo(cx, cy).moveTo(w, h, { geometry: { isInternal: true, isFilled: false }, dashStart: "skip", dashEnd: "outset" }).lineTo(cx, cy).moveTo(0, h, { geometry: { isInternal: true, isFilled: false }, dashStart: "skip", dashEnd: "outset" }).lineTo(cx, cy).moveTo(w, 0, { geometry: { isInternal: true, isFilled: false }, dashStart: "skip", dashEnd: "outset" }).lineTo(cx, cy); } const inset = dash === "draw" ? 0.62 : 0; path.moveTo((0, import_editor.clamp)(sw * inset, 0, w), (0, import_editor.clamp)(sw * inset, 0, h), { geometry: { isInternal: true, isFilled: false } }).lineTo((0, import_editor.clamp)(w - sw * inset, 0, w), (0, import_editor.clamp)(h - sw * inset, 0, h)).moveTo((0, import_editor.clamp)(w - sw * inset, 0, w), (0, import_editor.clamp)(sw * inset, 0, h)).lineTo((0, import_editor.clamp)(sw * inset, 0, w), (0, import_editor.clamp)(h - sw * inset, 0, h)); return path; } function getStadiumPath(w, h, isFilled) { if (h > w) { const r2 = w / 2; return new import_PathBuilder.PathBuilder().moveTo(0, r2, { geometry: { isFilled } }).arcTo(r2, r2, false, true, 0, w, r2).lineTo(w, h - r2).arcTo(r2, r2, false, true, 0, 0, h - r2).close(); } const r = h / 2; return new import_PathBuilder.PathBuilder().moveTo(r, h, { geometry: { isFilled } }).arcTo(r, r, false, true, 0, r, 0).lineTo(w - r, 0).arcTo(r, r, false, true, 0, w - r, h).close(); } function getStarPath(w, h, isFilled) { const sides = 5; const step = import_editor.PI2 / sides / 2; const rightMostIndex = Math.floor(sides / 4) * 2; const leftMostIndex = sides * 2 - rightMostIndex; const topMostIndex = 0; const bottomMostIndex = Math.floor(sides / 2) * 2; const maxX = Math.cos(-import_editor.HALF_PI + rightMostIndex * step) * w / 2; const minX = Math.cos(-import_editor.HALF_PI + leftMostIndex * step) * w / 2; const minY = Math.sin(-import_editor.HALF_PI + topMostIndex * step) * h / 2; const maxY = Math.sin(-import_editor.HALF_PI + bottomMostIndex * step) * h / 2; const diffX = w - Math.abs(maxX - minX); const diffY = h - Math.abs(maxY - minY); const offsetX = w / 2 + minX - (w / 2 - maxX); const offsetY = h / 2 + minY - (h / 2 - maxY); const ratio = 1; const cx = (w - offsetX) / 2; const cy = (h - offsetY) / 2; const ox = (w + diffX) / 2; const oy = (h + diffY) / 2; const ix = ox * ratio / 2; const iy = oy * ratio / 2; return import_PathBuilder.PathBuilder.lineThroughPoints( Array.from(Array(sides * 2), (_, i) => { const theta = -import_editor.HALF_PI + i * step; return new import_editor.Vec( cx + (i % 2 ? ix : ox) * Math.cos(theta), cy + (i % 2 ? iy : oy) * Math.sin(theta) ); }), { geometry: { isFilled } } ).close(); } function getOvalPerimeter(h, w) { if (h > w) return (import_editor.PI * (w / 2) + (h - w)) * 2; else return (import_editor.PI * (h / 2) + (w - h)) * 2; } function getPillPoints(width, height, numPoints) { const radius = Math.min(width, height) / 2; const longSide = Math.max(width, height) - radius * 2; const circumference = Math.PI * (radius * 2) + 2 * longSide; const spacing = circumference / numPoints; const sections = width > height ? [ { type: "straight", start: new import_editor.Vec(radius, 0), delta: new import_editor.Vec(1, 0) }, { type: "arc", center: new import_editor.Vec(width - radius, radius), startAngle: -import_editor.PI / 2 }, { type: "straight", start: new import_editor.Vec(width - radius, height), delta: new import_editor.Vec(-1, 0) }, { type: "arc", center: new import_editor.Vec(radius, radius), startAngle: import_editor.PI / 2 } ] : [ { type: "straight", start: new import_editor.Vec(width, radius), delta: new import_editor.Vec(0, 1) }, { type: "arc", center: new import_editor.Vec(radius, height - radius), startAngle: 0 }, { type: "straight", start: new import_editor.Vec(0, height - radius), delta: new import_editor.Vec(0, -1) }, { type: "arc", center: new import_editor.Vec(radius, radius), startAngle: import_editor.PI } ]; let sectionOffset = 0; const points = []; for (let i = 0; i < numPoints; i++) { const section = sections[0]; if (section.type === "straight") { points.push(import_editor.Vec.Add(section.start, import_editor.Vec.Mul(section.delta, sectionOffset))); } else { points.push( (0, import_editor.getPointOnCircle)(section.center, radius, section.startAngle + sectionOffset / radius) ); } sectionOffset += spacing; let sectionLength = section.type === "straight" ? longSide : import_editor.PI * radius; while (sectionOffset > sectionLength) { sectionOffset -= sectionLength; sections.push(sections.shift()); sectionLength = sections[0].type === "straight" ? longSide : import_editor.PI * radius; } } return points; } const SIZES = { s: 50, m: 70, l: 100, xl: 130 }; const BUMP_PROTRUSION = 0.2; function getCloudPath(width, height, seed, size, scale, isFilled) { const path = new import_PathBuilder.PathBuilder(); const getRandom = (0, import_editor.rng)(seed); const pillCircumference = getOvalPerimeter(width, height); const numBumps = Math.max( Math.ceil(pillCircumference / SIZES[size]), 6, Math.ceil(pillCircumference / Math.min(width, height)) ); const targetBumpProtrusion = pillCircumference / numBumps * BUMP_PROTRUSION; const innerWidth = Math.max(width - targetBumpProtrusion * 2, 1); const innerHeight = Math.max(height - targetBumpProtrusion * 2, 1); const innerCircumference = getOvalPerimeter(innerWidth, innerHeight); const distanceBetweenPointsOnPerimeter = innerCircumference / numBumps; const paddingX = (width - innerWidth) / 2; const paddingY = (height - innerHeight) / 2; const bumpPoints = getPillPoints(innerWidth, innerHeight, numBumps).map((p) => { return p.addXY(paddingX, paddingY); }); const maxWiggleX = width < 20 ? 0 : targetBumpProtrusion * 0.3; const maxWiggleY = height < 20 ? 0 : targetBumpProtrusion * 0.3; const wiggledPoints = bumpPoints.slice(0); for (let i = 0; i < Math.floor(numBumps / 2); i++) { wiggledPoints[i] = import_editor.Vec.AddXY( wiggledPoints[i], getRandom() * maxWiggleX * scale, getRandom() * maxWiggleY * scale ); wiggledPoints[numBumps - i - 1] = import_editor.Vec.AddXY( wiggledPoints[numBumps - i - 1], getRandom() * maxWiggleX * scale, getRandom() * maxWiggleY * scale ); } for (let i = 0; i < wiggledPoints.length; i++) { const j = i === wiggledPoints.length - 1 ? 0 : i + 1; const leftWigglePoint = wiggledPoints[i]; const rightWigglePoint = wiggledPoints[j]; const leftPoint = bumpPoints[i]; const rightPoint = bumpPoints[j]; const distanceBetweenOriginalPoints = import_editor.Vec.Dist(leftPoint, rightPoint); const curvatureOffset = distanceBetweenPointsOnPerimeter - distanceBetweenOriginalPoints; const distanceBetweenWigglePoints = import_editor.Vec.Dist(leftWigglePoint, rightWigglePoint); const relativeSize = distanceBetweenWigglePoints / distanceBetweenOriginalPoints; const finalDistance = (Math.max(paddingX, paddingY) + curvatureOffset) * relativeSize; const arcPoint = import_editor.Vec.Lrp(leftPoint, rightPoint, 0.5).add( import_editor.Vec.Sub(rightPoint, leftPoint).uni().per().mul(finalDistance) ); if (arcPoint.x < 0) { arcPoint.x = 0; } else if (arcPoint.x > width) { arcPoint.x = width; } if (arcPoint.y < 0) { arcPoint.y = 0; } else if (arcPoint.y > height) { arcPoint.y = height; } const center = (0, import_editor.centerOfCircleFromThreePoints)(leftWigglePoint, rightWigglePoint, arcPoint); const radius = import_editor.Vec.Dist( center ? center : import_editor.Vec.Average([leftWigglePoint, rightWigglePoint]), leftWigglePoint ); if (i === 0) { path.moveTo(leftWigglePoint.x, leftWigglePoint.y, { geometry: { isFilled } }); } path.circularArcTo(radius, false, true, rightWigglePoint.x, rightWigglePoint.y); } return path.close(); } //# sourceMappingURL=getGeoShapePath.js.map