Интерактивное масштабирование в связке с Canvas требует разделения двух уровней ответственности: управление состоянием трансформации и ручной перерисовки сцены. В отличие от SVG, где трансформации могут быть применены к DOM-элементам напрямую, Canvas остаётся императивным контекстом рисования, что делает контроль зума полностью зависимым от логики приложения.
Библиотека D3.js предоставляет модуль d3-zoom, который
работает не с рендерингом, а с абстрактным состоянием трансформации:
масштабом и смещением. Это ключевой момент — Canvas не знает о зуме, он
лишь перерисовывается с учётом текущей трансформации.
Состояние зума в D3 представляется объектом трансформации:
k — коэффициент масштабаx, y — смещениеЭта модель применяется ко всем координатам сцены:
[ x’ = kx + t_x] [ y’ = ky + t_y]
В Canvas это не применяется автоматически, поэтому вся логика должна быть отражена в отрисовке.
Основная архитектура состоит из трёх компонентов:
CanvasRenderingContext2D)d3.zoom)import * as d3 from "d3";
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const width = canvas.width = 900;
const height = canvas.height = 600;
let transform = d3.zoomIdentity;
Состояние transform хранит текущий зум и обновляется при
каждом взаимодействии пользователя.
Механизм управления масштабом создаётся через
d3.zoom():
const zoom = d3.zoom()
.scaleExtent([0.5, 20])
.on("zoom", handleZoom);
d3.select(canvas).call(zoom);
Функция handleZoom — центральная точка обновления
состояния.
Каждое движение колесика мыши или жест масштабирования вызывает обновление transform:
function handleZoom(event) {
transform = event.transform;
render();
}
Важно: здесь не происходит никакой работы с Canvas напрямую, только обновление состояния.
Перерисовка Canvas должна учитывать масштаб и смещение вручную:
function render() {
ctx.save();
ctx.clearRect(0, 0, width, height);
ctx.translate(transform.x, transform.y);
ctx.scale(transform.k, transform.k);
drawScene(ctx);
ctx.restore();
}
Ключевой момент — порядок операций:
Все объекты сцены рисуются в «мировых координатах», не учитывая зум:
function drawScene(ctx) {
for (const node of nodes) {
ctx.beginPath();
ctx.arc(node.x, node.y, 5, 0, Math.PI * 2);
ctx.fill();
}
}
После применения transform эти координаты автоматически
становятся экранными.
Для интерактивных сценариев часто требуется обратное преобразование координат мыши:
function getWorldPosition(event) {
const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left - transform.x) / transform.k;
const y = (event.clientY - rect.top - transform.y) / transform.k;
return { x, y };
}
Это позволяет:
При использовании осей важно пересчитывать шкалы:
const xScale = d3.scaleLinear().domain([0, 100]).range([0, width]);
const yScale = d3.scaleLinear().domain([0, 100]).range([height, 0]);
Однако при зуме через Canvas проще работать иначе: масштабировать не scale, а сам контекст.
Если требуется обновление шкал:
const zx = transform.rescaleX(xScale);
const zy = transform.rescaleY(yScale);
Это полезно при смешанных визуализациях (например, графики + Canvas).
Часто требуется ограничить панорамирование:
const zoom = d3.zoom()
.scaleExtent([1, 10])
.translateExtent([[0, 0], [width, height]])
.on("zoom", handleZoom);
scaleExtent ограничивает масштабtranslateExtent ограничивает смещениеДля сложных сцен необходимо проверять попадание мыши в объекты:
function isPointInNode(node, point) {
const dx = node.x - point.x;
const dy = node.y - point.y;
return dx * dx + dy * dy < node.r * node.r;
}
Перед этим координаты приводятся к мировым через inverse transform.
Canvas требует полной перерисовки сцены при каждом изменении трансформации. Это накладывает ограничения:
renderОптимизация часто включает:
Для плавного зума полезно синхронизировать рендеринг с циклом браузера:
let needsRender = false;
function scheduleRender() {
if (!needsRender) {
needsRender = true;
requestAnimationFrame(() => {
needsRender = false;
render();
});
}
}
function handleZoom(event) {
transform = event.transform;
scheduleRender();
}
Это снижает количество перерисовок при быстрых жестах.
Распространённый подход — Canvas для отрисовки данных и SVG для UI:
SVG-слой не масштабируется вместе с Canvas, а синхронизируется через transform:
svg.attr("transform", transform);
Для Retina-дисплеев важно учитывать devicePixelRatio:
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
Зум при этом применяется поверх масштабирования устройства, поэтому порядок трансформаций критичен.
При визуализации графов (force layout) зум часто применяется поверх симуляции:
simulation.on("tick", () => {
render();
});
Здесь позиции узлов обновляются отдельно, а Canvas только отображает текущее состояние с учётом transform.
Каждая из этих ошибок приводит к рассинхронизации визуального состояния и логики сцены.
Устойчивый подход к архитектуре:
state — данные сценыtransform — состояние камерыrender() — чистая функция отрисовкиhandlers — обновление transform и stateТакой подход позволяет изолировать взаимодействие от визуализации и делает поведение зума предсказуемым.
При сложных сценах полезно разделять слои отрисовки:
Каждый слой может перерисовываться с разной частотой, но все они используют один transform, обеспечивая согласованное масштабирование.