[Перевод] Как сократить код Canvas API в Svelte

tqghtcr4w2l9cpcatosz4hmpmxq.png

Разработчик из консалтинговой компании в области разработки This Dot Labs рассказывает, как использовать canvas в Svelte и как превратить многословный API Canvas в краткий, более декларативный. Подробности — к старту нашего курса по фронтенду.

Элемент и Canvas API позволяют рисовать на JavaScript, а с помощью Svelte его императивный API можно преобразовать в декларативный. Это потребует от вас знания Renderless-компонентов — компонентов, которые не отрисовываются.

Renderless

Все разделы файла .svelte, включая шаблон, необязательны. Поэтому можно создать компонент, который не отображается, но содержит логику в теге

После инициализации компонента выведем сообщение. Для этого перепишем точку входа App:

// src/main.ts
// import App from './App.svelte'
import Renderless from './lib/Renderless.svelte'

const app = new Renderless({
  target: document.getElementById('app')
})

export default app

Теперь запускаем сервер, открываем инструменты разработчика — и видим сообщение:

72242b0f05268748749539d7e035da89.png

Работает.

Обратите внимание, что методы жизненного цикла компонента по-прежнему доступны, то есть компонент ведёт себя как обычный, даже когда у него нет шаблона.

Проверим это:


После монтирования Renderless отображает второе сообщение, оба сообщения выводятся в ожидаемом порядке:

d6f9340a2e23ee1abf19ed0fcbc8627f.png

Это означает, что Renderless можно использовать как любой другой компонент Svelte. Вернём изменения main.ts и «отрисуем» компонент внутри App:

// src/main.ts
import App from './App.svelte'

const app = new App({
  target: document.getElementById('app')
})

export default app



Перепишем Renderless, чтобы логировать важные сообщения:


59ea6efac42dca3de85ef5b518d112a9.png

При создании компонентов без отрисовки и Canvas важно обратить внимание на порядок инициализации и монтирования компонентов.

Ещё один способ монтировать компонент — передать его как дочерний элемент другого компонента. Такая передача называется проекцией контента. Эту проекцию сделаем с помощью slot.

Напишем компонент Container, который будет отрисовывать добавленные в слот элементы:




The container of things

invisible things

С помощью prop добавим в компонент Renderless идентификатор:



Перепишем App для контейнера, затем передадим в App несколько экземпляров Renderless:




Ниже видно и Container, и компоненты без отрисовки, которые при инициализации и монтировании пишут в лог:

8da37a932b7d0617ce04b59cdcb602a2.png

А теперь воспользуемся компонентами без отрисовки в сочетании с .

HTML canvas и Canvas API

Элемент canvas не может содержать никаких дочерних элементов, кроме резервного элемента для отрисовки. Всё, что хочется показать в canvas, должно быть написано на императивном API.

Создадим новый компонент Canvas и отрисуем canvas:




Обновим App, чтобы воспользоваться Canvas:




А теперь откроем инструменты разработчика:

299b3a1fbda47667c53e27d8f0966ce6.png

Отрисовка элементов внутри canvas

Как уже говорилось, добавлять элементы прямо в canvas нельзя. Чтобы рисовать, нужно работать с API.

Ссылку на элемент получим через bind:this. Важно понимать, что для работы с API элемент должен быть доступен, то есть рисовать придётся после монтирования компонента:



Нарисуем линию. Для наглядности я убрал всё логирование:



Чтобы рисовать, canvas нужен контекст, так что делать это можно только после монтирования компонента:

1c4cee2c86c697a520bba095b1f0addc.png

Если захочется добавить вторую строку, придётся дописать и новый блок кода:

Мы рисуем простые фигуры —, а кода в компоненте всё больше и больше. Можно написать вспомогательные функции, сокращающие код линий:



Читать код легче, но вся ответственность по-прежнему делегируется Canvas, а это приводит к большой сложности компонента. Избежать большой сложности помогут компоненты без отрисовки и API Context.

И вот что нам уже известно:

  • Для рисования нам нужен Canvas.

  • Получить контекст можно после монтирования компонента.

  • Дочерние компоненты монтируются перед родительским компонентом.

  • Родительские компоненты инициализируются перед дочерними компонентами.

  • Дочерние компоненты можно использовать при монтировании.

Наш компонент нужно разделить на несколько компонентов. Здесь хочется, чтобы Line рисовал сам себя.

Canvas и Line связаны. Line нельзя отрисовать без Canvas, и ему нужен контекст canvas. Но контекст недоступен, когда монтируется дочерний компонент, ведь Line монтируется перед Canvas. Поэтому подход нужен другой.

Вместо передачи контекста для отрисовки самого себя сообщим родительскому компоненту, что рисовать нужно дочерний компонент. Canvas и Line соединим через Context.

Context — это способ взаимодействия двух и более компонентов. Его можно установить или получить только во время инициализации, а это нам и нужно: Canvas инициализируется перед Line.

Сначала давайте перенесём отрисовку линии в отдельный компонент, а некоторые типы — в их собственный файл, чтобы сделать их общими для компонентов:

// src/types.ts
export type Point = [number, number];
export type DrawFn = (ctx: CanvasRenderingContext2D) => void;
export type CanvasContext = {
  addDrawFn: (fn: DrawFn) => void;
  removeDrawFn: (fn: DrawFn) => void;
};

Это очень похоже на то, что было в Canvas, но абстрагировано до компонента. Теперь нужно организовать коммуникацию Canvas и Line.

Canvas будет работать как оркестратор всей отрисовки. Он инициализирует все дочерние компоненты, собирает функции отрисовки и выполняет отрисовку, когда это нужно:





Первое, что нужно отметить, — шаблон изменился, рядом с canvas появился элемент . Он будет использоваться для монтирования любых дочерних элементов, которые передаются в canvas, — это компоненты Line. Эти Line не добавят никаких элементов HTML.

Массив let fnsToDraw = [] as DrawFn[] в 

Функция регистрируется с помощью контекста, установленного Canvas, когда компонент монтируется. Выполнить регистрацию можно было и при инициализации, ведь контекст доступен в любом случае, но я предпочитаю делать это после монтирования компонента. Когда элемент уничтожается, он удаляет себя из списка функций отрисовки.

А теперь дополним App компонентами Canvas и Line:



3cf64b58cc54cc2ea9a60d98d98dd2d9.png

Компонент Canvas обновлён для декларативного программирования, но рисуем мы только один раз, когда он смонтирован.

А нам нужно, чтобы canvas отрисовывался часто и обновлялся при изменениях, если только вы не хотите обратного. Обратите внимание, что частую отрисовку пришлось бы делать с выбранным подходом или без него.

И вот распространённый способ обновления содержимого canvas:

Это достигается повторной отрисовкой canvas через requestAnimationFrame. Переданная функция запускается до перерисовки браузером. Новая переменная для текущего frameId потребуется при отмене анимации. Затем, когда компонент монтируется, вызывается requestAnimationFrame, и возвращённый идентификатор присваивается нашей переменной.

Пока конечный результат такой же, как и раньше. Отличие — в функции отрисовки, которая запрашивает новый кадр анимации после каждой отрисовки. Canvas очищается, а иначе при анимации каждый кадр отрисовывается поверх другого. Этот эффект может быть желательным — тогда установите clearFrame в false. Наш Canvas будет обновлять каждый кадр до уничтожения компонента и погашения текущей анимации с помощью сохранённого идентификатора.

Больше функциональности

Базовая функциональность компонентов работает, но мы можем захотеть большего.

В этом примере представлены события onmousemove и onmouseleave. Чтобы они работали, измените canvas вот так:

Теперь эти события можно обрабатывать в App:



followMouse(e)} on:mouseleave={() => { end = [0, 0]; }} >

Svelte отвечает за обновление конечного положения линии. Но Canvas используется для обновления содержимого canvas через requestAnimationFrame:

a989bd01b66124f19f0e8866c418fd19.gif

Итоги

Надеюсь, это руководство поможет вам как введение в применение canvas в Svelte, а также поможет понять, как превратить библиотеку с императивным API в более декларативную.

Есть примеры сложнее, например svelte-cubed или svelte-leaflet. Из документации svelte-cubed:

Это:

import * as THREE from 'three';

function render(element) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
element.clientWidth / element.clientHeight,
0.1,
2000
);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(element.clientWidth / element.clientHeight);
element.appendChild(renderer.domElement);

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshNormalMaterial();
const box = new THREE.Mesh(geometry, material);
scene.add(box);

camera.position.x = 2;
camera.position.y = 2;
camera.position.z = 5;

camera.lookAt(new THREE.Vector3(0, 0, 0));

renderer.render(scene, camera);
}

Превращается в:






Canvas API можно расширить и даже создать библиотеку.

А мы поможем прокачать ваши навыки или с самого начала освоить профессию, актуальную в любое время:

b16dac539a0a09cd1c28951311a610c7.png

© Habrahabr.ru