Add Tweening to zoom, fix saving canvas.
This commit is contained in:
parent
4433cf0e00
commit
76db788192
|
|
@ -8,6 +8,7 @@ const rotation: Record<Direction, string> = {
|
|||
left: "90",
|
||||
right: "270"
|
||||
};
|
||||
type Element = { type: 'node' | 'edge', id: string };
|
||||
|
||||
interface ActionMap {
|
||||
move: Position;
|
||||
|
|
@ -73,14 +74,16 @@ const canvas = defineModel<CanvasContent>({ required: true, });
|
|||
|
||||
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
||||
const focusing = ref<string>(), editing = ref<string>();
|
||||
const canvasRef = useTemplateRef('canvasRef');
|
||||
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
|
||||
const nodes = useTemplateRef<InstanceType<typeof CanvasNodeEditor>[]>('nodes');
|
||||
|
||||
const xTween = useTween(dispX, linear, updateTransform), yTween = useTween(dispY, linear, updateTransform), zoomTween = useTween(zoom, linear, updateTransform);
|
||||
|
||||
const focusedNode = computed(() => nodes.value?.find(e => !!e && e.id === focusing.value)), editedNode = computed(() => nodes.value?.find(e => !!e && e.id === editing.value));
|
||||
|
||||
const edges = computed(() => {
|
||||
return canvas.value.edges.map(e => {
|
||||
const from = canvas.value.nodes.find(f => f.id === e.fromNode), to = canvas.value.nodes.find(f => f.id === e.toNode);
|
||||
return canvas.value.edges?.map(e => {
|
||||
const from = canvas.value.nodes!.find(f => f.id === e.fromNode), to = canvas.value.nodes!.find(f => f.id === e.toNode);
|
||||
const path = getPath(from!, e.fromSide, to!, e.toSide)!;
|
||||
return { ...e, from, to, path };
|
||||
});
|
||||
|
|
@ -91,10 +94,15 @@ const historyPos = ref(-1);
|
|||
const historyCursor = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);
|
||||
|
||||
const reset = (_: MouseEvent) => {
|
||||
zoom.value = minZoom.value;
|
||||
zoom.value = minZoom.value;
|
||||
zoomTween.refresh();
|
||||
|
||||
dispX.value = 0;
|
||||
dispX.value = 0;
|
||||
xTween.refresh();
|
||||
dispY.value = 0;
|
||||
yTween.refresh();
|
||||
|
||||
updateTransform();
|
||||
}
|
||||
|
||||
function addAction<T extends Action = Action>(event: T, actions: HistoryAction<T>[])
|
||||
|
|
@ -111,6 +119,11 @@ onMounted(() => {
|
|||
dispY.value -= (lastY - e.clientY) / zoom.value;
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
|
||||
xTween.refresh();
|
||||
yTween.refresh();
|
||||
|
||||
updateTransform();
|
||||
};
|
||||
const dragEnd = (e: MouseEvent) => {
|
||||
window.removeEventListener('mouseup', dragEnd);
|
||||
|
|
@ -146,10 +159,12 @@ onMounted(() => {
|
|||
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;
|
||||
xTween.update(dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value), 250);
|
||||
yTween.update(dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value), 250);
|
||||
|
||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
||||
zoomTween.update(clamp(zoom.value * diff, minZoom.value, 3), 250);
|
||||
|
||||
updateTransform();
|
||||
}, { passive: true });
|
||||
canvasRef.value?.addEventListener('touchstart', (e) => {
|
||||
({ x: lastX, y: lastY } = center(e.touches));
|
||||
|
|
@ -193,19 +208,35 @@ onMounted(() => {
|
|||
if(e.touches.length === 2)
|
||||
{
|
||||
const dist = distance(e.touches);
|
||||
const diff = lastDistance / dist;
|
||||
const diff = dist / lastDistance;
|
||||
|
||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3); //@TODO
|
||||
}
|
||||
|
||||
zoomTween.refresh();
|
||||
xTween.refresh();
|
||||
yTween.refresh();
|
||||
|
||||
updateTransform();
|
||||
};
|
||||
|
||||
updateTransform();
|
||||
});
|
||||
|
||||
function updateTransform()
|
||||
{
|
||||
if(transformRef.value)
|
||||
{
|
||||
transformRef.value.style.transform = `scale3d(${zoomTween.current()}, ${zoomTween.current()}, 1) translate3d(${xTween.current()}px, ${yTween.current()}px, 0)`;
|
||||
transformRef.value.style.setProperty('--tw-scale', zoomTween.current().toString());
|
||||
}
|
||||
}
|
||||
function moveNode(ids: string[], deltax: number, deltay: number)
|
||||
{
|
||||
const actions: HistoryAction<'move'>[] = [];
|
||||
for(const id of ids)
|
||||
{
|
||||
const node = canvas.value.nodes.find(e => e.id === id)!;
|
||||
const node = canvas.value.nodes!.find(e => e.id === id)!;
|
||||
|
||||
actions.push({ element: id, from: { x: node.x - deltax, y: node.y - deltay }, to: { x: node.x, y: node.y } });
|
||||
}
|
||||
|
|
@ -217,7 +248,7 @@ function resizeNode(ids: string[], deltax: number, deltay: number, deltaw: numbe
|
|||
const actions: HistoryAction<'resize'>[] = [];
|
||||
for(const id of ids)
|
||||
{
|
||||
const node = canvas.value.nodes.find(e => e.id === id)!;
|
||||
const node = canvas.value.nodes!.find(e => e.id === id)!;
|
||||
|
||||
actions.push({ element: id, from: { x: node.x - deltax, y: node.y - deltay, w: node.width - deltaw, h: node.height - deltah }, to: { x: node.x, y: node.y, w: node.width, h: node.height } });
|
||||
}
|
||||
|
|
@ -248,7 +279,11 @@ function createNode(e: MouseEvent)
|
|||
{
|
||||
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||
const node: CanvasNode = { id: getID(16), x: (e.layerX / zoom.value) - box.width / 2 - 50, y: (e.layerY / zoom.value) - box.height / 2 - 25, width: 100, height: 50, type: 'text' };
|
||||
canvas.value.nodes.push(node);
|
||||
|
||||
if(!canvas.value.nodes)
|
||||
canvas.value.nodes = [node];
|
||||
else
|
||||
canvas.value.nodes.push(node);
|
||||
|
||||
addAction('create', [{ element: node.id, from: undefined, to: node }]);
|
||||
}
|
||||
|
|
@ -259,10 +294,8 @@ function removeNode(ids: string[])
|
|||
|
||||
for(const id of ids)
|
||||
{
|
||||
const index = canvas.value.nodes.findIndex(e => e.id === id);
|
||||
actions.push({ element: id, from: canvas.value.nodes.splice(index, 1)[0], to: undefined });
|
||||
|
||||
console.log("Removing %s", id);
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === id);
|
||||
actions.push({ element: id, from: canvas.value.nodes!.splice(index, 1)[0], to: undefined });
|
||||
}
|
||||
|
||||
addAction('remove', actions);
|
||||
|
|
@ -273,9 +306,9 @@ function editNodeProperty<T extends keyof CanvasNode>(ids: string[], property: T
|
|||
|
||||
for(const id of ids)
|
||||
{
|
||||
const copy = JSON.parse(JSON.stringify(canvas.value.nodes.find(e => e.id === id)!)) as CanvasNode;
|
||||
canvas.value.nodes.find(e => e.id === id)![property] = value;
|
||||
actions.push({ element: id, from: copy, to: canvas.value.nodes.find(e => e.id === id)! });
|
||||
const copy = JSON.parse(JSON.stringify(canvas.value.nodes!.find(e => e.id === id)!)) as CanvasNode;
|
||||
canvas.value.nodes!.find(e => e.id === id)![property] = value;
|
||||
actions.push({ element: id, from: copy, to: canvas.value.nodes!.find(e => e.id === id)! });
|
||||
}
|
||||
|
||||
addAction('property', actions);
|
||||
|
|
@ -304,7 +337,7 @@ const undo = () => {
|
|||
|
||||
for(const action of historyCursor.value.actions)
|
||||
{
|
||||
const node = canvas.value.nodes.find(e => e.id === action.element)!;
|
||||
const node = canvas.value.nodes!.find(e => e.id === action.element)!;
|
||||
switch(historyCursor.value.event)
|
||||
{
|
||||
case 'move':
|
||||
|
|
@ -332,21 +365,21 @@ const undo = () => {
|
|||
case 'create':
|
||||
{
|
||||
const a = action as HistoryAction<'create'>;
|
||||
const index = canvas.value.nodes.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes.splice(index, 1);
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes!.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
case 'remove':
|
||||
{
|
||||
const a = action as HistoryAction<'remove'>;
|
||||
canvas.value.nodes.push(a.from!);
|
||||
canvas.value.nodes!.push(a.from!);
|
||||
break;
|
||||
}
|
||||
case 'property':
|
||||
{
|
||||
const a = action as HistoryAction<'property'>;
|
||||
const index = canvas.value.nodes.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes[index] = a.from;
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes![index] = a.from;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -370,7 +403,7 @@ const redo = () => {
|
|||
|
||||
for(const action of historyCursor.value.actions)
|
||||
{
|
||||
const node = canvas.value.nodes.find(e => e.id === action.element)!;
|
||||
const node = canvas.value.nodes!.find(e => e.id === action.element)!;
|
||||
switch(historyCursor.value.event)
|
||||
{
|
||||
case 'move':
|
||||
|
|
@ -398,21 +431,21 @@ const redo = () => {
|
|||
case 'create':
|
||||
{
|
||||
const a = action as HistoryAction<'remove'>;
|
||||
canvas.value.nodes.push(a.to!);
|
||||
canvas.value.nodes!.push(a.to!);
|
||||
break;
|
||||
}
|
||||
case 'remove':
|
||||
{
|
||||
const a = action as HistoryAction<'remove'>;
|
||||
const index = canvas.value.nodes.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes.splice(index, 1);
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes!.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
case 'property':
|
||||
{
|
||||
const a = action as HistoryAction<'property'>;
|
||||
const index = canvas.value.nodes.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes[index] = a.to;
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes![index] = a.to;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -429,16 +462,16 @@ useShortcuts({
|
|||
</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)) }" @dblclick.left="createNode">
|
||||
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4">
|
||||
<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)) }" @dblclick.left.self="createNode">
|
||||
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4" @click="stopPropagation">
|
||||
<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">
|
||||
<div @click="zoomTween.update(clamp(zoom * 1.1, minZoom, 3), 250); 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" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<div @click="zoom = 1; zoomTween.refresh(); 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>
|
||||
|
|
@ -448,7 +481,7 @@ useShortcuts({
|
|||
</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">
|
||||
<div @click="zoomTween.update(clamp(zoom / 1.1, minZoom, 3), 250); 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>
|
||||
|
|
@ -495,91 +528,80 @@ useShortcuts({
|
|||
</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 v-if="focusing !== undefined && focusedNode !== undefined" class="absolute z-20 origin-bottom" :style="{transform: `translate(${focusedNode.x}px, ${focusedNode.y}px) translateY(-100%) translateY(-12px) translateX(-50%) translateX(${focusedNode.width / 2}px) scale(calc(1 / var(--tw-scale)))`}">
|
||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row">
|
||||
<PopoverRoot>
|
||||
<PopoverTrigger asChild>
|
||||
<div @click="stopPropagation">
|
||||
<Tooltip message="Couleur" side="top">
|
||||
<div class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<Icon icon="ph:palette" class="w-6 h-6" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div ref="transformRef" :style="{
|
||||
'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 v-if="focusing !== undefined && focusedNode !== undefined" class="absolute z-20 origin-bottom" :style="{transform: `translate(${focusedNode.x}px, ${focusedNode.y}px) translateY(-100%) translateY(-12px) translateX(-50%) translateX(${focusedNode.width / 2}px) scale(calc(1 / var(--tw-scale)))`}">
|
||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row">
|
||||
<PopoverRoot>
|
||||
<PopoverTrigger asChild>
|
||||
<div @click="stopPropagation">
|
||||
<Tooltip message="Couleur" side="top">
|
||||
<div class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<Icon icon="ph:palette" class="w-6 h-6" />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverPortal disabled>
|
||||
<PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
|
||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
|
||||
<div @click="editNodeProperty([focusing], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block"></span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverPortal disabled>
|
||||
<PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
|
||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
|
||||
<div @click="editNodeProperty([focusing], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
|
||||
</div>
|
||||
<label>
|
||||
<div @click="stopPropagation" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block relative"></span><input @change="(e: Event) => editNodeProperty([focusing!], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</PopoverRoot>
|
||||
<Tooltip message="Supprimer" side="top">
|
||||
<div @click="removeNode([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</PopoverRoot>
|
||||
<Tooltip message="Supprimer" side="top">
|
||||
<div @click="removeNode([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CanvasNodeEditor v-for="(node, index) of canvas.nodes" :key="node.id" ref="nodes" :node="node" :index="index" :zoom="zoom" @select="selectNode" @edit="editNode" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" />
|
||||
</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>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div><!--
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem @select="(e) => canvas.value.nodes.push({ id: useId(), })" >Nouveau</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuRoot> -->
|
||||
</div>
|
||||
<div>
|
||||
<CanvasNodeEditor v-for="(node, index) of canvas.nodes" :key="node.id" ref="nodes" :node="node" :index="index" :zoom="zoom" @select="selectNode" @edit="editNode" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" />
|
||||
</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 w-1 h-1 overflow-visible">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { clamp, lerp } from "~/shared/general.utils";
|
||||
|
||||
export const linear = (progress: number): number => progress;
|
||||
export const ease = (progress: number): number => -(Math.cos(Math.PI * progress) - 1) / 2;
|
||||
|
||||
export function useTween(ref: Ref<number>, animation: (progress: number) => number, then: () => void)
|
||||
{
|
||||
let initial = ref.value, current = ref.value, end = ref.value, progress = 0, time = 0, animationFrame: number, stop = true, last = 0;
|
||||
|
||||
function loop(t: DOMHighResTimeStamp)
|
||||
{
|
||||
const elapsed = t - last;
|
||||
progress = clamp(progress + elapsed, 0, time);
|
||||
last = t;
|
||||
|
||||
const step = animation(clamp(progress / time, 0, 1));
|
||||
current = lerp(initial, end, step);
|
||||
then();
|
||||
|
||||
if(progress < time && !stop)
|
||||
{
|
||||
animationFrame = requestAnimationFrame(loop);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress = 0;
|
||||
stop = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
stop: () => {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
stop = true;
|
||||
},
|
||||
update: (target: number, duration: number) => {
|
||||
initial = current;
|
||||
time = duration + progress;
|
||||
end = target;
|
||||
|
||||
ref.value = target;
|
||||
|
||||
if(stop)
|
||||
{
|
||||
stop = false;
|
||||
last = performance.now();
|
||||
|
||||
loop(performance.now());
|
||||
}
|
||||
},
|
||||
refresh: () => { current = ref.value; },
|
||||
current: () => { return current; },
|
||||
};
|
||||
}
|
||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -206,7 +206,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
||||
import { convertContent, parsePath } from '#shared/general.utils';
|
||||
import { convertContentFromText, convertContentToText, parsePath } from '#shared/general.utils';
|
||||
import type { CanvasContent, ExploreContent, FileType, TreeItem } from '~/types/content';
|
||||
import { iconByType } from '#shared/general.utils';
|
||||
import FakeA from '~/components/prose/FakeA.vue';
|
||||
|
|
@ -247,7 +247,7 @@ watch(selected, async (value, old) => {
|
|||
|
||||
if(storedEdit)
|
||||
{
|
||||
selected.value.content = convertContent(selected.value.type, storedEdit);
|
||||
selected.value.content = convertContentFromText(selected.value.type, storedEdit);
|
||||
contentStatus.value = 'success';
|
||||
}
|
||||
else
|
||||
|
|
@ -518,11 +518,12 @@ function rebuildPath(tree: TreeItemEditable[] | null | undefined, parentPath: st
|
|||
}
|
||||
async function save(redirect: boolean): Promise<void>
|
||||
{
|
||||
const map = (e: TreeItemEditable[]): TreeItemEditable[] => e.map(f => ({ ...f, content: f.content ? convertContentToText(f.type, f.content) : undefined, children: f.children ? map(f.children) : undefined }));
|
||||
saveStatus.value = 'pending';
|
||||
try {
|
||||
const result = await $fetch(`/api/project`, {
|
||||
method: 'post',
|
||||
body: navigation.value,
|
||||
body: map(navigation.value),
|
||||
});
|
||||
saveStatus.value = 'success';
|
||||
edited.value = false;
|
||||
|
|
@ -537,6 +538,7 @@ async function save(redirect: boolean): Promise<void>
|
|||
toaster.add({
|
||||
type: 'error', content: e.message, timer: true, duration: 10000
|
||||
})
|
||||
console.error(e);
|
||||
saveStatus.value = 'error';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { eq, sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { convertContent } from '~/shared/general.utils';
|
||||
import { convertContentFromText } from '~/shared/general.utils';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
|
||||
|
|
@ -47,7 +47,7 @@ export default defineEventHandler(async (e) => {
|
|||
content.content = convertFromStorableLinks(content.content);
|
||||
}
|
||||
|
||||
return convertContent(content.type, content.content);
|
||||
return convertContentFromText(content.type, content.content);
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { CanvasContent } from '~/types/canvas';
|
||||
import type { FileType } from '~/types/content';
|
||||
import type { ContentMap, FileType } from '~/types/content';
|
||||
|
||||
export function unifySlug(slug: string | string[]): string
|
||||
{
|
||||
|
|
@ -47,7 +47,10 @@ export function clamp(x: number, min: number, max: number): number {
|
|||
return min;
|
||||
return x;
|
||||
}
|
||||
export function convertContent(type: FileType, content: string): CanvasContent | string {
|
||||
export function lerp(a: number, b: number, t: number) {
|
||||
return a + t * (b - a);
|
||||
}
|
||||
export function convertContentFromText(type: FileType, content: string): CanvasContent | string {
|
||||
switch(type)
|
||||
{
|
||||
case 'canvas':
|
||||
|
|
@ -61,6 +64,20 @@ export function convertContent(type: FileType, content: string): CanvasContent |
|
|||
return content;
|
||||
}
|
||||
}
|
||||
export function convertContentToText(type: FileType, content: any): string {
|
||||
switch(type)
|
||||
{
|
||||
case 'canvas':
|
||||
return JSON.stringify(content);
|
||||
case 'map':
|
||||
case 'file':
|
||||
case 'folder':
|
||||
case 'markdown':
|
||||
return content;
|
||||
default:
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
export const iconByType: Record<FileType, string> = {
|
||||
'folder': 'lucide:folder',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export interface CanvasContent {
|
||||
nodes: CanvasNode[];
|
||||
edges: CanvasEdge[];
|
||||
groups: CanvasGroup[];
|
||||
nodes?: CanvasNode[];
|
||||
edges?: CanvasEdge[];
|
||||
groups?: CanvasGroup[];
|
||||
}
|
||||
export type CanvasColor = {
|
||||
class?: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue