diff --git a/src/lib/holocene/icon/paths.ts b/src/lib/holocene/icon/paths.ts index d92b45ef13..c211d81a42 100644 --- a/src/lib/holocene/icon/paths.ts +++ b/src/lib/holocene/icon/paths.ts @@ -91,6 +91,7 @@ import merge from './svg/merge.svelte'; import microchip from './svg/microchip.svelte'; import microsoft from './svg/microsoft.svelte'; import minimize from './svg/minimize.svelte'; +import minus from './svg/minus.svelte'; import moon from './svg/moon.svelte'; import namespaceSwitcher from './svg/namespace-switcher.svelte'; import namespace from './svg/namespace.svelte'; @@ -102,6 +103,7 @@ import pencil from './svg/pencil.svelte'; import pinFilled from './svg/pin-filled.svelte'; import pin from './svg/pin.svelte'; import play from './svg/play.svelte'; +import plus from './svg/plus.svelte'; import regions from './svg/regions.svelte'; import relationship from './svg/relationship.svelte'; import retention from './svg/retention.svelte'; @@ -249,6 +251,7 @@ export const icons = { microchip, microsoft, minimize, + minus, moon, 'namespace-switcher': namespaceSwitcher, namespace, @@ -256,6 +259,7 @@ export const icons = { 'office-buildings': officeBuildings, overview, play, + plus, pause, pencil, 'pin-filled': pinFilled, diff --git a/src/lib/holocene/icon/svg/minus.svelte b/src/lib/holocene/icon/svg/minus.svelte new file mode 100644 index 0000000000..46ebb1453f --- /dev/null +++ b/src/lib/holocene/icon/svg/minus.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/lib/holocene/icon/svg/plus.svelte b/src/lib/holocene/icon/svg/plus.svelte new file mode 100644 index 0000000000..c840b281f3 --- /dev/null +++ b/src/lib/holocene/icon/svg/plus.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/lib/holocene/zoom-svg.svelte b/src/lib/holocene/zoom-svg.svelte index 0f78cc462f..a8573b3c71 100644 --- a/src/lib/holocene/zoom-svg.svelte +++ b/src/lib/holocene/zoom-svg.svelte @@ -12,14 +12,16 @@ export let pannable = true; let zoomLevel = initialZoom; + let panX = 0; + let panY = 0; let svg; $: viewBox = { - x: 0, - y: 0, - width, - height, + x: panX, + y: panY, + width: (width * zoomLevel) / initialZoom, + height: (height * zoomLevel) / initialZoom, }; let isPanning = false; @@ -28,6 +30,24 @@ let panOffsetX = 0; let panOffsetY = 0; + const PAN_STEP_RATIO = 0.1; + const ZOOM_STEP = 0.1; + + function panBy(dx: number, dy: number) { + if (!pannable) return; + panX += dx * viewBox.width; + panY += dy * viewBox.height; + } + + function zoomBy(factor: number, centerX = width / 2, centerY = height / 2) { + if (!zoomable) return; + const newZoomLevel = zoomLevel + factor; + if (newZoomLevel < maxZoomIn || newZoomLevel > maxZoomOut) return; + panX += (centerX * (zoomLevel - newZoomLevel)) / initialZoom; + panY += (centerY * (zoomLevel - newZoomLevel)) / initialZoom; + zoomLevel = newZoomLevel; + } + const handleWheel = (event: WheelEvent) => { if (!zoomable) return; event.preventDefault(); @@ -36,20 +56,12 @@ const mouseX = event.clientX - rect.left; const mouseY = event.clientY - rect.top; - const zoomAmount = event.deltaY * 0.001; - let newZoomLevel = zoomLevel + zoomAmount; + const newZoomLevel = zoomLevel + event.deltaY * 0.001; if (newZoomLevel < maxZoomIn || newZoomLevel > maxZoomOut) return; - const zoomRatio = newZoomLevel / zoomLevel; - const newWidth = viewBox.width * zoomRatio; - const newHeight = viewBox.height * zoomRatio; - - viewBox.x = mouseX - (mouseX - viewBox.x) * zoomRatio; - viewBox.y = mouseY - (mouseY - viewBox.y) * zoomRatio; - viewBox.width = newWidth; - viewBox.height = newHeight; - + panX += (mouseX * (zoomLevel - newZoomLevel)) / initialZoom; + panY += (mouseY * (zoomLevel - newZoomLevel)) / initialZoom; zoomLevel = newZoomLevel; }; @@ -58,8 +70,8 @@ isPanning = true; startX = event.clientX; startY = event.clientY; - panOffsetX = viewBox.x; - panOffsetY = viewBox.y; + panOffsetX = panX; + panOffsetY = panY; } function handleMouseMove(event: MouseEvent) { @@ -68,8 +80,8 @@ const dx = (startX - event.clientX) * (viewBox.width / svg.clientWidth); const dy = (startY - event.clientY) * (viewBox.height / svg.clientHeight); - viewBox.x = panOffsetX + dx; - viewBox.y = panOffsetY + dy; + panX = panOffsetX + dx; + panY = panOffsetY + dy; } function handleMouseUp() { @@ -81,28 +93,125 @@ } function onCenter() { - viewBox.x = 0; - viewBox.y = 0; - viewBox.width = width; - viewBox.height = height; + panX = 0; + panY = 0; zoomLevel = initialZoom; } + + function handleKeydown(event: KeyboardEvent) { + switch (event.key) { + case 'ArrowUp': + event.preventDefault(); + panBy(0, -PAN_STEP_RATIO); + break; + case 'ArrowDown': + event.preventDefault(); + panBy(0, PAN_STEP_RATIO); + break; + case 'ArrowLeft': + event.preventDefault(); + panBy(-PAN_STEP_RATIO, 0); + break; + case 'ArrowRight': + event.preventDefault(); + panBy(PAN_STEP_RATIO, 0); + break; + case '+': + case '=': + event.preventDefault(); + zoomBy(-ZOOM_STEP); + break; + case '-': + case '_': + event.preventDefault(); + zoomBy(ZOOM_STEP); + break; + } + } +
+
+
+ {#if pannable} + +