UNPKG

tldraw

Version:

A tiny little drawing editor.

242 lines (241 loc) • 7.52 kB
import { jsx } from "react/jsx-runtime"; import { Circle2d, Polygon2d, SVGContainer, ShapeUtil, highlightShapeMigrations, highlightShapeProps, last, lerp, rng, useValue } from "@tldraw/editor"; import { getHighlightFreehandSettings, getPointsFromSegments } from "../draw/getPath.mjs"; import { FONT_SIZES } from "../shared/default-shape-constants.mjs"; import { getStrokeOutlinePoints } from "../shared/freehand/getStrokeOutlinePoints.mjs"; import { getStrokePoints } from "../shared/freehand/getStrokePoints.mjs"; import { setStrokePointRadii } from "../shared/freehand/setStrokePointRadii.mjs"; import { getSvgPathFromStrokePoints } from "../shared/freehand/svg.mjs"; import { interpolateSegments } from "../shared/interpolate-props.mjs"; import { useColorSpace } from "../shared/useColorSpace.mjs"; import { useDefaultColorTheme } from "../shared/useDefaultColorTheme.mjs"; const OVERLAY_OPACITY = 0.35; const UNDERLAY_OPACITY = 0.82; class HighlightShapeUtil extends ShapeUtil { static type = "highlight"; static props = highlightShapeProps; static migrations = highlightShapeMigrations; hideResizeHandles(shape) { return getIsDot(shape); } hideRotateHandle(shape) { return getIsDot(shape); } hideSelectionBoundsFg(shape) { return getIsDot(shape); } getDefaultProps() { return { segments: [], color: "black", size: "m", isComplete: false, isPen: false, scale: 1 }; } getGeometry(shape) { const strokeWidth = getStrokeWidth(shape); if (getIsDot(shape)) { return new Circle2d({ x: -strokeWidth / 2, y: -strokeWidth / 2, radius: strokeWidth / 2, isFilled: true }); } const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, true); const opts = getHighlightFreehandSettings({ strokeWidth: sw, showAsComplete: true }); setStrokePointRadii(strokePoints, opts); return new Polygon2d({ points: getStrokeOutlinePoints(strokePoints, opts), isFilled: true }); } component(shape) { const forceSolid = useHighlightForceSolid(this.editor, shape); const strokeWidth = getStrokeWidth(shape); return /* @__PURE__ */ jsx(SVGContainer, { children: /* @__PURE__ */ jsx( HighlightRenderer, { shape, forceSolid, strokeWidth, opacity: OVERLAY_OPACITY } ) }); } backgroundComponent(shape) { const forceSolid = useHighlightForceSolid(this.editor, shape); const strokeWidth = getStrokeWidth(shape); return /* @__PURE__ */ jsx(SVGContainer, { children: /* @__PURE__ */ jsx( HighlightRenderer, { shape, forceSolid, strokeWidth, opacity: UNDERLAY_OPACITY } ) }); } indicator(shape) { const forceSolid = useHighlightForceSolid(this.editor, shape); const strokeWidth = getStrokeWidth(shape); const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, forceSolid); const allPointsFromSegments = getPointsFromSegments(shape.props.segments); let strokePath; if (strokePoints.length < 2) { strokePath = getIndicatorDot(allPointsFromSegments[0], sw); } else { strokePath = getSvgPathFromStrokePoints(strokePoints, false); } return /* @__PURE__ */ jsx("path", { d: strokePath }); } toSvg(shape) { const strokeWidth = getStrokeWidth(shape); const forceSolid = strokeWidth < 1.5; const scaleFactor = 1 / shape.props.scale; return /* @__PURE__ */ jsx("g", { transform: `scale(${scaleFactor})`, children: /* @__PURE__ */ jsx( HighlightRenderer, { forceSolid, strokeWidth, shape, opacity: OVERLAY_OPACITY } ) }); } toBackgroundSvg(shape) { const strokeWidth = getStrokeWidth(shape); const forceSolid = strokeWidth < 1.5; const scaleFactor = 1 / shape.props.scale; return /* @__PURE__ */ jsx("g", { transform: `scale(${scaleFactor})`, children: /* @__PURE__ */ jsx( HighlightRenderer, { forceSolid, strokeWidth, shape, opacity: UNDERLAY_OPACITY } ) }); } 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: scaleX * x, y: scaleY * y, z }; }) }); } return { props: { segments: newSegments } }; } getInterpolatedProps(startShape, endShape, t) { return { ...(t > 0.5 ? endShape.props : startShape.props), ...endShape.props, segments: interpolateSegments(startShape.props.segments, endShape.props.segments, t), scale: lerp(startShape.props.scale, endShape.props.scale, t) }; } } function getShapeDot(point) { const r = 0.1; 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 getIndicatorDot(point, sw) { const r = sw / 2; 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 getHighlightStrokePoints(shape, strokeWidth, forceSolid) { const allPointsFromSegments = getPointsFromSegments(shape.props.segments); const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === "straight"; let sw = strokeWidth; if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) { sw += rng(shape.id)() * (strokeWidth / 6); } const options = getHighlightFreehandSettings({ strokeWidth: sw, showAsComplete }); const strokePoints = getStrokePoints(allPointsFromSegments, options); return { strokePoints, sw }; } function getStrokeWidth(shape) { return FONT_SIZES[shape.props.size] * 1.12 * shape.props.scale; } function getIsDot(shape) { return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2; } function HighlightRenderer({ strokeWidth, forceSolid, shape, opacity }) { const theme = useDefaultColorTheme(); const allPointsFromSegments = getPointsFromSegments(shape.props.segments); let sw = strokeWidth; if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) { sw += rng(shape.id)() * (sw / 6); } const options = getHighlightFreehandSettings({ strokeWidth: sw, showAsComplete: shape.props.isComplete || last(shape.props.segments)?.type === "straight" }); const strokePoints = getStrokePoints(allPointsFromSegments, options); const solidStrokePath = strokePoints.length > 1 ? getSvgPathFromStrokePoints(strokePoints, false) : getShapeDot(shape.props.segments[0].points[0]); const colorSpace = useColorSpace(); const color = theme[shape.props.color].highlight[colorSpace]; return /* @__PURE__ */ jsx( "path", { d: solidStrokePath, strokeLinecap: "round", fill: "none", pointerEvents: "all", stroke: color, strokeWidth: sw, opacity } ); } function useHighlightForceSolid(editor, shape) { return useValue( "forceSolid", () => { const sw = getStrokeWidth(shape); const zoomLevel = editor.getZoomLevel(); if (sw / zoomLevel < 1.5) { return true; } return false; }, [editor] ); } export { HighlightShapeUtil }; //# sourceMappingURL=HighlightShapeUtil.mjs.map