tldraw
Version:
A tiny little drawing editor.
275 lines (274 loc) • 8.48 kB
JavaScript
import { jsx } from "react/jsx-runtime";
import {
Circle2d,
Polygon2d,
SVGContainer,
ShapeUtil,
getColorValue,
highlightShapeMigrations,
highlightShapeProps,
last,
lerp,
rng,
useValue
} from "@tldraw/editor";
import { getHighlightFreehandSettings, getPointsFromDrawSegments } 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";
class HighlightShapeUtil extends ShapeUtil {
static type = "highlight";
static props = highlightShapeProps;
static migrations = highlightShapeMigrations;
options = {
maxPointsPerShape: 600,
underlayOpacity: 0.82,
overlayOpacity: 0.35
};
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,
scaleX: 1,
scaleY: 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: this.options.overlayOpacity
}
) });
}
backgroundComponent(shape) {
const forceSolid = useHighlightForceSolid(this.editor, shape);
const strokeWidth = getStrokeWidth(shape);
return /* @__PURE__ */ jsx(SVGContainer, { children: /* @__PURE__ */ jsx(
HighlightRenderer,
{
shape,
forceSolid,
strokeWidth,
opacity: this.options.underlayOpacity
}
) });
}
indicator(shape) {
const forceSolid = useHighlightForceSolid(this.editor, shape);
const strokeWidth = getStrokeWidth(shape);
const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, forceSolid);
const allPointsFromSegments = getPointsFromDrawSegments(
shape.props.segments,
shape.props.scaleX,
shape.props.scaleY
);
let strokePath;
if (strokePoints.length < 2) {
strokePath = getIndicatorDot(allPointsFromSegments[0], sw);
} else {
strokePath = getSvgPathFromStrokePoints(strokePoints, false);
}
return /* @__PURE__ */ jsx("path", { d: strokePath });
}
useLegacyIndicator() {
return false;
}
getIndicatorPath(shape) {
const strokeWidth = getStrokeWidth(shape);
const zoomLevel = this.editor.getEfficientZoomLevel();
const forceSolid = strokeWidth / zoomLevel < 1.5;
const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, forceSolid);
const allPointsFromSegments = getPointsFromDrawSegments(
shape.props.segments,
shape.props.scaleX,
shape.props.scaleY
);
let strokePath;
if (strokePoints.length < 2) {
strokePath = getIndicatorDot(allPointsFromSegments[0], sw);
} else {
strokePath = getSvgPathFromStrokePoints(strokePoints, false);
}
return new Path2D(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: this.options.overlayOpacity
}
) });
}
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: this.options.underlayOpacity
}
) });
}
onResize(shape, info) {
const newScaleX = info.scaleX * shape.props.scaleX;
const newScaleY = info.scaleY * shape.props.scaleY;
if (newScaleX === 0 || newScaleY === 0) return;
return {
props: {
scaleX: newScaleX,
scaleY: newScaleY
}
};
}
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 = getPointsFromDrawSegments(
shape.props.segments,
shape.props.scaleX,
shape.props.scaleY
);
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].path.length < 24;
}
function HighlightRenderer({
strokeWidth,
forceSolid,
shape,
opacity
}) {
const theme = useDefaultColorTheme();
const allPointsFromSegments = getPointsFromDrawSegments(
shape.props.segments,
shape.props.scaleX,
shape.props.scaleY
);
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(allPointsFromSegments[0]);
const colorSpace = useColorSpace();
const color = getColorValue(
theme,
shape.props.color,
colorSpace === "p3" ? "highlightP3" : "highlightSrgb"
);
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.getEfficientZoomLevel();
if (sw / zoomLevel < 1.5) {
return true;
}
return false;
},
[editor]
);
}
export {
HighlightShapeUtil
};
//# sourceMappingURL=HighlightShapeUtil.mjs.map