@yuzhouu/canvas-kit 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -0
- package/_vendor/canvas-core/bindings/BindingUtil.ts +92 -0
- package/_vendor/canvas-core/bindings/BindingUtilRegistry.ts +22 -0
- package/_vendor/canvas-core/bindings/arrow/arrowBindingHelpers.ts +52 -0
- package/_vendor/canvas-core/contextMenu.ts +85 -0
- package/_vendor/canvas-core/editor/Editor.ts +2614 -0
- package/_vendor/canvas-core/editor/HistoryManager.ts +83 -0
- package/_vendor/canvas-core/editor/StateNode.ts +162 -0
- package/_vendor/canvas-core/editor/camera.ts +6 -0
- package/_vendor/canvas-core/editor/clipboard.ts +478 -0
- package/_vendor/canvas-core/editor/commands.ts +833 -0
- package/_vendor/canvas-core/editor/edgeScroll.ts +37 -0
- package/_vendor/canvas-core/editor/editorComputedHelpers.ts +257 -0
- package/_vendor/canvas-core/editor/editorSelectionTransforms.ts +116 -0
- package/_vendor/canvas-core/editor/events.ts +93 -0
- package/_vendor/canvas-core/editor/index.ts +12 -0
- package/_vendor/canvas-core/editor/interactions.ts +101 -0
- package/_vendor/canvas-core/editor/internal.ts +22 -0
- package/_vendor/canvas-core/editor/managers/BindingManager.ts +262 -0
- package/_vendor/canvas-core/editor/managers/CameraManager.ts +147 -0
- package/_vendor/canvas-core/editor/managers/ClipboardManager.ts +32 -0
- package/_vendor/canvas-core/editor/managers/DocumentSnapshotManager.ts +256 -0
- package/_vendor/canvas-core/editor/managers/InputsManager.ts +319 -0
- package/_vendor/canvas-core/editor/managers/InteractionTransientManager.ts +218 -0
- package/_vendor/canvas-core/editor/managers/QueryManager.ts +837 -0
- package/_vendor/canvas-core/editor/managers/RenderWindowManager.ts +347 -0
- package/_vendor/canvas-core/editor/managers/SelectionLayoutManager.ts +196 -0
- package/_vendor/canvas-core/editor/managers/SelectionManager.ts +98 -0
- package/_vendor/canvas-core/editor/managers/SelectionOverlayManager.ts +308 -0
- package/_vendor/canvas-core/editor/managers/ShapeEditingManager.ts +226 -0
- package/_vendor/canvas-core/editor/managers/ShapeGeometryManager.ts +162 -0
- package/_vendor/canvas-core/editor/managers/ShapeHierarchyManager.ts +797 -0
- package/_vendor/canvas-core/editor/managers/SideEffectManager.ts +83 -0
- package/_vendor/canvas-core/editor/managers/SnapManager.ts +433 -0
- package/_vendor/canvas-core/editor/managers/SnapSnapshotBuilder.ts +430 -0
- package/_vendor/canvas-core/editor/managers/SpatialIndexManager.ts +354 -0
- package/_vendor/canvas-core/editor/managers/ToolManager.ts +81 -0
- package/_vendor/canvas-core/editor/managers/queryState.ts +583 -0
- package/_vendor/canvas-core/editor/managers/selectionGeometry.ts +287 -0
- package/_vendor/canvas-core/editor/managers/shapeOrder.ts +350 -0
- package/_vendor/canvas-core/editor/managers/shapeReparenting.ts +160 -0
- package/_vendor/canvas-core/editor/managers/snapCalculations.ts +412 -0
- package/_vendor/canvas-core/editor/managers/snapGeometry.ts +202 -0
- package/_vendor/canvas-core/editor/managers/snapIndicators.ts +403 -0
- package/_vendor/canvas-core/editor/managers/snapTypes.ts +122 -0
- package/_vendor/canvas-core/editor/queries/shapeTransforms.ts +171 -0
- package/_vendor/canvas-core/editor/renderWindow.ts +439 -0
- package/_vendor/canvas-core/editor/resizeGeometry.ts +654 -0
- package/_vendor/canvas-core/editor/selection.ts +489 -0
- package/_vendor/canvas-core/editor/selectionHandleInteractions.ts +43 -0
- package/_vendor/canvas-core/editor/selectionOverlay.ts +567 -0
- package/_vendor/canvas-core/exports/integrity.ts +174 -0
- package/_vendor/canvas-core/exports/svg.ts +494 -0
- package/_vendor/canvas-core/geometry/Box.ts +80 -0
- package/_vendor/canvas-core/geometry/Ellipse2d.ts +67 -0
- package/_vendor/canvas-core/geometry/Geometry2d.ts +52 -0
- package/_vendor/canvas-core/geometry/Mat.ts +92 -0
- package/_vendor/canvas-core/geometry/Polyline2d.ts +63 -0
- package/_vendor/canvas-core/geometry/Rectangle2d.ts +64 -0
- package/_vendor/canvas-core/geometry/Vec.ts +123 -0
- package/_vendor/canvas-core/geometry/index.ts +13 -0
- package/_vendor/canvas-core/geometry/segments.ts +65 -0
- package/_vendor/canvas-core/index.ts +391 -0
- package/_vendor/canvas-core/plugins/index.ts +5 -0
- package/_vendor/canvas-core/plugins/types.ts +53 -0
- package/_vendor/canvas-core/reactivity.ts +16 -0
- package/_vendor/canvas-core/schema/records.ts +221 -0
- package/_vendor/canvas-core/schema/snapshots.ts +527 -0
- package/_vendor/canvas-core/shapes/ShapeUtil.ts +516 -0
- package/_vendor/canvas-core/shapes/ShapeUtilRegistry.ts +26 -0
- package/_vendor/canvas-core/shapes/capabilities.ts +67 -0
- package/_vendor/canvas-core/shapes/types.ts +20 -0
- package/_vendor/canvas-core/toolbar.ts +148 -0
- package/_vendor/canvas-core/tools/hand/HandTool.ts +75 -0
- package/_vendor/canvas-core/tools/index.ts +2 -0
- package/_vendor/canvas-core/tools/select/SelectTool.ts +33 -0
- package/_vendor/canvas-core/tools/select/selectContainment.ts +104 -0
- package/_vendor/canvas-core/tools/select/selectToolHelpers.ts +801 -0
- package/_vendor/canvas-core/tools/select/states/BrushingState.ts +145 -0
- package/_vendor/canvas-core/tools/select/states/DraggingSelectionHandleInteractionState.ts +116 -0
- package/_vendor/canvas-core/tools/select/states/IdleState.ts +159 -0
- package/_vendor/canvas-core/tools/select/states/PointingCanvasState.ts +41 -0
- package/_vendor/canvas-core/tools/select/states/PointingResizeHandleState.ts +78 -0
- package/_vendor/canvas-core/tools/select/states/PointingRotateHandleState.ts +64 -0
- package/_vendor/canvas-core/tools/select/states/PointingSelectionHandleInteractionState.ts +34 -0
- package/_vendor/canvas-core/tools/select/states/PointingShapeState.ts +140 -0
- package/_vendor/canvas-core/tools/select/states/ResizingState.ts +171 -0
- package/_vendor/canvas-core/tools/select/states/RotatingState.ts +124 -0
- package/_vendor/canvas-core/tools/select/states/SelectStateNode.ts +128 -0
- package/_vendor/canvas-core/tools/select/states/TranslatingState.ts +139 -0
- package/_vendor/canvas-core/tools/select/states/index.ts +11 -0
- package/_vendor/canvas-plugin-api/feature.ts +204 -0
- package/_vendor/canvas-plugin-api/index.ts +142 -0
- package/_vendor/canvas-product-react/api/apiRegistry.ts +35 -0
- package/_vendor/canvas-product-react/api/canvasProductApi.ts +422 -0
- package/_vendor/canvas-product-react/api/index.ts +31 -0
- package/_vendor/canvas-product-react/capabilities/clipboard/clipboardService.ts +36 -0
- package/_vendor/canvas-product-react/capabilities/clipboard/systemClipboard.ts +245 -0
- package/_vendor/canvas-product-react/capabilities/context-menu/systemContextMenu.ts +396 -0
- package/_vendor/canvas-product-react/capabilities/context-menu/useContextMenuSurface.ts +59 -0
- package/_vendor/canvas-product-react/capabilities/export/useSvgExport.ts +156 -0
- package/_vendor/canvas-product-react/capabilities/history/useHistoryControls.ts +23 -0
- package/_vendor/canvas-product-react/capabilities/pages/usePageControls.ts +41 -0
- package/_vendor/canvas-product-react/capabilities/toolbar/systemToolbar.ts +490 -0
- package/_vendor/canvas-product-react/capabilities/toolbar/useToolbarSurfaces.ts +92 -0
- package/_vendor/canvas-product-react/capabilities/zoom/useZoomControls.ts +27 -0
- package/_vendor/canvas-product-react/index.ts +84 -0
- package/_vendor/canvas-product-react/persistence/index.ts +18 -0
- package/_vendor/canvas-product-react/persistence/localSnapshotPersistence.ts +294 -0
- package/_vendor/canvas-product-react/runtime/CanvasProductRoot.tsx +250 -0
- package/_vendor/canvas-product-react/runtime/productDefinition.ts +72 -0
- package/_vendor/canvas-product-react/runtime/productServices.ts +96 -0
- package/_vendor/canvas-react/components/CanvasView.tsx +796 -0
- package/_vendor/canvas-react/components/OverlayLayer.tsx +879 -0
- package/_vendor/canvas-react/components/PageSpace.tsx +34 -0
- package/_vendor/canvas-react/components/ShapeLayer.tsx +428 -0
- package/_vendor/canvas-react/components/index.ts +4 -0
- package/_vendor/canvas-react/hooks/useEditor.ts +11 -0
- package/_vendor/canvas-react/hooks/useValue.ts +14 -0
- package/_vendor/canvas-react/host/CanvasRendererRegistryContext.tsx +14 -0
- package/_vendor/canvas-react/host/contextMenuRegistry.ts +61 -0
- package/_vendor/canvas-react/host/domEventGuards.ts +61 -0
- package/_vendor/canvas-react/host/index.ts +38 -0
- package/_vendor/canvas-react/host/pluginHost.ts +635 -0
- package/_vendor/canvas-react/host/rendererRegistry.tsx +58 -0
- package/_vendor/canvas-react/host/toolbarRegistry.ts +100 -0
- package/_vendor/canvas-react/index.ts +3 -0
- package/_vendor/plugin-arrow/ArrowBindingUtil.ts +239 -0
- package/_vendor/plugin-arrow/ArrowShapeUtil.ts +311 -0
- package/_vendor/plugin-arrow/ArrowTool.ts +208 -0
- package/_vendor/plugin-arrow/arrowGeometry.ts +441 -0
- package/_vendor/plugin-arrow/arrowTerminalBindingResolver.ts +253 -0
- package/_vendor/plugin-arrow/arrowTerminalCommands.ts +230 -0
- package/_vendor/plugin-arrow/canvas.tsx +92 -0
- package/_vendor/plugin-arrow/core.ts +409 -0
- package/_vendor/plugin-arrow/feature.ts +20 -0
- package/_vendor/plugin-arrow/index.ts +28 -0
- package/_vendor/plugin-arrow/selectionHandleInteractions.ts +154 -0
- package/_vendor/plugin-arrow/types.ts +63 -0
- package/_vendor/plugin-draw/DrawShapeRenderer.tsx +60 -0
- package/_vendor/plugin-draw/DrawShapeUtil.ts +372 -0
- package/_vendor/plugin-draw/DrawTool.ts +444 -0
- package/_vendor/plugin-draw/canvas.tsx +24 -0
- package/_vendor/plugin-draw/core.ts +515 -0
- package/_vendor/plugin-draw/drawGeometry.ts +491 -0
- package/_vendor/plugin-draw/drawHitTest.ts +271 -0
- package/_vendor/plugin-draw/drawInterpolation.ts +250 -0
- package/_vendor/plugin-draw/drawPath.ts +550 -0
- package/_vendor/plugin-draw/drawSegments.ts +106 -0
- package/_vendor/plugin-draw/drawSelectionTransform.ts +67 -0
- package/_vendor/plugin-draw/drawShapeGeometry.ts +307 -0
- package/_vendor/plugin-draw/drawStrokeGeometry.ts +454 -0
- package/_vendor/plugin-draw/feature.ts +20 -0
- package/_vendor/plugin-draw/index.ts +31 -0
- package/_vendor/plugin-draw/types.ts +36 -0
- package/_vendor/plugin-eraser/EraserTool.ts +297 -0
- package/_vendor/plugin-eraser/canvas.tsx +616 -0
- package/_vendor/plugin-eraser/core.ts +49 -0
- package/_vendor/plugin-eraser/feature.ts +20 -0
- package/_vendor/plugin-eraser/index.ts +4 -0
- package/_vendor/plugin-frame/FrameShapeUtil.ts +173 -0
- package/_vendor/plugin-frame/FrameTool.ts +225 -0
- package/_vendor/plugin-frame/canvas.tsx +75 -0
- package/_vendor/plugin-frame/core.ts +534 -0
- package/_vendor/plugin-frame/feature.ts +20 -0
- package/_vendor/plugin-frame/index.ts +6 -0
- package/_vendor/plugin-geo/GeoShapeUtil.ts +463 -0
- package/_vendor/plugin-geo/GeoTools.ts +247 -0
- package/_vendor/plugin-geo/canvas.tsx +90 -0
- package/_vendor/plugin-geo/core.ts +495 -0
- package/_vendor/plugin-geo/feature.ts +20 -0
- package/_vendor/plugin-geo/index.ts +7 -0
- package/_vendor/plugin-geo/types.ts +27 -0
- package/_vendor/plugin-group/GroupShapeUtil.ts +59 -0
- package/_vendor/plugin-group/canvas.tsx +59 -0
- package/_vendor/plugin-group/core.ts +40 -0
- package/_vendor/plugin-group/feature.ts +20 -0
- package/_vendor/plugin-group/index.ts +5 -0
- package/_vendor/plugin-image/ImageShapeUtil.ts +95 -0
- package/_vendor/plugin-image/canvas.tsx +212 -0
- package/_vendor/plugin-image/core.ts +542 -0
- package/_vendor/plugin-image/crop.ts +770 -0
- package/_vendor/plugin-image/cropInteractions.ts +350 -0
- package/_vendor/plugin-image/cropSession.ts +38 -0
- package/_vendor/plugin-image/feature.ts +20 -0
- package/_vendor/plugin-image/index.ts +29 -0
- package/_vendor/plugin-image/types.ts +55 -0
- package/_vendor/plugin-image-product-react/ImageImportController.tsx +143 -0
- package/_vendor/plugin-image-product-react/imageAssetService.ts +52 -0
- package/_vendor/plugin-image-product-react/imageBlobStore.ts +315 -0
- package/_vendor/plugin-image-product-react/imageHostActions.ts +438 -0
- package/_vendor/plugin-image-product-react/imageProductApi.ts +88 -0
- package/_vendor/plugin-image-product-react/imageToolbarHostBridge.ts +48 -0
- package/_vendor/plugin-image-product-react/imageToolbarRuntime.ts +21 -0
- package/_vendor/plugin-image-product-react/index.ts +60 -0
- package/_vendor/plugin-image-product-react/snapshotGuards.ts +33 -0
- package/_vendor/plugin-image-product-react/svgImageAssetInliner.ts +54 -0
- package/_vendor/plugin-image-product-react/useImageImport.ts +64 -0
- package/_vendor/plugin-laser/LaserTool.ts +154 -0
- package/_vendor/plugin-laser/canvas.tsx +645 -0
- package/_vendor/plugin-laser/core.ts +49 -0
- package/_vendor/plugin-laser/feature.ts +20 -0
- package/_vendor/plugin-laser/index.ts +4 -0
- package/_vendor/plugin-text/ArrowLabelBindingUtil.ts +336 -0
- package/_vendor/plugin-text/GeoLabelBindingUtil.ts +391 -0
- package/_vendor/plugin-text/TextShapeSvgExporter.ts +101 -0
- package/_vendor/plugin-text/TextShapeUtil.ts +284 -0
- package/_vendor/plugin-text/TextTool.ts +228 -0
- package/_vendor/plugin-text/canvas.tsx +115 -0
- package/_vendor/plugin-text/core.ts +422 -0
- package/_vendor/plugin-text/feature.ts +20 -0
- package/_vendor/plugin-text/host/RichTextEditingBox.tsx +144 -0
- package/_vendor/plugin-text/host/RichTextToolbar.tsx +266 -0
- package/_vendor/plugin-text/host/TiptapTextEditor.tsx +191 -0
- package/_vendor/plugin-text/host/richTextRendering.tsx +49 -0
- package/_vendor/plugin-text/host/textDomSelection.ts +93 -0
- package/_vendor/plugin-text/host/textEditingBoxStyle.ts +36 -0
- package/_vendor/plugin-text/host/textEntryFocus.ts +307 -0
- package/_vendor/plugin-text/host/textFontLoader.ts +149 -0
- package/_vendor/plugin-text/host/textMeasurement.ts +258 -0
- package/_vendor/plugin-text/host/textMeasurementSync.ts +77 -0
- package/_vendor/plugin-text/host/textRangeSelection.ts +134 -0
- package/_vendor/plugin-text/host/textShapeRendererRuntime.tsx +89 -0
- package/_vendor/plugin-text/host/textShapeSync.ts +173 -0
- package/_vendor/plugin-text/host/tiptapTextEditorController.ts +148 -0
- package/_vendor/plugin-text/index.ts +18 -0
- package/_vendor/plugin-text/richTextUtils.ts +259 -0
- package/_vendor/plugin-text/textInteractions.ts +366 -0
- package/_vendor/plugin-text/textLabelHelpers.ts +235 -0
- package/_vendor/plugin-text/textShapeHelpers.ts +123 -0
- package/_vendor/plugin-text/textStyle.ts +96 -0
- package/_vendor/plugin-text/textTypes.ts +48 -0
- package/_vendor/signals/atom.ts +74 -0
- package/_vendor/signals/computed.ts +132 -0
- package/_vendor/signals/core.ts +130 -0
- package/_vendor/signals/effect.ts +117 -0
- package/_vendor/signals/index.ts +9 -0
- package/_vendor/signals/types.ts +17 -0
- package/_vendor/store/Store.ts +171 -0
- package/_vendor/store/diff.ts +229 -0
- package/_vendor/store/history.ts +177 -0
- package/_vendor/store/index.ts +32 -0
- package/_vendor/store/types.ts +37 -0
- package/api/canvasKitApi.d.ts +29 -0
- package/index.css +3 -0
- package/index.d.ts +16 -0
- package/index.mjs +1 -0
- package/internal-modules.d.ts +187 -0
- package/package.json +48 -0
- package/product/Shell.d.ts +19 -0
- package/product/chromeContext.d.ts +70 -0
- package/product/createCanvasKitEditor.d.ts +6 -0
- package/product/index.d.ts +7 -0
- package/product/persistence.d.ts +3 -0
- package/product/product.d.ts +22 -0
- package/product/useCanvasClipboardShortcuts.d.ts +6 -0
- package/product-iyBbEPIs.mjs +4 -0
- package/product.mjs +1 -0
- package/standard/extensions.d.ts +1 -0
- package/standard/index.d.ts +3 -0
- package/standard/standardApi.d.ts +91 -0
- package/standard-BmrxwqpJ.mjs +44 -0
- package/standard.mjs +1 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { memo, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { type LandCanvasPlugin } from "@land/canvas-plugin-api";
|
|
3
|
+
import { type LaserTrailState } from "@land/canvas-core";
|
|
4
|
+
import { PageSpace, useEditor, useValue } from "@land/canvas-react";
|
|
5
|
+
|
|
6
|
+
export function createLaserCanvasPlugin(): LandCanvasPlugin {
|
|
7
|
+
return {
|
|
8
|
+
id: "land.laser.react",
|
|
9
|
+
canvasComponents: [
|
|
10
|
+
{
|
|
11
|
+
id: "land.laser.trail-overlay",
|
|
12
|
+
component: LaserTrailOverlay,
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
setup() {
|
|
16
|
+
// Laser does not render shape records.
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LaserTrailOverlay = memo(function LaserTrailOverlay() {
|
|
22
|
+
const editor = useEditor();
|
|
23
|
+
const isTrailActive = useValue(editor.laserTrailActiveSignal);
|
|
24
|
+
|
|
25
|
+
if (!isTrailActive) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<PageSpace
|
|
31
|
+
dataUi="laser-trail-page-space"
|
|
32
|
+
className="pointer-events-none absolute inset-0"
|
|
33
|
+
>
|
|
34
|
+
<svg
|
|
35
|
+
width="100%"
|
|
36
|
+
height="100%"
|
|
37
|
+
style={{
|
|
38
|
+
display: "block",
|
|
39
|
+
width: "100%",
|
|
40
|
+
height: "100%",
|
|
41
|
+
overflow: "visible",
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<LaserTrailContent />
|
|
45
|
+
</svg>
|
|
46
|
+
</PageSpace>
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const LaserTrailContent = memo(function LaserTrailContent() {
|
|
51
|
+
const editor = useEditor();
|
|
52
|
+
const trail = useValue(editor.laserTrailSignal);
|
|
53
|
+
const zoom = useValue(editor.cameraZoomSignal);
|
|
54
|
+
const safeZoom = Math.max(zoom, 0.0001);
|
|
55
|
+
const clockTime = useAnimationClock();
|
|
56
|
+
const sampleState = useRef<LaserTrailSampleState>({
|
|
57
|
+
sessionId: 0,
|
|
58
|
+
samples: [],
|
|
59
|
+
sourceLength: 0,
|
|
60
|
+
});
|
|
61
|
+
const renderTime = Math.max(clockTime, getNowMs());
|
|
62
|
+
|
|
63
|
+
let trailPoints: TrailRenderPoint[] = [];
|
|
64
|
+
let headTime: number | undefined;
|
|
65
|
+
if (trail) {
|
|
66
|
+
syncLaserTrailSampleState(sampleState.current, trail, safeZoom, renderTime);
|
|
67
|
+
trailPoints = getRenderableLaserTrailPoints(
|
|
68
|
+
sampleState.current.samples,
|
|
69
|
+
renderTime,
|
|
70
|
+
);
|
|
71
|
+
headTime =
|
|
72
|
+
sampleState.current.samples[sampleState.current.samples.length - 1]?.time;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isEnding = trail?.phase === "ending";
|
|
76
|
+
const exhausted = trailPoints.length === 0;
|
|
77
|
+
const endingSessionId = isEnding && exhausted ? trail.sessionId : null;
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (endingSessionId !== null) {
|
|
81
|
+
editor.clearLaserTrail(endingSessionId);
|
|
82
|
+
}
|
|
83
|
+
}, [editor, endingSessionId]);
|
|
84
|
+
|
|
85
|
+
if (!trail || exhausted) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fadeOpacity =
|
|
90
|
+
isEnding && headTime !== undefined
|
|
91
|
+
? clamp(1 - (renderTime - headTime) / LASER_FADE_OUT_DURATION, 0, 1)
|
|
92
|
+
: 1;
|
|
93
|
+
|
|
94
|
+
const path = getLaserScribblePath(
|
|
95
|
+
trailPoints,
|
|
96
|
+
LASER_SCRIBBLE_SIZE / safeZoom,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!path) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<g data-ui="laser-trail-overlay" style={{ opacity: fadeOpacity }}>
|
|
105
|
+
<path
|
|
106
|
+
data-ui="laser-trail-glow"
|
|
107
|
+
d={path}
|
|
108
|
+
fill={LASER_GLOW_FILL}
|
|
109
|
+
style={{ filter: `blur(${LASER_GLOW_BLUR / safeZoom}px)` }}
|
|
110
|
+
/>
|
|
111
|
+
<path data-ui="laser-trail-beam" d={path} fill={LASER_BEAM_FILL} />
|
|
112
|
+
</g>
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
interface TrailRenderPoint {
|
|
117
|
+
x: number;
|
|
118
|
+
y: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface LaserTrailTimedPoint extends TrailRenderPoint {
|
|
122
|
+
time: number;
|
|
123
|
+
velocity: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface LaserTrailSampleState {
|
|
127
|
+
sessionId: number;
|
|
128
|
+
samples: LaserTrailTimedPoint[];
|
|
129
|
+
sourceLength: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface LaserScribbleSample {
|
|
133
|
+
point: TrailRenderPoint;
|
|
134
|
+
direction: TrailRenderPoint;
|
|
135
|
+
radius: number;
|
|
136
|
+
runningLength: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const LASER_BEAM_FILL = "rgb(255, 38, 38)";
|
|
140
|
+
const LASER_GLOW_FILL = "rgba(255, 48, 48, 0.55)";
|
|
141
|
+
const LASER_GLOW_BLUR = 4;
|
|
142
|
+
const LASER_SCRIBBLE_MAX_POINTS = 64;
|
|
143
|
+
const LASER_SCRIBBLE_MIN_POINT_DISTANCE = 1;
|
|
144
|
+
const LASER_SCRIBBLE_SIZE = 12;
|
|
145
|
+
const LASER_SCRIBBLE_STREAMLINE = 0.32;
|
|
146
|
+
const LASER_SCRIBBLE_CAP_STEPS = 8;
|
|
147
|
+
const LASER_SCRIBBLE_DOT_STEPS = 13;
|
|
148
|
+
const LASER_SCRIBBLE_INITIAL_SAMPLE_INTERVAL = 16;
|
|
149
|
+
const LASER_SCRIBBLE_MIN_SAMPLE_INTERVAL = 8;
|
|
150
|
+
const LASER_SCRIBBLE_BASE_LIFETIME = 600;
|
|
151
|
+
const LASER_SCRIBBLE_SPEED_LIFETIME = 300;
|
|
152
|
+
const LASER_SCRIBBLE_MAX_LIFETIME = 1400;
|
|
153
|
+
const LASER_FADE_OUT_DURATION = 400;
|
|
154
|
+
|
|
155
|
+
function useAnimationClock(): number {
|
|
156
|
+
const [time, setTime] = useState(() => getNowMs());
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (typeof window === "undefined") {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let frame = 0;
|
|
164
|
+
const tick = (nextTime: number) => {
|
|
165
|
+
setTime(nextTime);
|
|
166
|
+
frame = window.requestAnimationFrame(tick);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
frame = window.requestAnimationFrame(tick);
|
|
170
|
+
return () => {
|
|
171
|
+
window.cancelAnimationFrame(frame);
|
|
172
|
+
};
|
|
173
|
+
}, []);
|
|
174
|
+
|
|
175
|
+
return time;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getNowMs(): number {
|
|
179
|
+
return typeof performance === "undefined" ? Date.now() : performance.now();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function syncLaserTrailSampleState(
|
|
183
|
+
state: LaserTrailSampleState,
|
|
184
|
+
trail: LaserTrailState,
|
|
185
|
+
zoom: number,
|
|
186
|
+
now: number,
|
|
187
|
+
): void {
|
|
188
|
+
if (trail.sessionId !== state.sessionId) {
|
|
189
|
+
state.sessionId = trail.sessionId;
|
|
190
|
+
state.samples = [];
|
|
191
|
+
state.sourceLength = 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const points = trail.points;
|
|
195
|
+
pruneLaserTrailSamples(state, now);
|
|
196
|
+
if (points.length < state.sourceLength) {
|
|
197
|
+
state.samples = [];
|
|
198
|
+
state.sourceLength = 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (points.length === state.sourceLength) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const firstNewIndex = state.sourceLength;
|
|
206
|
+
const newPointCount = points.length - firstNewIndex;
|
|
207
|
+
const previousTime =
|
|
208
|
+
state.samples[state.samples.length - 1]?.time ??
|
|
209
|
+
now - LASER_SCRIBBLE_INITIAL_SAMPLE_INTERVAL * newPointCount;
|
|
210
|
+
for (let i = firstNewIndex; i < points.length; i++) {
|
|
211
|
+
const point = points[i];
|
|
212
|
+
const progress = (i - firstNewIndex + 1) / newPointCount;
|
|
213
|
+
appendLaserTrailSample(
|
|
214
|
+
state,
|
|
215
|
+
{ x: point.x, y: point.y },
|
|
216
|
+
zoom,
|
|
217
|
+
lerpNumber(previousTime, now, progress),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
state.sourceLength = points.length;
|
|
222
|
+
pruneLaserTrailSamples(state, now);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function appendLaserTrailSample(
|
|
226
|
+
state: LaserTrailSampleState,
|
|
227
|
+
point: TrailRenderPoint,
|
|
228
|
+
zoom: number,
|
|
229
|
+
time: number,
|
|
230
|
+
): void {
|
|
231
|
+
const previous = state.samples[state.samples.length - 1];
|
|
232
|
+
if (!previous) {
|
|
233
|
+
state.samples.push({ ...point, time, velocity: 0 });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const minDistance = LASER_SCRIBBLE_MIN_POINT_DISTANCE / zoom;
|
|
238
|
+
const distance = getTrailPointDistance(previous, point);
|
|
239
|
+
if (distance < minDistance) {
|
|
240
|
+
state.samples[state.samples.length - 1] = {
|
|
241
|
+
...previous,
|
|
242
|
+
...point,
|
|
243
|
+
time,
|
|
244
|
+
};
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const elapsed = Math.max(
|
|
249
|
+
LASER_SCRIBBLE_MIN_SAMPLE_INTERVAL,
|
|
250
|
+
time - previous.time,
|
|
251
|
+
);
|
|
252
|
+
const screenVelocity = (distance * zoom) / elapsed;
|
|
253
|
+
state.samples.push({
|
|
254
|
+
...point,
|
|
255
|
+
time,
|
|
256
|
+
velocity: lerpNumber(previous.velocity, screenVelocity, 0.68),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function pruneLaserTrailSamples(
|
|
261
|
+
state: LaserTrailSampleState,
|
|
262
|
+
now: number,
|
|
263
|
+
): void {
|
|
264
|
+
state.samples = getActiveLaserTrailSamples(state.samples, now);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getRenderableLaserTrailPoints(
|
|
268
|
+
samples: readonly LaserTrailTimedPoint[],
|
|
269
|
+
now: number,
|
|
270
|
+
): TrailRenderPoint[] {
|
|
271
|
+
const activeSamples = getActiveLaserTrailSamples(samples, now);
|
|
272
|
+
const activePoints =
|
|
273
|
+
activeSamples.length > LASER_SCRIBBLE_MAX_POINTS
|
|
274
|
+
? activeSamples.slice(-LASER_SCRIBBLE_MAX_POINTS)
|
|
275
|
+
: activeSamples;
|
|
276
|
+
return getStreamlinedTrailPoints(activePoints, LASER_SCRIBBLE_STREAMLINE);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getActiveLaserTrailSamples(
|
|
280
|
+
samples: readonly LaserTrailTimedPoint[],
|
|
281
|
+
now: number,
|
|
282
|
+
): LaserTrailTimedPoint[] {
|
|
283
|
+
const head = samples[samples.length - 1];
|
|
284
|
+
if (!head) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const cutoffTime = now - getLaserTrailSampleLifetime(head);
|
|
289
|
+
if (head.time < cutoffTime) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const firstActiveIndex = samples.findIndex(
|
|
294
|
+
(sample) => sample.time >= cutoffTime,
|
|
295
|
+
);
|
|
296
|
+
if (firstActiveIndex === -1) {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const activeSamples = samples.slice(firstActiveIndex);
|
|
301
|
+
const previous = samples[firstActiveIndex - 1];
|
|
302
|
+
const first = activeSamples[0];
|
|
303
|
+
if (previous && first.time > cutoffTime) {
|
|
304
|
+
const t = clamp(
|
|
305
|
+
(cutoffTime - previous.time) / (first.time - previous.time),
|
|
306
|
+
0,
|
|
307
|
+
1,
|
|
308
|
+
);
|
|
309
|
+
return [lerpLaserTrailSample(previous, first, t), ...activeSamples];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return activeSamples;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function lerpLaserTrailSample(
|
|
316
|
+
a: LaserTrailTimedPoint,
|
|
317
|
+
b: LaserTrailTimedPoint,
|
|
318
|
+
t: number,
|
|
319
|
+
): LaserTrailTimedPoint {
|
|
320
|
+
return {
|
|
321
|
+
...lerpTrailPoint(a, b, t),
|
|
322
|
+
time: lerpNumber(a.time, b.time, t),
|
|
323
|
+
velocity: lerpNumber(a.velocity, b.velocity, t),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function getLaserTrailSampleLifetime(sample: LaserTrailTimedPoint): number {
|
|
328
|
+
return clamp(
|
|
329
|
+
LASER_SCRIBBLE_BASE_LIFETIME +
|
|
330
|
+
sample.velocity * LASER_SCRIBBLE_SPEED_LIFETIME,
|
|
331
|
+
LASER_SCRIBBLE_BASE_LIFETIME,
|
|
332
|
+
LASER_SCRIBBLE_MAX_LIFETIME,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function getStreamlinedTrailPoints(
|
|
337
|
+
points: readonly TrailRenderPoint[],
|
|
338
|
+
streamline: number,
|
|
339
|
+
): TrailRenderPoint[] {
|
|
340
|
+
if (points.length <= 1) {
|
|
341
|
+
return [...points];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const t = 0.15 + (1 - streamline) * 0.85;
|
|
345
|
+
const result: TrailRenderPoint[] = [points[0]];
|
|
346
|
+
for (let i = 1; i < points.length; i++) {
|
|
347
|
+
result.push(lerpTrailPoint(result[result.length - 1], points[i], t));
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getLaserScribblePath(
|
|
353
|
+
points: readonly TrailRenderPoint[],
|
|
354
|
+
size: number,
|
|
355
|
+
): string | null {
|
|
356
|
+
if (points.length === 0 || size <= 0) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (points.length === 1) {
|
|
361
|
+
return getCircleTrailPath(points[0], size / 2);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const samples = getLaserScribbleSamples(points, size);
|
|
365
|
+
const outline = getLaserScribbleOutline(samples);
|
|
366
|
+
return getSmoothClosedTrailPathFromPoints(outline);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function getLaserScribbleSamples(
|
|
370
|
+
points: readonly TrailRenderPoint[],
|
|
371
|
+
size: number,
|
|
372
|
+
): LaserScribbleSample[] {
|
|
373
|
+
const runningLengths = getTrailRunningLengths(points);
|
|
374
|
+
const totalLength = runningLengths[runningLengths.length - 1] ?? 0;
|
|
375
|
+
const taperLength = Math.max(size, totalLength);
|
|
376
|
+
|
|
377
|
+
return points.map((point, index) => {
|
|
378
|
+
const runningLength = runningLengths[index] ?? 0;
|
|
379
|
+
const taper = clamp(runningLength / taperLength, 0, 1);
|
|
380
|
+
return {
|
|
381
|
+
point,
|
|
382
|
+
direction: getTrailPointDirection(points, index),
|
|
383
|
+
radius: (size / 2) * taper,
|
|
384
|
+
runningLength,
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function getLaserScribbleOutline(
|
|
390
|
+
samples: readonly LaserScribbleSample[],
|
|
391
|
+
): TrailRenderPoint[] {
|
|
392
|
+
if (samples.length === 0) {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (samples.length === 1) {
|
|
397
|
+
return getCircleTrailPoints(
|
|
398
|
+
samples[0].point,
|
|
399
|
+
Math.max(samples[0].radius, 0.01),
|
|
400
|
+
LASER_SCRIBBLE_DOT_STEPS,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const leftPoints: TrailRenderPoint[] = [];
|
|
405
|
+
const rightPoints: TrailRenderPoint[] = [];
|
|
406
|
+
|
|
407
|
+
for (const sample of samples) {
|
|
408
|
+
const normal = getTrailPointPerpendicular(sample.direction);
|
|
409
|
+
appendDistinctTrailPoint(
|
|
410
|
+
leftPoints,
|
|
411
|
+
subtractTrailPoints(sample.point, multiplyTrailPoint(normal, sample.radius)),
|
|
412
|
+
);
|
|
413
|
+
appendDistinctTrailPoint(
|
|
414
|
+
rightPoints,
|
|
415
|
+
addTrailPoints(sample.point, multiplyTrailPoint(normal, sample.radius)),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const outline: TrailRenderPoint[] = [];
|
|
420
|
+
for (const point of leftPoints) {
|
|
421
|
+
appendDistinctTrailPoint(outline, point);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const lastSample = samples[samples.length - 1];
|
|
425
|
+
const lastNormal = getTrailPointPerpendicular(lastSample.direction);
|
|
426
|
+
const endCap = getRoundEndCapTrailPoints({
|
|
427
|
+
center: lastSample.point,
|
|
428
|
+
direction: lastSample.direction,
|
|
429
|
+
normal: lastNormal,
|
|
430
|
+
radius: lastSample.radius,
|
|
431
|
+
});
|
|
432
|
+
for (const point of endCap) {
|
|
433
|
+
appendDistinctTrailPoint(outline, point);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for (let i = rightPoints.length - 1; i >= 0; i--) {
|
|
437
|
+
appendDistinctTrailPoint(outline, rightPoints[i]);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return outline;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function getTrailRunningLengths(
|
|
444
|
+
points: readonly TrailRenderPoint[],
|
|
445
|
+
): number[] {
|
|
446
|
+
const runningLengths = [0];
|
|
447
|
+
for (let i = 1; i < points.length; i++) {
|
|
448
|
+
runningLengths.push(
|
|
449
|
+
runningLengths[i - 1] + getTrailPointDistance(points[i - 1], points[i]),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
return runningLengths;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function getTrailPointDirection(
|
|
456
|
+
points: readonly TrailRenderPoint[],
|
|
457
|
+
index: number,
|
|
458
|
+
): TrailRenderPoint {
|
|
459
|
+
if (points.length <= 1) {
|
|
460
|
+
return { x: 1, y: 0 };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const previous = points[index - 1] ?? points[index];
|
|
464
|
+
const next = points[index + 1] ?? points[index];
|
|
465
|
+
const direction =
|
|
466
|
+
index === 0
|
|
467
|
+
? subtractTrailPoints(next, points[index])
|
|
468
|
+
: index === points.length - 1
|
|
469
|
+
? subtractTrailPoints(points[index], previous)
|
|
470
|
+
: subtractTrailPoints(next, previous);
|
|
471
|
+
|
|
472
|
+
return normalizeTrailPoint(direction);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function getRoundEndCapTrailPoints({
|
|
476
|
+
center,
|
|
477
|
+
direction,
|
|
478
|
+
normal,
|
|
479
|
+
radius,
|
|
480
|
+
}: {
|
|
481
|
+
center: TrailRenderPoint;
|
|
482
|
+
direction: TrailRenderPoint;
|
|
483
|
+
normal: TrailRenderPoint;
|
|
484
|
+
radius: number;
|
|
485
|
+
}): TrailRenderPoint[] {
|
|
486
|
+
if (radius <= 0) {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const points: TrailRenderPoint[] = [];
|
|
491
|
+
for (let step = 1; step < LASER_SCRIBBLE_CAP_STEPS; step++) {
|
|
492
|
+
const t = step / LASER_SCRIBBLE_CAP_STEPS;
|
|
493
|
+
points.push(
|
|
494
|
+
addTrailPoints(
|
|
495
|
+
center,
|
|
496
|
+
addTrailPoints(
|
|
497
|
+
multiplyTrailPoint(direction, Math.sin(Math.PI * t) * radius),
|
|
498
|
+
multiplyTrailPoint(normal, -Math.cos(Math.PI * t) * radius),
|
|
499
|
+
),
|
|
500
|
+
),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
return points;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function getCircleTrailPath(center: TrailRenderPoint, radius: number): string {
|
|
507
|
+
const points = getCircleTrailPoints(center, radius, LASER_SCRIBBLE_DOT_STEPS);
|
|
508
|
+
return getSmoothClosedTrailPathFromPoints(points) ?? "";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function getCircleTrailPoints(
|
|
512
|
+
center: TrailRenderPoint,
|
|
513
|
+
radius: number,
|
|
514
|
+
steps: number,
|
|
515
|
+
): TrailRenderPoint[] {
|
|
516
|
+
const points: TrailRenderPoint[] = [];
|
|
517
|
+
for (let step = 0; step < steps; step++) {
|
|
518
|
+
const angle = (Math.PI * 2 * step) / steps;
|
|
519
|
+
points.push({
|
|
520
|
+
x: center.x + Math.cos(angle) * radius,
|
|
521
|
+
y: center.y + Math.sin(angle) * radius,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return points;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function getTrailPointDistance(a: TrailRenderPoint, b: TrailRenderPoint): number {
|
|
528
|
+
return Math.sqrt(getTrailPointDistanceSquared(a, b));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function getTrailPointDistanceSquared(
|
|
532
|
+
a: TrailRenderPoint,
|
|
533
|
+
b: TrailRenderPoint,
|
|
534
|
+
): number {
|
|
535
|
+
const dx = a.x - b.x;
|
|
536
|
+
const dy = a.y - b.y;
|
|
537
|
+
return dx * dx + dy * dy;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function getTrailPointPerpendicular(point: TrailRenderPoint): TrailRenderPoint {
|
|
541
|
+
return { x: -point.y, y: point.x };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function normalizeTrailPoint(point: TrailRenderPoint): TrailRenderPoint {
|
|
545
|
+
const length = Math.sqrt(point.x * point.x + point.y * point.y);
|
|
546
|
+
if (length <= 0) {
|
|
547
|
+
return { x: 1, y: 0 };
|
|
548
|
+
}
|
|
549
|
+
return { x: point.x / length, y: point.y / length };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function addTrailPoints(
|
|
553
|
+
a: TrailRenderPoint,
|
|
554
|
+
b: TrailRenderPoint,
|
|
555
|
+
): TrailRenderPoint {
|
|
556
|
+
return { x: a.x + b.x, y: a.y + b.y };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function subtractTrailPoints(
|
|
560
|
+
a: TrailRenderPoint,
|
|
561
|
+
b: TrailRenderPoint,
|
|
562
|
+
): TrailRenderPoint {
|
|
563
|
+
return { x: a.x - b.x, y: a.y - b.y };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function multiplyTrailPoint(
|
|
567
|
+
point: TrailRenderPoint,
|
|
568
|
+
scale: number,
|
|
569
|
+
): TrailRenderPoint {
|
|
570
|
+
return { x: point.x * scale, y: point.y * scale };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function lerpTrailPoint(
|
|
574
|
+
a: TrailRenderPoint,
|
|
575
|
+
b: TrailRenderPoint,
|
|
576
|
+
t: number,
|
|
577
|
+
): TrailRenderPoint {
|
|
578
|
+
return {
|
|
579
|
+
x: lerpNumber(a.x, b.x, t),
|
|
580
|
+
y: lerpNumber(a.y, b.y, t),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function lerpNumber(a: number, b: number, t: number): number {
|
|
585
|
+
return a + (b - a) * t;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function clamp(value: number, min: number, max: number): number {
|
|
589
|
+
return Math.max(min, Math.min(max, value));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function appendDistinctTrailPoint(
|
|
593
|
+
points: TrailRenderPoint[],
|
|
594
|
+
point: TrailRenderPoint,
|
|
595
|
+
): void {
|
|
596
|
+
const previous = points[points.length - 1];
|
|
597
|
+
if (!previous || getTrailPointDistanceSquared(previous, point) > 0.04) {
|
|
598
|
+
points.push(point);
|
|
599
|
+
} else {
|
|
600
|
+
points[points.length - 1] = point;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function getSmoothClosedTrailPathFromPoints(
|
|
605
|
+
points: readonly TrailRenderPoint[],
|
|
606
|
+
): string | null {
|
|
607
|
+
if (points.length < 2) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (points.length === 2) {
|
|
612
|
+
return `M ${formatTrailSvgPoint(points[0])} L ${formatTrailSvgPoint(points[1])} Z`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const commands = [
|
|
616
|
+
`M ${formatTrailSvgPoint(getTrailMidpoint(points[0], points[1]))}`,
|
|
617
|
+
];
|
|
618
|
+
for (let i = 1; i < points.length; i++) {
|
|
619
|
+
const current = points[i];
|
|
620
|
+
const next = points[(i + 1) % points.length];
|
|
621
|
+
commands.push(
|
|
622
|
+
`Q ${formatTrailSvgPoint(current)} ${formatTrailSvgPoint(getTrailMidpoint(current, next))}`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
commands.push("Z");
|
|
626
|
+
|
|
627
|
+
return commands.join(" ");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function getTrailMidpoint(
|
|
631
|
+
a: TrailRenderPoint,
|
|
632
|
+
b: TrailRenderPoint,
|
|
633
|
+
): TrailRenderPoint {
|
|
634
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function formatTrailSvgPoint(point: TrailRenderPoint): string {
|
|
638
|
+
return `${formatTrailSvgNumber(point.x)},${formatTrailSvgNumber(point.y)}`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function formatTrailSvgNumber(value: number): string {
|
|
642
|
+
return Number.isInteger(value) ? `${value}` : value.toFixed(3);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SET_ACTIVE_TOOL_TOOLBAR_ACTION_ID,
|
|
3
|
+
type LandCorePlugin,
|
|
4
|
+
type ToolbarContribution,
|
|
5
|
+
} from "@land/canvas-core";
|
|
6
|
+
import { LaserTool } from "./LaserTool";
|
|
7
|
+
|
|
8
|
+
export function createLaserCorePlugin(): LandCorePlugin {
|
|
9
|
+
return {
|
|
10
|
+
id: "land.laser",
|
|
11
|
+
toolbarContributions: [createLaserToolToolbarContribution()],
|
|
12
|
+
setup(editor) {
|
|
13
|
+
editor.tools.register(
|
|
14
|
+
"laser",
|
|
15
|
+
(currentEditor) => new LaserTool(currentEditor),
|
|
16
|
+
{ shortcut: "l" },
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createLaserToolToolbarContribution(): ToolbarContribution {
|
|
23
|
+
return {
|
|
24
|
+
id: "laser.tool-toolbar",
|
|
25
|
+
surface: "tool",
|
|
26
|
+
placement: "chrome",
|
|
27
|
+
order: 55,
|
|
28
|
+
actionIds: [SET_ACTIVE_TOOL_TOOLBAR_ACTION_ID],
|
|
29
|
+
getGroups() {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
id: "laser.tools",
|
|
33
|
+
items: [
|
|
34
|
+
{
|
|
35
|
+
kind: "button",
|
|
36
|
+
id: "laser.tool.laser",
|
|
37
|
+
actionId: SET_ACTIVE_TOOL_TOOLBAR_ACTION_ID,
|
|
38
|
+
value: "laser",
|
|
39
|
+
label: "Laser",
|
|
40
|
+
shortcut: "L",
|
|
41
|
+
icon: "wand",
|
|
42
|
+
dataUi: "laser-tool-button",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineLandExtension,
|
|
3
|
+
type LandFeatureBundle,
|
|
4
|
+
} from "@land/canvas-plugin-api";
|
|
5
|
+
import { createLaserCanvasPlugin } from "./canvas";
|
|
6
|
+
import { createLaserCorePlugin } from "./core";
|
|
7
|
+
|
|
8
|
+
export function createLaserFeatureBundle(): LandFeatureBundle {
|
|
9
|
+
return {
|
|
10
|
+
id: "land.laser.bundle",
|
|
11
|
+
corePlugins: [createLaserCorePlugin()],
|
|
12
|
+
canvasPlugins: [createLaserCanvasPlugin()],
|
|
13
|
+
shapeTypes: [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Laser = defineLandExtension({
|
|
18
|
+
name: "laser",
|
|
19
|
+
createFeature: createLaserFeatureBundle,
|
|
20
|
+
});
|