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

Live

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.vue und Artboard.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 ist makeSortableTree auf 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; makeSelectionArea zeichnet den Auswahlrahmen, und makeDragArea gibt jedem Drag, der auf der Leinwand startet, seine Regeln.
  • editor.ts, geometry.ts und design.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().

Shape.vue
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.

Shape.vue
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:

useLayerDrop.ts
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.

Artboard.vue
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.

Canvas.vue
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').

Artboard.vue
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.

LayersPanel.vue
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