Dokumentation/Übersicht
Design-Canvas
Ein Editor wie Figma – Frames und Ebenen auf einer zoombaren Leinwand, Smart Guides, ein Auswahlrahmen, Alt zum Duplizieren und ein Ebenen-Panel, das der Baum des Designs ist.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider, type Announcements } from '@vue-dnd-kit/core';
import DesignCanvas from './DesignCanvas.vue';
import { KINDS } from './design';
import type { DragData } from './editor';
import type { ToolData } from './useLayerDrop';
// Screen readers hear names, not ids: a layer's, or the layer a tool adds. Rows of the
// layers panel keep the built-in texts (`undefined`), which say where they are in the tree.
const nameOf = (data: unknown) => {
const { layer, tool } = (data ?? {}) as Partial<DragData & ToolData>;
return layer?.name ?? (tool && `New ${KINDS[tool].name.toLowerCase()}`);
};
const placeOf = (over: { id: unknown; data: unknown }) =>
over.id === 'canvas' ? 'the canvas' : `frame ${(over.data as { name: string }).name}`;
const announcements: Partial<Announcements> = {
onDragStart: ({ active }, { count }) => {
const name = nameOf(active.data);
if (!name) return undefined;
const others = count > 1 ? ` and ${count - 1} more` : '';
return `Picked up ${name}${others}. Arrow keys move it, Enter drops it, Escape cancels.`;
},
onDragOver: ({ active, over }) => (nameOf(active.data) ? (over ? `Over ${placeOf(over)}` : 'Off the canvas') : undefined),
onDragEnd: ({ active, over, dropped }) => {
const name = nameOf(active.data);
if (!name) return undefined;
return dropped && over ? `${name} dropped on ${placeOf(over)}.` : `${name} went back.`;
},
onDragCancel: ({ active }) => (nameOf(active.data) ? `${nameOf(active.data)} went back.` : undefined),
};
</script>
<template>
<!-- Space pans, as in any design tool, so Enter alone picks a layer up. Every dragged
layer gets its clone at pickup, and the page doesn't scroll under a drag. -->
<DnDProvider
:keyboard="{ keys: { start: ['Enter'], drop: ['Enter'] }, step: 10 }"
overlay-previews="all"
:auto-scroll="false"
:announcements="announcements"
screen-reader-instructions="Press Enter to pick up a layer, the arrow keys to move it, Enter to drop it and Escape to cancel. Without a drag, the arrow keys nudge the selected layers."
>
<DesignCanvas />
</DnDProvider>
</template>Worauf du achten solltest
Shape.vueundArtboard.vue: Jede Ebene ist ein einfaches ziehbares Element, und ein Frame wird an seinem Namen gezogen (handle), mit seinen Ebenen darin. Ein Drücken wählt sofort aus, wie in Figma, also nimmt ein Drag die Auswahl des Providers mit: Drücke auf eine von mehreren ausgewählten Ebenen, und alle bewegen sich.useLayerDrop.ts: Die Leinwand und jeder Frame sind Drop-Ziele, die gleich funktionieren. Eine über einem Frame losgelassene Ebene wandert in ihn hinein; über der Leinwand aus ihm heraus. Halte Alt, und stattdessen landen Kopien, während die Ebenen bleiben, wo sie waren.LayersPanel.vue: Das Panel istmakeSortableTreeauf dem Design selbst. Die Reihenfolge der Zeilen ist die Stapelreihenfolge, und eine Zeile in einen Frame abzulegen verschiebt die Ebene in diesen Frame.Canvas.vue: Die Smart Guides sind ein eigener Modifier der Demo;makeSelectionAreazeichnet den Auswahlrahmen, undmakeDragAreagibt jedem Drag, der auf der Leinwand startet, seine Regeln.editor.ts,geometry.tsunddesign.ts: das Design, sein Verlauf, die Ansicht und die Mathematik des Einrastens und Ausrichtens, ganz ohne Drag and Drop.
Im Zoom gezeichnet
Die Leinwand zoomt ohne CSS-scale(): Jede Ebene wird in ihrer Größe mal dem Zoom angeordnet, in
Bildschirmpixeln. Was die Bibliothek misst, ist dann das, was auf dem Bildschirm ist, also
brauchen die Drop-Ziele, der Auswahlrahmen und die Klone im Overlay nichts Besonderes. Die Welt in
der Leinwand wird allein durch das Verschieben der Ansicht bewegt, mit translate().
const style = computed(() => {
const { zoom } = editor.view;
const { x, y, width, height } = props.shape;
return { left: `${x * zoom}px`, top: `${y * zoom}px`, width: `${width * zoom}px`, height: `${height * zoom}px` };
});
Das Mausrad verschiebt und zoomt (Ctrl oder ein Pinch), ebenso Ziehen mit
Space, das Hand-Werkzeug und die Buttons in der Ecke: einfache Zeiger- und
Mausrad-Events, wie auch die Resize-Handles. Da Space die Hand ist, nimmt allein
Enter eine Ebene auf (keyboard.keys des Providers).
Ebenen zwischen Frames verschieben
Eine Ebene ist ein einfaches ziehbares Element mit einem eigenen Overlay, DragGroup.vue. Es legt
die gezogenen Ebenen so aus, wie sie liegen: eine DragPreview (der beim Aufnehmen angelegte
Klon) von jeder, platziert aus dem data ihrer Ebene, dort, wo sie gezeichnet ist. Die Ebenen
selbst treten zur Seite, während sich ihre Klone bewegen, außer Alt wird gehalten.
const { isDragging, selected } = makeDraggable(el, {
id: () => props.shape.id,
type: 'layer',
data: () => editor.dragData(props.shape), // { layer, left, top }
overlay: DragGroup,
});
Beim Drop ist position die Stelle im Ziel, an der die Ecke der gegriffenen Ebene losgelassen
wurde, in Bildschirmpixeln. Durch den Zoom geteilt, sagt sie, wie weit diese Ebene in der Welt
gewandert ist, und jede gezogene Ebene wandert genauso weit, in den Frame oder auf die Leinwand:
const corner = target ? editor.boxOf(target) : { x: -view.x / view.zoom, y: -view.y / view.zoom };
const toWorld = (x: number, y: number) => ({ x: corner.x + x / view.zoom, y: corner.y + y / view.zoom });
const from = editor.boxOf(grabbed); // the layer of `active.id`
const to = toWorld(position.x, position.y);
editor.move(active.ids, { x: to.x - from.x, y: to.y - from.y }, target, editor.altHeld.value);
Ein Frame nimmt Ebenen an, keine Frames: Ein Drag, der einen Frame trägt, geht über die anderen
Frames hinweg (disabled, live gelesen) auf die Leinwand. Die Ziele haben overDropAnimation: false,
da die Ebenen genau dort erscheinen, wo ihre Klone losgelassen wurden. Werkzeuge in der
Werkzeugleiste sind ebenfalls einfache ziehbare Elemente: Auf einem Frame abgelegt, fügen sie ihre
Ebene in ihn ein, zentriert auf den Drop-Punkt; angeklickt, in der Mitte der Ansicht.
makeDroppable(body, {
id: () => props.frame.id,
accept: 'layer',
disabled: () => !!active.value?.items.some((item) => item.type.includes('frame')),
overDropAnimation: false,
onDrop: useLayerDrop(() => props.frame),
});
Smart Guides
smartGuides ist ein Modifier: Er bekommt den Versatz des Drags, rechnet aus, wo die gezogenen
Ebenen in der Welt wären, und zieht sie auf die nächste Kante oder Mitte einer anderen Ebene
innerhalb von 6 Bildschirmpixeln. Das Overlay, das Ziel darunter und der Drop folgen alle. Ein
Monitor zeichnet die pinken Linien und die Abstände aus dem delta des Drags, dem Versatz nach
den Modifiern.
const smartGuides: Modifier = ({ transform, active }) => {
const moving = editor.boundsOf(active.ids);
if (!moving) return transform; // a tool from the toolbar
const { zoom } = view;
const box = offsetBox(moving, transform.x / zoom, transform.y / zoom);
const targets = editor.snapTargets(active.ids);
return {
x: transform.x + snapBox(box, targets, 'x', SNAP / zoom) * zoom,
y: transform.y + snapBox(box, targets, 'y', SNAP / zoom) * zoom,
};
};
// Every drag that starts on the canvas, frames and layers alike, gets the guides.
makeDragArea(viewport, { modifiers: [smartGuides], disabled: panning });
Die Resize-Handles rasten an denselben Linien ein und zeichnen dieselben Hilfslinien.
Auswählen
Ein auf der leeren Leinwand gezogener Auswahlrahmen wählt Frames und lose Ebenen aus; in einem
Frame gezogen, die Ebenen des Frames. Beide sind makeSelectionArea: Der innerste Bereich nimmt
das Drücken und besitzt die ziehbaren Elemente darin. Shift fügt zur Auswahl hinzu
(strategy: 'toggle').
const { isSelecting, style: band } = makeSelectionArea(body, {
strategy: () => (editor.shiftHeld.value ? 'toggle' : 'select'),
disabled: editor.panning,
});
Der Editor wählt nie einen Frame zusammen mit einer Ebene darin aus: Gezogen, würde sich die Ebene
doppelt bewegen. Ein Rahmen fängt den Zeiger ein, wenn er startet, ein Klick auf die leere Fläche
eines Frames erreicht den Frame also nie: Artboard.vue wählt den Frame aus, wenn ein Drücken
dort an derselben Stelle losgelassen wird, an der es begann.
Das Ebenen-Panel
Die Elemente des Baums sind das Design: Frames mit ihren children, die oberste Zeile vorn.
canNest lässt Frames Ebenen annehmen und hält Frames auf der obersten Ebene. Zeilen haben eigene
Schlüssel (row:…), da die IDs der Ebenen ihren ziehbaren Elementen auf der Leinwand gehören; die
Auswahl ist die der Leinwand, eine Zeile leuchtet also mit ihrer Ebene auf.
makeSortableTree(tree, {
items: editor.layers,
getKey: (layer) => `row:${layer.id}`,
canNest: (layer, parent) => parent.kind === 'frame' && layer.kind !== 'frame',
maxDepth: 1,
animateChanges: true,
onDrop: ({ items, from, to }) => {
// Positions count from the frame's corner: a layer changing frames takes the
// difference, so it stays where it is on the canvas.
},
});
onDrop läuft, bevor der Baum geschrieben wird, der Editor weiß also noch, aus welchem Frame jede
Ebene kommt, und sichert das Design für Undo. Eine Ebene, die in einen Frame verschoben wird, der
sie nicht abdeckt, würde unsichtbar abgeschnitten: Sie kommt stattdessen in die Mitte des Frames.
Mit animateChanges gleiten Zeilen auch bei Änderungen auf der Leinwand und durch Undo. Eine Zeile hat
einen Typ (type: 'row'), also lehnen die Leinwand und die Frames, die Ebenen und Werkzeuge
annehmen, sie ab.
Tastatur
Springe mit Tab zu einer Ebene (sie wird ausgewählt), dann nimmt Enter sie auf, die
Pfeile bewegen sie um 10 Pixel (Shift: 40), und Enter legt sie in dem
Frame darunter ab. Ohne Drag schieben die Pfeile die Auswahl um ein Pixel. Ein Werkzeug wird
genauso aufgenommen, und seine Pfeile springen zwischen der Leinwand und den Frames
(keyboardMode: 'droppables'). Die anderen Tastenkürzel stehen im Design-Panel, wenn nichts
ausgewählt ist.
Mehr dazu: Drag and Drop · Ein eigener Modifier · Regeln für einen Teil der Seite · Auswahl per Rahmen · Bäume