UNPKG

tldraw

Version:

A tiny little drawing editor.

229 lines (228 loc) • 7.76 kB
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