Documentación/Resumen

Lienzo de diseño

Un editor al estilo de Figma — frames y capas en un lienzo con zoom, guías inteligentes, un rectángulo de selección, Alt para duplicar, y un panel de capas que es el árbol del diseño.

Editar esta página

Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés

Live

Cargando la demo…

<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>

Qué observar

  • Shape.vue y Artboard.vue: cada capa es un elemento arrastrable simple, y un frame se arrastra por su nombre (handle), con sus capas dentro. Una pulsación selecciona de inmediato, como en Figma, así que la selección del provider es lo que se lleva un arrastre: pulsa una de varias capas seleccionadas y se mueven todas.
  • useLayerDrop.ts: el lienzo y cada frame son destinos de drop que funcionan igual. Una capa soltada sobre un frame entra en él; sobre el lienzo, sale de él. Mantén Alt y lo que aterriza son copias, y las capas se quedan donde estaban.
  • LayersPanel.vue: el panel es makeSortableTree sobre el propio diseño. El orden de las filas es el orden de apilamiento, y soltar una fila dentro de un frame mueve la capa a ese frame.
  • Canvas.vue: las guías inteligentes son un modificador propio de la demo; makeSelectionArea dibuja el rectángulo de selección, y makeDragArea da sus reglas a cada arrastre que empieza en el lienzo.
  • editor.ts, geometry.ts y design.ts: el diseño, su historial, la vista y las cuentas del ajuste y la alineación, sin nada de arrastrar y soltar.

Dibujado al zoom

El lienzo hace zoom sin scale() de CSS: cada capa se maqueta a su tamaño multiplicado por el zoom, en píxeles de pantalla. Lo que mide la biblioteca es entonces lo que hay en pantalla, así que los destinos de drop, el rectángulo de selección y los clones del overlay no necesitan nada especial. El mundo dentro del lienzo solo lo mueve el desplazamiento, con 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` };
});

La rueda desplaza y hace zoom (Ctrl o un pellizco), y también lo hacen arrastrar con Space, la herramienta mano y los botones de la esquina: eventos de puntero y de rueda normales, como las asas de redimensionar. Como Espacio es la mano, una capa se agarra solo con Enter (el keyboard.keys del provider).

Mover capas entre frames

Una capa es un elemento arrastrable simple con un overlay propio, DragGroup.vue. Coloca las capas arrastradas tal como están: un DragPreview (el clon tomado al agarrar) de cada una, colocado a partir del data de su capa, donde se dibuja. Las propias capas se apartan mientras sus clones se mueven, salvo que se mantenga Alt.

Shape.vue
const { isDragging, selected } = makeDraggable(el, {
  id: () => props.shape.id,
  type: 'layer',
  data: () => editor.dragData(props.shape), // { layer, left, top }
  overlay: DragGroup,
});

Al soltar, position es donde se soltó la esquina de la capa agarrada dentro del destino, en píxeles de pantalla. Dividido entre el zoom, dice cuánto se movió esa capa en el mundo, y cada capa arrastrada se mueve lo mismo, dentro del frame o sobre el lienzo:

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);

Un frame acepta capas, no frames: un arrastre que lleva un frame pasa por encima de los otros frames (disabled, leído en vivo) hasta el lienzo. Los destinos tienen overDropAnimation: false, porque las capas aparecen justo donde se soltaron sus clones. Las herramientas de la barra también son elementos arrastrables simples: soltadas en un frame, añaden su capa dentro de él, centrada en el punto del drop; con un clic, en el centro de la vista.

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),
});

Guías inteligentes

smartGuides es un modificador: recibe el desplazamiento del arrastre, calcula dónde estarían las capas arrastradas en el mundo, y las atrae al borde o al centro más cercano de otra capa a menos de 6 píxeles de pantalla. El overlay, el destino de debajo y el drop lo siguen. Un monitor dibuja las líneas rosas y las distancias a partir del delta del arrastre, el desplazamiento después de los modificadores.

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 });

Las asas de redimensionar se ajustan a las mismas líneas y dibujan las mismas guías.

Seleccionar

Un rectángulo de selección dibujado en el lienzo vacío selecciona frames y capas sueltas; dibujado dentro de un frame, las capas del frame. Los dos son makeSelectionArea: el área más interior se queda la pulsación, y es dueña de los elementos arrastrables que contiene. Shift añade a la selección (strategy: 'toggle').

Artboard.vue
const { isSelecting, style: band } = makeSelectionArea(body, {
  strategy: () => (editor.shiftHeld.value ? 'toggle' : 'select'),
  disabled: editor.panning,
});

El editor nunca selecciona un frame junto con una capa que contiene: al arrastrarla, la capa se movería dos veces. Un rectángulo captura el puntero al empezar, así que un clic en el espacio vacío de un frame nunca llega al frame: Artboard.vue selecciona el frame cuando una pulsación ahí se suelta donde empezó.

El panel de capas

Los ítems del árbol son el diseño: frames con sus children, la fila de arriba delante. canNest deja que los frames acepten capas y mantiene los frames en el nivel superior. Las filas tienen claves propias (row:…), porque los ids de las capas pertenecen a sus elementos arrastrables en el lienzo; la selección es la del lienzo, así que una fila se ilumina con su capa.

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 se ejecuta antes de que se escriba el árbol, así que el editor todavía sabe de qué frame viene cada capa, y guarda el diseño para deshacer. Una capa movida a un frame que no la cubre quedaría recortada fuera de la vista: en su lugar va al centro del frame. Con animateChanges, las filas también se deslizan con los cambios hechos en el lienzo y al deshacer. Una fila tiene tipo (type: 'row'), así que el lienzo y los frames, que aceptan capas y herramientas, la rechazan.

Teclado

Llega a una capa con Tab (queda seleccionada), luego Enter la agarra, las flechas la mueven 10 píxeles (Shift: 40), y Enter la suelta en el frame de debajo. Sin arrastre, las flechas mueven la selección un píxel. Una herramienta se agarra igual, y sus flechas saltan entre el lienzo y los frames (keyboardMode: 'droppables'). Los demás atajos aparecen en el panel Design cuando no hay nada seleccionado.

Más información: Arrastrar y soltar · Un modificador propio · Reglas para una parte de la página · Rectángulo de selección · Árboles