Remove Tweening (looked laggy). Add edge property editing. Improve Edge selection and visualisation.
This commit is contained in:
parent
348c991c54
commit
f32c51ca38
|
|
@ -25,8 +25,6 @@ interface HistoryAction<T extends Action>
|
||||||
type NodeEditor = InstanceType<typeof CanvasNodeEditor>;
|
type NodeEditor = InstanceType<typeof CanvasNodeEditor>;
|
||||||
type EdgeEditor = InstanceType<typeof CanvasEdgeEditor>;
|
type EdgeEditor = InstanceType<typeof CanvasEdgeEditor>;
|
||||||
|
|
||||||
const TWEEN_DURATION = 200;
|
|
||||||
|
|
||||||
const cancelEvent = (e: Event) => e.preventDefault();
|
const cancelEvent = (e: Event) => e.preventDefault();
|
||||||
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
||||||
function getID(length: number)
|
function getID(length: number)
|
||||||
|
|
@ -72,8 +70,6 @@ const focusing = ref<Element>(), editing = ref<Element>();
|
||||||
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
|
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
|
||||||
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
|
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
|
||||||
|
|
||||||
const xTween = useTween(dispX, linear, updateTransform), yTween = useTween(dispY, linear, updateTransform), zoomTween = useTween(zoom, linear, updateTransform);
|
|
||||||
|
|
||||||
const focused = computed(() => focusing.value ? focusing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === focusing.value!.id) : edges.value?.find(e => !!e && e.id === focusing.value!.id) : undefined), edited = computed(() => editing.value ? editing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === editing.value!.id) : edges.value?.find(e => !!e && e.id === editing.value!.id) : undefined);
|
const focused = computed(() => focusing.value ? focusing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === focusing.value!.id) : edges.value?.find(e => !!e && e.id === focusing.value!.id) : undefined), edited = computed(() => editing.value ? editing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === editing.value!.id) : edges.value?.find(e => !!e && e.id === editing.value!.id) : undefined);
|
||||||
|
|
||||||
const history = ref<HistoryEvent[]>([]);
|
const history = ref<HistoryEvent[]>([]);
|
||||||
|
|
@ -82,12 +78,9 @@ const historyCursor = computed(() => history.value.length > 0 && historyPos.valu
|
||||||
|
|
||||||
const reset = (_: MouseEvent) => {
|
const reset = (_: MouseEvent) => {
|
||||||
zoom.value = minZoom.value;
|
zoom.value = minZoom.value;
|
||||||
zoomTween.refresh();
|
|
||||||
|
|
||||||
dispX.value = 0;
|
dispX.value = 0;
|
||||||
xTween.refresh();
|
|
||||||
dispY.value = 0;
|
dispY.value = 0;
|
||||||
yTween.refresh();
|
|
||||||
|
|
||||||
updateTransform();
|
updateTransform();
|
||||||
}
|
}
|
||||||
|
|
@ -102,14 +95,12 @@ onMounted(() => {
|
||||||
let lastX = 0, lastY = 0, lastDistance = 0;
|
let lastX = 0, lastY = 0, lastDistance = 0;
|
||||||
let box = canvasRef.value?.getBoundingClientRect()!;
|
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||||
const dragMove = (e: MouseEvent) => {
|
const dragMove = (e: MouseEvent) => {
|
||||||
dispX.value -= (lastX - e.clientX) / zoom.value;
|
dispX.value = dispX.value - (lastX - e.clientX) / zoom.value;
|
||||||
dispY.value -= (lastY - e.clientY) / zoom.value;
|
dispY.value = dispY.value - (lastY - e.clientY) / zoom.value;
|
||||||
|
|
||||||
lastX = e.clientX;
|
lastX = e.clientX;
|
||||||
lastY = e.clientY;
|
lastY = e.clientY;
|
||||||
|
|
||||||
xTween.refresh();
|
|
||||||
yTween.refresh();
|
|
||||||
|
|
||||||
updateTransform();
|
updateTransform();
|
||||||
};
|
};
|
||||||
const dragEnd = (e: MouseEvent) => {
|
const dragEnd = (e: MouseEvent) => {
|
||||||
|
|
@ -146,10 +137,10 @@ onMounted(() => {
|
||||||
const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
|
const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
|
||||||
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
||||||
|
|
||||||
xTween.update(dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value), TWEEN_DURATION);
|
dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
|
||||||
yTween.update(dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value), TWEEN_DURATION);
|
dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);
|
||||||
|
|
||||||
zoomTween.update(clamp(zoom.value * diff, minZoom.value, 3), TWEEN_DURATION);
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3)
|
||||||
|
|
||||||
updateTransform();
|
updateTransform();
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
@ -187,8 +178,8 @@ onMounted(() => {
|
||||||
};
|
};
|
||||||
const touchmove = (e: TouchEvent) => {
|
const touchmove = (e: TouchEvent) => {
|
||||||
const pos = center(e.touches);
|
const pos = center(e.touches);
|
||||||
dispX.value -= (lastX - pos.x) / zoom.value;
|
dispX.value = dispX.value - (lastX - pos.x) / zoom.value;
|
||||||
dispY.value -= (lastY - pos.y) / zoom.value;
|
dispY.value = dispY.value - (lastY - pos.y) / zoom.value;
|
||||||
lastX = pos.x;
|
lastX = pos.x;
|
||||||
lastY = pos.y;
|
lastY = pos.y;
|
||||||
|
|
||||||
|
|
@ -197,13 +188,9 @@ onMounted(() => {
|
||||||
const dist = distance(e.touches);
|
const dist = distance(e.touches);
|
||||||
const diff = dist / lastDistance;
|
const diff = dist / lastDistance;
|
||||||
|
|
||||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3); //@TODO
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
zoomTween.refresh();
|
|
||||||
xTween.refresh();
|
|
||||||
yTween.refresh();
|
|
||||||
|
|
||||||
updateTransform();
|
updateTransform();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -214,8 +201,8 @@ function updateTransform()
|
||||||
{
|
{
|
||||||
if(transformRef.value)
|
if(transformRef.value)
|
||||||
{
|
{
|
||||||
transformRef.value.style.transform = `scale3d(${zoomTween.current()}, ${zoomTween.current()}, 1) translate3d(${xTween.current()}px, ${yTween.current()}px, 0)`;
|
transformRef.value.style.transform = `scale3d(${zoom.value}, ${zoom.value}, 1) translate3d(${dispX.value}px, ${dispY.value}px, 0)`;
|
||||||
transformRef.value.style.setProperty('--tw-scale', zoomTween.current().toString());
|
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function moveNode(ids: string[], deltax: number, deltay: number)
|
function moveNode(ids: string[], deltax: number, deltay: number)
|
||||||
|
|
@ -271,7 +258,10 @@ function edit(element: Element)
|
||||||
function createNode(e: MouseEvent)
|
function createNode(e: MouseEvent)
|
||||||
{
|
{
|
||||||
let box = canvasRef.value?.getBoundingClientRect()!;
|
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' };
|
const width = 250, height = 100;
|
||||||
|
const x = (e.layerX / zoom.value) - dispX.value - (width / 2);
|
||||||
|
const y = (e.layerY / zoom.value) - dispY.value - (height / 2);
|
||||||
|
const node: CanvasNode = { id: getID(16), x, y, width, height, type: 'text' };
|
||||||
|
|
||||||
if(!canvas.value.nodes)
|
if(!canvas.value.nodes)
|
||||||
canvas.value.nodes = [node];
|
canvas.value.nodes = [node];
|
||||||
|
|
@ -331,8 +321,25 @@ function editNodeProperty<T extends keyof CanvasNode>(ids: string[], property: T
|
||||||
|
|
||||||
addAction('property', actions);
|
addAction('property', actions);
|
||||||
}
|
}
|
||||||
|
function editEdgeProperty<T extends keyof CanvasEdge>(ids: string[], property: T, value: CanvasEdge[T])
|
||||||
|
{
|
||||||
|
if(ids.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const actions: HistoryAction<'property'>[] = [];
|
||||||
|
|
||||||
|
for(const id of ids)
|
||||||
|
{
|
||||||
|
const copy = JSON.parse(JSON.stringify(canvas.value.edges!.find(e => e.id === id)!)) as CanvasEdge;
|
||||||
|
canvas.value.edges!.find(e => e.id === id)![property] = value;
|
||||||
|
actions.push({ element: { type: 'edge', id }, from: copy, to: canvas.value.edges!.find(e => e.id === id)! });
|
||||||
|
}
|
||||||
|
|
||||||
|
addAction('property', actions);
|
||||||
|
}
|
||||||
|
|
||||||
const unselect = () => {
|
const unselect = () => {
|
||||||
|
if(focusing.value) console.log("Unselecting %s (%s)", focusing.value.id, focusing.value.type);
|
||||||
if(focusing.value !== undefined)
|
if(focusing.value !== undefined)
|
||||||
{
|
{
|
||||||
focused.value?.dom?.removeEventListener('click', stopPropagation);
|
focused.value?.dom?.removeEventListener('click', stopPropagation);
|
||||||
|
|
@ -488,16 +495,16 @@ useShortcuts({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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.self="createNode">
|
<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" @click="stopPropagation">
|
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4" @click="stopPropagation" @dblclick="stopPropagation">
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||||
<Tooltip message="Zoom avant" side="right">
|
<Tooltip message="Zoom avant" side="right">
|
||||||
<div @click="zoomTween.update(clamp(zoom * 1.1, minZoom, 3), TWEEN_DURATION); updateTransform()" 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 = 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" />
|
<Icon icon="radix-icons:plus" />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip message="Reset" side="right">
|
<Tooltip message="Reset" side="right">
|
||||||
<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">
|
<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" />
|
<Icon icon="radix-icons:reload" />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -507,7 +514,7 @@ useShortcuts({
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip message="Zoom arrière" side="right">
|
<Tooltip message="Zoom arrière" side="right">
|
||||||
<div @click="zoomTween.update(clamp(zoom / 1.1, minZoom, 3), TWEEN_DURATION); updateTransform()" 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 = 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" />
|
<Icon icon="radix-icons:minus" />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -614,7 +621,7 @@ useShortcuts({
|
||||||
<CanvasNodeEditor v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom" @select="select" @edit="edit" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" @input="(id, text) => editNodeProperty([id], node.type === 'group' ? 'label' : 'text', text)" />
|
<CanvasNodeEditor v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom" @select="select" @edit="edit" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" @input="(id, text) => editNodeProperty([id], node.type === 'group' ? 'label' : 'text', text)" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CanvasEdgeEditor v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" @select="select" @edit="edit" />
|
<CanvasEdgeEditor v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" @select="select" @edit="edit" @input="(id, text) => editEdgeProperty([id], 'label', text)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="absolute overflow-visible h-px w-px">
|
<div class="absolute overflow-visible">
|
||||||
<div v-if="edge.label" :style="{ transform: labelPos }" 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>
|
<input v-if="editing" @click="e => e.stopImmediatePropagation()" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20" v-model="edge.label" />
|
||||||
<input v-else-if="editing" @click="e => e.stopImmediatePropagation()" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20" :v-model="edge.label" />
|
<div v-else-if="edge.label" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20" @click.left="select" @dblclick.left="edit">{{ edge.label }}</div>
|
||||||
<svg ref="dom" class="absolute top-0 overflow-visible h-px w-px" @click="select" @dblclick="edit">
|
<svg ref="dom" class="absolute top-0 overflow-visible h-px w-px">
|
||||||
<g :style="{'--canvas-color': edge.color?.hex}" class="z-0">
|
<g :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'), { 'outline-4': focusing }]" class="fill-none stroke-[4px]" :d="path!.path"></path>
|
|
||||||
<g :style="`transform: translate(${path!.to.x}px, ${path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[path!.side]}deg);`">
|
<g :style="`transform: translate(${path!.to.x}px, ${path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[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>
|
<polygon :class="style.fill" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
|
||||||
</g>
|
</g>
|
||||||
|
<path :style="`stroke-width: calc(${focusing ? 6 : 3}px * var(--zoom-multiplier));`" style="stroke-linecap: butt;" :class="style.stroke" class="fill-none stroke-[4px]" :d="path!.path"></path>
|
||||||
|
<path style="stroke-width: calc(22px * var(--zoom-multiplier));" class="fill-none transition-opacity z-30 opacity-0 hover:opacity-25" :class="[style.stroke, { 'opacity-25': focusing }]" :d="path!.path" @click="select" @dblclick="edit"></path>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -34,24 +35,33 @@ const emit = defineEmits<{
|
||||||
(e: 'select', id: Element): void,
|
(e: 'select', id: Element): void,
|
||||||
(e: 'edit', id: Element): void,
|
(e: 'edit', id: Element): void,
|
||||||
(e: 'move', id: string, from: string, to: string): void,
|
(e: 'move', id: string, from: string, to: string): void,
|
||||||
|
(e: 'input', id: string, text?: string): void,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dom = useTemplateRef('dom');
|
const dom = useTemplateRef('dom');
|
||||||
const focusing = ref(false), editing = ref(false);
|
const focusing = ref(false), editing = ref(false);
|
||||||
|
|
||||||
const from = computed(() => nodes!.find(f => f.id === edge.fromNode))
|
const from = computed(() => nodes!.find(f => f.id === edge.fromNode));
|
||||||
const to = computed(() => nodes!.find(f => f.id === edge.toNode));
|
const to = computed(() => nodes!.find(f => f.id === edge.toNode));
|
||||||
const path = computed(() => getPath(from.value!, edge.fromSide, to.value!, edge.toSide));
|
const path = computed(() => getPath(from.value!, edge.fromSide, to.value!, edge.toSide));
|
||||||
const labelPos = computed(() => labelCenter(from.value!, edge.fromSide, to.value!, edge.toSide));
|
const labelPos = computed(() => labelCenter(from.value!, edge.fromSide, to.value!, edge.toSide));
|
||||||
|
|
||||||
|
let oldText = edge.label;
|
||||||
|
|
||||||
function select(e: Event) {
|
function select(e: Event) {
|
||||||
if(editing.value)
|
if(editing.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
console.log("Selecting %s (edge)", edge.id);
|
||||||
|
|
||||||
focusing.value = true;
|
focusing.value = true;
|
||||||
emit('select', { type: 'edge', id: edge.id });
|
emit('select', { type: 'edge', id: edge.id });
|
||||||
}
|
}
|
||||||
function edit(e: Event) {
|
function edit(e: Event) {
|
||||||
|
oldText = edge.label;
|
||||||
|
|
||||||
|
console.log("Editing %s (edge)", edge.id);
|
||||||
|
|
||||||
focusing.value = true;
|
focusing.value = true;
|
||||||
editing.value = true;
|
editing.value = true;
|
||||||
|
|
||||||
|
|
@ -59,9 +69,27 @@ function edit(e: Event) {
|
||||||
emit('edit', { type: 'edge', id: edge.id });
|
emit('edit', { type: 'edge', id: edge.id });
|
||||||
}
|
}
|
||||||
function unselect() {
|
function unselect() {
|
||||||
|
if(editing.value)
|
||||||
|
{
|
||||||
|
const text = edge.label;
|
||||||
|
|
||||||
|
if(text !== oldText)
|
||||||
|
{
|
||||||
|
edge.label = oldText;
|
||||||
|
|
||||||
|
emit('input', edge.id, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
focusing.value = false;
|
focusing.value = false;
|
||||||
editing.value = false;
|
editing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ unselect, dom, id: edge.id });
|
defineExpose({ unselect, dom, id: edge.id });
|
||||||
|
|
||||||
|
const style = computed(() => {
|
||||||
|
return edge.color ? edge.color?.class ?
|
||||||
|
{ fill: `fill-light-${edge.color?.class} dark:fill-dark-${edge.color?.class}`, stroke: `stroke-light-${edge.color?.class} dark:stroke-dark-${edge.color?.class}`, outline: `outline-light-${edge.color?.class} dark:outline-dark-${edge.color?.class}` } :
|
||||||
|
{ fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]`, outline: `outline-[color:var(--canvas-color)]` } :
|
||||||
|
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40`, outline: `outline-light-40 dark:outline-dark-40` }
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="absolute" ref="dom" :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 class="absolute" ref="dom" :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 || node.type === 'group'" style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4 cursor-move': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full">
|
<div v-if="!editing || node.type === 'group'" style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4 cursor-move': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
||||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :class="style.bg" @click.left="(e) => { if(node.type !== 'group') selectNode(e) }" @dblclick.left="(e) => { if(node.type !== 'group') editNode(e) }">
|
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg" @click.left="(e) => { if(node.type !== 'group') selectNode(e) }" @dblclick.left="(e) => { if(node.type !== 'group') editNode(e) }">
|
||||||
<div v-if="node.text?.length > 0" class="flex items-center">
|
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||||
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -108,12 +108,14 @@ function unselect() {
|
||||||
if(editing.value)
|
if(editing.value)
|
||||||
{
|
{
|
||||||
const text = node.type === 'group' ? node.label : node.text;
|
const text = node.type === 'group' ? node.label : node.text;
|
||||||
|
|
||||||
if(text !== oldText)
|
if(text !== oldText)
|
||||||
{
|
{
|
||||||
if(node.type === 'group')
|
if(node.type === 'group')
|
||||||
node.label = oldText;
|
node.label = oldText;
|
||||||
else
|
else
|
||||||
node.text = oldText;
|
node.text = oldText;
|
||||||
|
|
||||||
emit('input', node.id, text);
|
emit('input', node.id, text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import { clamp } 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)
|
|
||||||
{
|
|
||||||
// State variables for animation
|
|
||||||
let initial = ref.value;
|
|
||||||
let current = ref.value;
|
|
||||||
let end = ref.value;
|
|
||||||
let animationFrame: number;
|
|
||||||
let stop = true;
|
|
||||||
let last = 0;
|
|
||||||
|
|
||||||
// Velocity tracking with sign preservation
|
|
||||||
let velocity = 0;
|
|
||||||
const velocityDecay = 0.92; // Slightly faster decay for more responsive feel
|
|
||||||
|
|
||||||
function loop(t: DOMHighResTimeStamp) {
|
|
||||||
// Cap the elapsed time to prevent jumps during lag or tab switches
|
|
||||||
const elapsed = Math.min(t - last, 32);
|
|
||||||
last = t;
|
|
||||||
|
|
||||||
// Update velocity considering the sign of the movement
|
|
||||||
velocity = velocity * velocityDecay + (end - current) * (1 - velocityDecay);
|
|
||||||
|
|
||||||
// Apply velocity to current position
|
|
||||||
// Normalize by elapsed time to maintain consistent speed regardless of frame rate
|
|
||||||
current = clamp(current + velocity * (elapsed / 16), initial, end);
|
|
||||||
|
|
||||||
if(current === end)
|
|
||||||
stop = true;
|
|
||||||
|
|
||||||
// Trigger callback
|
|
||||||
then();
|
|
||||||
|
|
||||||
// Continue animation if not stopped
|
|
||||||
if (!stop) {
|
|
||||||
animationFrame = requestAnimationFrame(loop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
stop: () => {
|
|
||||||
cancelAnimationFrame(animationFrame);
|
|
||||||
stop = true;
|
|
||||||
velocity = 0; // Reset velocity when stopping
|
|
||||||
},
|
|
||||||
update: (target: number, duration: number) => {
|
|
||||||
if (stop) {
|
|
||||||
// Only reset initial position when starting a new animation
|
|
||||||
initial = current;
|
|
||||||
last = performance.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
end = target;
|
|
||||||
|
|
||||||
if (stop) {
|
|
||||||
stop = false;
|
|
||||||
loop(performance.now());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refresh: () => {
|
|
||||||
current = ref.value;
|
|
||||||
velocity = 0; // Reset velocity on refresh
|
|
||||||
},
|
|
||||||
current: () => 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.
Loading…
Reference in New Issue