From 76db7881922fee0c2857bf696dddec2a28f354d9 Mon Sep 17 00:00:00 2001 From: Peaceultime Date: Tue, 14 Jan 2025 00:04:14 +0100 Subject: [PATCH] Add Tweening to zoom, fix saving canvas. --- components/CanvasEditor.vue | 256 ++++++++++++++------------ composables/useTween.ts | 54 ++++++ db.sqlite | Bin 585728 -> 585728 bytes db.sqlite-shm | Bin 32768 -> 32768 bytes db.sqlite-wal | Bin 1359632 -> 0 bytes pages/explore/edit/index.vue | 8 +- server/api/file/content/[path].get.ts | 4 +- shared/general.utils.ts | 21 ++- types/canvas.d.ts | 6 +- 9 files changed, 222 insertions(+), 127 deletions(-) create mode 100644 composables/useTween.ts diff --git a/components/CanvasEditor.vue b/components/CanvasEditor.vue index d29d2a3..6c1b3a5 100644 --- a/components/CanvasEditor.vue +++ b/components/CanvasEditor.vue @@ -8,6 +8,7 @@ const rotation: Record = { left: "90", right: "270" }; +type Element = { type: 'node' | 'edge', id: string }; interface ActionMap { move: Position; @@ -73,14 +74,16 @@ const canvas = defineModel({ required: true, }); const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5); const focusing = ref(), editing = ref(); -const canvasRef = useTemplateRef('canvasRef'); +const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'); const nodes = useTemplateRef[]>('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(event: T, actions: HistoryAction[]) @@ -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(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({ \ No newline at end of file diff --git a/composables/useTween.ts b/composables/useTween.ts new file mode 100644 index 0000000..fe9b7df --- /dev/null +++ b/composables/useTween.ts @@ -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, 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; }, + }; +} \ No newline at end of file diff --git a/db.sqlite b/db.sqlite index 4bb3e5f344cac1ad8571bf56e0dba63dcb951bc3..eee28c5cfbd2402444f539d78ccb31b53de21aa8 100644 GIT binary patch delta 3688 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 delta 3444 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 diff --git a/db.sqlite-wal b/db.sqlite-wal index 3089977853c462adee1d91a90f9db5925e88de4d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 1359632 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