@markup-canvas/core 1.0.0
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 +245 -0
- package/dist/index.d.ts +2 -0
- package/dist/lib/MarkupCanvas.d.ts +78 -0
- package/dist/lib/canvas/calcVisibleArea.d.ts +10 -0
- package/dist/lib/canvas/checkContainerDimensions.d.ts +1 -0
- package/dist/lib/canvas/config.d.ts +2 -0
- package/dist/lib/canvas/createCanvas.d.ts +2 -0
- package/dist/lib/canvas/createCanvasLayers.d.ts +6 -0
- package/dist/lib/canvas/getCanvasBounds.d.ts +2 -0
- package/dist/lib/canvas/getCanvasMethods.d.ts +12 -0
- package/dist/lib/canvas/getEmptyBounds.d.ts +2 -0
- package/dist/lib/canvas/index.d.ts +3 -0
- package/dist/lib/canvas/moveExistingContent.d.ts +1 -0
- package/dist/lib/canvas/setupCanvasContainer.d.ts +1 -0
- package/dist/lib/canvas/setupContentLayer.d.ts +1 -0
- package/dist/lib/canvas/setupTransformLayer.d.ts +2 -0
- package/dist/lib/config/constants.d.ts +2 -0
- package/dist/lib/config/createMarkupCanvasConfig.d.ts +2 -0
- package/dist/lib/constants.d.ts +7 -0
- package/dist/lib/events/EventEmitter.d.ts +7 -0
- package/dist/lib/events/constants.d.ts +7 -0
- package/dist/lib/events/index.d.ts +6 -0
- package/dist/lib/events/keyboard/handleKeyDown.d.ts +4 -0
- package/dist/lib/events/keyboard/handleKeyUp.d.ts +6 -0
- package/dist/lib/events/keyboard/setupKeyboardEvents.d.ts +2 -0
- package/dist/lib/events/keyboard/setupKeyboardNavigation.d.ts +2 -0
- package/dist/lib/events/mouse/handleClickToZoom.d.ts +2 -0
- package/dist/lib/events/mouse/handleMouseDown.d.ts +11 -0
- package/dist/lib/events/mouse/handleMouseLeave.d.ts +5 -0
- package/dist/lib/events/mouse/handleMouseMove.d.ts +7 -0
- package/dist/lib/events/mouse/handleMouseUp.d.ts +7 -0
- package/dist/lib/events/mouse/setupMouseDrag.d.ts +4 -0
- package/dist/lib/events/mouse/setupMouseEvents.d.ts +4 -0
- package/dist/lib/events/touch/getTouchCenter.d.ts +4 -0
- package/dist/lib/events/touch/getTouchDistance.d.ts +1 -0
- package/dist/lib/events/touch/handleTouchEnd.d.ts +2 -0
- package/dist/lib/events/touch/handleTouchMove.d.ts +2 -0
- package/dist/lib/events/touch/handleTouchStart.d.ts +2 -0
- package/dist/lib/events/touch/setupTouchEvents.d.ts +2 -0
- package/dist/lib/events/trackpad/createTrackpadPanHandler.d.ts +4 -0
- package/dist/lib/events/trackpad/detectTrackpadGesture.d.ts +2 -0
- package/dist/lib/events/utils/getAdaptiveZoomSpeed.d.ts +2 -0
- package/dist/lib/events/utils/resetClickState.d.ts +4 -0
- package/dist/lib/events/utils/resetDragState.d.ts +5 -0
- package/dist/lib/events/utils/updateCursor.d.ts +2 -0
- package/dist/lib/events/wheel/handleWheel.d.ts +2 -0
- package/dist/lib/events/wheel/setupWheelEvents.d.ts +2 -0
- package/dist/lib/events/wheel/setupWheelHandler.d.ts +2 -0
- package/dist/lib/helpers/index.d.ts +6 -0
- package/dist/lib/helpers/withClampedZoom.d.ts +2 -0
- package/dist/lib/helpers/withDebounce.d.ts +1 -0
- package/dist/lib/helpers/withFeatureEnabled.d.ts +2 -0
- package/dist/lib/helpers/withRAF.d.ts +4 -0
- package/dist/lib/helpers/withRulerCheck.d.ts +18 -0
- package/dist/lib/helpers/withRulerOffset.d.ts +3 -0
- package/dist/lib/matrix/canvasToContent.d.ts +2 -0
- package/dist/lib/matrix/clampZoom.d.ts +2 -0
- package/dist/lib/matrix/contentToCanvas.d.ts +2 -0
- package/dist/lib/matrix/createMatrix.d.ts +1 -0
- package/dist/lib/matrix/createMatrixString.d.ts +1 -0
- package/dist/lib/matrix/getZoomToMouseTransform.d.ts +2 -0
- package/dist/lib/matrix/index.d.ts +5 -0
- package/dist/lib/rulers/RulerElements.d.ts +6 -0
- package/dist/lib/rulers/constants.d.ts +19 -0
- package/dist/lib/rulers/createCornerBox.d.ts +2 -0
- package/dist/lib/rulers/createGridOverlay.d.ts +2 -0
- package/dist/lib/rulers/createHorizontalRuler.d.ts +2 -0
- package/dist/lib/rulers/createRulerElements.d.ts +3 -0
- package/dist/lib/rulers/createRulers.d.ts +2 -0
- package/dist/lib/rulers/createVerticalRuler.d.ts +2 -0
- package/dist/lib/rulers/index.d.ts +2 -0
- package/dist/lib/rulers/setupRulerEvents.d.ts +2 -0
- package/dist/lib/rulers/ticks/calculateTickSpacing.d.ts +1 -0
- package/dist/lib/rulers/ticks/createHorizontalTick.d.ts +2 -0
- package/dist/lib/rulers/ticks/createVerticalTick.d.ts +2 -0
- package/dist/lib/rulers/ticks/index.d.ts +3 -0
- package/dist/lib/rulers/updateGrid.d.ts +1 -0
- package/dist/lib/rulers/updateHorizontalRuler.d.ts +2 -0
- package/dist/lib/rulers/updateRulers.d.ts +2 -0
- package/dist/lib/rulers/updateVerticalRuler.d.ts +2 -0
- package/dist/lib/transform/applyTransform.d.ts +1 -0
- package/dist/lib/transform/applyZoomToCanvas.d.ts +2 -0
- package/dist/lib/transform/hardware-acceleration.d.ts +1 -0
- package/dist/lib/transform/index.d.ts +2 -0
- package/dist/lib/transition/disableTransition.d.ts +7 -0
- package/dist/lib/transition/enableTransition.d.ts +7 -0
- package/dist/lib/transition/index.d.ts +3 -0
- package/dist/lib/transition/withTransition.d.ts +2 -0
- package/dist/markup-canvas.cjs.js +2000 -0
- package/dist/markup-canvas.esm.js +1995 -0
- package/dist/markup-canvas.umd.js +2003 -0
- package/dist/markup-canvas.umd.min.js +1 -0
- package/dist/types/canvas.d.ts +86 -0
- package/dist/types/config.d.ts +38 -0
- package/dist/types/events.d.ts +33 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/matrix.d.ts +17 -0
- package/dist/types/rulers.d.ts +31 -0
- package/dist/umd.d.ts +1 -0
- package/package.json +56 -0
- package/src/index.ts +19 -0
- package/src/lib/MarkupCanvas.ts +434 -0
- package/src/lib/canvas/calcVisibleArea.ts +20 -0
- package/src/lib/canvas/checkContainerDimensions.ts +20 -0
- package/src/lib/canvas/config.ts +29 -0
- package/src/lib/canvas/createCanvas.ts +61 -0
- package/src/lib/canvas/createCanvasLayers.ts +39 -0
- package/src/lib/canvas/getCanvasBounds.ts +68 -0
- package/src/lib/canvas/getCanvasMethods.ts +104 -0
- package/src/lib/canvas/getEmptyBounds.ts +22 -0
- package/src/lib/canvas/index.ts +3 -0
- package/src/lib/canvas/moveExistingContent.ts +9 -0
- package/src/lib/canvas/setupCanvasContainer.ts +22 -0
- package/src/lib/canvas/setupContentLayer.ts +6 -0
- package/src/lib/canvas/setupTransformLayer.ts +15 -0
- package/src/lib/config/constants.ts +56 -0
- package/src/lib/config/createMarkupCanvasConfig.ts +56 -0
- package/src/lib/constants.ts +16 -0
- package/src/lib/events/EventEmitter.ts +34 -0
- package/src/lib/events/constants.ts +9 -0
- package/src/lib/events/index.ts +6 -0
- package/src/lib/events/keyboard/handleKeyDown.ts +18 -0
- package/src/lib/events/keyboard/handleKeyUp.ts +28 -0
- package/src/lib/events/keyboard/setupKeyboardEvents.ts +114 -0
- package/src/lib/events/keyboard/setupKeyboardNavigation.ts +115 -0
- package/src/lib/events/mouse/handleClickToZoom.ts +54 -0
- package/src/lib/events/mouse/handleMouseDown.ts +45 -0
- package/src/lib/events/mouse/handleMouseLeave.ts +18 -0
- package/src/lib/events/mouse/handleMouseMove.ts +57 -0
- package/src/lib/events/mouse/handleMouseUp.ts +40 -0
- package/src/lib/events/mouse/setupMouseDrag.ts +159 -0
- package/src/lib/events/mouse/setupMouseEvents.ts +158 -0
- package/src/lib/events/touch/getTouchCenter.ts +6 -0
- package/src/lib/events/touch/getTouchDistance.ts +5 -0
- package/src/lib/events/touch/handleTouchEnd.ts +9 -0
- package/src/lib/events/touch/handleTouchMove.ts +58 -0
- package/src/lib/events/touch/handleTouchStart.ts +14 -0
- package/src/lib/events/touch/setupTouchEvents.ts +40 -0
- package/src/lib/events/trackpad/createTrackpadPanHandler.ts +35 -0
- package/src/lib/events/trackpad/detectTrackpadGesture.ts +22 -0
- package/src/lib/events/utils/getAdaptiveZoomSpeed.ts +21 -0
- package/src/lib/events/utils/resetClickState.ts +4 -0
- package/src/lib/events/utils/resetDragState.ts +17 -0
- package/src/lib/events/utils/updateCursor.ts +20 -0
- package/src/lib/events/wheel/handleWheel.ts +67 -0
- package/src/lib/events/wheel/setupWheelEvents.ts +24 -0
- package/src/lib/events/wheel/setupWheelHandler.ts +24 -0
- package/src/lib/helpers/index.ts +12 -0
- package/src/lib/helpers/withClampedZoom.ts +7 -0
- package/src/lib/helpers/withDebounce.ts +15 -0
- package/src/lib/helpers/withFeatureEnabled.ts +8 -0
- package/src/lib/helpers/withRAF.ts +38 -0
- package/src/lib/helpers/withRulerCheck.ts +52 -0
- package/src/lib/helpers/withRulerOffset.ts +14 -0
- package/src/lib/matrix/canvasToContent.ts +20 -0
- package/src/lib/matrix/clampZoom.ts +5 -0
- package/src/lib/matrix/contentToCanvas.ts +20 -0
- package/src/lib/matrix/createMatrix.ts +3 -0
- package/src/lib/matrix/createMatrixString.ts +3 -0
- package/src/lib/matrix/getZoomToMouseTransform.ts +46 -0
- package/src/lib/matrix/index.ts +5 -0
- package/src/lib/rulers/RulerElements.ts +6 -0
- package/src/lib/rulers/constants.ts +23 -0
- package/src/lib/rulers/createCornerBox.ts +27 -0
- package/src/lib/rulers/createGridOverlay.ts +22 -0
- package/src/lib/rulers/createHorizontalRuler.ts +24 -0
- package/src/lib/rulers/createRulerElements.ts +27 -0
- package/src/lib/rulers/createRulers.ts +94 -0
- package/src/lib/rulers/createVerticalRuler.ts +24 -0
- package/src/lib/rulers/index.ts +2 -0
- package/src/lib/rulers/setupRulerEvents.ts +23 -0
- package/src/lib/rulers/ticks/calculateTickSpacing.ts +15 -0
- package/src/lib/rulers/ticks/createHorizontalTick.ts +41 -0
- package/src/lib/rulers/ticks/createVerticalTick.ts +43 -0
- package/src/lib/rulers/ticks/index.ts +3 -0
- package/src/lib/rulers/updateGrid.ts +11 -0
- package/src/lib/rulers/updateHorizontalRuler.ts +32 -0
- package/src/lib/rulers/updateRulers.ts +33 -0
- package/src/lib/rulers/updateVerticalRuler.ts +31 -0
- package/src/lib/transform/applyTransform.ts +15 -0
- package/src/lib/transform/applyZoomToCanvas.ts +7 -0
- package/src/lib/transform/hardware-acceleration.ts +11 -0
- package/src/lib/transform/index.ts +2 -0
- package/src/lib/transition/disableTransition.ts +33 -0
- package/src/lib/transition/enableTransition.ts +26 -0
- package/src/lib/transition/index.ts +3 -0
- package/src/lib/transition/withTransition.ts +13 -0
- package/src/types/canvas.ts +89 -0
- package/src/types/config.ts +54 -0
- package/src/types/events.ts +31 -0
- package/src/types/index.ts +28 -0
- package/src/types/matrix.ts +19 -0
- package/src/types/rulers.ts +35 -0
- package/src/umd.ts +1 -0
|
@@ -0,0 +1,1995 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markup Canvas
|
|
3
|
+
* High-performance markup canvas with zoom and pan capabilities
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
// Default transform values
|
|
7
|
+
const DEFAULT_ZOOM = 1.0;
|
|
8
|
+
// Validation thresholds
|
|
9
|
+
const ZOOM_CHANGE_THRESHOLD = 0.001;
|
|
10
|
+
// CSS transition values
|
|
11
|
+
const FALLBACK_TRANSITION_DURATION = 0.2;
|
|
12
|
+
// Zoom to fit padding factor
|
|
13
|
+
const ZOOM_FIT_PADDING = 0.9;
|
|
14
|
+
// CSS class names
|
|
15
|
+
const CANVAS_CONTAINER_CLASS = "canvas-container";
|
|
16
|
+
const TRANSFORM_LAYER_CLASS = "transform-layer";
|
|
17
|
+
const CONTENT_LAYER_CLASS = "content-layer";
|
|
18
|
+
|
|
19
|
+
function moveExistingContent(existingContent, contentLayer, transformLayer) {
|
|
20
|
+
existingContent.forEach((child) => {
|
|
21
|
+
if (child !== transformLayer && !child.classList.contains(TRANSFORM_LAYER_CLASS)) {
|
|
22
|
+
contentLayer.appendChild(child);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setupContentLayer(contentLayer) {
|
|
28
|
+
contentLayer.style.position = "relative";
|
|
29
|
+
contentLayer.style.width = "100%";
|
|
30
|
+
contentLayer.style.height = "100%";
|
|
31
|
+
contentLayer.style.pointerEvents = "auto";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Rulers
|
|
35
|
+
const RULER_SIZE = 24;
|
|
36
|
+
const RULER_Z_INDEX = {
|
|
37
|
+
GRID: 100,
|
|
38
|
+
RULERS: 1000,
|
|
39
|
+
CORNER: 1001,
|
|
40
|
+
};
|
|
41
|
+
const TICK_SETTINGS = {
|
|
42
|
+
MAJOR_HEIGHT: 6,
|
|
43
|
+
MINOR_HEIGHT: 4,
|
|
44
|
+
MAJOR_WIDTH: 8,
|
|
45
|
+
MINOR_WIDTH: 4,
|
|
46
|
+
MAJOR_MULTIPLIER: 5,
|
|
47
|
+
LABEL_INTERVAL: 100,
|
|
48
|
+
};
|
|
49
|
+
const GRID_SETTINGS = {
|
|
50
|
+
BASE_SIZE: 100,
|
|
51
|
+
MIN_SIZE: 20,
|
|
52
|
+
MAX_SIZE: 200,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Sets up the transform layer with proper styles and dimensions
|
|
56
|
+
function setupTransformLayer(transformLayer, config) {
|
|
57
|
+
transformLayer.style.position = "absolute";
|
|
58
|
+
const rulerOffset = RULER_SIZE;
|
|
59
|
+
transformLayer.style.top = `${rulerOffset}px`;
|
|
60
|
+
transformLayer.style.left = `${rulerOffset}px`;
|
|
61
|
+
transformLayer.style.width = `${config.width}px`;
|
|
62
|
+
transformLayer.style.height = `${config.height}px`;
|
|
63
|
+
transformLayer.style.transformOrigin = "0 0";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createCanvasLayers(container, config) {
|
|
67
|
+
const existingContent = Array.from(container.children);
|
|
68
|
+
// Create or find transform layer
|
|
69
|
+
let transformLayer = container.querySelector(`.${TRANSFORM_LAYER_CLASS}`);
|
|
70
|
+
if (!transformLayer) {
|
|
71
|
+
transformLayer = document.createElement("div");
|
|
72
|
+
transformLayer.className = TRANSFORM_LAYER_CLASS;
|
|
73
|
+
container.appendChild(transformLayer);
|
|
74
|
+
}
|
|
75
|
+
setupTransformLayer(transformLayer, config);
|
|
76
|
+
// Create or find content layer
|
|
77
|
+
let contentLayer = transformLayer.querySelector(`.${CONTENT_LAYER_CLASS}`);
|
|
78
|
+
if (!contentLayer) {
|
|
79
|
+
contentLayer = document.createElement("div");
|
|
80
|
+
contentLayer.className = CONTENT_LAYER_CLASS;
|
|
81
|
+
transformLayer.appendChild(contentLayer);
|
|
82
|
+
moveExistingContent(existingContent, contentLayer, transformLayer);
|
|
83
|
+
}
|
|
84
|
+
// Set content layer properties
|
|
85
|
+
setupContentLayer(contentLayer);
|
|
86
|
+
return { transformLayer, contentLayer };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function canvasToContent(canvasX, canvasY, matrix) {
|
|
90
|
+
if (!matrix?.inverse) {
|
|
91
|
+
return { x: canvasX, y: canvasY };
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const inverseMatrix = matrix.inverse();
|
|
95
|
+
const point = new DOMPoint(canvasX, canvasY);
|
|
96
|
+
const transformed = point.matrixTransform(inverseMatrix);
|
|
97
|
+
return {
|
|
98
|
+
x: transformed.x,
|
|
99
|
+
y: transformed.y,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.warn("Canvas to content conversion failed:", error);
|
|
104
|
+
return { x: canvasX, y: canvasY };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function clampZoom(scale, config) {
|
|
109
|
+
return Math.max(config.minZoom, Math.min(config.maxZoom, scale));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createMatrix(scale, translateX, translateY) {
|
|
113
|
+
return new DOMMatrix([scale, 0, 0, scale, translateX, translateY]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getZoomToMouseTransform(mouseX, mouseY, currentTransform, zoomFactor, config) {
|
|
117
|
+
const rulerOffset = config.enableRulers ? -RULER_SIZE : 0;
|
|
118
|
+
const transform = currentTransform || {
|
|
119
|
+
scale: DEFAULT_ZOOM,
|
|
120
|
+
translateX: rulerOffset,
|
|
121
|
+
translateY: rulerOffset,
|
|
122
|
+
};
|
|
123
|
+
const { scale, translateX, translateY } = transform;
|
|
124
|
+
// Calculate new scale with clamping
|
|
125
|
+
const newScale = clampZoom(scale * zoomFactor, config);
|
|
126
|
+
// Early return if zoom didn't change (hit bounds)
|
|
127
|
+
if (Math.abs(newScale - scale) < ZOOM_CHANGE_THRESHOLD) {
|
|
128
|
+
return { scale, translateX, translateY };
|
|
129
|
+
}
|
|
130
|
+
// Convert mouse position to content space
|
|
131
|
+
// Formula: contentPos = (mousePos - translate) / scale
|
|
132
|
+
const contentX = (mouseX - translateX) / scale;
|
|
133
|
+
const contentY = (mouseY - translateY) / scale;
|
|
134
|
+
// Calculate new translation
|
|
135
|
+
// Formula: newTranslate = mousePos - (contentPos * newScale)
|
|
136
|
+
const newTranslateX = mouseX - contentX * newScale;
|
|
137
|
+
const newTranslateY = mouseY - contentY * newScale;
|
|
138
|
+
return {
|
|
139
|
+
scale: newScale,
|
|
140
|
+
translateX: newTranslateX,
|
|
141
|
+
translateY: newTranslateY,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function calculateVisibleArea(canvasWidth, canvasHeight, contentWidth, contentHeight, transform) {
|
|
146
|
+
const topLeft = canvasToContent(0, 0, createMatrix(transform.scale, transform.translateX, transform.translateY));
|
|
147
|
+
const bottomRight = canvasToContent(canvasWidth, canvasHeight, createMatrix(transform.scale, transform.translateX, transform.translateY));
|
|
148
|
+
return {
|
|
149
|
+
x: Math.max(0, Math.min(contentWidth, topLeft.x)),
|
|
150
|
+
y: Math.max(0, Math.min(contentHeight, topLeft.y)),
|
|
151
|
+
width: Math.max(0, Math.min(contentWidth - topLeft.x, bottomRight.x - topLeft.x)),
|
|
152
|
+
height: Math.max(0, Math.min(contentHeight - topLeft.y, bottomRight.y - topLeft.y)),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getEmptyBounds() {
|
|
157
|
+
return {
|
|
158
|
+
width: 0,
|
|
159
|
+
height: 0,
|
|
160
|
+
contentWidth: 0,
|
|
161
|
+
contentHeight: 0,
|
|
162
|
+
scale: 1,
|
|
163
|
+
translateX: 0,
|
|
164
|
+
translateY: 0,
|
|
165
|
+
visibleArea: { x: 0, y: 0, width: 0, height: 0 },
|
|
166
|
+
scaledContentWidth: 0,
|
|
167
|
+
scaledContentHeight: 0,
|
|
168
|
+
canPanLeft: false,
|
|
169
|
+
canPanRight: false,
|
|
170
|
+
canPanUp: false,
|
|
171
|
+
canPanDown: false,
|
|
172
|
+
canZoomIn: false,
|
|
173
|
+
canZoomOut: false,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function withClampedZoom(config, operation) {
|
|
178
|
+
const clampFunction = (scale) => clampZoom(scale, config);
|
|
179
|
+
return operation(clampFunction);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const debounceTimers = new Map();
|
|
183
|
+
function withDebounce(key, delay, operation) {
|
|
184
|
+
const existingTimer = debounceTimers.get(key);
|
|
185
|
+
if (existingTimer) {
|
|
186
|
+
clearTimeout(existingTimer);
|
|
187
|
+
}
|
|
188
|
+
const timer = window.setTimeout(() => {
|
|
189
|
+
operation();
|
|
190
|
+
debounceTimers.delete(key);
|
|
191
|
+
}, delay);
|
|
192
|
+
debounceTimers.set(key, timer);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function withFeatureEnabled(config, feature, operation) {
|
|
196
|
+
if (config[feature]) {
|
|
197
|
+
return operation();
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function withRAFThrottle(func) {
|
|
203
|
+
let rafId = null;
|
|
204
|
+
let lastArgs = null;
|
|
205
|
+
const throttled = (...args) => {
|
|
206
|
+
lastArgs = args;
|
|
207
|
+
if (rafId === null) {
|
|
208
|
+
rafId = requestAnimationFrame(() => {
|
|
209
|
+
if (lastArgs) {
|
|
210
|
+
func(...lastArgs);
|
|
211
|
+
}
|
|
212
|
+
rafId = null;
|
|
213
|
+
lastArgs = null;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
throttled.cleanup = () => {
|
|
218
|
+
if (rafId !== null) {
|
|
219
|
+
cancelAnimationFrame(rafId);
|
|
220
|
+
rafId = null;
|
|
221
|
+
lastArgs = null;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
return throttled;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function withRulerSize(canvas, operation) {
|
|
228
|
+
const hasRulers = canvas.container.querySelector(".canvas-ruler") !== null;
|
|
229
|
+
const rulerSize = hasRulers ? RULER_SIZE : 0;
|
|
230
|
+
return operation(rulerSize);
|
|
231
|
+
}
|
|
232
|
+
function withRulerOffsets(canvas, x, y, operation) {
|
|
233
|
+
return withRulerSize(canvas, (rulerSize) => {
|
|
234
|
+
const adjustedX = x - rulerSize;
|
|
235
|
+
const adjustedY = y - rulerSize;
|
|
236
|
+
return operation(adjustedX, adjustedY);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function withRulerOffsetObject(canvas, coords, operation) {
|
|
240
|
+
return withRulerSize(canvas, (rulerSize) => {
|
|
241
|
+
const adjusted = {
|
|
242
|
+
...coords,
|
|
243
|
+
x: coords.x - rulerSize,
|
|
244
|
+
y: coords.y - rulerSize,
|
|
245
|
+
};
|
|
246
|
+
return operation(adjusted);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function withRulerOffset(canvas, x, y, operation) {
|
|
251
|
+
const hasRulers = canvas.container.querySelector(".canvas-ruler") !== null;
|
|
252
|
+
const adjustedX = hasRulers ? x - RULER_SIZE : x;
|
|
253
|
+
const adjustedY = hasRulers ? y - RULER_SIZE : y;
|
|
254
|
+
return operation(adjustedX, adjustedY);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const DEFAULT_CONFIG = {
|
|
258
|
+
// Canvas dimensions
|
|
259
|
+
width: 8000,
|
|
260
|
+
height: 8000,
|
|
261
|
+
enableAcceleration: true,
|
|
262
|
+
// Interaction controls
|
|
263
|
+
enableZoom: true,
|
|
264
|
+
enablePan: true,
|
|
265
|
+
enableTouch: true,
|
|
266
|
+
enableKeyboard: true,
|
|
267
|
+
limitKeyboardEventsToCanvas: false,
|
|
268
|
+
// Zoom behavior
|
|
269
|
+
zoomSpeed: 1.5,
|
|
270
|
+
minZoom: 0.05,
|
|
271
|
+
maxZoom: 80,
|
|
272
|
+
enableTransition: true,
|
|
273
|
+
transitionDuration: 0.2,
|
|
274
|
+
enableAdaptiveSpeed: true,
|
|
275
|
+
// Pan behavior
|
|
276
|
+
enableLeftDrag: true,
|
|
277
|
+
enableMiddleDrag: true,
|
|
278
|
+
requireSpaceForMouseDrag: false,
|
|
279
|
+
// Keyboard behavior
|
|
280
|
+
keyboardPanStep: 50,
|
|
281
|
+
keyboardFastMultiplier: 20,
|
|
282
|
+
keyboardZoomStep: 0.2,
|
|
283
|
+
// Click-to-zoom
|
|
284
|
+
enableClickToZoom: true,
|
|
285
|
+
clickZoomLevel: 1.0,
|
|
286
|
+
requireOptionForClickZoom: false,
|
|
287
|
+
// Visual elements
|
|
288
|
+
enableRulers: true,
|
|
289
|
+
enableGrid: true,
|
|
290
|
+
gridColor: "rgba(0, 123, 255, 0.1)",
|
|
291
|
+
// Ruler styling
|
|
292
|
+
rulerBackgroundColor: "rgba(255, 255, 255, 0.95)",
|
|
293
|
+
rulerBorderColor: "#ddd",
|
|
294
|
+
rulerTextColor: "#666",
|
|
295
|
+
rulerMajorTickColor: "#999",
|
|
296
|
+
rulerMinorTickColor: "#ccc",
|
|
297
|
+
rulerFontSize: 10,
|
|
298
|
+
rulerFontFamily: "Monaco, Menlo, monospace",
|
|
299
|
+
rulerUnits: "px",
|
|
300
|
+
// Callbacks
|
|
301
|
+
onTransformUpdate: () => { },
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
function getCanvasBounds(canvas) {
|
|
305
|
+
try {
|
|
306
|
+
const container = canvas.container;
|
|
307
|
+
const config = canvas.config;
|
|
308
|
+
const transform = canvas.transform || {
|
|
309
|
+
scale: 1.0,
|
|
310
|
+
translateX: 0,
|
|
311
|
+
translateY: 0,
|
|
312
|
+
};
|
|
313
|
+
// Get canvas dimensions
|
|
314
|
+
const containerRect = container.getBoundingClientRect();
|
|
315
|
+
const totalWidth = containerRect.width || container.clientWidth || 0;
|
|
316
|
+
const totalHeight = containerRect.height || container.clientHeight || 0;
|
|
317
|
+
// Calculate canvas dimensions accounting for rulers
|
|
318
|
+
const canvasWidth = withRulerSize({ container }, (rulerSize) => Math.max(0, totalWidth - rulerSize));
|
|
319
|
+
const canvasHeight = withRulerSize({ container }, (rulerSize) => Math.max(0, totalHeight - rulerSize));
|
|
320
|
+
// Get content dimensions
|
|
321
|
+
const contentWidth = config.width || DEFAULT_CONFIG.width;
|
|
322
|
+
const contentHeight = config.height || DEFAULT_CONFIG.height;
|
|
323
|
+
// Calculate visible area in content coordinates
|
|
324
|
+
const visibleArea = calculateVisibleArea(canvasWidth, canvasHeight, contentWidth, contentHeight, transform);
|
|
325
|
+
return {
|
|
326
|
+
// Canvas dimensions
|
|
327
|
+
width: canvasWidth,
|
|
328
|
+
height: canvasHeight,
|
|
329
|
+
// Content dimensions
|
|
330
|
+
contentWidth,
|
|
331
|
+
contentHeight,
|
|
332
|
+
// Current transform
|
|
333
|
+
scale: transform.scale,
|
|
334
|
+
translateX: transform.translateX,
|
|
335
|
+
translateY: transform.translateY,
|
|
336
|
+
// Visible area in content coordinates
|
|
337
|
+
visibleArea,
|
|
338
|
+
// Calculated properties
|
|
339
|
+
scaledContentWidth: contentWidth * transform.scale,
|
|
340
|
+
scaledContentHeight: contentHeight * transform.scale,
|
|
341
|
+
// Bounds checking
|
|
342
|
+
canPanLeft: transform.translateX < 0,
|
|
343
|
+
canPanRight: transform.translateX + contentWidth * transform.scale > canvasWidth,
|
|
344
|
+
canPanUp: transform.translateY < 0,
|
|
345
|
+
canPanDown: transform.translateY + contentHeight * transform.scale > canvasHeight,
|
|
346
|
+
// Zoom bounds
|
|
347
|
+
canZoomIn: transform.scale < 3.5,
|
|
348
|
+
canZoomOut: transform.scale > 0.1,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
console.error("Failed to calculate canvas bounds:", error);
|
|
353
|
+
return getEmptyBounds();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function createMatrixString(matrix) {
|
|
358
|
+
return `matrix3d(${matrix.m11}, ${matrix.m12}, ${matrix.m13}, ${matrix.m14}, ${matrix.m21}, ${matrix.m22}, ${matrix.m23}, ${matrix.m24}, ${matrix.m31}, ${matrix.m32}, ${matrix.m33}, ${matrix.m34}, ${matrix.m41}, ${matrix.m42}, ${matrix.m43}, ${matrix.m44})`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function applyTransform(element, matrix) {
|
|
362
|
+
if (!element?.style || !matrix) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
element.style.transform = createMatrixString(matrix);
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
console.warn("Transform application failed:", error);
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function enableHardwareAcceleration(element) {
|
|
376
|
+
try {
|
|
377
|
+
// Set CSS properties for hardware acceleration
|
|
378
|
+
element.style.transform = element.style.transform || "translateZ(0)";
|
|
379
|
+
element.style.backfaceVisibility = "hidden";
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
console.error("Failed to enable hardware acceleration:", error);
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function disableTransition(element, config) {
|
|
389
|
+
try {
|
|
390
|
+
if (config.enableTransition) {
|
|
391
|
+
if (window.__markupCanvasTransitionTimeout) {
|
|
392
|
+
clearTimeout(window.__markupCanvasTransitionTimeout);
|
|
393
|
+
window.__markupCanvasTransitionTimeout = undefined;
|
|
394
|
+
}
|
|
395
|
+
const delay = (config.transitionDuration ?? FALLBACK_TRANSITION_DURATION) * 1000;
|
|
396
|
+
withDebounce("disableTransition", delay, () => {
|
|
397
|
+
element.style.transition = "none";
|
|
398
|
+
window.__markupCanvasTransitionTimeout = undefined;
|
|
399
|
+
});
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
console.error("Failed to disable transitions:", error);
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function enableTransition(element, config) {
|
|
411
|
+
try {
|
|
412
|
+
if (config.enableTransition) {
|
|
413
|
+
if (window.__markupCanvasTransitionTimeout) {
|
|
414
|
+
clearTimeout(window.__markupCanvasTransitionTimeout);
|
|
415
|
+
window.__markupCanvasTransitionTimeout = undefined;
|
|
416
|
+
}
|
|
417
|
+
element.style.transition = `transform ${config.transitionDuration}s linear`;
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
console.error("Failed to enable transitions:", error);
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function withTransition(element, config, operation) {
|
|
429
|
+
enableTransition(element, config);
|
|
430
|
+
try {
|
|
431
|
+
const result = operation();
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
disableTransition(element, config);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function getCanvasMethods() {
|
|
440
|
+
return {
|
|
441
|
+
// Utility methods
|
|
442
|
+
getBounds: function () {
|
|
443
|
+
return getCanvasBounds(this);
|
|
444
|
+
},
|
|
445
|
+
// Transform methods
|
|
446
|
+
updateTransform: function (newTransform) {
|
|
447
|
+
this.transform = { ...this.transform, ...newTransform };
|
|
448
|
+
const matrix = createMatrix(this.transform.scale, this.transform.translateX, this.transform.translateY);
|
|
449
|
+
const result = applyTransform(this.transformLayer, matrix);
|
|
450
|
+
withFeatureEnabled(this.config, "onTransformUpdate", () => {
|
|
451
|
+
this.config.onTransformUpdate(this.transform);
|
|
452
|
+
});
|
|
453
|
+
return result;
|
|
454
|
+
},
|
|
455
|
+
// Reset method
|
|
456
|
+
reset: function () {
|
|
457
|
+
const resetTransform = {
|
|
458
|
+
scale: 1.0,
|
|
459
|
+
translateX: 0,
|
|
460
|
+
translateY: 0,
|
|
461
|
+
};
|
|
462
|
+
return this.updateTransform(resetTransform);
|
|
463
|
+
},
|
|
464
|
+
// Handle canvas resize
|
|
465
|
+
handleResize: function () {
|
|
466
|
+
this.container.getBoundingClientRect();
|
|
467
|
+
return true;
|
|
468
|
+
},
|
|
469
|
+
// Set zoom level
|
|
470
|
+
setZoom: function (zoomLevel) {
|
|
471
|
+
const newScale = withClampedZoom(this.config, (clamp) => clamp(zoomLevel));
|
|
472
|
+
return this.updateTransform({ scale: newScale });
|
|
473
|
+
},
|
|
474
|
+
// Convert canvas coordinates to content coordinates
|
|
475
|
+
canvasToContent: function (x, y) {
|
|
476
|
+
const matrix = createMatrix(this.transform.scale, this.transform.translateX, this.transform.translateY);
|
|
477
|
+
return canvasToContent(x, y, matrix);
|
|
478
|
+
},
|
|
479
|
+
// Zoom to a specific point with animation
|
|
480
|
+
zoomToPoint: function (x, y, targetScale) {
|
|
481
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
482
|
+
const newTransform = getZoomToMouseTransform(x, y, this.transform, targetScale / this.transform.scale, this.config);
|
|
483
|
+
return this.updateTransform(newTransform);
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
// Reset view with animation
|
|
487
|
+
resetView: function () {
|
|
488
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
489
|
+
return withRulerSize(this, (rulerSize) => {
|
|
490
|
+
const resetTransform = {
|
|
491
|
+
scale: 1.0,
|
|
492
|
+
translateX: rulerSize * -1,
|
|
493
|
+
translateY: rulerSize * -1,
|
|
494
|
+
};
|
|
495
|
+
return this.updateTransform(resetTransform);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
},
|
|
499
|
+
// Zoom to fit content in canvas
|
|
500
|
+
zoomToFitContent: function () {
|
|
501
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
502
|
+
const bounds = this.getBounds();
|
|
503
|
+
const scaleX = bounds.width / this.config.width;
|
|
504
|
+
const scaleY = bounds.height / this.config.height;
|
|
505
|
+
const fitScale = withClampedZoom(this.config, (clamp) => clamp(Math.min(scaleX, scaleY) * ZOOM_FIT_PADDING));
|
|
506
|
+
// Center the content
|
|
507
|
+
const scaledWidth = this.config.width * fitScale;
|
|
508
|
+
const scaledHeight = this.config.height * fitScale;
|
|
509
|
+
const centerX = (bounds.width - scaledWidth) / 2;
|
|
510
|
+
const centerY = (bounds.height - scaledHeight) / 2;
|
|
511
|
+
return this.updateTransform({
|
|
512
|
+
scale: fitScale,
|
|
513
|
+
translateX: centerX,
|
|
514
|
+
translateY: centerY,
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function checkContainerDimensions(container) {
|
|
522
|
+
const containerRect = container.getBoundingClientRect();
|
|
523
|
+
const computedStyle = getComputedStyle(container);
|
|
524
|
+
if (containerRect.height === 0 && computedStyle.height === "auto") {
|
|
525
|
+
console.error("MarkupCanvas: Container height is 0. Please set a height on your container element using CSS.", "Examples: height: 100vh, height: 500px, or use flexbox/grid layout.", container);
|
|
526
|
+
}
|
|
527
|
+
if (containerRect.width === 0 && computedStyle.width === "auto") {
|
|
528
|
+
console.error("MarkupCanvas: Container width is 0. Please set a width on your container element using CSS.", "Examples: width: 100vw, width: 800px, or use flexbox/grid layout.", container);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function setupCanvasContainer(container) {
|
|
533
|
+
const currentPosition = getComputedStyle(container).position;
|
|
534
|
+
if (currentPosition === "static") {
|
|
535
|
+
container.style.position = "relative";
|
|
536
|
+
}
|
|
537
|
+
container.style.overflow = "hidden";
|
|
538
|
+
container.style.cursor = "grab";
|
|
539
|
+
container.style.overscrollBehavior = "none";
|
|
540
|
+
if (!container.hasAttribute("tabindex")) {
|
|
541
|
+
container.setAttribute("tabindex", "0");
|
|
542
|
+
}
|
|
543
|
+
checkContainerDimensions(container);
|
|
544
|
+
if (!container.classList.contains(CANVAS_CONTAINER_CLASS)) {
|
|
545
|
+
container.classList.add(CANVAS_CONTAINER_CLASS);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Creates and initializes a canvas with the required DOM structure
|
|
550
|
+
function createCanvas(container, config) {
|
|
551
|
+
if (!container?.appendChild) {
|
|
552
|
+
console.error("Invalid container element provided to createCanvas");
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
setupCanvasContainer(container);
|
|
557
|
+
const { transformLayer, contentLayer } = createCanvasLayers(container, config);
|
|
558
|
+
// Enable hardware acceleration if requested
|
|
559
|
+
if (config.enableAcceleration) {
|
|
560
|
+
enableHardwareAcceleration(transformLayer);
|
|
561
|
+
}
|
|
562
|
+
const rulerOffset = config.enableRulers ? -RULER_SIZE : 0;
|
|
563
|
+
const initialTransform = {
|
|
564
|
+
scale: DEFAULT_ZOOM,
|
|
565
|
+
translateX: rulerOffset,
|
|
566
|
+
translateY: rulerOffset,
|
|
567
|
+
};
|
|
568
|
+
// Apply initial transform
|
|
569
|
+
const initialMatrix = createMatrix(initialTransform.scale, initialTransform.translateX, initialTransform.translateY);
|
|
570
|
+
applyTransform(transformLayer, initialMatrix);
|
|
571
|
+
const canvas = {
|
|
572
|
+
// DOM references
|
|
573
|
+
container,
|
|
574
|
+
transformLayer,
|
|
575
|
+
contentLayer,
|
|
576
|
+
// Configuration
|
|
577
|
+
config: config,
|
|
578
|
+
// Current state
|
|
579
|
+
transform: initialTransform,
|
|
580
|
+
// Add all canvas methods
|
|
581
|
+
...getCanvasMethods(),
|
|
582
|
+
};
|
|
583
|
+
return canvas;
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
console.error("Failed to create canvas:", error);
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function createMarkupCanvasConfig(options = {}) {
|
|
592
|
+
const config = {
|
|
593
|
+
...DEFAULT_CONFIG,
|
|
594
|
+
...options,
|
|
595
|
+
};
|
|
596
|
+
if (typeof config.width !== "number" || config.width <= 0) {
|
|
597
|
+
console.warn("Invalid width, using default");
|
|
598
|
+
config.width = DEFAULT_CONFIG.width;
|
|
599
|
+
}
|
|
600
|
+
if (typeof config.height !== "number" || config.height <= 0) {
|
|
601
|
+
console.warn("Invalid height, using default");
|
|
602
|
+
config.height = DEFAULT_CONFIG.height;
|
|
603
|
+
}
|
|
604
|
+
if (typeof config.zoomSpeed !== "number" || config.zoomSpeed <= 0) {
|
|
605
|
+
console.warn("Invalid zoomSpeed, using default");
|
|
606
|
+
config.zoomSpeed = DEFAULT_CONFIG.zoomSpeed;
|
|
607
|
+
}
|
|
608
|
+
if (typeof config.minZoom !== "number" || config.minZoom <= 0) {
|
|
609
|
+
console.warn("Invalid minZoom, using default");
|
|
610
|
+
config.minZoom = DEFAULT_CONFIG.minZoom;
|
|
611
|
+
}
|
|
612
|
+
if (typeof config.maxZoom !== "number" || config.maxZoom <= config.minZoom) {
|
|
613
|
+
console.warn("Invalid maxZoom, using default");
|
|
614
|
+
config.maxZoom = DEFAULT_CONFIG.maxZoom;
|
|
615
|
+
}
|
|
616
|
+
if (typeof config.keyboardPanStep !== "number" || config.keyboardPanStep <= 0) {
|
|
617
|
+
console.warn("Invalid keyboardPanStep, using default");
|
|
618
|
+
config.keyboardPanStep = DEFAULT_CONFIG.keyboardPanStep;
|
|
619
|
+
}
|
|
620
|
+
if (typeof config.keyboardFastMultiplier !== "number" || config.keyboardFastMultiplier <= 0) {
|
|
621
|
+
console.warn("Invalid keyboardFastMultiplier, using default");
|
|
622
|
+
config.keyboardFastMultiplier = DEFAULT_CONFIG.keyboardFastMultiplier;
|
|
623
|
+
}
|
|
624
|
+
if (typeof config.clickZoomLevel !== "number" || config.clickZoomLevel <= 0) {
|
|
625
|
+
console.warn("Invalid clickZoomLevel, using default");
|
|
626
|
+
config.clickZoomLevel = DEFAULT_CONFIG.clickZoomLevel;
|
|
627
|
+
}
|
|
628
|
+
if (typeof config.rulerFontSize !== "number" || config.rulerFontSize <= 0) {
|
|
629
|
+
console.warn("Invalid rulerFontSize, using default");
|
|
630
|
+
config.rulerFontSize = DEFAULT_CONFIG.rulerFontSize;
|
|
631
|
+
}
|
|
632
|
+
return config;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
class EventEmitter {
|
|
636
|
+
constructor() {
|
|
637
|
+
this.listeners = new Map();
|
|
638
|
+
}
|
|
639
|
+
on(event, handler) {
|
|
640
|
+
if (!this.listeners.has(event)) {
|
|
641
|
+
this.listeners.set(event, new Set());
|
|
642
|
+
}
|
|
643
|
+
this.listeners.get(event).add(handler);
|
|
644
|
+
}
|
|
645
|
+
off(event, handler) {
|
|
646
|
+
const handlers = this.listeners.get(event);
|
|
647
|
+
if (handlers) {
|
|
648
|
+
handlers.delete(handler);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
emit(event, data) {
|
|
652
|
+
const handlers = this.listeners.get(event);
|
|
653
|
+
if (handlers) {
|
|
654
|
+
handlers.forEach((handler) => {
|
|
655
|
+
try {
|
|
656
|
+
handler(data);
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
console.error(`Error in event handler for "${String(event)}":`, error);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
removeAllListeners() {
|
|
665
|
+
this.listeners.clear();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const REFERENCE_DISPLAY_AREA = 1920 * 1080;
|
|
670
|
+
const TRACKPAD_PINCH_SPEED_FACTOR = 0.05;
|
|
671
|
+
const ADAPTIVE_ZOOM_FACTOR = 1;
|
|
672
|
+
const CLICK_THRESHOLDS = {
|
|
673
|
+
MAX_DURATION: 300,
|
|
674
|
+
MAX_MOVEMENT: 5,
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
function getAdaptiveZoomSpeed(canvas, baseSpeed) {
|
|
678
|
+
if (!canvas?.getBounds) {
|
|
679
|
+
return baseSpeed;
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
const bounds = canvas.getBounds();
|
|
683
|
+
const displayArea = bounds.width * bounds.height;
|
|
684
|
+
const rawScaleFactor = (displayArea / REFERENCE_DISPLAY_AREA) ** ADAPTIVE_ZOOM_FACTOR;
|
|
685
|
+
const adaptiveSpeed = baseSpeed * rawScaleFactor;
|
|
686
|
+
return adaptiveSpeed;
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
console.warn("Failed to calculate adaptive zoom speed, using base speed:", error);
|
|
690
|
+
return baseSpeed;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function setupKeyboardEvents(canvas, config) {
|
|
695
|
+
// Track mouse position
|
|
696
|
+
let lastMouseX = 0;
|
|
697
|
+
let lastMouseY = 0;
|
|
698
|
+
function handleMouseMove(event) {
|
|
699
|
+
const rect = canvas.container.getBoundingClientRect();
|
|
700
|
+
const rawMouseX = event.clientX - rect.left;
|
|
701
|
+
const rawMouseY = event.clientY - rect.top;
|
|
702
|
+
withRulerOffsets(canvas, rawMouseX, rawMouseY, (adjustedX, adjustedY) => {
|
|
703
|
+
lastMouseX = adjustedX;
|
|
704
|
+
lastMouseY = adjustedY;
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
function handleKeyDown(event) {
|
|
708
|
+
if (!(event instanceof KeyboardEvent))
|
|
709
|
+
return;
|
|
710
|
+
if (config.limitKeyboardEventsToCanvas && document.activeElement !== canvas.container)
|
|
711
|
+
return;
|
|
712
|
+
const isFastPan = event.shiftKey;
|
|
713
|
+
const panDistance = config.keyboardPanStep * (isFastPan ? config.keyboardFastMultiplier : 1);
|
|
714
|
+
let handled = false;
|
|
715
|
+
const newTransform = {};
|
|
716
|
+
switch (event.key) {
|
|
717
|
+
case "ArrowLeft":
|
|
718
|
+
newTransform.translateX = canvas.transform.translateX + panDistance;
|
|
719
|
+
handled = true;
|
|
720
|
+
break;
|
|
721
|
+
case "ArrowRight":
|
|
722
|
+
newTransform.translateX = canvas.transform.translateX - panDistance;
|
|
723
|
+
handled = true;
|
|
724
|
+
break;
|
|
725
|
+
case "ArrowUp":
|
|
726
|
+
newTransform.translateY = canvas.transform.translateY + panDistance;
|
|
727
|
+
handled = true;
|
|
728
|
+
break;
|
|
729
|
+
case "ArrowDown":
|
|
730
|
+
newTransform.translateY = canvas.transform.translateY - panDistance;
|
|
731
|
+
handled = true;
|
|
732
|
+
break;
|
|
733
|
+
case "=":
|
|
734
|
+
case "+":
|
|
735
|
+
{
|
|
736
|
+
const adaptiveZoomStep = config.enableAdaptiveSpeed
|
|
737
|
+
? getAdaptiveZoomSpeed(canvas, config.keyboardZoomStep)
|
|
738
|
+
: config.keyboardZoomStep;
|
|
739
|
+
newTransform.scale = clampZoom(canvas.transform.scale * (1 + adaptiveZoomStep), config);
|
|
740
|
+
handled = true;
|
|
741
|
+
}
|
|
742
|
+
break;
|
|
743
|
+
case "-":
|
|
744
|
+
{
|
|
745
|
+
const adaptiveZoomStep = config.enableAdaptiveSpeed
|
|
746
|
+
? getAdaptiveZoomSpeed(canvas, config.keyboardZoomStep)
|
|
747
|
+
: config.keyboardZoomStep;
|
|
748
|
+
newTransform.scale = clampZoom(canvas.transform.scale * (1 - adaptiveZoomStep), config);
|
|
749
|
+
handled = true;
|
|
750
|
+
}
|
|
751
|
+
break;
|
|
752
|
+
case "0":
|
|
753
|
+
if (event.metaKey || event.ctrlKey) {
|
|
754
|
+
const targetScale = 1.0;
|
|
755
|
+
const zoomFactor = targetScale / canvas.transform.scale;
|
|
756
|
+
const zoomTransform = getZoomToMouseTransform(lastMouseX, lastMouseY, canvas.transform, zoomFactor, config);
|
|
757
|
+
Object.assign(newTransform, zoomTransform);
|
|
758
|
+
handled = true;
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
case "g":
|
|
762
|
+
case "G":
|
|
763
|
+
if (canvas.toggleGrid) {
|
|
764
|
+
canvas.toggleGrid();
|
|
765
|
+
}
|
|
766
|
+
handled = true;
|
|
767
|
+
break;
|
|
768
|
+
case "r":
|
|
769
|
+
case "R":
|
|
770
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey && canvas.toggleRulers) {
|
|
771
|
+
canvas.toggleRulers();
|
|
772
|
+
handled = true;
|
|
773
|
+
}
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
if (handled) {
|
|
777
|
+
event.preventDefault();
|
|
778
|
+
if (Object.keys(newTransform).length > 0) {
|
|
779
|
+
canvas.updateTransform(newTransform);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const keyboardTarget = config.limitKeyboardEventsToCanvas ? canvas.container : document;
|
|
784
|
+
keyboardTarget.addEventListener("keydown", handleKeyDown);
|
|
785
|
+
canvas.container.addEventListener("mousemove", handleMouseMove);
|
|
786
|
+
return () => {
|
|
787
|
+
keyboardTarget.removeEventListener("keydown", handleKeyDown);
|
|
788
|
+
canvas.container.removeEventListener("mousemove", handleMouseMove);
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function updateCursor(canvas, config, isDragEnabled, isSpacePressed, isDragging) {
|
|
793
|
+
if (!isDragEnabled) {
|
|
794
|
+
canvas.container.style.cursor = "default";
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (config.requireSpaceForMouseDrag) {
|
|
798
|
+
canvas.container.style.cursor = isSpacePressed ? "grab" : "default";
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
canvas.container.style.cursor = isDragging ? "grabbing" : "grab";
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function handleKeyDown(event, canvas, config, isDragEnabled, isDragging, setters) {
|
|
806
|
+
if (config.requireSpaceForMouseDrag && event.key === " ") {
|
|
807
|
+
setters.setIsSpacePressed(true);
|
|
808
|
+
updateCursor(canvas, config, isDragEnabled, true, isDragging);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function resetDragState(canvas, config, isDragEnabled, isSpacePressed, setters) {
|
|
813
|
+
setters.setIsDragging(false);
|
|
814
|
+
setters.setDragButton(-1);
|
|
815
|
+
updateCursor(canvas, config, isDragEnabled, isSpacePressed, false);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function handleKeyUp(event, canvas, config, isDragEnabled, isDragging, setters) {
|
|
819
|
+
if (config.requireSpaceForMouseDrag && event.key === " ") {
|
|
820
|
+
setters.setIsSpacePressed(false);
|
|
821
|
+
updateCursor(canvas, config, isDragEnabled, false, isDragging);
|
|
822
|
+
// Stop dragging if currently dragging
|
|
823
|
+
if (isDragging) {
|
|
824
|
+
resetDragState(canvas, config, isDragEnabled, false, {
|
|
825
|
+
setIsDragging: setters.setIsDragging,
|
|
826
|
+
setDragButton: setters.setDragButton,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function handleMouseDown(event, canvas, config, isDragEnabled, isSpacePressed, setters) {
|
|
833
|
+
const isLeftButton = event.button === 0;
|
|
834
|
+
const isMiddleButton = event.button === 1;
|
|
835
|
+
if (isLeftButton) {
|
|
836
|
+
setters.setMouseDownTime(Date.now());
|
|
837
|
+
setters.setMouseDownX(event.clientX);
|
|
838
|
+
setters.setMouseDownY(event.clientY);
|
|
839
|
+
setters.setHasDragged(false);
|
|
840
|
+
}
|
|
841
|
+
if (!isDragEnabled)
|
|
842
|
+
return;
|
|
843
|
+
// Check if drag is allowed based on configuration
|
|
844
|
+
const canDrag = config.requireSpaceForMouseDrag ? isSpacePressed : true;
|
|
845
|
+
if (canDrag && ((isLeftButton && config.enableLeftDrag) || (isMiddleButton && config.enableMiddleDrag))) {
|
|
846
|
+
event.preventDefault();
|
|
847
|
+
// Don't set isDragging to true yet - wait for mouse move
|
|
848
|
+
setters.setDragButton(event.button);
|
|
849
|
+
setters.setLastMouseX(event.clientX);
|
|
850
|
+
setters.setLastMouseY(event.clientY);
|
|
851
|
+
updateCursor(canvas, config, isDragEnabled, isSpacePressed, false); // ← Changed to false
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function handleMouseLeave(canvas, config, isDragEnabled, isSpacePressed, isDragging, setters) {
|
|
856
|
+
if (isDragging) {
|
|
857
|
+
resetDragState(canvas, config, isDragEnabled, isSpacePressed, setters);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function handleMouseMove(event, canvas, isDragEnabled, isDragging, mouseDownTime, mouseDownX, mouseDownY, lastMouseX, lastMouseY, setters) {
|
|
862
|
+
if (mouseDownTime > 0) {
|
|
863
|
+
const deltaX = Math.abs(event.clientX - mouseDownX);
|
|
864
|
+
const deltaY = Math.abs(event.clientY - mouseDownY);
|
|
865
|
+
if (deltaX > CLICK_THRESHOLDS.MAX_MOVEMENT || deltaY > CLICK_THRESHOLDS.MAX_MOVEMENT) {
|
|
866
|
+
setters.setHasDragged(true);
|
|
867
|
+
if (!isDragging && isDragEnabled) {
|
|
868
|
+
setters.setIsDragging(true);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (!isDragging || !isDragEnabled)
|
|
873
|
+
return;
|
|
874
|
+
event.preventDefault();
|
|
875
|
+
const handleMouseMoveThrottled = withRAFThrottle((...args) => {
|
|
876
|
+
const moveEvent = args[0];
|
|
877
|
+
if (!isDragging || !isDragEnabled)
|
|
878
|
+
return;
|
|
879
|
+
const deltaX = moveEvent.clientX - lastMouseX;
|
|
880
|
+
const deltaY = moveEvent.clientY - lastMouseY;
|
|
881
|
+
const newTransform = {
|
|
882
|
+
translateX: canvas.transform.translateX + deltaX,
|
|
883
|
+
translateY: canvas.transform.translateY + deltaY,
|
|
884
|
+
};
|
|
885
|
+
canvas.updateTransform(newTransform);
|
|
886
|
+
setters.setLastMouseX(moveEvent.clientX);
|
|
887
|
+
setters.setLastMouseY(moveEvent.clientY);
|
|
888
|
+
});
|
|
889
|
+
handleMouseMoveThrottled(event);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function handleClickToZoom(event, canvas, config, mouseDownTime, hasDragged, isDragging) {
|
|
893
|
+
const clickDuration = Date.now() - mouseDownTime;
|
|
894
|
+
// Check if Option/Alt key is required and pressed
|
|
895
|
+
const optionKeyPressed = event.altKey;
|
|
896
|
+
const shouldZoom = config.requireOptionForClickZoom ? optionKeyPressed : true;
|
|
897
|
+
if (clickDuration < CLICK_THRESHOLDS.MAX_DURATION && !hasDragged && !isDragging && shouldZoom) {
|
|
898
|
+
event.preventDefault();
|
|
899
|
+
const rect = canvas.container.getBoundingClientRect();
|
|
900
|
+
const rawClickX = event.clientX - rect.left;
|
|
901
|
+
const rawClickY = event.clientY - rect.top;
|
|
902
|
+
const { clickX, clickY } = withRulerOffset(canvas, rawClickX, rawClickY, (adjustedX, adjustedY) => ({
|
|
903
|
+
clickX: adjustedX,
|
|
904
|
+
clickY: adjustedY,
|
|
905
|
+
}));
|
|
906
|
+
// Convert canvas coordinates to content coordinates at current scale
|
|
907
|
+
const contentCoords = canvas.canvasToContent(clickX, clickY);
|
|
908
|
+
// Calculate the center of the canvas
|
|
909
|
+
const canvasCenterX = rect.width / 2;
|
|
910
|
+
const canvasCenterY = rect.height / 2;
|
|
911
|
+
const newScale = config.clickZoomLevel;
|
|
912
|
+
const newTranslateX = canvasCenterX - contentCoords.x * newScale;
|
|
913
|
+
const newTranslateY = canvasCenterY - contentCoords.y * newScale;
|
|
914
|
+
const newTransform = {
|
|
915
|
+
scale: newScale,
|
|
916
|
+
translateX: newTranslateX,
|
|
917
|
+
translateY: newTranslateY,
|
|
918
|
+
};
|
|
919
|
+
withTransition(canvas.transformLayer, canvas.config, () => {
|
|
920
|
+
canvas.updateTransform(newTransform);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function resetClickState(setters) {
|
|
926
|
+
setters.setMouseDownTime(0);
|
|
927
|
+
setters.setHasDragged(false);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function handleMouseUp(event, canvas, config, isDragEnabled, isSpacePressed, isDragging, dragButton, mouseDownTime, hasDragged, setters) {
|
|
931
|
+
if (isDragging && event.button === dragButton) {
|
|
932
|
+
resetDragState(canvas, config, isDragEnabled, isSpacePressed, {
|
|
933
|
+
setIsDragging: setters.setIsDragging,
|
|
934
|
+
setDragButton: setters.setDragButton,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (isDragEnabled && event.button === 0 && config.enableClickToZoom && mouseDownTime > 0) {
|
|
938
|
+
handleClickToZoom(event, canvas, config, mouseDownTime, hasDragged, isDragging);
|
|
939
|
+
}
|
|
940
|
+
if (event.button === 0) {
|
|
941
|
+
resetClickState({
|
|
942
|
+
setMouseDownTime: setters.setMouseDownTime,
|
|
943
|
+
setHasDragged: setters.setHasDragged,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function setupMouseEvents(canvas, config, withControls = true) {
|
|
949
|
+
// State management
|
|
950
|
+
let isDragEnabled = true;
|
|
951
|
+
let isDragging = false;
|
|
952
|
+
let lastMouseX = 0;
|
|
953
|
+
let lastMouseY = 0;
|
|
954
|
+
let dragButton = -1;
|
|
955
|
+
let isSpacePressed = false;
|
|
956
|
+
// Click-to-zoom tracking
|
|
957
|
+
let mouseDownTime = 0;
|
|
958
|
+
let mouseDownX = 0;
|
|
959
|
+
let mouseDownY = 0;
|
|
960
|
+
let hasDragged = false;
|
|
961
|
+
// State setters for passing to functions
|
|
962
|
+
const setters = {
|
|
963
|
+
setIsDragging: (value) => {
|
|
964
|
+
isDragging = value;
|
|
965
|
+
},
|
|
966
|
+
setDragButton: (value) => {
|
|
967
|
+
dragButton = value;
|
|
968
|
+
},
|
|
969
|
+
setIsSpacePressed: (value) => {
|
|
970
|
+
isSpacePressed = value;
|
|
971
|
+
},
|
|
972
|
+
setMouseDownTime: (value) => {
|
|
973
|
+
mouseDownTime = value;
|
|
974
|
+
},
|
|
975
|
+
setMouseDownX: (value) => {
|
|
976
|
+
mouseDownX = value;
|
|
977
|
+
},
|
|
978
|
+
setMouseDownY: (value) => {
|
|
979
|
+
mouseDownY = value;
|
|
980
|
+
},
|
|
981
|
+
setHasDragged: (value) => {
|
|
982
|
+
hasDragged = value;
|
|
983
|
+
},
|
|
984
|
+
setLastMouseX: (value) => {
|
|
985
|
+
lastMouseX = value;
|
|
986
|
+
},
|
|
987
|
+
setLastMouseY: (value) => {
|
|
988
|
+
lastMouseY = value;
|
|
989
|
+
},
|
|
990
|
+
};
|
|
991
|
+
// Event handler wrappers
|
|
992
|
+
const keyDownHandler = (event) => {
|
|
993
|
+
handleKeyDown(event, canvas, config, isDragEnabled, isDragging, {
|
|
994
|
+
setIsSpacePressed: setters.setIsSpacePressed,
|
|
995
|
+
});
|
|
996
|
+
};
|
|
997
|
+
const keyUpHandler = (event) => {
|
|
998
|
+
handleKeyUp(event, canvas, config, isDragEnabled, isDragging, {
|
|
999
|
+
setIsSpacePressed: setters.setIsSpacePressed,
|
|
1000
|
+
setIsDragging: setters.setIsDragging,
|
|
1001
|
+
setDragButton: setters.setDragButton,
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
const mouseDownHandler = (event) => {
|
|
1005
|
+
handleMouseDown(event, canvas, config, isDragEnabled, isSpacePressed, setters);
|
|
1006
|
+
};
|
|
1007
|
+
const mouseMoveHandler = (event) => {
|
|
1008
|
+
handleMouseMove(event, canvas, isDragEnabled, isDragging, mouseDownTime, mouseDownX, mouseDownY, lastMouseX, lastMouseY, {
|
|
1009
|
+
setHasDragged: setters.setHasDragged,
|
|
1010
|
+
setIsDragging: setters.setIsDragging,
|
|
1011
|
+
setLastMouseX: setters.setLastMouseX,
|
|
1012
|
+
setLastMouseY: setters.setLastMouseY,
|
|
1013
|
+
});
|
|
1014
|
+
};
|
|
1015
|
+
const mouseUpHandler = (event) => {
|
|
1016
|
+
handleMouseUp(event, canvas, config, isDragEnabled, isSpacePressed, isDragging, dragButton, mouseDownTime, hasDragged, {
|
|
1017
|
+
setIsDragging: setters.setIsDragging,
|
|
1018
|
+
setDragButton: setters.setDragButton,
|
|
1019
|
+
setMouseDownTime: setters.setMouseDownTime,
|
|
1020
|
+
setHasDragged: setters.setHasDragged,
|
|
1021
|
+
});
|
|
1022
|
+
};
|
|
1023
|
+
const mouseLeaveHandler = () => {
|
|
1024
|
+
handleMouseLeave(canvas, config, isDragEnabled, isSpacePressed, isDragging, {
|
|
1025
|
+
setIsDragging: setters.setIsDragging,
|
|
1026
|
+
setDragButton: setters.setDragButton,
|
|
1027
|
+
});
|
|
1028
|
+
};
|
|
1029
|
+
// Set up event listeners
|
|
1030
|
+
canvas.container.addEventListener("mousedown", mouseDownHandler);
|
|
1031
|
+
document.addEventListener("mousemove", mouseMoveHandler);
|
|
1032
|
+
document.addEventListener("mouseup", mouseUpHandler);
|
|
1033
|
+
canvas.container.addEventListener("mouseleave", mouseLeaveHandler);
|
|
1034
|
+
if (config.requireSpaceForMouseDrag) {
|
|
1035
|
+
document.addEventListener("keydown", keyDownHandler);
|
|
1036
|
+
document.addEventListener("keyup", keyUpHandler);
|
|
1037
|
+
}
|
|
1038
|
+
updateCursor(canvas, config, isDragEnabled, isSpacePressed, isDragging);
|
|
1039
|
+
const cleanup = () => {
|
|
1040
|
+
canvas.container.removeEventListener("mousedown", mouseDownHandler);
|
|
1041
|
+
document.removeEventListener("mousemove", mouseMoveHandler);
|
|
1042
|
+
document.removeEventListener("mouseup", mouseUpHandler);
|
|
1043
|
+
canvas.container.removeEventListener("mouseleave", mouseLeaveHandler);
|
|
1044
|
+
if (config.requireSpaceForMouseDrag) {
|
|
1045
|
+
document.removeEventListener("keydown", keyDownHandler);
|
|
1046
|
+
document.removeEventListener("keyup", keyUpHandler);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
if (withControls) {
|
|
1050
|
+
return {
|
|
1051
|
+
cleanup,
|
|
1052
|
+
enable: () => {
|
|
1053
|
+
isDragEnabled = true;
|
|
1054
|
+
updateCursor(canvas, config, isDragEnabled, isSpacePressed, isDragging);
|
|
1055
|
+
return true;
|
|
1056
|
+
},
|
|
1057
|
+
disable: () => {
|
|
1058
|
+
isDragEnabled = false;
|
|
1059
|
+
// Stop any current dragging
|
|
1060
|
+
if (isDragging) {
|
|
1061
|
+
resetDragState(canvas, config, isDragEnabled, isSpacePressed, {
|
|
1062
|
+
setIsDragging: setters.setIsDragging,
|
|
1063
|
+
setDragButton: setters.setDragButton,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
updateCursor(canvas, config, isDragEnabled, isSpacePressed, isDragging);
|
|
1067
|
+
return true;
|
|
1068
|
+
},
|
|
1069
|
+
isEnabled: () => isDragEnabled,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
return cleanup;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function handleTouchEnd(event, touchState) {
|
|
1076
|
+
touchState.touches = Array.from(event.touches);
|
|
1077
|
+
if (touchState.touches.length < 2) {
|
|
1078
|
+
touchState.lastDistance = 0;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function getTouchCenter(touch1, touch2) {
|
|
1083
|
+
return {
|
|
1084
|
+
x: (touch1.clientX + touch2.clientX) / 2,
|
|
1085
|
+
y: (touch1.clientY + touch2.clientY) / 2,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function getTouchDistance(touch1, touch2) {
|
|
1090
|
+
const dx = touch1.clientX - touch2.clientX;
|
|
1091
|
+
const dy = touch1.clientY - touch2.clientY;
|
|
1092
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function applyZoomToCanvas(canvas, rawZoomFactor, centerX, centerY) {
|
|
1096
|
+
const newTransform = getZoomToMouseTransform(centerX, centerY, canvas.transform, rawZoomFactor, canvas.config);
|
|
1097
|
+
return canvas.updateTransform(newTransform);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function handleTouchMove(event, canvas, touchState) {
|
|
1101
|
+
event.preventDefault();
|
|
1102
|
+
const currentTouches = Array.from(event.touches);
|
|
1103
|
+
// Enhanced RAF-throttled touch move handler for smooth gesture performance
|
|
1104
|
+
const handleTouchMoveThrottled = withRAFThrottle((...args) => {
|
|
1105
|
+
const touches = args[0];
|
|
1106
|
+
if (touches.length === 1) {
|
|
1107
|
+
// Single touch pan
|
|
1108
|
+
if (touchState.touches.length === 1) {
|
|
1109
|
+
const deltaX = touches[0].clientX - touchState.touches[0].clientX;
|
|
1110
|
+
const deltaY = touches[0].clientY - touchState.touches[0].clientY;
|
|
1111
|
+
const newTransform = {
|
|
1112
|
+
translateX: canvas.transform.translateX + deltaX,
|
|
1113
|
+
translateY: canvas.transform.translateY + deltaY,
|
|
1114
|
+
};
|
|
1115
|
+
canvas.updateTransform(newTransform);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
else if (touches.length === 2) {
|
|
1119
|
+
// Two finger pinch zoom
|
|
1120
|
+
const currentDistance = getTouchDistance(touches[0], touches[1]);
|
|
1121
|
+
const currentCenter = getTouchCenter(touches[0], touches[1]);
|
|
1122
|
+
if (touchState.lastDistance > 0) {
|
|
1123
|
+
const rawZoomFactor = currentDistance / touchState.lastDistance;
|
|
1124
|
+
// Get center relative to canvas content area (accounting for rulers)
|
|
1125
|
+
const rect = canvas.container.getBoundingClientRect();
|
|
1126
|
+
let centerX = currentCenter.x - rect.left;
|
|
1127
|
+
let centerY = currentCenter.y - rect.top;
|
|
1128
|
+
// Account for ruler offset if rulers are present
|
|
1129
|
+
const adjustedCenter = withRulerOffsetObject(canvas, { x: centerX, y: centerY }, (adjusted) => adjusted);
|
|
1130
|
+
centerX = adjustedCenter.x;
|
|
1131
|
+
centerY = adjustedCenter.y;
|
|
1132
|
+
// Touch zoom uses global transition settings
|
|
1133
|
+
applyZoomToCanvas(canvas, rawZoomFactor, centerX, centerY);
|
|
1134
|
+
}
|
|
1135
|
+
touchState.lastDistance = currentDistance;
|
|
1136
|
+
touchState.lastCenter = currentCenter;
|
|
1137
|
+
}
|
|
1138
|
+
touchState.touches = touches;
|
|
1139
|
+
});
|
|
1140
|
+
handleTouchMoveThrottled(currentTouches);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function handleTouchStart(event, touchState) {
|
|
1144
|
+
event.preventDefault();
|
|
1145
|
+
touchState.touches = Array.from(event.touches);
|
|
1146
|
+
if (touchState.touches.length === 2) {
|
|
1147
|
+
touchState.lastDistance = getTouchDistance(touchState.touches[0], touchState.touches[1]);
|
|
1148
|
+
touchState.lastCenter = getTouchCenter(touchState.touches[0], touchState.touches[1]);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function setupTouchEvents(canvas) {
|
|
1153
|
+
const touchState = {
|
|
1154
|
+
touches: [],
|
|
1155
|
+
lastDistance: 0,
|
|
1156
|
+
lastCenter: { },
|
|
1157
|
+
};
|
|
1158
|
+
const touchStartHandler = (event) => {
|
|
1159
|
+
handleTouchStart(event, touchState);
|
|
1160
|
+
};
|
|
1161
|
+
const touchMoveHandler = (event) => {
|
|
1162
|
+
handleTouchMove(event, canvas, touchState);
|
|
1163
|
+
};
|
|
1164
|
+
const touchEndHandler = (event) => {
|
|
1165
|
+
handleTouchEnd(event, touchState);
|
|
1166
|
+
};
|
|
1167
|
+
canvas.container.addEventListener("touchstart", touchStartHandler, {
|
|
1168
|
+
passive: false,
|
|
1169
|
+
});
|
|
1170
|
+
canvas.container.addEventListener("touchmove", touchMoveHandler, {
|
|
1171
|
+
passive: false,
|
|
1172
|
+
});
|
|
1173
|
+
canvas.container.addEventListener("touchend", touchEndHandler, {
|
|
1174
|
+
passive: false,
|
|
1175
|
+
});
|
|
1176
|
+
return () => {
|
|
1177
|
+
canvas.container.removeEventListener("touchstart", touchStartHandler);
|
|
1178
|
+
canvas.container.removeEventListener("touchmove", touchMoveHandler);
|
|
1179
|
+
canvas.container.removeEventListener("touchend", touchEndHandler);
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function detectTrackpadGesture(event) {
|
|
1184
|
+
const isZoomIntent = event.ctrlKey || event.metaKey;
|
|
1185
|
+
const isPixelMode = event.deltaMode === 0;
|
|
1186
|
+
const hasSmallDelta = Math.abs(event.deltaY) < 50;
|
|
1187
|
+
const hasFractionalDelta = event.deltaY % 1 !== 0;
|
|
1188
|
+
const hasMultiAxis = Math.abs(event.deltaX) > 0 && Math.abs(event.deltaY) > 0;
|
|
1189
|
+
const trackpadCriteria = [isPixelMode, hasSmallDelta, hasFractionalDelta, hasMultiAxis];
|
|
1190
|
+
const trackpadMatches = trackpadCriteria.filter(Boolean).length;
|
|
1191
|
+
const isTrackpad = trackpadMatches >= 2;
|
|
1192
|
+
return {
|
|
1193
|
+
isTrackpad,
|
|
1194
|
+
isMouseWheel: !isTrackpad,
|
|
1195
|
+
isTrackpadScroll: isTrackpad && !isZoomIntent,
|
|
1196
|
+
isTrackpadPinch: isTrackpad && isZoomIntent,
|
|
1197
|
+
isZoomGesture: isZoomIntent,
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const createTrackpadPanHandler = (canvas) => withRAFThrottle((...args) => {
|
|
1202
|
+
const event = args[0];
|
|
1203
|
+
if (!event || !canvas?.updateTransform) {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
const currentTransform = canvas.transform;
|
|
1208
|
+
// Calculate pan delta based on trackpad scroll
|
|
1209
|
+
const panSensitivity = 1.0;
|
|
1210
|
+
const deltaX = event.deltaX * panSensitivity;
|
|
1211
|
+
const deltaY = event.deltaY * panSensitivity;
|
|
1212
|
+
// Apply pan by adjusting translation
|
|
1213
|
+
const newTransform = {
|
|
1214
|
+
scale: currentTransform.scale,
|
|
1215
|
+
translateX: currentTransform.translateX - deltaX,
|
|
1216
|
+
translateY: currentTransform.translateY - deltaY,
|
|
1217
|
+
};
|
|
1218
|
+
disableTransition(canvas.transformLayer, canvas.config);
|
|
1219
|
+
// Apply the new transform
|
|
1220
|
+
return canvas.updateTransform(newTransform);
|
|
1221
|
+
}
|
|
1222
|
+
catch (error) {
|
|
1223
|
+
console.error("Error handling trackpad pan:", error);
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
function handleWheel(event, canvas, config) {
|
|
1229
|
+
if (!event || typeof event.deltaY !== "number") {
|
|
1230
|
+
console.warn("Invalid wheel event provided");
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
if (!canvas?.updateTransform) {
|
|
1234
|
+
console.warn("Invalid canvas provided to handleWheelEvent");
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
event.preventDefault();
|
|
1239
|
+
// Get mouse position
|
|
1240
|
+
const rect = canvas.container.getBoundingClientRect();
|
|
1241
|
+
const rawMouseX = event.clientX - rect.left;
|
|
1242
|
+
const rawMouseY = event.clientY - rect.top;
|
|
1243
|
+
// Account for ruler offset
|
|
1244
|
+
const { mouseX, mouseY } = withRulerOffset(canvas, rawMouseX, rawMouseY, (adjustedX, adjustedY) => ({
|
|
1245
|
+
mouseX: adjustedX,
|
|
1246
|
+
mouseY: adjustedY,
|
|
1247
|
+
}));
|
|
1248
|
+
// Use the standard zoom speed
|
|
1249
|
+
const baseZoomSpeed = config.zoomSpeed;
|
|
1250
|
+
// Simple gesture detection
|
|
1251
|
+
const gestureInfo = detectTrackpadGesture(event);
|
|
1252
|
+
if (!gestureInfo.isZoomGesture) {
|
|
1253
|
+
// Not a zoom gesture, ignore
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
// Apply display-size adaptive scaling if enabled
|
|
1257
|
+
const currentZoomSpeed = config.enableAdaptiveSpeed ? getAdaptiveZoomSpeed(canvas, baseZoomSpeed) : baseZoomSpeed;
|
|
1258
|
+
// Simple device-based zoom speed adjustment
|
|
1259
|
+
let deviceZoomSpeed = currentZoomSpeed;
|
|
1260
|
+
if (gestureInfo.isTrackpadPinch) {
|
|
1261
|
+
const baseTrackpadSpeed = config.zoomSpeed * TRACKPAD_PINCH_SPEED_FACTOR;
|
|
1262
|
+
deviceZoomSpeed = config.enableAdaptiveSpeed ? getAdaptiveZoomSpeed(canvas, baseTrackpadSpeed) : baseTrackpadSpeed;
|
|
1263
|
+
}
|
|
1264
|
+
// Calculate zoom delta
|
|
1265
|
+
const zoomDirection = event.deltaY < 0 ? 1 : -1;
|
|
1266
|
+
// Use exponential zoom for more natural feel
|
|
1267
|
+
const rawZoomMultiplier = zoomDirection > 0 ? 1 + deviceZoomSpeed : 1 / (1 + deviceZoomSpeed);
|
|
1268
|
+
return applyZoomToCanvas(canvas, rawZoomMultiplier, mouseX, mouseY);
|
|
1269
|
+
}
|
|
1270
|
+
catch (error) {
|
|
1271
|
+
console.error("Error handling wheel event:", error);
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function setupWheelEvents(canvas, config) {
|
|
1277
|
+
const trackpadPanHandler = createTrackpadPanHandler(canvas);
|
|
1278
|
+
const wheelHandler = (event) => {
|
|
1279
|
+
const gestureInfo = detectTrackpadGesture(event);
|
|
1280
|
+
if (gestureInfo.isTrackpadScroll) {
|
|
1281
|
+
return trackpadPanHandler(event);
|
|
1282
|
+
}
|
|
1283
|
+
return handleWheel(event, canvas, config);
|
|
1284
|
+
};
|
|
1285
|
+
canvas.container.addEventListener("wheel", wheelHandler, { passive: false });
|
|
1286
|
+
return () => {
|
|
1287
|
+
canvas.container.removeEventListener("wheel", wheelHandler);
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function createCornerBox(config) {
|
|
1292
|
+
const corner = document.createElement("div");
|
|
1293
|
+
corner.className = "canvas-ruler corner-box";
|
|
1294
|
+
corner.style.cssText = `
|
|
1295
|
+
position: absolute;
|
|
1296
|
+
top: 0;
|
|
1297
|
+
left: 0;
|
|
1298
|
+
width: ${RULER_SIZE}px;
|
|
1299
|
+
height: ${RULER_SIZE}px;
|
|
1300
|
+
background: ${config.backgroundColor};
|
|
1301
|
+
border-right: 1px solid ${config.borderColor};
|
|
1302
|
+
border-bottom: 1px solid ${config.borderColor};
|
|
1303
|
+
z-index: ${RULER_Z_INDEX.CORNER};
|
|
1304
|
+
display: flex;
|
|
1305
|
+
align-items: center;
|
|
1306
|
+
justify-content: center;
|
|
1307
|
+
font-family: ${config.fontFamily};
|
|
1308
|
+
font-size: ${config.fontSize - 2}px;
|
|
1309
|
+
color: ${config.textColor};
|
|
1310
|
+
pointer-events: none;
|
|
1311
|
+
`;
|
|
1312
|
+
corner.textContent = config.units;
|
|
1313
|
+
return corner;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function createGridOverlay(config) {
|
|
1317
|
+
const grid = document.createElement("div");
|
|
1318
|
+
grid.className = "canvas-ruler grid-overlay";
|
|
1319
|
+
grid.style.cssText = `
|
|
1320
|
+
position: absolute;
|
|
1321
|
+
top: ${RULER_SIZE}px;
|
|
1322
|
+
left: ${RULER_SIZE}px;
|
|
1323
|
+
right: 0;
|
|
1324
|
+
bottom: 0;
|
|
1325
|
+
pointer-events: none;
|
|
1326
|
+
z-index: ${RULER_Z_INDEX.GRID};
|
|
1327
|
+
background-image:
|
|
1328
|
+
linear-gradient(${config.gridColor} 1px, transparent 1px),
|
|
1329
|
+
linear-gradient(90deg, ${config.gridColor} 1px, transparent 1px);
|
|
1330
|
+
background-size: 100px 100px;
|
|
1331
|
+
opacity: 0.5;
|
|
1332
|
+
`;
|
|
1333
|
+
return grid;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function createHorizontalRuler(config) {
|
|
1337
|
+
const ruler = document.createElement("div");
|
|
1338
|
+
ruler.className = "canvas-ruler horizontal-ruler";
|
|
1339
|
+
ruler.style.cssText = `
|
|
1340
|
+
position: absolute;
|
|
1341
|
+
top: 0;
|
|
1342
|
+
left: ${RULER_SIZE}px;
|
|
1343
|
+
right: 0;
|
|
1344
|
+
height: ${RULER_SIZE}px;
|
|
1345
|
+
background: ${config.backgroundColor};
|
|
1346
|
+
border-bottom: 1px solid ${config.borderColor};
|
|
1347
|
+
border-right: 1px solid ${config.borderColor};
|
|
1348
|
+
z-index: ${RULER_Z_INDEX.RULERS};
|
|
1349
|
+
pointer-events: none;
|
|
1350
|
+
font-family: ${config.fontFamily};
|
|
1351
|
+
font-size: ${config.fontSize}px;
|
|
1352
|
+
color: ${config.textColor};
|
|
1353
|
+
overflow: hidden;
|
|
1354
|
+
`;
|
|
1355
|
+
return ruler;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function createVerticalRuler(config) {
|
|
1359
|
+
const ruler = document.createElement("div");
|
|
1360
|
+
ruler.className = "canvas-ruler vertical-ruler";
|
|
1361
|
+
ruler.style.cssText = `
|
|
1362
|
+
position: absolute;
|
|
1363
|
+
top: ${RULER_SIZE}px;
|
|
1364
|
+
left: 0;
|
|
1365
|
+
bottom: 0;
|
|
1366
|
+
width: ${RULER_SIZE}px;
|
|
1367
|
+
background: ${config.backgroundColor};
|
|
1368
|
+
border-right: 1px solid ${config.borderColor};
|
|
1369
|
+
border-bottom: 1px solid ${config.borderColor};
|
|
1370
|
+
z-index: ${RULER_Z_INDEX.RULERS};
|
|
1371
|
+
pointer-events: none;
|
|
1372
|
+
font-family: ${config.fontFamily};
|
|
1373
|
+
font-size: ${config.fontSize}px;
|
|
1374
|
+
color: ${config.textColor};
|
|
1375
|
+
overflow: hidden;
|
|
1376
|
+
`;
|
|
1377
|
+
return ruler;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function createRulerElements(container, config) {
|
|
1381
|
+
const horizontalRuler = createHorizontalRuler(config);
|
|
1382
|
+
const verticalRuler = createVerticalRuler(config);
|
|
1383
|
+
const cornerBox = createCornerBox(config);
|
|
1384
|
+
const gridOverlay = config.showGrid ? createGridOverlay(config) : undefined;
|
|
1385
|
+
container.appendChild(horizontalRuler);
|
|
1386
|
+
container.appendChild(verticalRuler);
|
|
1387
|
+
container.appendChild(cornerBox);
|
|
1388
|
+
if (gridOverlay) {
|
|
1389
|
+
container.appendChild(gridOverlay);
|
|
1390
|
+
}
|
|
1391
|
+
return {
|
|
1392
|
+
horizontalRuler,
|
|
1393
|
+
verticalRuler,
|
|
1394
|
+
cornerBox,
|
|
1395
|
+
gridOverlay,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function setupRulerEvents(canvas, updateCallback) {
|
|
1400
|
+
const throttledUpdateCallback = withRAFThrottle(updateCallback);
|
|
1401
|
+
const originalUpdateTransform = canvas.updateTransform;
|
|
1402
|
+
canvas.updateTransform = function (newTransform) {
|
|
1403
|
+
const result = originalUpdateTransform.call(this, newTransform);
|
|
1404
|
+
throttledUpdateCallback();
|
|
1405
|
+
return result;
|
|
1406
|
+
};
|
|
1407
|
+
const resizeHandler = withRAFThrottle(updateCallback);
|
|
1408
|
+
window.addEventListener("resize", resizeHandler);
|
|
1409
|
+
return () => {
|
|
1410
|
+
window.removeEventListener("resize", resizeHandler);
|
|
1411
|
+
canvas.updateTransform = originalUpdateTransform;
|
|
1412
|
+
throttledUpdateCallback.cleanup();
|
|
1413
|
+
resizeHandler.cleanup();
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function updateGrid(gridOverlay, scale, translateX, translateY) {
|
|
1418
|
+
let gridSize = GRID_SETTINGS.BASE_SIZE * scale;
|
|
1419
|
+
while (gridSize < GRID_SETTINGS.MIN_SIZE)
|
|
1420
|
+
gridSize *= 2;
|
|
1421
|
+
while (gridSize > GRID_SETTINGS.MAX_SIZE)
|
|
1422
|
+
gridSize /= 2;
|
|
1423
|
+
gridOverlay.style.backgroundSize = `${gridSize}px ${gridSize}px`;
|
|
1424
|
+
gridOverlay.style.backgroundPosition = `${translateX % gridSize}px ${translateY % gridSize}px`;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function calculateTickSpacing(contentSize, canvasSize) {
|
|
1428
|
+
const targetTicks = Math.max(5, Math.min(20, canvasSize / 50));
|
|
1429
|
+
const rawSpacing = contentSize / targetTicks;
|
|
1430
|
+
const magnitude = 10 ** Math.floor(Math.log10(rawSpacing));
|
|
1431
|
+
const normalized = rawSpacing / magnitude;
|
|
1432
|
+
let niceSpacing;
|
|
1433
|
+
if (normalized <= 1)
|
|
1434
|
+
niceSpacing = 1;
|
|
1435
|
+
else if (normalized <= 2)
|
|
1436
|
+
niceSpacing = 2;
|
|
1437
|
+
else if (normalized <= 5)
|
|
1438
|
+
niceSpacing = 5;
|
|
1439
|
+
else
|
|
1440
|
+
niceSpacing = 10;
|
|
1441
|
+
return niceSpacing * magnitude;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function createHorizontalTick(container, position, pixelPos, tickSpacing, config) {
|
|
1445
|
+
const tick = document.createElement("div");
|
|
1446
|
+
const isMajor = position % (tickSpacing * TICK_SETTINGS.MAJOR_MULTIPLIER) === 0;
|
|
1447
|
+
const tickHeight = isMajor ? TICK_SETTINGS.MAJOR_HEIGHT : TICK_SETTINGS.MINOR_HEIGHT;
|
|
1448
|
+
tick.style.cssText = `
|
|
1449
|
+
position: absolute;
|
|
1450
|
+
left: ${pixelPos}px;
|
|
1451
|
+
bottom: 0;
|
|
1452
|
+
width: 1px;
|
|
1453
|
+
height: ${tickHeight}px;
|
|
1454
|
+
background: ${isMajor ? config.majorTickColor : config.minorTickColor};
|
|
1455
|
+
`;
|
|
1456
|
+
container.appendChild(tick);
|
|
1457
|
+
const shouldShowLabel = isMajor || position % TICK_SETTINGS.LABEL_INTERVAL === 0;
|
|
1458
|
+
if (shouldShowLabel) {
|
|
1459
|
+
const label = document.createElement("div");
|
|
1460
|
+
label.style.cssText = `
|
|
1461
|
+
position: absolute;
|
|
1462
|
+
left: ${pixelPos}px;
|
|
1463
|
+
bottom: ${tickHeight}px;
|
|
1464
|
+
font-size: ${config.fontSize}px;
|
|
1465
|
+
color: ${config.textColor};
|
|
1466
|
+
white-space: nowrap;
|
|
1467
|
+
pointer-events: none;
|
|
1468
|
+
`;
|
|
1469
|
+
label.textContent = Math.round(position).toString();
|
|
1470
|
+
container.appendChild(label);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function updateHorizontalRuler(ruler, contentLeft, contentRight, canvasWidth, scale, config) {
|
|
1475
|
+
const rulerWidth = canvasWidth;
|
|
1476
|
+
const contentWidth = contentRight - contentLeft;
|
|
1477
|
+
const tickSpacing = calculateTickSpacing(contentWidth, rulerWidth);
|
|
1478
|
+
const fragment = document.createDocumentFragment();
|
|
1479
|
+
const startTick = Math.floor(contentLeft / tickSpacing) * tickSpacing;
|
|
1480
|
+
const endTick = Math.ceil(contentRight / tickSpacing) * tickSpacing;
|
|
1481
|
+
for (let pos = startTick; pos <= endTick; pos += tickSpacing) {
|
|
1482
|
+
const pixelPos = (pos - contentLeft) * scale;
|
|
1483
|
+
if (pixelPos >= -50 && pixelPos <= rulerWidth + 50) {
|
|
1484
|
+
createHorizontalTick(fragment, pos, pixelPos, tickSpacing, config);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
ruler.innerHTML = "";
|
|
1488
|
+
ruler.appendChild(fragment);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function createVerticalTick(container, position, pixelPos, tickSpacing, config) {
|
|
1492
|
+
const tick = document.createElement("div");
|
|
1493
|
+
const isMajor = position % (tickSpacing * TICK_SETTINGS.MAJOR_MULTIPLIER) === 0;
|
|
1494
|
+
const tickWidth = isMajor ? TICK_SETTINGS.MAJOR_WIDTH : TICK_SETTINGS.MINOR_WIDTH;
|
|
1495
|
+
tick.style.cssText = `
|
|
1496
|
+
position: absolute;
|
|
1497
|
+
top: ${pixelPos}px;
|
|
1498
|
+
right: 0;
|
|
1499
|
+
width: ${tickWidth}px;
|
|
1500
|
+
height: 1px;
|
|
1501
|
+
background: ${isMajor ? config.majorTickColor : config.minorTickColor};
|
|
1502
|
+
`;
|
|
1503
|
+
container.appendChild(tick);
|
|
1504
|
+
const shouldShowLabel = isMajor || position % TICK_SETTINGS.LABEL_INTERVAL === 0;
|
|
1505
|
+
if (shouldShowLabel) {
|
|
1506
|
+
const label = document.createElement("div");
|
|
1507
|
+
label.style.cssText = `
|
|
1508
|
+
position: absolute;
|
|
1509
|
+
top: ${pixelPos - 6}px;
|
|
1510
|
+
right: ${tickWidth + 6}px;
|
|
1511
|
+
font-size: ${config.fontSize}px;
|
|
1512
|
+
color: ${config.textColor};
|
|
1513
|
+
white-space: nowrap;
|
|
1514
|
+
pointer-events: none;
|
|
1515
|
+
transform: rotate(-90deg);
|
|
1516
|
+
transform-origin: right center;
|
|
1517
|
+
`;
|
|
1518
|
+
label.textContent = Math.round(position).toString();
|
|
1519
|
+
container.appendChild(label);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function updateVerticalRuler(ruler, contentTop, contentBottom, canvasHeight, scale, config) {
|
|
1524
|
+
const rulerHeight = canvasHeight;
|
|
1525
|
+
const contentHeight = contentBottom - contentTop;
|
|
1526
|
+
const tickSpacing = calculateTickSpacing(contentHeight, rulerHeight);
|
|
1527
|
+
const fragment = document.createDocumentFragment();
|
|
1528
|
+
const startTick = Math.floor(contentTop / tickSpacing) * tickSpacing;
|
|
1529
|
+
const endTick = Math.ceil(contentBottom / tickSpacing) * tickSpacing;
|
|
1530
|
+
for (let pos = startTick; pos <= endTick; pos += tickSpacing) {
|
|
1531
|
+
const pixelPos = (pos - contentTop) * scale;
|
|
1532
|
+
if (pixelPos >= -50 && pixelPos <= rulerHeight + 50) {
|
|
1533
|
+
createVerticalTick(fragment, pos, pixelPos, tickSpacing, config);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
ruler.innerHTML = "";
|
|
1537
|
+
ruler.appendChild(fragment);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function updateRulers(canvas, horizontalRuler, verticalRuler, gridOverlay, config) {
|
|
1541
|
+
const bounds = canvas.getBounds();
|
|
1542
|
+
const scale = bounds.scale || 1;
|
|
1543
|
+
const translateX = bounds.translateX || 0;
|
|
1544
|
+
const translateY = bounds.translateY || 0;
|
|
1545
|
+
const canvasWidth = bounds.width - RULER_SIZE;
|
|
1546
|
+
const canvasHeight = bounds.height - RULER_SIZE;
|
|
1547
|
+
const contentLeft = -translateX / scale;
|
|
1548
|
+
const contentTop = -translateY / scale;
|
|
1549
|
+
const contentRight = contentLeft + canvasWidth / scale;
|
|
1550
|
+
const contentBottom = contentTop + canvasHeight / scale;
|
|
1551
|
+
updateHorizontalRuler(horizontalRuler, contentLeft, contentRight, canvasWidth, scale, config);
|
|
1552
|
+
updateVerticalRuler(verticalRuler, contentTop, contentBottom, canvasHeight, scale, config);
|
|
1553
|
+
if (gridOverlay) {
|
|
1554
|
+
updateGrid(gridOverlay, scale, translateX, translateY);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function createRulers(canvas, config) {
|
|
1559
|
+
if (!canvas?.container) {
|
|
1560
|
+
console.error("Invalid canvas provided to createRulers");
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
let elements;
|
|
1564
|
+
let cleanupEvents = null;
|
|
1565
|
+
let isDestroyed = false;
|
|
1566
|
+
const rulerOptions = {
|
|
1567
|
+
backgroundColor: config.rulerBackgroundColor,
|
|
1568
|
+
borderColor: config.rulerBorderColor,
|
|
1569
|
+
textColor: config.rulerTextColor,
|
|
1570
|
+
majorTickColor: config.rulerMajorTickColor,
|
|
1571
|
+
minorTickColor: config.rulerMinorTickColor,
|
|
1572
|
+
fontSize: config.rulerFontSize,
|
|
1573
|
+
fontFamily: config.rulerFontFamily,
|
|
1574
|
+
showGrid: config.enableGrid,
|
|
1575
|
+
gridColor: config.gridColor,
|
|
1576
|
+
units: config.rulerUnits,
|
|
1577
|
+
};
|
|
1578
|
+
const safeUpdate = () => {
|
|
1579
|
+
if (isDestroyed || !elements.horizontalRuler || !elements.verticalRuler)
|
|
1580
|
+
return;
|
|
1581
|
+
updateRulers(canvas, elements.horizontalRuler, elements.verticalRuler, elements.gridOverlay, rulerOptions);
|
|
1582
|
+
};
|
|
1583
|
+
try {
|
|
1584
|
+
elements = createRulerElements(canvas.container, rulerOptions);
|
|
1585
|
+
cleanupEvents = setupRulerEvents(canvas, safeUpdate);
|
|
1586
|
+
safeUpdate();
|
|
1587
|
+
return {
|
|
1588
|
+
horizontalRuler: elements.horizontalRuler,
|
|
1589
|
+
verticalRuler: elements.verticalRuler,
|
|
1590
|
+
cornerBox: elements.cornerBox,
|
|
1591
|
+
gridOverlay: elements.gridOverlay,
|
|
1592
|
+
update: safeUpdate,
|
|
1593
|
+
show: () => {
|
|
1594
|
+
if (elements.horizontalRuler)
|
|
1595
|
+
elements.horizontalRuler.style.display = "block";
|
|
1596
|
+
if (elements.verticalRuler)
|
|
1597
|
+
elements.verticalRuler.style.display = "block";
|
|
1598
|
+
if (elements.cornerBox)
|
|
1599
|
+
elements.cornerBox.style.display = "flex";
|
|
1600
|
+
if (elements.gridOverlay)
|
|
1601
|
+
elements.gridOverlay.style.display = "block";
|
|
1602
|
+
},
|
|
1603
|
+
hide: () => {
|
|
1604
|
+
if (elements.horizontalRuler)
|
|
1605
|
+
elements.horizontalRuler.style.display = "none";
|
|
1606
|
+
if (elements.verticalRuler)
|
|
1607
|
+
elements.verticalRuler.style.display = "none";
|
|
1608
|
+
if (elements.cornerBox)
|
|
1609
|
+
elements.cornerBox.style.display = "none";
|
|
1610
|
+
if (elements.gridOverlay)
|
|
1611
|
+
elements.gridOverlay.style.display = "none";
|
|
1612
|
+
},
|
|
1613
|
+
toggleGrid: () => {
|
|
1614
|
+
if (elements.gridOverlay) {
|
|
1615
|
+
const isVisible = elements.gridOverlay.style.display !== "none";
|
|
1616
|
+
elements.gridOverlay.style.display = isVisible ? "none" : "block";
|
|
1617
|
+
}
|
|
1618
|
+
},
|
|
1619
|
+
destroy: () => {
|
|
1620
|
+
isDestroyed = true;
|
|
1621
|
+
if (cleanupEvents) {
|
|
1622
|
+
cleanupEvents();
|
|
1623
|
+
}
|
|
1624
|
+
if (elements.horizontalRuler?.parentNode) {
|
|
1625
|
+
elements.horizontalRuler.parentNode.removeChild(elements.horizontalRuler);
|
|
1626
|
+
}
|
|
1627
|
+
if (elements.verticalRuler?.parentNode) {
|
|
1628
|
+
elements.verticalRuler.parentNode.removeChild(elements.verticalRuler);
|
|
1629
|
+
}
|
|
1630
|
+
if (elements.cornerBox?.parentNode) {
|
|
1631
|
+
elements.cornerBox.parentNode.removeChild(elements.cornerBox);
|
|
1632
|
+
}
|
|
1633
|
+
if (elements.gridOverlay?.parentNode) {
|
|
1634
|
+
elements.gridOverlay.parentNode.removeChild(elements.gridOverlay);
|
|
1635
|
+
}
|
|
1636
|
+
},
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
catch (error) {
|
|
1640
|
+
console.error("Failed to create rulers:", error);
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
class MarkupCanvas {
|
|
1646
|
+
constructor(container, options = {}) {
|
|
1647
|
+
this.cleanupFunctions = [];
|
|
1648
|
+
this.rulers = null;
|
|
1649
|
+
this.dragSetup = null;
|
|
1650
|
+
this._isReady = false;
|
|
1651
|
+
this.listen = new EventEmitter();
|
|
1652
|
+
if (!container) {
|
|
1653
|
+
throw new Error("Container element is required");
|
|
1654
|
+
}
|
|
1655
|
+
this.config = createMarkupCanvasConfig(options);
|
|
1656
|
+
const canvas = createCanvas(container, this.config);
|
|
1657
|
+
if (!canvas) {
|
|
1658
|
+
throw new Error("Failed to create canvas");
|
|
1659
|
+
}
|
|
1660
|
+
this.baseCanvas = canvas;
|
|
1661
|
+
this.setupEventHandlers();
|
|
1662
|
+
this._isReady = true;
|
|
1663
|
+
// Emit ready event
|
|
1664
|
+
this.listen.emit("ready", this);
|
|
1665
|
+
}
|
|
1666
|
+
setupEventHandlers() {
|
|
1667
|
+
try {
|
|
1668
|
+
// Wheel zoom
|
|
1669
|
+
withFeatureEnabled(this.config, "enableZoom", () => {
|
|
1670
|
+
const wheelCleanup = setupWheelEvents(this, this.config);
|
|
1671
|
+
this.cleanupFunctions.push(wheelCleanup);
|
|
1672
|
+
});
|
|
1673
|
+
// Mouse events (drag and click-to-zoom)
|
|
1674
|
+
// Set up mouse events if either pan or click-to-zoom is enabled
|
|
1675
|
+
if (this.config.enablePan || this.config.enableClickToZoom) {
|
|
1676
|
+
this.dragSetup = setupMouseEvents(this, this.config, true);
|
|
1677
|
+
this.cleanupFunctions.push(this.dragSetup.cleanup);
|
|
1678
|
+
}
|
|
1679
|
+
// Keyboard navigation
|
|
1680
|
+
withFeatureEnabled(this.config, "enableKeyboard", () => {
|
|
1681
|
+
const keyboardCleanup = setupKeyboardEvents(this, this.config);
|
|
1682
|
+
this.cleanupFunctions.push(keyboardCleanup);
|
|
1683
|
+
});
|
|
1684
|
+
// Touch events (if enabled)
|
|
1685
|
+
withFeatureEnabled(this.config, "enableTouch", () => {
|
|
1686
|
+
const touchCleanup = setupTouchEvents(this);
|
|
1687
|
+
this.cleanupFunctions.push(touchCleanup);
|
|
1688
|
+
});
|
|
1689
|
+
// Set up rulers and grid
|
|
1690
|
+
withFeatureEnabled(this.config, "enableRulers", () => {
|
|
1691
|
+
this.rulers = createRulers(this.baseCanvas, this.config);
|
|
1692
|
+
this.cleanupFunctions.push(() => {
|
|
1693
|
+
if (this.rulers) {
|
|
1694
|
+
this.rulers.destroy();
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
catch (error) {
|
|
1700
|
+
console.error("Failed to set up event handlers:", error);
|
|
1701
|
+
this.cleanup();
|
|
1702
|
+
throw error;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
// Base canvas properties and methods
|
|
1706
|
+
get container() {
|
|
1707
|
+
return this.baseCanvas.container;
|
|
1708
|
+
}
|
|
1709
|
+
get transformLayer() {
|
|
1710
|
+
return this.baseCanvas.transformLayer;
|
|
1711
|
+
}
|
|
1712
|
+
get contentLayer() {
|
|
1713
|
+
return this.baseCanvas.contentLayer;
|
|
1714
|
+
}
|
|
1715
|
+
get transform() {
|
|
1716
|
+
return this.baseCanvas.transform;
|
|
1717
|
+
}
|
|
1718
|
+
// State management getters for React integration
|
|
1719
|
+
get isReady() {
|
|
1720
|
+
return this._isReady;
|
|
1721
|
+
}
|
|
1722
|
+
get isTransforming() {
|
|
1723
|
+
return this.dragSetup?.isEnabled() || false;
|
|
1724
|
+
}
|
|
1725
|
+
get visibleBounds() {
|
|
1726
|
+
return this.getVisibleArea();
|
|
1727
|
+
}
|
|
1728
|
+
getBounds() {
|
|
1729
|
+
return this.baseCanvas.getBounds();
|
|
1730
|
+
}
|
|
1731
|
+
updateTransform(newTransform) {
|
|
1732
|
+
const result = this.baseCanvas.updateTransform(newTransform);
|
|
1733
|
+
if (result) {
|
|
1734
|
+
this.emitTransformEvents();
|
|
1735
|
+
}
|
|
1736
|
+
return result;
|
|
1737
|
+
}
|
|
1738
|
+
emitTransformEvents() {
|
|
1739
|
+
const transform = this.baseCanvas.transform;
|
|
1740
|
+
this.listen.emit("transform", transform);
|
|
1741
|
+
this.listen.emit("zoom", transform.scale);
|
|
1742
|
+
this.listen.emit("pan", { x: transform.translateX, y: transform.translateY });
|
|
1743
|
+
}
|
|
1744
|
+
reset() {
|
|
1745
|
+
return this.baseCanvas.reset();
|
|
1746
|
+
}
|
|
1747
|
+
handleResize() {
|
|
1748
|
+
return this.baseCanvas.handleResize();
|
|
1749
|
+
}
|
|
1750
|
+
setZoom(zoomLevel) {
|
|
1751
|
+
return this.baseCanvas.setZoom(zoomLevel);
|
|
1752
|
+
}
|
|
1753
|
+
canvasToContent(x, y) {
|
|
1754
|
+
return this.baseCanvas.canvasToContent(x, y);
|
|
1755
|
+
}
|
|
1756
|
+
zoomToPoint(x, y, targetScale) {
|
|
1757
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
1758
|
+
const result = this.baseCanvas.zoomToPoint(x, y, targetScale);
|
|
1759
|
+
if (result) {
|
|
1760
|
+
this.emitTransformEvents();
|
|
1761
|
+
}
|
|
1762
|
+
return result;
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
resetView() {
|
|
1766
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
1767
|
+
const result = this.baseCanvas.resetView ? this.baseCanvas.resetView() : false;
|
|
1768
|
+
if (result) {
|
|
1769
|
+
this.emitTransformEvents();
|
|
1770
|
+
}
|
|
1771
|
+
return result;
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
zoomToFitContent() {
|
|
1775
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
1776
|
+
const result = this.baseCanvas.zoomToFitContent();
|
|
1777
|
+
if (result) {
|
|
1778
|
+
this.emitTransformEvents();
|
|
1779
|
+
}
|
|
1780
|
+
return result;
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
// Pan methods
|
|
1784
|
+
panLeft(distance) {
|
|
1785
|
+
const panDistance = distance ?? this.config.keyboardPanStep;
|
|
1786
|
+
const newTransform = {
|
|
1787
|
+
translateX: this.baseCanvas.transform.translateX + panDistance,
|
|
1788
|
+
};
|
|
1789
|
+
return this.updateTransform(newTransform);
|
|
1790
|
+
}
|
|
1791
|
+
panRight(distance) {
|
|
1792
|
+
const panDistance = distance ?? this.config.keyboardPanStep;
|
|
1793
|
+
const newTransform = {
|
|
1794
|
+
translateX: this.baseCanvas.transform.translateX - panDistance,
|
|
1795
|
+
};
|
|
1796
|
+
return this.updateTransform(newTransform);
|
|
1797
|
+
}
|
|
1798
|
+
panUp(distance) {
|
|
1799
|
+
const panDistance = distance ?? this.config.keyboardPanStep;
|
|
1800
|
+
const newTransform = {
|
|
1801
|
+
translateY: this.baseCanvas.transform.translateY + panDistance,
|
|
1802
|
+
};
|
|
1803
|
+
return this.updateTransform(newTransform);
|
|
1804
|
+
}
|
|
1805
|
+
panDown(distance) {
|
|
1806
|
+
const panDistance = distance ?? this.config.keyboardPanStep;
|
|
1807
|
+
const newTransform = {
|
|
1808
|
+
translateY: this.baseCanvas.transform.translateY - panDistance,
|
|
1809
|
+
};
|
|
1810
|
+
return this.updateTransform(newTransform);
|
|
1811
|
+
}
|
|
1812
|
+
// Zoom methods
|
|
1813
|
+
zoomIn(factor = 0.1) {
|
|
1814
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
1815
|
+
return withClampedZoom(this.config, (clamp) => {
|
|
1816
|
+
const newScale = clamp(this.baseCanvas.transform.scale * (1 + factor));
|
|
1817
|
+
const newTransform = {
|
|
1818
|
+
scale: newScale,
|
|
1819
|
+
};
|
|
1820
|
+
return this.updateTransform(newTransform);
|
|
1821
|
+
});
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
zoomOut(factor = 0.1) {
|
|
1825
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
1826
|
+
return withClampedZoom(this.config, (clamp) => {
|
|
1827
|
+
const newScale = clamp(this.baseCanvas.transform.scale * (1 - factor));
|
|
1828
|
+
const newTransform = {
|
|
1829
|
+
scale: newScale,
|
|
1830
|
+
};
|
|
1831
|
+
return this.updateTransform(newTransform);
|
|
1832
|
+
});
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
resetZoom() {
|
|
1836
|
+
return this.resetView();
|
|
1837
|
+
}
|
|
1838
|
+
// Mouse drag control methods
|
|
1839
|
+
enableMouseDrag() {
|
|
1840
|
+
return this.dragSetup?.enable() ?? false;
|
|
1841
|
+
}
|
|
1842
|
+
disableMouseDrag() {
|
|
1843
|
+
return this.dragSetup?.disable() ?? false;
|
|
1844
|
+
}
|
|
1845
|
+
isMouseDragEnabled() {
|
|
1846
|
+
return this.dragSetup?.isEnabled() ?? false;
|
|
1847
|
+
}
|
|
1848
|
+
// Grid control methods
|
|
1849
|
+
toggleGrid() {
|
|
1850
|
+
if (this.rulers?.toggleGrid) {
|
|
1851
|
+
this.rulers.toggleGrid();
|
|
1852
|
+
return true;
|
|
1853
|
+
}
|
|
1854
|
+
return false;
|
|
1855
|
+
}
|
|
1856
|
+
showGrid() {
|
|
1857
|
+
if (this.rulers?.gridOverlay) {
|
|
1858
|
+
this.rulers.gridOverlay.style.display = "block";
|
|
1859
|
+
return true;
|
|
1860
|
+
}
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
hideGrid() {
|
|
1864
|
+
if (this.rulers?.gridOverlay) {
|
|
1865
|
+
this.rulers.gridOverlay.style.display = "none";
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1868
|
+
return false;
|
|
1869
|
+
}
|
|
1870
|
+
isGridVisible() {
|
|
1871
|
+
if (this.rulers?.gridOverlay) {
|
|
1872
|
+
return this.rulers.gridOverlay.style.display !== "none";
|
|
1873
|
+
}
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
// Ruler control methods
|
|
1877
|
+
toggleRulers() {
|
|
1878
|
+
if (this.rulers) {
|
|
1879
|
+
const areVisible = this.areRulersVisible();
|
|
1880
|
+
if (areVisible) {
|
|
1881
|
+
this.rulers.hide();
|
|
1882
|
+
}
|
|
1883
|
+
else {
|
|
1884
|
+
this.rulers.show();
|
|
1885
|
+
}
|
|
1886
|
+
return true;
|
|
1887
|
+
}
|
|
1888
|
+
return false;
|
|
1889
|
+
}
|
|
1890
|
+
showRulers() {
|
|
1891
|
+
if (this.rulers) {
|
|
1892
|
+
this.rulers.show();
|
|
1893
|
+
return true;
|
|
1894
|
+
}
|
|
1895
|
+
return false;
|
|
1896
|
+
}
|
|
1897
|
+
hideRulers() {
|
|
1898
|
+
if (this.rulers) {
|
|
1899
|
+
this.rulers.hide();
|
|
1900
|
+
return true;
|
|
1901
|
+
}
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
areRulersVisible() {
|
|
1905
|
+
if (this.rulers?.horizontalRuler) {
|
|
1906
|
+
return this.rulers.horizontalRuler.style.display !== "none";
|
|
1907
|
+
}
|
|
1908
|
+
return false;
|
|
1909
|
+
}
|
|
1910
|
+
// Utility methods
|
|
1911
|
+
centerContent() {
|
|
1912
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
1913
|
+
const bounds = this.baseCanvas.getBounds();
|
|
1914
|
+
const centerX = (bounds.width - bounds.contentWidth * this.baseCanvas.transform.scale) / 2;
|
|
1915
|
+
const centerY = (bounds.height - bounds.contentHeight * this.baseCanvas.transform.scale) / 2;
|
|
1916
|
+
return this.updateTransform({
|
|
1917
|
+
translateX: centerX,
|
|
1918
|
+
translateY: centerY,
|
|
1919
|
+
});
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
fitToScreen() {
|
|
1923
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
1924
|
+
const result = this.baseCanvas.zoomToFitContent();
|
|
1925
|
+
if (result) {
|
|
1926
|
+
this.emitTransformEvents();
|
|
1927
|
+
}
|
|
1928
|
+
return result;
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
getVisibleArea() {
|
|
1932
|
+
const bounds = this.baseCanvas.getBounds();
|
|
1933
|
+
return bounds.visibleArea;
|
|
1934
|
+
}
|
|
1935
|
+
isPointVisible(x, y) {
|
|
1936
|
+
const visibleArea = this.getVisibleArea();
|
|
1937
|
+
return x >= visibleArea.x && x <= visibleArea.x + visibleArea.width && y >= visibleArea.y && y <= visibleArea.y + visibleArea.height;
|
|
1938
|
+
}
|
|
1939
|
+
scrollToPoint(x, y) {
|
|
1940
|
+
return withTransition(this.transformLayer, this.config, () => {
|
|
1941
|
+
const bounds = this.baseCanvas.getBounds();
|
|
1942
|
+
const centerX = bounds.width / 2;
|
|
1943
|
+
const centerY = bounds.height / 2;
|
|
1944
|
+
// Calculate new translation to center the point
|
|
1945
|
+
const newTranslateX = centerX - x * this.baseCanvas.transform.scale;
|
|
1946
|
+
const newTranslateY = centerY - y * this.baseCanvas.transform.scale;
|
|
1947
|
+
return this.updateTransform({
|
|
1948
|
+
translateX: newTranslateX,
|
|
1949
|
+
translateY: newTranslateY,
|
|
1950
|
+
});
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
// Configuration access
|
|
1954
|
+
getConfig() {
|
|
1955
|
+
return { ...this.config };
|
|
1956
|
+
}
|
|
1957
|
+
updateConfig(newConfig) {
|
|
1958
|
+
this.config = createMarkupCanvasConfig({ ...this.config, ...newConfig });
|
|
1959
|
+
}
|
|
1960
|
+
// Cleanup method
|
|
1961
|
+
cleanup() {
|
|
1962
|
+
this.cleanupFunctions.forEach((cleanup) => {
|
|
1963
|
+
try {
|
|
1964
|
+
cleanup();
|
|
1965
|
+
}
|
|
1966
|
+
catch (cleanupError) {
|
|
1967
|
+
console.warn("Error during cleanup:", cleanupError);
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
this.cleanupFunctions = [];
|
|
1971
|
+
// Remove all event listeners
|
|
1972
|
+
this.removeAllListeners();
|
|
1973
|
+
}
|
|
1974
|
+
// Event emitter delegation methods
|
|
1975
|
+
on(event, handler) {
|
|
1976
|
+
this.listen.on(event, handler);
|
|
1977
|
+
}
|
|
1978
|
+
off(event, handler) {
|
|
1979
|
+
this.listen.off(event, handler);
|
|
1980
|
+
}
|
|
1981
|
+
emit(event, data) {
|
|
1982
|
+
this.listen.emit(event, data);
|
|
1983
|
+
}
|
|
1984
|
+
removeAllListeners() {
|
|
1985
|
+
this.listen.removeAllListeners();
|
|
1986
|
+
}
|
|
1987
|
+
destroy() {
|
|
1988
|
+
this.cleanup();
|
|
1989
|
+
if (window.__markupCanvasTransitionTimeout) {
|
|
1990
|
+
clearTimeout(window.__markupCanvasTransitionTimeout);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
export { MarkupCanvas, MarkupCanvas as default };
|