287 lines
9.9 KiB
Vue
287 lines
9.9 KiB
Vue
<script lang="ts">
|
|
import { type Position } from '#shared/canvas.util';
|
|
import { hasPermissions } from '~/shared/auth.util';
|
|
|
|
const cancelEvent = (e: Event) => e.preventDefault();
|
|
function center(touches: TouchList): Position
|
|
{
|
|
const pos = { x: 0, y: 0 };
|
|
|
|
for(const touch of touches)
|
|
{
|
|
pos.x += touch.clientX;
|
|
pos.y += touch.clientY;
|
|
}
|
|
|
|
pos.x /= touches.length;
|
|
pos.y /= touches.length;
|
|
return pos;
|
|
}
|
|
function distance(touches: TouchList): number
|
|
{
|
|
const [A, B] = touches;
|
|
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
|
|
}
|
|
|
|
/*
|
|
|
|
stroke-light-red
|
|
stroke-light-orange
|
|
stroke-light-yellow
|
|
stroke-light-green
|
|
stroke-light-cyan
|
|
stroke-light-purple
|
|
dark:stroke-dark-red
|
|
dark:stroke-dark-orange
|
|
dark:stroke-dark-yellow
|
|
dark:stroke-dark-green
|
|
dark:stroke-dark-cyan
|
|
dark:stroke-dark-purple
|
|
fill-light-red
|
|
fill-light-orange
|
|
fill-light-yellow
|
|
fill-light-green
|
|
fill-light-cyan
|
|
fill-light-purple
|
|
dark:fill-dark-red
|
|
dark:fill-dark-orange
|
|
dark:fill-dark-yellow
|
|
dark:fill-dark-green
|
|
dark:fill-dark-cyan
|
|
dark:fill-dark-purple
|
|
bg-light-red
|
|
bg-light-orange
|
|
bg-light-yellow
|
|
bg-light-green
|
|
bg-light-cyan
|
|
bg-light-purple
|
|
dark:bg-dark-red
|
|
dark:bg-dark-orange
|
|
dark:bg-dark-yellow
|
|
dark:bg-dark-green
|
|
dark:bg-dark-cyan
|
|
dark:bg-dark-purple
|
|
border-light-red
|
|
border-light-orange
|
|
border-light-yellow
|
|
border-light-green
|
|
border-light-cyan
|
|
border-light-purple
|
|
dark:border-dark-red
|
|
dark:border-dark-orange
|
|
dark:border-dark-yellow
|
|
dark:border-dark-green
|
|
dark:border-dark-cyan
|
|
dark:border-dark-purple
|
|
outline-light-red
|
|
outline-light-orange
|
|
outline-light-yellow
|
|
outline-light-green
|
|
outline-light-cyan
|
|
outline-light-purple
|
|
dark:outline-dark-red
|
|
dark:outline-dark-orange
|
|
dark:outline-dark-yellow
|
|
dark:outline-dark-green
|
|
dark:outline-dark-cyan
|
|
dark:outline-dark-purple
|
|
|
|
*/
|
|
</script>
|
|
|
|
<script setup lang="ts">
|
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
import { clamp } from '#shared/general.util';
|
|
import type { CanvasContent } from '~/types/content';
|
|
|
|
const { path } = defineProps<{
|
|
path: string
|
|
}>();
|
|
|
|
const { user } = useUserSession();
|
|
const { content, get } = useContent();
|
|
const overview = computed(() => content.value.find(e => e.path === path) as CanvasContent | undefined);
|
|
const isOwner = computed(() => user.value?.id === overview.value?.owner);
|
|
|
|
const loading = ref(false);
|
|
if(overview.value && !overview.value.content)
|
|
{
|
|
loading.value = true;
|
|
await get(path);
|
|
loading.value = false;
|
|
}
|
|
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
|
|
|
|
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
|
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
|
|
|
|
const reset = (_: MouseEvent) => {
|
|
zoom.value = minZoom.value;
|
|
|
|
dispX.value = 0;
|
|
dispY.value = 0;
|
|
|
|
updateTransform();
|
|
}
|
|
|
|
onMounted(() => {
|
|
let lastX = 0, lastY = 0, lastDistance = 0;
|
|
let box = canvasRef.value?.getBoundingClientRect()!;
|
|
const dragMove = (e: MouseEvent) => {
|
|
dispX.value = dispX.value - (lastX - e.clientX) / zoom.value;
|
|
dispY.value = dispY.value - (lastY - e.clientY) / zoom.value;
|
|
|
|
lastX = e.clientX;
|
|
lastY = e.clientY;
|
|
|
|
updateTransform();
|
|
};
|
|
const dragEnd = (e: MouseEvent) => {
|
|
window.removeEventListener('mouseup', dragEnd);
|
|
window.removeEventListener('mousemove', dragMove);
|
|
};
|
|
canvasRef.value?.addEventListener('mouseenter', () => {
|
|
window.addEventListener('wheel', cancelEvent, { passive: false });
|
|
document.addEventListener('gesturestart', cancelEvent);
|
|
document.addEventListener('gesturechange', cancelEvent);
|
|
|
|
canvasRef.value?.addEventListener('mouseleave', () => {
|
|
window.removeEventListener('wheel', cancelEvent);
|
|
document.removeEventListener('gesturestart', cancelEvent);
|
|
document.removeEventListener('gesturechange', cancelEvent);
|
|
});
|
|
})
|
|
window.addEventListener('resize', () => box = canvasRef.value?.getBoundingClientRect()!);
|
|
canvasRef.value?.addEventListener('mousedown', (e) => {
|
|
if(e.button === 1)
|
|
{
|
|
lastX = e.clientX;
|
|
lastY = e.clientY;
|
|
|
|
window.addEventListener('mouseup', dragEnd, { passive: true });
|
|
window.addEventListener('mousemove', dragMove, { passive: true });
|
|
}
|
|
}, { passive: true });
|
|
canvasRef.value?.addEventListener('wheel', (e) => {
|
|
if((zoom.value >= 3 && e.deltaY < 0) || (zoom.value <= minZoom.value && e.deltaY > 0))
|
|
return;
|
|
|
|
const diff = Math.exp(e.deltaY * -0.001);
|
|
const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
|
|
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
|
|
|
dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
|
|
dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);
|
|
|
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3)
|
|
|
|
updateTransform();
|
|
}, { passive: true });
|
|
canvasRef.value?.addEventListener('touchstart', (e) => {
|
|
({ x: lastX, y: lastY } = center(e.touches));
|
|
|
|
if(e.touches.length > 1)
|
|
{
|
|
lastDistance = distance(e.touches);
|
|
}
|
|
|
|
canvasRef.value?.addEventListener('touchend', touchend, { passive: true });
|
|
canvasRef.value?.addEventListener('touchcancel', touchcancel, { passive: true });
|
|
canvasRef.value?.addEventListener('touchmove', touchmove, { passive: true });
|
|
}, { passive: true });
|
|
const touchend = (e: TouchEvent) => {
|
|
if(e.touches.length > 1)
|
|
{
|
|
({ x: lastX, y: lastY } = center(e.touches));
|
|
}
|
|
|
|
canvasRef.value?.removeEventListener('touchend', touchend);
|
|
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
|
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
|
};
|
|
const touchcancel = (e: TouchEvent) => {
|
|
if(e.touches.length > 1)
|
|
{
|
|
({ x: lastX, y: lastY } = center(e.touches));
|
|
}
|
|
|
|
canvasRef.value?.removeEventListener('touchend', touchend);
|
|
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
|
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
|
};
|
|
const touchmove = (e: TouchEvent) => {
|
|
const pos = center(e.touches);
|
|
dispX.value = dispX.value - (lastX - pos.x) / zoom.value;
|
|
dispY.value = dispY.value - (lastY - pos.y) / zoom.value;
|
|
lastX = pos.x;
|
|
lastY = pos.y;
|
|
|
|
if(e.touches.length === 2)
|
|
{
|
|
const dist = distance(e.touches);
|
|
const diff = dist / lastDistance;
|
|
|
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
|
}
|
|
|
|
updateTransform();
|
|
};
|
|
|
|
updateTransform();
|
|
});
|
|
|
|
function updateTransform()
|
|
{
|
|
if(transformRef.value)
|
|
{
|
|
transformRef.value.style.transform = `scale3d(${zoom.value}, ${zoom.value}, 1) translate3d(${dispX.value}px, ${dispY.value}px, 0)`;
|
|
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" :style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
|
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4">
|
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
|
<Tooltip message="Zoom avant" side="right">
|
|
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
<Icon icon="radix-icons:plus" />
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip message="Reset" side="right">
|
|
<div @click="zoom = 1; updateTransform();" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
<Icon icon="radix-icons:reload" />
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip message="Tout contenir" side="right">
|
|
<div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
<Icon icon="radix-icons:corners" />
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip message="Zoom arrière" side="right">
|
|
<div @click="zoom = clamp(zoom / 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
<Icon icon="radix-icons:minus" />
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
<NuxtLink v-if="overview && isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])" :to="{ name: 'explore-edit', hash: `#${encodeURIComponent(overview!.path)}` }" class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
|
<Tooltip message="Modifier" side="right">
|
|
<Icon icon="radix-icons:pencil-1" class="w-8 h-8 p-2" />
|
|
</Tooltip>
|
|
</NuxtLink>
|
|
</div>
|
|
<div ref="transformRef" :style="{
|
|
'transform-origin': 'center center',
|
|
}" class="h-full">
|
|
<div v-if="canvas" class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
|
|
<div>
|
|
<CanvasNode v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom" />
|
|
</div>
|
|
<div>
|
|
<CanvasEdge v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template> |