tldraw
Version:
A tiny little drawing editor.
338 lines (337 loc) • 15 kB
JavaScript
"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