obsidian-visualiser/components/CanvasEditor.vue

338 lines
18 KiB
Vue

<script lang="ts">
import type { Position } from '#shared/canvas.util';
type Direction = 'bottom' | 'top' | 'left' | 'right';
const rotation: Record<Direction, string> = {
top: "180",
bottom: "0",
left: "90",
right: "270"
};
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);
}
</script>
<script setup lang="ts">
import { useDrag, useHover, usePinch, useWheel } from '@vueuse/gesture';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { clamp } from '#shared/general.utils';
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';
import { labelCenter, getPath } from '#shared/canvas.util';
import FakeA from './prose/FakeA.vue';
const { canvas } = defineProps<{
canvas: CanvasContent
}>();
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
const focusing = ref<CanvasNode | CanvasEdge>(), editing = ref<CanvasNode | CanvasEdge>();
const canvasRef = useTemplateRef('canvasRef');
const nodes = computed(() => {
return canvas.nodes.map(e => ({ ...e, class: e.color ? e.color?.class ?
{ bg: `bg-light-${e.color?.class} dark:bg-dark-${e.color?.class}`, border: `border-light-${e.color?.class} dark:border-dark-${e.color?.class}`, outline: `outline-light-${e.color?.class} dark:outline-dark-${e.color?.class}` } :
{ bg: `bg-colored`, border: `border-[color:var(--canvas-color)]`, outline: `outline-[color:var(--canvas-color)]` } :
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40`, outline: `outline-light-40 dark:outline-dark-40` } }))
});
const edges = computed(() => {
return canvas.edges.map(e => {
const from = canvas.nodes.find(f => f.id === e.fromNode), to = canvas.nodes.find(f => f.id === e.toNode);
const path = getPath(from!, e.fromSide, to!, e.toSide)!;
return { ...e, from, to, path };
});
});
const reset = (_: MouseEvent) => {
zoom.value = minZoom.value;
dispX.value = 0;
dispY.value = 0;
}
onMounted(() => {
let lastX = 0, lastY = 0, lastDistance = 0;
let box = canvasRef.value?.getBoundingClientRect()!;
const dragMove = (e: MouseEvent) => {
dispX.value -= (lastX - e.clientX) / zoom.value;
dispY.value -= (lastY - e.clientY) / zoom.value;
lastX = e.clientX;
lastY = e.clientY;
};
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(!editing.value)
{
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 -= mousex / (diff * zoom.value) - mousex / zoom.value;
dispY.value -= mousey / (diff * zoom.value) - mousey / zoom.value;
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
}
}, { 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 -= (lastX - pos.x) / zoom.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 = lastDistance / dist;
zoom.value = clamp(zoom.value * diff, minZoom.value, 3); //@TODO
}
};
})
function selectNode(node: CanvasNode, e: MouseEvent)
{
const target = e.currentTarget as HTMLElement;
let lastX = 0, lastY = 0;
const unselect = (_e: MouseEvent) => {
if(_e.button === 0 && _e.target !== target)
{
focusing.value = undefined;
}
};
const dragStart = (_e: MouseEvent) => {
if(e.button !== 0)
return;
lastX = _e.clientX;
lastY= _e.clientY;
window.addEventListener('mousemove', dragMove, { passive: true });
window.addEventListener('mouseup', dragEnd, { passive: true });
};
const dragMove = (_e: MouseEvent) => {
node.x -= (lastX - e.clientX)/ zoom.value;
node.y -= (lastY - e.clientY)/ zoom.value;
focusing.value = node;
lastX = _e.clientX;
lastY= _e.clientY;
}
const dragEnd = (_e: MouseEvent) => {
window.removeEventListener('mousemove', dragMove);
window.removeEventListener('mouseup', dragEnd);
}
window.removeEventListener('mousemove', dragMove);
window.removeEventListener('mouseup', dragEnd);
target.removeEventListener('mousedown', dragStart);
focusing.value = node;
//canvasRef.value?.addEventListener('mousedown', unselect, { once: true, passive: true });
target.addEventListener('mousedown', dragStart, { passive: true });
}
function editNode(node: CanvasNode, e: MouseEvent)
{
editing.value = node;
}
function resizeNode(e: MouseEvent, x: number, y: number)
{
const target = e.currentTarget;
}
</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)" 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" 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 * 0.9, minZoom, 3)" 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>
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
<Tooltip message="Aide" side="right">
<Dialog title="Aide" iconClose>
<template #trigger>
<div 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:question-mark-circled" />
</div>
</template>
<template #default>
<div class="flex flex-row justify-between px-4">
<div class="flex flex-col gap-2">
<ProseH4>Ordinateur</ProseH4>
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Selectionner</div>
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Modifier</div>
<div class="flex items-center"><Icon icon="ph:mouse-middle-click-fill" class="w-6 h-6"/>: Déplacer</div>
<div class="flex items-center"><Icon icon="ph:mouse-right-click-fill" class="w-6 h-6"/>: Menu</div>
</div>
<div class="flex flex-col gap-2">
<ProseH4>Mobile</ProseH4>
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Selectionner</div>
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Modifier</div>
<div class="flex items-center"><Icon icon="mdi:gesture-pinch" class="w-6 h-6"/>: Zoomer</div>
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/> maintenu: Menu</div>
</div>
</div>
</template>
</Dialog>
</Tooltip>
</div>
</div>
<!-- <ContextMenuRoot>
<ContextMenuTrigger asChild> -->
<div :style="{
'--tw-translate-x': `${dispX}px`,
'--tw-translate-y': `${dispY}px`,
'--tw-scale': `${zoom}`,
'transform': 'scale3d(var(--tw-scale), var(--tw-scale), 1) translate3d(var(--tw-translate-x), var(--tw-translate-y), 0)',
'transform-origin': 'center center',
}" class="h-full">
<div class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
<div>
<div v-for="node of nodes" :key="node.id" class="absolute" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
<div v-if="editing?.id !== node.id" style="outline-style: solid;" :class="[node.class.border, node.class.outline, { '!outline-4 cursor-move': focusing?.id === node.id }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full flex">
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :class="node.class.bg" @click.left="(e) => selectNode(node, e)" @dblclick.left="(e) => editNode(node, e)">
<div v-if="node.text?.length > 0" class="flex items-center">
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
</div>
</div>
<div v-if="focusing?.id === node.id">
<span @mousedown="(e) => resizeNode(e, 0, -1)" id=" n" class="cursor-n-resize absolute -top-2 left-0 right-0 h-4"></span> <!-- North -->
<span @mousedown="(e) => resizeNode(e, 0, 1)" id=" s" class="cursor-s-resize absolute -bottom-2 left-0 right-0 h-4"></span> <!-- South -->
<span @mousedown="(e) => resizeNode(e, -1, 0)" id=" e" class="cursor-e-resize absolute top-0 bottom-0 -left-2 w-4"></span> <!-- East -->
<span @mousedown="(e) => resizeNode(e, 1, 0)" id=" w" class="cursor-w-resize absolute top-0 bottom-0 -right-2 w-4"></span> <!-- West -->
<span @mousedown="(e) => resizeNode(e, 1, -1)" id="nw" class="cursor-nw-resize absolute -top-2 -left-2 w-4 h-4"></span> <!-- North West -->
<span @mousedown="(e) => resizeNode(e, -1, -1)" id="ne" class="cursor-ne-resize absolute -top-2 -right-2 w-4 h-4"></span> <!-- North East -->
<span @mousedown="(e) => resizeNode(e, -1, 1)" id="se" class="cursor-se-resize absolute -bottom-2 -right-2 w-4 h-4"></span> <!-- South East -->
<span @mousedown="(e) => resizeNode(e, 1, 1)" id="sw" class="cursor-sw-resize absolute -bottom-2 -left-2 w-4 h-4"></span> <!-- South West -->
</div>
</div>
<div v-else style="outline-style: solid;" :class="[node.class.border, node.class.outline, { '!outline-4': focusing?.id === node.id }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex" >
<Editor v-model="node.text" />
</div>
<div v-if="node.type === 'group' && node.label !== undefined" :class="node.class.border" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>
</div>
</div>
<template v-for="edge of edges">
<div :key="edge.id" v-if="edge.label" class="absolute z-10"
:style="{ transform: labelCenter(edge.from!, edge.fromSide, edge.to!, edge.toSide) }">
<div class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 -translate-x-[50%] -translate-y-[50%]">{{ edge.label }}</div>
</div>
</template>
<svg class="absolute top-0 left-0 overflow-visible w-full h-full origin-top pointer-events-none">
<g v-for="edge of edges" :key="edge.id" :style="{'--canvas-color': edge.color?.hex}" class="z-0">
<path :style="`stroke-linecap: butt; stroke-width: calc(3px * var(--zoom-multiplier));`" :class="edge.color?.class ? `stroke-light-${edge.color.class} dark:stroke-dark-${edge.color.class}` : ((edge.color && edge.color?.hex !== undefined) ? 'stroke-[color:var(--canvas-color)]' : 'stroke-light-40 dark:stroke-dark-40')" class="fill-none stroke-[4px]" :d="edge.path.path"></path>
<g :style="`transform: translate(${edge.path.to.x}px, ${edge.path.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[edge.path.side]}deg);`">
<polygon :class="edge.color?.class ? `fill-light-${edge.color.class} dark:fill-dark-${edge.color.class}` : ((edge.color && edge.color?.hex !== undefined) ? 'fill-[color:var(--canvas-color)]' : 'fill-light-40 dark:fill-dark-40')" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
</g>
</g>
</svg>
</div>
</div><!--
</ContextMenuTrigger>
<ContextMenuPortal>
<ContextMenuContent>
<ContextMenuItem @select="(e) => canvas.nodes.push({ id: useId(), })" >Nouveau</ContextMenuItem>
</ContextMenuContent>
</ContextMenuPortal>
</ContextMenuRoot> -->
</div>
</template>