tldraw
Version:
A tiny little drawing editor.
229 lines (228 loc) • 7.76 kB
JavaScript
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import {
Box,
Circle2d,
Polygon2d,
Polyline2d,
SVGContainer,
ShapeUtil,
drawShapeMigrations,
drawShapeProps,
last,
lerp,
rng,
toFixed,
useEditor,
useValue
} from "@tldraw/editor";
import { ShapeFill } from "../shared/ShapeFill.mjs";
import { STROKE_SIZES } from "../shared/default-shape-constants.mjs";
import { getFillDefForCanvas, getFillDefForExport } from "../shared/defaultStyleDefs.mjs";
import { getStrokePoints } from "../shared/freehand/getStrokePoints.mjs";
import { getSvgPathFromStrokePoints } from "../shared/freehand/svg.mjs";
import { svgInk } from "../shared/freehand/svgInk.mjs";
import { interpolateSegments } from "../shared/interpolate-props.mjs";
import { useDefaultColorTheme } from "../shared/useDefaultColorTheme.mjs";
import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments } from "./getPath.mjs";
class DrawShapeUtil extends ShapeUtil {
static type = "draw";
static props = drawShapeProps;
static migrations = drawShapeMigrations;
hideResizeHandles(shape) {
return getIsDot(shape);
}
hideRotateHandle(shape) {
return getIsDot(shape);
}
hideSelectionBoundsFg(shape) {
return getIsDot(shape);
}
getDefaultProps() {
return {
segments: [],
color: "black",
fill: "none",
dash: "draw",
size: "m",
isComplete: false,
isClosed: false,
isPen: false,
scale: 1
};
}
getGeometry(shape) {
const points = getPointsFromSegments(shape.props.segments);
const sw = (STROKE_SIZES[shape.props.size] + 1) * shape.props.scale;
if (shape.props.segments.length === 1) {
const box = Box.FromPoints(points);
if (box.width < sw * 2 && box.height < sw * 2) {
return new Circle2d({
x: -sw,
y: -sw,
radius: sw,
isFilled: true
});
}
}
const strokePoints = getStrokePoints(
points,
getFreehandOptions(shape.props, sw, shape.props.isPen, true)
).map((p) => p.point);
if (shape.props.isClosed) {
return new Polygon2d({
points: strokePoints,
isFilled: shape.props.fill !== "none"
});
}
return new Polyline2d({
points: strokePoints
});
}
component(shape) {
return /* @__PURE__ */ jsx(SVGContainer, { children: /* @__PURE__ */ jsx(DrawShapeSvg, { shape }) });
}
indicator(shape) {
const allPointsFromSegments = getPointsFromSegments(shape.props.segments);
let sw = (STROKE_SIZES[shape.props.size] + 1) * shape.props.scale;
const zoomLevel = this.editor.getZoomLevel();
const forceSolid = zoomLevel < 0.5 && zoomLevel < 1.5 / sw;
if (!forceSolid && !shape.props.isPen && shape.props.dash === "draw" && allPointsFromSegments.length === 1) {
sw += rng(shape.id)() * (sw / 6);
}
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === "straight";
const options = getFreehandOptions(shape.props, sw, showAsComplete, true);
const strokePoints = getStrokePoints(allPointsFromSegments, options);
const solidStrokePath = strokePoints.length > 1 ? getSvgPathFromStrokePoints(strokePoints, shape.props.isClosed) : getDot(allPointsFromSegments[0], sw);
return /* @__PURE__ */ jsx("path", { d: solidStrokePath });
}
toSvg(shape, ctx) {
ctx.addExportDef(getFillDefForExport(shape.props.fill));
const scaleFactor = 1 / shape.props.scale;
return /* @__PURE__ */ jsx("g", { transform: `scale(${scaleFactor})`, children: /* @__PURE__ */ jsx(DrawShapeSvg, { shape, zoomOverride: 1 }) });
}
getCanvasSvgDefs() {
return [getFillDefForCanvas()];
}
onResize(shape, info) {
const { scaleX, scaleY } = info;
const newSegments = [];
for (const segment of shape.props.segments) {
newSegments.push({
...segment,
points: segment.points.map(({ x, y, z }) => {
return {
x: toFixed(scaleX * x),
y: toFixed(scaleY * y),
z
};
})
});
}
return {
props: {
segments: newSegments
}
};
}
expandSelectionOutlinePx(shape) {
const multiplier = shape.props.dash === "draw" ? 1.6 : 1;
return STROKE_SIZES[shape.props.size] * multiplier / 2 * shape.props.scale;
}
getInterpolatedProps(startShape, endShape, t) {
return {
...(t > 0.5 ? endShape.props : startShape.props),
segments: interpolateSegments(startShape.props.segments, endShape.props.segments, t),
scale: lerp(startShape.props.scale, endShape.props.scale, t)
};
}
}
function getDot(point, sw) {
const r = (sw + 1) * 0.5;
return `M ${point.x} ${point.y} m -${r}, 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${r * 2},0`;
}
function getIsDot(shape) {
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2;
}
function DrawShapeSvg({ shape, zoomOverride }) {
const theme = useDefaultColorTheme();
const editor = useEditor();
const allPointsFromSegments = getPointsFromSegments(shape.props.segments);
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === "straight";
let sw = (STROKE_SIZES[shape.props.size] + 1) * shape.props.scale;
const forceSolid = useValue(
"force solid",
() => {
const zoomLevel = zoomOverride ?? editor.getZoomLevel();
return zoomLevel < 0.5 && zoomLevel < 1.5 / sw;
},
[editor, sw, zoomOverride]
);
const dotAdjustment = useValue(
"dot adjustment",
() => {
const zoomLevel = zoomOverride ?? editor.getZoomLevel();
return zoomLevel < 0.2 ? 0 : 0.1;
},
[editor, zoomOverride]
);
if (!forceSolid && !shape.props.isPen && shape.props.dash === "draw" && allPointsFromSegments.length === 1) {
sw += rng(shape.id)() * (sw / 6);
}
const options = getFreehandOptions(shape.props, sw, showAsComplete, forceSolid);
if (!forceSolid && shape.props.dash === "draw") {
return /* @__PURE__ */ jsxs(Fragment, { children: [
shape.props.isClosed && shape.props.fill && allPointsFromSegments.length > 1 ? /* @__PURE__ */ jsx(
ShapeFill,
{
d: getSvgPathFromStrokePoints(
getStrokePoints(allPointsFromSegments, options),
shape.props.isClosed
),
theme,
color: shape.props.color,
fill: shape.props.isClosed ? shape.props.fill : "none",
scale: shape.props.scale
}
) : null,
/* @__PURE__ */ jsx(
"path",
{
d: svgInk(allPointsFromSegments, options),
strokeLinecap: "round",
fill: theme[shape.props.color].solid
}
)
] });
}
const strokePoints = getStrokePoints(allPointsFromSegments, options);
const isDot = strokePoints.length < 2;
const solidStrokePath = isDot ? getDot(allPointsFromSegments[0], 0) : getSvgPathFromStrokePoints(strokePoints, shape.props.isClosed);
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
ShapeFill,
{
d: solidStrokePath,
theme,
color: shape.props.color,
fill: isDot || shape.props.isClosed ? shape.props.fill : "none",
scale: shape.props.scale
}
),
/* @__PURE__ */ jsx(
"path",
{
d: solidStrokePath,
strokeLinecap: "round",
fill: isDot ? theme[shape.props.color].solid : "none",
stroke: theme[shape.props.color].solid,
strokeWidth: sw,
strokeDasharray: isDot ? "none" : getDrawShapeStrokeDashArray(shape, sw, dotAdjustment),
strokeDashoffset: "0"
}
)
] });
}
export {
DrawShapeUtil
};
//# sourceMappingURL=DrawShapeUtil.mjs.map