Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/lib/holocene/icon/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -249,13 +251,15 @@ export const icons = {
microchip,
microsoft,
minimize,
minus,
moon,
'namespace-switcher': namespaceSwitcher,
namespace,
nexus,
'office-buildings': officeBuildings,
overview,
play,
plus,
pause,
pencil,
'pin-filled': pinFilled,
Expand Down
8 changes: 8 additions & 0 deletions src/lib/holocene/icon/svg/minus.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import Svg from '../svg.svelte';
let props = $props();
</script>

<Svg {...props}>
<path d="M19 13H5V11H19V13Z" fill="currentcolor" />
</Svg>
8 changes: 8 additions & 0 deletions src/lib/holocene/icon/svg/plus.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import Svg from '../svg.svelte';
let props = $props();
</script>

<Svg {...props}>
<path d="M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z" fill="currentcolor" />
</Svg>
155 changes: 132 additions & 23 deletions src/lib/holocene/zoom-svg.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
export let pannable = true;

let zoomLevel = initialZoom;
let panX = 0;
let panY = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Variable 'svg' implicitly has type 'any' in some locations where its type cannot be determined.

let svg;

$: viewBox = {
x: 0,
y: 0,
width,
height,
x: panX,
y: panY,
width: (width * zoomLevel) / initialZoom,
height: (height * zoomLevel) / initialZoom,
};

let isPanning = false;
Expand All @@ -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();
Expand All @@ -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;
};

Expand All @@ -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) {
Expand All @@ -68,8 +80,8 @@
const dx = (startX - event.clientX) * (viewBox.width / svg.clientWidth);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Variable 'svg' implicitly has an 'any' type.

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() {
Expand All @@ -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;
}
}
</script>

<!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_noninteractive_element_interactions -->
<div
class="relative overflow-hidden"
tabindex="0"
role="group"
aria-label="Zoomable workflow graph. Use arrow keys to pan, plus and minus to zoom."
on:keydown={handleKeydown}
bind:clientWidth={width}
bind:clientHeight={height}
style="height: min({containerHeight}px, calc(100dvh - 8rem));"
>
<div class="absolute right-4 top-4 z-20 flex items-center gap-2">
<slot name="controls" />
</div>
<div class="absolute bottom-4 right-4 z-20 flex items-center gap-2">
{#if pannable}
<Tooltip text="Pan up" bottom>
<Button
variant="secondary"
size="sm"
leadingIcon="chevron-up"
aria-label="Pan up"
on:click={() => panBy(0, -PAN_STEP_RATIO)}
/>
</Tooltip>
<Tooltip text="Pan down" bottom>
<Button
variant="secondary"
size="sm"
leadingIcon="chevron-down"
aria-label="Pan down"
on:click={() => panBy(0, PAN_STEP_RATIO)}
/>
</Tooltip>
<Tooltip text="Pan left" bottom>
<Button
variant="secondary"
size="sm"
leadingIcon="chevron-left"
aria-label="Pan left"
on:click={() => panBy(-PAN_STEP_RATIO, 0)}
/>
</Tooltip>
<Tooltip text="Pan right" bottom>
<Button
variant="secondary"
size="sm"
leadingIcon="chevron-right"
aria-label="Pan right"
on:click={() => panBy(PAN_STEP_RATIO, 0)}
/>
</Tooltip>
{/if}
{#if zoomable}
<Tooltip text="Zoom in" bottom>
<Button
variant="secondary"
size="sm"
leadingIcon="plus"
aria-label="Zoom in"
disabled={zoomLevel - ZOOM_STEP < maxZoomIn}
on:click={() => zoomBy(-ZOOM_STEP)}
/>
</Tooltip>
<Tooltip text="Zoom out" bottom>
<Button
variant="secondary"
size="sm"
leadingIcon="minus"
aria-label="Zoom out"
disabled={zoomLevel + ZOOM_STEP > maxZoomOut}
on:click={() => zoomBy(ZOOM_STEP)}
/>
</Tooltip>
{/if}
<Tooltip text="Center" bottom>
<Button
class="cursor-pointer"
variant="secondary"
size="sm"
leadingIcon="target"
aria-label="Center"
on:click={() => {
onCenter();
}}
Expand Down
Loading