Хорошо работает на адаптивных страницах Bootstrap 5

Интерактивное поле выбора на адаптивном изображении

Чистое решение Javascript для перетаскиваемого и изменяемого размера выделенного прямоугольника; прямоугольник выбора работает в адаптивном HTML-холсте.

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

Мы хотели реализовать эту функцию с помощью Javascript без сторонних библиотек или пакетов.

Анимация показывает результат нашей реализации: вы можете поиграть с демонстрационной страницей здесь. [В нашем случае многоугольник выбора на самом деле квадратно наш код javascript можно легко адаптировать для управления прямоугольной формой].

Один из способов реализовать эту функциональность — наложить прозрачный HTML <canvas> на элемент <img>, который требует операции выбора; затем нарисуйте прямоугольник на <canvas>. Оба элемента <img> и <canvas> заключены в отзывчивый элемент <div>, как показано на рисунке 1.

Звучит достаточно просто, но, как вы увидите, дьявол кроется в деталях реализации HTML + Javascript. Дополнительная трудность возникает при работе с отзывчивыми страницами, где базовое изображение может свободно перемещаться и изменять размер на веб-странице в запрограммированных контрольных точках.

Любое решение проблемы адаптивного прямоугольника выбора должно решать две проблемы.

  1. На адаптивном сайте изображение меняет размер и положение всякий раз, когда пользователь изменяет размер окна браузера (или когда сайт просматривается на мобильном устройстве с непредсказуемым размером экрана). Затем холст должен «следовать» размеру и положению изображения, а прямоугольник выделения должен перерисовываться при каждой операции «изменения размера», пересчитывая свои новые координаты left, top, right и bottom.
  2. Когда фигура рисуется на <canvas>, ее нельзя на самом деле «перемещать»: не существует объектной модели, которая может дать дескриптор нарисованной фигуры (не в по крайней мере, на момент написания этой статьи). Это связано с тем, что после выполнения команд рисования <canvas> вновь нарисованная форма смешивается с уже существующими пикселями рисунка <canvas>.
    Чтобы создать впечатление парящего прямоугольника, необходимо постоянно выполнять быструю задачу очистить холст и перерисовать прямоугольник, реагируя на события перетаскивания, запускаемые событиями перетаскивания или жесты.

Собрав вместе различные идеи, найденные в Интернете (мы всегда будем благодарны ангелам Stack Overflow), мы, наконец, собрали жизнеспособное решение HTML + Javascript.

1. Настройка элементов HTML

Если вы посмотрите на исходный код нашей демонстрационной HTML-страницы, вы можете заметить важные HTML-теги: <img> и <canvas> в адаптивном <div>, как в фрагменте кода ниже.

...
<div class="row mb-4">
   <div class="col-md-7"> <!-- responsive div -->
      <img id="full-image" src="[image source]"
         class="card-img-top" alt="..."
         style="position:relative">
      <canvas id="canvas" 
         style="position:absolute; left: 0px; top: 0px">
      </canvas>
   </div>
   <div class="col-md-5 mt-3 mt-md-0">
   ...
   </div>
</div>
...

<canvas> имеет абсолютную позицию в окружающем <div>; будучи помещенным в left: 0px; top: 0, он окажется точно поверх изображения. И <canvas>, и <img> имеют свои собственные id, поэтому Javascript может с ними работать.

Остальной код в приведенном выше фрагменте — это просто материал Bootstrap 5, предназначенный для размещения адаптивных и красивых строк и столбцов.

2. Краткое объяснение кода javascript

Код javascript является частью исходного кода демонстрационной веб-страницы:
https://catloader-api.variancedigital.com/bo/selectthumbnail
Вы можете увидеть код с помощью показать исходный код страницы вашего браузера функция. Вы найдете точный полный код в разделе 3 ниже, поэтому вам будет легче следовать пояснительным примечаниям следующих подразделов.

2.1 Обработка событий и позиционирование/отслеживание холста

При загрузке страницы функция init() регистрирует все необходимые прослушиватели событий для отслеживания движений мыши и сенсорных жестов. Затем initCanvas() помещает холст над изображением; прямоугольник выделения инициализируется initRect() и рисуется drawRectInCanvas() (в разделе 3 посмотрите код функции initCanvas(), это не банально).

...
function init(){
  canvas.addEventListener('mousedown', mouseDown, false);  
  canvas.addEventListener('mouseup', mouseUp, false);
  canvas.addEventListener('mousemove', mouseMove, false);
  canvas.addEventListener('touchstart', mouseDown);
  canvas.addEventListener('touchmove', mouseMove);
  canvas.addEventListener('touchend', mouseUp);
  initCanvas();
  initRect();
  drawRectInCanvas();
}
window.addEventListener('load',init)
window.addEventListener('resize', repositionCanvas)
...

Обратите внимание, что событие resize встречается с функцией repositionCanvas(): если вы посмотрите на код этой функции, вы увидите, что она позволяет <canvas> совмещать положение и размер элемента <img>.

repositionCanvas() также вызывает upadateCurrentCanvasRect(), где обновляется глобальный current_canvas_rect. Этот глобальный параметр имеет решающее значение: когда размер холста по какой-либо причине изменяется, нам нужны его «предыдущие» положение и размер, чтобы вычислить правильные пропорции прямоугольника выделения и отрисовать его с новым правильным размером (см., как current_canvas_rect используется в функции upadteCurrentCanvasRect()) .

2.2 Реагировать на события мыши/касания, чтобы имитировать движение перетаскивания

Всякий раз, когда на холсте «щелкает» курсор мыши (или касается пальцев пользователя), срабатывающая функция mouseDown() проверяет, находится ли курсор внутри прямоугольника выбора или курсор близок к одной из круглых ручек прямоугольника.

Если это так, переменные «перетаскивания» устанавливаются вtrue, так что любое последующее движение мыши считается операцией перетаскивания — это продолжается до тех пор, пока событие поднятия мыши не вызовет функцию mouseUp(), которая устанавливает перетащите переменные в false.

...
//drag global variables
var dragTL = dragBL = dragTR = dragBR = false;
var dragWholeRect = false;
...
function mouseDown(e) {
  var pos = getMousePos(this,e);
  mouseX = pos.x;
  mouseY = pos.y;
  // 0. inside movable rectangle
  if (checkInRect(mouseX, mouseY, rect)){
      dragWholeRect=true;
      startX = mouseX;
      startY = mouseY;
  }
  // 1. top left
  else if (checkCloseEnough(mouseX, rect.left) &&
           checkCloseEnough(mouseY, rect.top)) {
      dragTL = true;
  }
  // 2. top right
  else if (checkCloseEnough(mouseX, rect.left + rect.width) &&
           checkCloseEnough(mouseY, rect.top))
  {
      dragTR = true;  
  }
  // 3. bottom left
  ... etc.
}
function mouseUp(e) {
  dragTL = dragTR = dragBL = dragBR = false;
  dragWholeRect = false;
}
...

При перетаскивании сработавшая функция mouseMove() вычисляет — в каждом конкретном случае — новый размер и положение прямоугольника выбора и в конечном итоге рисует его.

...

function mouseMove(e) {
  var pos = getMousePos(this, e);
  mouseX = pos.x;  mouseY = pos.y;
  if (dragWholeRect) {
      e.preventDefault();
      e.stopPropagation();
      dx = mouseX - startX;
      dy = mouseY - startY;
      if ((rect.left+dx)>0 &&
          (rect.left+dx+rect.width)<canvas.width)
      {
        rect.left += dx;
      }
      if ((rect.top+dy)>0 &&
          (rect.top+dy+rect.height)<canvas.height)
      {
        rect.top += dy;
      }
      startX = mouseX;
      startY = mouseY;
  } else if (dragTL) {
      e.preventDefault();
      e.stopPropagation();
      var newSide = (Math.abs(rect.left+rect.width - mouseX) +
                   Math.abs(rect.height + rect.top - mouseY))/2;
      if (newSide>150){
        rect.left = rect.left + rect.width - newSide;
        rect.top = rect.height + rect.top - newSide;
        rect.width = rect.height = newSide;
      }
  } else if (dragTR) {
     ... etc.
  }
  drawRectInCanvas();
}
...

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

Если вам нужно настроить этот код так, чтобы перетаскиваемая фигура с изменяемым размером представляла собой прямоугольник (а не квадрат), вам следует изменить эту mouseMove() функцию.

2.3 Непрерывное рисование на холсте

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

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

...
function drawRectInCanvas(){
  var ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.beginPath();  ctx.lineWidth = "6";
  ctx.fillStyle = "rgba(199, 87, 231, 0.2)";
  ctx.strokeStyle = "#c757e7";
  ctx.rect(rect.left, rect.top, rect.width, rect.height);
  ctx.fill();
  ctx.stroke();
  drawHandles();
  updateHiddenInputs()
}
...

Последней инструкцией drawRectInCanvas() является updateHiddenInputs(), которая сохраняет координаты поля выбора, чтобы их можно было отобразить на веб-странице (или отправить на сервер).

3. Полный код JavaScript

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

3. Выводы (следите за обновлениями)

Вы видели код и играли с его работающей реализацией онлайн. Веб-страница с окном выбора является частью нашего демонстрационного проекта CatLoader, который будет полностью раскрыт в течение нескольких дней. Следите за обновлениями!

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