Документация/Обзор
Холст для дизайна
Редактор в духе Figma — фреймы и слои на масштабируемом холсте, умные направляющие, рамка выделения, Alt для дублирования и панель слоёв, которая и есть дерево дизайна.
Страница переведена ИИ и может содержать неточности. Читать английский оригинал
Загружаем демо…
<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>На что обратить внимание
Shape.vueиArtboard.vue: каждый слой — обычный перетаскиваемый элемент, а фрейм тащат за его имя (handle), вместе со слоями внутри. Нажатие выделяет сразу, как в Figma, так что драг берёт выделение провайдера: нажмите на один из нескольких выделенных слоёв, и двинутся все.useLayerDrop.ts: холст и каждый фрейм — цели, которые работают одинаково. Слой, отпущенный над фреймом, перемещается в него; над холстом — из него. Удерживайте Alt, и вместо этого приземляются копии, а слои остаются на месте.LayersPanel.vue: панель — этоmakeSortableTreeна самом дизайне. Порядок строк — порядок наложения, а бросок строки во фрейм перемещает слой в этот фрейм.Canvas.vue: умные направляющие — собственный модификатор демо;makeSelectionAreaрисует рамку выделения, аmakeDragAreaдаёт свои правила каждому драгу, который начинается на холсте.editor.ts,geometry.tsиdesign.ts: дизайн, его история, вид и математика привязки и выравнивания, без всякого drag and drop.
Нарисовано в масштабе
Холст масштабируется без CSS scale(): каждый слой раскладывается в своём размере, умноженном на
масштаб, в экранных пикселях. Тогда то, что измеряет библиотека, — это то, что на экране, так
что целям, рамке выделения и клонам в оверлее не нужно ничего особенного. Мир внутри холста
сдвигается только панорамированием, через 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` };
});
Колесо панорамирует и масштабирует (Ctrl или щипок), так же как драг с
Space, инструмент «рука» и кнопки в углу: обычные события указателя и колеса, как и
ручки изменения размера. Раз Space — это рука, слой берётся одной клавишей Enter
(keyboard.keys провайдера).
Перемещение слоёв между фреймами
Слой — обычный перетаскиваемый элемент с собственным оверлеем, DragGroup.vue. Он раскладывает
перетаскиваемые слои так, как они лежат: DragPreview (клон, снятый при захвате) каждого,
размещённый по data его слоя, там, где тот нарисован. Сами слои отходят в сторону, пока
двигаются их клоны, если не зажат Alt.
const { isDragging, selected } = makeDraggable(el, {
id: () => props.shape.id,
type: 'layer',
data: () => editor.dragData(props.shape), // { layer, left, top }
overlay: DragGroup,
});
При броске position — место внутри цели, где отпустили угол взятого слоя, в экранных пикселях.
Делённое на масштаб, оно говорит, насколько этот слой сдвинулся в мире, и каждый
перетаскиваемый слой сдвигается на столько же — во фрейм или на холст:
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);
Фрейм принимает слои, а не фреймы: драг, который несёт фрейм, проходит над другими фреймами
(disabled, читается вживую) на холст. У целей overDropAnimation: false, так как слои появляются
ровно там, где отпустили их клоны. Инструменты на панели инструментов — тоже обычные
перетаскиваемые элементы: брошенные на фрейм, они добавляют в него свой слой с центром в точке
броска; по клику — в середину вида.
makeDroppable(body, {
id: () => props.frame.id,
accept: 'layer',
disabled: () => !!active.value?.items.some((item) => item.type.includes('frame')),
overDropAnimation: false,
onDrop: useLayerDrop(() => props.frame),
});
Умные направляющие
smartGuides — модификатор: он получает смещение драга, вычисляет, где в мире оказались бы
перетаскиваемые слои, и притягивает их к ближайшему краю или центру другого слоя в пределах 6
экранных пикселей. Оверлей, цель под ним и бросок следуют за этим. Монитор рисует розовые линии и
расстояния по delta драга — смещению после модификаторов.
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 });
Ручки изменения размера привязываются по тем же линиям и рисуют те же направляющие.
Выделение
Рамка, нарисованная на пустом холсте, выделяет фреймы и отдельные слои; нарисованная внутри
фрейма — слои фрейма. Обе — makeSelectionArea: нажатие забирает самая внутренняя область, и
ей принадлежат перетаскиваемые элементы внутри неё. Shift добавляет к выделению
(strategy: 'toggle').
const { isSelecting, style: band } = makeSelectionArea(body, {
strategy: () => (editor.shiftHeld.value ? 'toggle' : 'select'),
disabled: editor.panning,
});
Редактор никогда не выделяет фрейм вместе со слоем внутри него: при драге слой сдвинулся бы
дважды. Рамка захватывает указатель, когда начинается, поэтому клик по пустому месту фрейма
никогда не доходит до фрейма: Artboard.vue выделяет фрейм, когда нажатие там отпущено в том же
месте, где началось.
Панель слоёв
Элементы дерева — это дизайн: фреймы со своими children, верхняя строка — впереди. canNest
позволяет фреймам принимать слои и держит фреймы на верхнем уровне. У строк собственные ключи
(row:…), так как id слоёв принадлежат их перетаскиваемым элементам на холсте; выделение — то же,
что у холста, так что строка подсвечивается вместе со своим слоем.
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 выполняется до записи дерева, поэтому редактор ещё знает, из какого фрейма пришёл
каждый слой, и сохраняет дизайн для отмены. Слой, перемещённый во фрейм, который его не
покрывает, был бы обрезан и не виден: вместо этого он встаёт в середину фрейма. С animateChanges
строки скользят и при изменениях, сделанных на холсте и отменой. У строки есть тип
(type: 'row'), поэтому холст и фреймы, которые принимают слои и инструменты, её отклоняют.
Клавиатура
Перейдите табом к слою (он выделится), затем Enter берёт его, стрелки двигают его на
10 пикселей (Shift: на 40), а Enter бросает его во фрейм под ним. Без
драга стрелки сдвигают выделение на пиксель. Инструмент берётся так же, а его стрелки прыгают
между холстом и фреймами (keyboardMode: 'droppables'). Остальные сочетания клавиш перечислены в
панели Design, когда ничего не выделено.
Подробнее: drag and drop · свой модификатор · правила для части страницы · выделение рамкой · деревья