Приложения на основе холста — отличный способ расширить свой портфель веб-разработок за пределы простых операций CRUD. В этом уроке я расскажу вам, как добавить интерактивный «рисуемый» HTML-холст в ваш следующий проект React.js.

Это отличный способ отточить свои знания ключевых хуков React, таких как useRef, useEffect и UseState, а также развить ваше понимание мощного элемента «холст» HTML 5, который используется некоторыми из наиболее впечатляющих и широко используемых визуальных эффектов. инструменты в Интернете (Excalidraw, Figma и Three.js используют холст HTML 5 в той или иной степени).

Когда вы закончите читать, у вас будет HTML Canvas с React вплоть до искусства.

Что вам понадобится.

Я не буду слишком предписывающим в том, что необходимо для продолжения. Базовые знания React, JSX и HTML, конечно, будут полезны, а некоторое понимание React Hooks поможет вам действительно понять, что происходит. Тем не менее, я проведу вас через настройку и все шаги, которые я выполнил при создании холста, похожего на Microsoft Paint, с помощью React, так что не стесняйтесь присоединиться к поездке, если у вас мало предварительных знаний.

Если у вас есть подходящий инструмент для настройки нового приложения React или вы думаете о добавлении холста в существующий проект, вы можете пропустить следующий раздел. Create-React-App, Gatsby или что-то еще, что вы используете, это нормально, но я буду использовать Vite.

Настройка вашего приложения React.

Убедитесь, что на вашем компьютере установлен Node.js и что его версия не ниже 14.18. Вы можете проверить это, выполнив приведенную ниже команду в своем терминале.

node -v

Если ваша версия старше 14.18, вам придется сменить версию с помощью менеджера версий или загрузить более новую версию с веб-сайта Node JS здесь:



В противном случае перейдите в каталог, в котором вы хотите запустить новый проект, и выполните следующую команду:

npm create vite react_canvas

Vite имеет отличный пользовательский интерфейс терминала, который поможет вам в процессе установки, поэтому я не буду просто повторять его здесь. Просто знайте, что я решил написать код для этой статьи с помощью старого доброго Javascript и React. Конечно, вы можете использовать Typescript, если хотите.

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

npm i
npm run dev

Чтобы убедиться, что все работает должным образом, скопируйте IP-адрес с вашего терминала в панель навигации вашего браузера. Мой: 127.0.0.1:5173.

Как только вы подтвердите, что все работает, мы можем начать удаление содержимого некоторых файлов, так что мы начинаем с совершенно пустого проекта.

Удалите содержимое следующих файлов:

src/App.css
src/index.css

Затем замените содержимое App.jsx на это:

import './App.css'

function App() {
  return (
    <div></div>
  )
}

export default App

Не беспокойтесь, что ваш браузер показывает пустую страницу — это именно то, что нам нужно. Теперь мы можем приступить к созданию нашего холста.

Добавление холста.

Мы начнем с создания компонента React для нашего холста. Просто чтобы все было организовано, создайте папку «components» в своем проекте, а внутри этой папки добавьте папку «Canvas». Здесь мы будем хранить весь код, относящийся к нашему компоненту Canvas. Ваша файловая структура должна выглядеть так:

/src/components/Canvas

В папку Canvas добавьте два файла:

Canvas.jsx
Canvas.css

Файл .jsx будет содержать код React/Javascript для вашего компонента, файл .css будет хранить стиль.

Откройте Canvas.jsx и создайте компонент холста следующим образом:

import './Canvas.css'

export default function Canvas() {
  return (
    <canvas className='canvas' />
  )
}

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

Вы заметите, что элемент холста находится между скобками оператора return. Для тех, кто не знаком с JSX, вы, вероятно, думаете, что это похоже на HTML. Это сделано намеренно. Со временем наш компонент Canvas превратится в HTML, но не заблуждайтесь — это все JavaScript. Создатели JSX разработали его так, чтобы он отражал внешний вид HTML, чтобы улучшить опыт разработчиков, поскольку мы можем визуализировать, как в конечном итоге наш код будет отображаться в браузере.

Также обновите файл Canvas.css, чтобы добавить цвет фона на холст, например:

.canvas {
  background-color: red;
}

Давайте возьмем наш элемент холста и добавим его в наше приложение.

Перейдите к /src/App.jsx и обновите файл, включив в него наш компонент Canvas. Все, что нам нужно сделать, это добавить оператор импорта в начало файла и вложить компонент Canvas внутрь тегов div:

import Canvas from './components/Canvas/Canvas'
import './App.css'

function App() {
  return (
    <div>
      <Canvas />
    </div>
  )
}

export default App

Если вы сейчас зайдете в браузер, то увидите, что появился маленький красный прямоугольник — это наш холст! Вы заметите, что в настоящее время щелчок или перетаскивание по нему… абсолютно ничего не делает. Нам придется самим добавить интерактивную функциональность.

Контекст — король.

Чтобы начать манипулировать размером, размерами и содержимым нашего холста, нам потребуется доступ к его 2D-контексту. По сути, мы можем понимать 2D-контекст как набор полезных объектов и методов, предоставляемых HTML 5, которые позволяют нам редактировать и настраивать наш холст.

Итак, как мы можем использовать их?

В Vanilla Javascript это так же просто, как получить доступ к холсту из HTML-документа и использовать метод .getContext(‘2d’). В React нам нужно сделать еще несколько шагов, прежде чем мы сможем это сделать. Итак, вернитесь к вашему Canvas.jsx, и мы можем начать процесс.

Чтобы получить доступ к 2d-контексту нашего элемента холста, нам придется использовать некоторые из основных хуков React: useRef, useEffect и useState.

Если вы не знакомы с хуками, я бы порекомендовал ознакомиться с этой темой, прежде чем продолжить, просто чтобы заложить фундамент знаний и понять, почему я делаю то, что делаю. Школы W3 — хорошая отправная точка:



Давайте импортируем все наши хуки в наш компонент Canvas. Добавьте это в первую строку файла Canvas.jsx:

import { useRef, useEffect, useState } from 'react'

Первый хук, который мы будем использовать, это «useRef». Создав ссылку и назначив ее элементу, мы можем получить доступ к этому элементу и управлять им в том виде, в каком он существует в DOM.

Создать ссылку и добавить ее в наш холст так же просто, как объявить переменную для хранения нашей ссылки и добавить ее в наш элемент холста JSX:

export default function Canvas() {
  const canvasRef = useRef(null)
  return (
    <canvas ref={canvasRef} className='canvas' />
  )
}

Однако, если мы попытаемся выполнить console.log(canvasRef), нас встретит undefined, а не наш элемент холста. Это связано с тем, что когда мы объявляем canvasRef, элемент холста не был отображен в DOM. Его еще не существует. Итак, как мы можем получить к нему доступ?

Здесь в игру вступает «useEffect».

Вызывая useEffect с пустым массивом зависимостей, мы можем запустить некоторый JavaScript один раз после того, как компонент отобразится в DOM.

Чтобы проиллюстрировать, если мы попытаемся записать в console.log значение canvasRef из useEffect (как показано ниже), мы сможем увидеть объект, который содержит наш элемент холста. Эй!

export default function Canvas() {
  const canvasRef = useRef(null)

  useEffect(() => {
    console.log(canvasRef)
  }, [])
  
  return (
    <canvas ref={canvasRef} className='canvas' />
  )
}

Консоль вашего браузера должна выглядеть примерно так:

Object { current: canvas.canvas }

Мы еще не совсем закончили с крючками. Поскольку мы хотим иметь возможность рисовать и рисовать на нашем холсте, нам нужно будет обращаться к нему и неоднократно обновлять его, поэтому нам нужен способ сохранить наш элемент холста вне блока useEffect. Для этого мы можем использовать последний из наших хуков React: useState.

useState позволяет нам обновлять и сохранять состояние в функциональных компонентах React — useState полезен в этом сценарии, поскольку позволяет нам сохранять нашу ссылку на элемент холста между рендерами и постепенно добавлять на холст по мере того, как мы начинаем рисовать на нем.

Что мы хотим сделать, так это создать часть состояния с именем canvasElement, которую мы обновим значением canvasRef.current после рендеринга первой страницы. Помните, что useState возвращает массив с новой частью состояния и специальной функцией для его обновления.

Мой код выглядит так:

export default function Canvas() {
  const canvasRef = useRef(null)
  const [canvasElement, setCanvasElement] = useState(null);

  useEffect(() => {
    setCanvasElement(canvasRef.current);
  }, [])

  return (
    <canvas ref={canvasRef} className='canvas' />
  )
}

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

export default function Canvas() {
  const canvasRef = useRef(null)
  const [canvasElement, setCanvasElement] = useState(null);
  const [isDrawing, setIsDrawing] = useState(false);
  const [previousCoords, setPreviousCoords] = useState(null);

  useEffect(() => {
    setCanvasElement(canvasRef);
  }, [])

  return (
    <canvas ref={canvasRef} className='canvas' />
  )
}

3, 2, 1… Ничья!

Теперь у нас есть доступ к нашему элементу холста между рендерами, и мы можем начать с забавных вещей, а именно с рисования на холсте.

Мы добавим серию событий на наш холст, чтобы начать работу. Нам понадобится:

  • onMouseDown: чтобы начать рисовать, когда мы нажимаем кнопку мыши.
  • onMouseUp: остановить рисование, когда мы отпускаем кнопку мыши.
  • onMouseMove: чтобы пометить холст, когда мы двигаем мышь.

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

    <canvas
      ref={canvasRef}
      className='canvas'
      onMouseMove={markCanvas}
      onMouseDown={() => setIsDrawing(true)}
      onMouseUp={() => {
        setIsDrawing(false)
      }}
    />

Добавление приведенного выше кода означает, что наш холст прослушивает нажатие и отпускание кнопок мыши, а также движение мыши. Код, содержащийся в обратном вызове (обозначенный скобками, стрелкой и фигурными скобками, () => {} ), будет выполняться при каждом действии.

Как видите, мы используем функции, возвращаемые useState, для отслеживания нашего взаимодействия с холстом. Когда наша кнопка мыши нажата, мы устанавливаем isDrawing в true, а когда отпускаем, устанавливаем в false. Когда мы двигаем мышь, мы помечаем холст (то есть после того, как мы написали функцию markCanvas!).

Функция markCanvas.

Функция markCanvas — это то, что мы будем использовать для фактического рисования на холсте.

Его можно разбить на 3 основных этапа.

  1. Если кнопка мыши не нажата, ничего не делать.
  2. Если кнопка мыши нажата, сделайте отметку на холсте.
  3. Установите предыдущие координаты туда, куда переместилась мышь.

Первый шаг самый простой и состоит всего из одной строки кода:

const markCanvas = (event) => {
    if (!isDrawing) return;
}

Второй шаг немного сложнее, так как мы должны отслеживать координаты нашей мыши относительно холста.

Во-первых, нам нужно получить доступ к пропорциям холста. Для этого мы можем использовать getBoundingClientRect() холста, который вернет информацию о размере и положении элемента холста по отношению к области просмотра клиента. У Mozilla есть отличная статья (с диаграммами), объясняющая getBoundingClientRect, с которой вы можете ознакомиться здесь:



Мы можем использовать возвращаемые значения getBoundingClientRect() (в частности, расстояние холста от верхней и левой части области просмотра), чтобы вычислить, где мы должны добавить «чернила» на холст. Поскольку markCanvas вызывается для события мыши, ему автоматически передается параметр «event», который содержит координаты X и Y мыши относительно области просмотра. Расчет координат для отметки холста отображается в обновленной функции ниже.

const markCanvas = (event) => {
    if (!isDrawing) return;
    const rect = canvasElement.getBoundingClientRect();
    
    const coords = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    }
}

Теперь мы можем (наконец-то) получить доступ к 2d-контексту нашего холста и использовать его встроенные методы для добавления на холст.

Чтобы получить доступ к этим полезным методам, мы должны присвоить 2d Context переменной, которую я назвал canvasContext:

const markCanvas = (event) => {
    if (!isDrawing) return;
    const rect = canvasElement.getBoundingClientRect();
    
    const coords = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    }

    const canvasContext = canvasElement.getContext('2d');
}

Теперь мы можем начать путь, по которому будет идти наша линия, с помощью .beginPath():

const markCanvas = (event) => {
    if (!isDrawing) return;
    const rect = canvasElement.getBoundingClientRect();
    
    const coords = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    }

    const canvasContext = canvasElement.getContext('2d');

    canvasContext.beginPath();
}

Но нам нужно указать холсту, откуда и куда провести линию. Для этого мы можем проверить, назначены ли нам предыдущие координаты, и если да, то мы можем указать контексту холста связать текущие координаты с предыдущими, например так:

const markCanvas = (event) => {
    if (!isDrawing) return;
    const rect = canvasElement.getBoundingClientRect();
    
    const coords = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    }

    const canvasContext = canvasElement.getContext('2d');

    canvasContext.beginPath();
    
    if (previousCoords) {
        canvasContext.moveTo(previousCoords.x, previousCoords.y);
        canvasContext.lineTo(coords.x, coords.y);
      }
}

Осталось только создать визуальную линию с помощью .stroke() и изменить предыдущие координаты на текущие координаты, чтобы функция продолжала работать при последующих взаимодействиях.

const markCanvas = (event) => {
    if (!isDrawing) return;
    const rect = canvasElement.getBoundingClientRect();

    const coords = {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    }

    const canvasContext = canvasElement.getContext('2d');
    canvasContext.beginPath();
    if (previousCoords) {
      canvasContext.moveTo(previousCoords.x, previousCoords.y);
      canvasContext.lineTo(coords.x, coords.y);
    }
    canvasContext.stroke();
    setPreviousCoords(coords);
}

Теперь, когда вы переходите на страницу в своем браузере, вы сможете взаимодействовать с вашим холстом, рисуя линии с помощью мыши!

Почему такой маленький?

Несмотря на то, что наш холст запущен и работает, я уверен, вы заметили, насколько он маленький. Вероятно, мы хотим, чтобы наш холст был как можно большего размера, или хотя бы хотим иметь некоторый контроль над его пропорциями. Возможно, вы уже пытались изменить размер холста через css и заметили некоторые странные результаты.

Мы должны быть осторожны при изменении размера нашего холста. Чего мы не хотим делать, так это просто растягивать его с помощью css, так как это создаст всевозможные причудливые эффекты, такие как размытие и линии, которые появляются далеко от указателя мыши. Нам нужно убедиться, что сам холст имеет то же количество пикселей, что и пространство, которое он заполняет. Это еще одна работа для getBoundingClientRect().

Удобно, что getBoundingClientRect() возвращает ширину и высоту контейнера, который заполняет холст. Мы можем просто взять эти значения и присвоить их холсту, поэтому независимо от размера контейнера холст будет совпадать. Я нашел лучшее место для этого в обратном вызове useEffect компонента Canvas:

useEffect(() => {
    const rect = canvasRef.current.getBoundingClientRect();
    canvasRef.current.width = rect.width;
    canvasRef.current.height = rect.height;
    setCanvasElement(canvasRef.current);
  }, [])

Также не забудьте обновить элемент холста до высоты и ширины 100% в файле Canvas.css:

.canvas {
  background-color: red;
  width: 100%;
  height: 100%;
}

Теперь вы можете перейти к App.jsx и изменить размер div, в который мы вложили компонент Canvas, на любой, какой вам нравится — наши функции рисования по-прежнему будут работать отлично!

Последние мысли

Конечно, мы только коснулись того, что может сделать холст HTML 5, но в одну статью можно поместить только столько. У меня есть планы написать дополнительные учебники по этой теме. В частности, я хотел бы написать на такие темы, как сохранение содержимого холста на внутреннем сервере, организация кода для рисования в пользовательских хуках и изменение цвета/размера пера. Если у вас есть какие-либо вопросы или вы хотите связаться со мной, не стесняйтесь обращаться ко мне в LinkedIn:

https://www.linkedin.com/in/charlie-whiteside-196211275/

Надеюсь, вы нашли эту статью полезной. Большое спасибо за чтение, я очень ценю, что вы нашли время!