Статьи

Нежное введение в создание интерактивного холста HTML5

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

Из этого туториала вы узнаете, как создать простую структуру данных для фигур на холсте HTML5 и как их выбрать. Готовый холст будет выглядеть так:

Код этой статьи написан в первую очередь, чтобы было легко понять. Мы рассмотрим несколько вещей, которые важны для интерактивных приложений, таких как игры (цикл рисования, тестирование попаданий), и в следующих уроках я, вероятно, превращу этот пример в небольшую игру. Я попытаюсь приспособить начинающих JavaScript, но это введение ожидает хотя бы элементарного понимания JS. Не каждый фрагмент кода объясняется в тексте, но почти каждый фрагмент кода тщательно комментируется!

HTML5 Canvas

Canvas создается с помощью тега <canvas> в HTML:

   <canvas id="canvas" width="400" height="300">
    This text is displayed if your browser does not support HTML5 Canvas.
   </canvas>

Холст не умный: это просто место для рисования пикселей. Если вы попросите его нарисовать что-нибудь, он выполнит команду рисования, а затем сразу забудет все о том, что он только что нарисовал. Это иногда упоминается как поверхность непосредственного рисования, в отличие от SVG как сохраненной поверхности рисования, поскольку SVG сохраняет ссылку на все нарисованное. Поскольку у нас нет таких ссылок, мы должны следить за всеми вещами, которые мы хотим нарисовать (и перерисовать) каждого кадра.

Canvas также не имеет встроенного способа работы с анимацией. Если вы хотите, чтобы то, что вы нарисовали, двигалось, вы должны очистить весь холст и перерисовать все объекты, переместив один или несколько из них. И вам придется делать это часто, конечно, если вы хотите подобия анимации или движения.

Поэтому нам нужно добавить:

  1. Код для отслеживания объектов
  2. Код для отслеживания состояния холста
  3. Код для событий мыши
  4. Код для рисования объектов по мере их создания и перемещения

 

Отслеживание того, что мы рисуем

Для простоты в этом примере мы начнем с класса Shape для представления прямоугольных объектов.

С технической точки зрения в JavaScript нет классов, но это не проблема, потому что программисты на JavaScript очень хорошо умеют играть. Функционально (ну, для нашего примера) у нас будет класс Shape и мы создадим экземпляры формы с ним. На самом деле мы определяем функцию Shape и добавляем функции в прототип Shape. Вы можете создавать новые экземпляры функции Shape, и все экземпляры будут использовать функции, определенные в прототипе Shape.

Если вы никогда ранее не сталкивались с прототипами в JavaScript или если вышеперечисленное звучит смущающе, я настоятельно рекомендую прочитать «Крокфордский JavaScript: хорошие части» . Книга представляет собой промежуточный обзор JavaScript, который дает хорошее понимание того, почему программисты предпочитают создавать объекты по-разному, почему некоторые соглашения не одобряются, и что делает JavaScript таким разным.

Вот наш конструктор Shape и один из двух методов-прототипов, которые сравнимы с методами экземпляра класса:

// Constructor for Shape objects to hold data for all drawn objects.
// For now they will just be defined as rectangles.
function Shape(x, y, w, h, fill) {
  // This is a very simple and unsafe constructor. 
  // All we're doing is checking if the values exist.
  // "x || 0" just means "if there is a value for x, use that. Otherwise use 0."
  this.x = x || 0;
  this.y = y || 0;
  this.w = w || 1;
  this.h = h || 1;
  this.fill = fill || '#AAAAAA';
}
 
// Draws this shape to a given context
Shape.prototype.draw = function(ctx) {
  ctx.fillStyle = this.fill;
  ctx.fillRect(this.x, this.y, this.w, this.h);
}

Отслеживание состояния холста

У нас будет второй класс / функция с именем CanvasState. Мы собираемся сделать только один экземпляр этого класса, и он будет содержать все состояния в этом руководстве, которые не связаны с самими Shapes.

CanvasState в первую очередь нуждается в ссылке на Canvas и нескольких других полях для удобства. Мы также собираемся вычислить и сохранить границу и отступы (если они есть), чтобы мы могли получить точные координаты мыши.

В конструкторе CanvasState у нас также будет коллекция состояний, относящихся к объектам на холсте и текущему состоянию перетаскивания. Мы создадим массив фигур, флаг «перетаскивание», который будет истинным во время перетаскивания, поле для отслеживания того, какой объект выбран, и «допустимый» флаг, который будет установлен в «ложь», заставит Canvas очистить все и перерисовать.

понадобится массив Shapes, чтобы отслеживать то, что было нарисовано до сих пор.

Я собираюсь добавить кучу переменных для отслеживания состояния чертежа и мыши. Я уже добавил box [] для отслеживания каждого объекта, но нам также понадобится переменная для холста, 2d-контекст холста (где выполняется рисование стены), независимо от того, перетаскивается ли мышь, ширина / высота холста , и так далее. Мы также хотим сделать второй холст, для целей выбора, но я поговорю об этом позже.

function CanvasState(canvas) {
 
  // ...
 
  // I removed some setup code to save space
  // See the full source at the end
 
 
  // **** Keep track of state! ****
 
  this.valid = false; // when set to true, the canvas will redraw everything
  this.shapes = [];  // the collection of things to be drawn
  this.dragging = false; // Keep track of when we are dragging
  // the current selected object.
  // In the future we could turn this into an array for multiple selection
  this.selection = null;
  this.dragoffx = 0; // See mousedown and mousemove events for explanation
  this.dragoffy = 0;

События мыши

Мы добавим события для mousedown, mouseup и mousemove, которые будут контролировать, когда объект запускается и прекращает перетаскивание. Мы также отключим событие selectstart, которое останавливает двойной щелчок на холсте от случайного выбора текста на странице. Наконец, мы добавим событие двойного щелчка, которое создаст новый Shape и добавит его в список фигур CanvasState.

Событие mousedown начинается с вызова getMouse в нашем CanvasState, чтобы вернуть позицию мыши по оси x и y. Затем мы перебираем список фигур, чтобы увидеть, содержит ли какая-либо из них позицию мыши. Мы просматриваем их в обратном направлении, потому что они нарисованы вперед, и мы хотим выбрать тот, который кажется самым верхним, поэтому мы должны найти потенциальную форму, которая была нарисована последней.

Если мы находим фигуру, мы сохраняем смещение, сохраняем эту фигуру как наш выбор, устанавливаем перетаскивание в true и устанавливаем допустимый флаг в false. Мы уже использовали большую часть нашего штата! Наконец, если мы не нашли никаких объектов, нам нужно посмотреть, был ли выбор сохранен с прошлого раза. Если есть, мы должны очистить его. Поскольку мы ничего не нажимали, мы явно не нажимали на уже выбранный объект! Очистка выделения означает, что нам придется очистить холст и перерисовать все без кольца выбора, поэтому мы устанавливаем допустимый флаг в false.

 
  // ...
  // (We are still in the CanvasState constructor)
 
  // This is an example of a closure!
  // Right here "this" means the CanvasState. But we are making events on the Canvas itself,
  // and when the events are fired on the canvas the variable "this" is going to mean the canvas!
  // Since we still want to use this particular CanvasState in the events we have to save a reference to it.
  // This is our reference!
  var myState = this;
 
  //fixes a problem where double clicking causes text to get selected on the canvas
  canvas.addEventListener('selectstart', function(e) { e.preventDefault(); return false; }, false);
  // Up, down, and move are for dragging
  canvas.addEventListener('mousedown', function(e) {
    var mouse = myState.getMouse(e);
    var mx = mouse.x;
    var my = mouse.y;
    var shapes = myState.shapes;
    var l = shapes.length;
    for (var i = l-1; i >= 0; i--) {
      if (shapes[i].contains(mx, my)) {
        var mySel = shapes[i];
        // Keep track of where in the object we clicked
        // so we can move it smoothly (see mousemove)
        myState.dragoffx = mx - mySel.x;
        myState.dragoffy = my - mySel.y;
        myState.dragging = true;
        myState.selection = mySel;
        myState.valid = false;
        return;
      }
    }
    // havent returned means we have failed to select anything.
    // If there was an object selected, we deselect it
    if (myState.selection) {
      myState.selection = null;
      myState.valid = false; // Need to clear the old selection border
    }
  }, true);

Событие mousemove проверяет, установили ли мы флаг перетаскивания в true. Если он у нас есть, он получает текущее положение мыши и перемещает выбранный объект в эту позицию, помня смещение того места, где мы его захватывали. Если флаг перетаскивания имеет значение false, событие mousemove ничего не делает.

  canvas.addEventListener('mousemove', function(e) {
    if (myState.dragging){
      var mouse = myState.getMouse(e);
      // We don't want to drag the object by its top-left corner,
      // we want to drag from where we clicked.
      // Thats why we saved the offset and use it here
      myState.selection.x = mouse.x - myState.dragoffx;
      myState.selection.y = mouse.y - myState.dragoffy;   
      myState.valid = false; // Something's dragging so we must redraw
    }
  }, true);

Событие mouseup простое, все, что нужно сделать, это обновить CanvasState, чтобы мы больше не перетаскивали! Поэтому, когда вы поднимаете мышь, событие mousemove возвращается к бездействию.

  canvas.addEventListener('mouseup', function(e) {
    myState.dragging = false;
  }, true);

Событие двойного щелчка, которое мы будем использовать, чтобы добавить больше фигур на наш холст. Он вызывает addShape для CanvasState с новым экземпляром Shape. все, что делает addShape, это добавляет аргумент в список Shapes в CanvasState.

  // double click for making new Shapes
  canvas.addEventListener('dblclick', function(e) {
    var mouse = myState.getMouse(e);
    myState.addShape(new Shape(mouse.x - 10, mouse.y - 10, 20, 20,
                               'rgba(0,255,0,.6)'));
  }, true);

Я реализовал несколько вариантов, как выглядит кольцо выбора и как часто мы перерисовываем. setInterval просто вызывает метод рисования нашего CanvasState. Наш интервал 30 означает, что мы вызываем метод draw каждые 30 миллисекунд.

  // **** Options! ****
 
  this.selectionColor = '#CC0000';
  this.selectionWidth = 2;  
  this.interval = 30;
  setInterval(function() { myState.draw(); }, myState.interval);
}

рисунок

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

Из-за этого мы очищаем весь холст перед каждым кадром Draw. Это может дорого обойтись, и мы хотим рисовать, только если что-то действительно изменилось в нашей структуре, поэтому у нас есть флаг «valid» в нашем CanvasState.

После того, как все нарисовано, метод draw установит допустимый флаг в true. Затем мы делаем что-то вроде добавления новой фигуры или попытки перетащить фигуру, состояние становится недействительным, а draw () очищается, перерисовывает все объекты и снова устанавливает действительный флаг.

// While draw is called as often as the INTERVAL variable demands,
// It only ever does something if the canvas gets invalidated by our code
CanvasState.prototype.draw = function() {
  // if our state is invalid, redraw and validate!
  if (!this.valid) {
    var ctx = this.ctx;
    var shapes = this.shapes;
    this.clear();
 
    // ** Add stuff you want drawn in the background all the time here **
 
    // draw all shapes
    var l = shapes.length;
    for (var i = 0; i < l; i++) {
      var shape = shapes[i];
      // We can skip the drawing of elements that have moved off the screen:
      if (shape.x > this.width || shape.y > this.height ||
          shape.x + shape.w < 0 || shape.y + shape.h < 0) return;
      shapes[i].draw(ctx);
    }
 
    // draw selection
    // right now this is just a stroke along the edge of the selected Shape
    if (this.selection != null) {
      ctx.strokeStyle = this.selectionColor;
      ctx.lineWidth = this.selectionWidth;
      var mySel = this.selection;
      ctx.strokeRect(mySel.x,mySel.y,mySel.w,mySel.h);
    }
 
    // ** Add stuff you want drawn on top all the time here **
 
    this.valid = true;
  }
}

Мы проходим все фигуры [] и рисуем каждую по порядку. Это придаст приятный вид более поздним фигурам, выглядящим так, как будто они находятся поверх более ранних фигур. После того, как все фигуры нарисованы, дескриптор выделения (если есть выделение) рисуется вокруг фигуры, на которую ссылается this.selection.

Если вам нужен фон (например, город) или передний план (например, облака), один из способов добавить их — поместить их до или после двух основных битов рисования. Хотя часто есть лучшие способы, например, использование нескольких полотен или CSS-фоновое изображение, но мы не будем здесь останавливаться на этом.

Получение координат мыши на холсте

Получение хороших координат мыши немного сложно на Canvas. Вы можете использовать offsetX / Y и LayerX / Y, но LayerX / Y не рекомендуется использовать в веб-наборе (Chrome и Safari), а Firefox не имеет offsetX / Y.

Самый пуленепробиваемый способ получить правильное положение мыши показан ниже. Вы должны идти вверх по дереву, складывая смещения. Затем вы должны добавить любой отступ или границу к смещению. Наконец, чтобы исправить проблемы с координатами, когда у вас есть элементы с фиксированным положением на странице (например, админ-панель wordpress или панель stumbleupon), вы должны добавить offsetTop и offsetLeft <html>.

Затем вы просто вычитаете это смещение из значений e.pageX / Y, и вы получите идеальные координаты практически во всех возможных ситуациях.

// Creates an object with x and y defined,
// set to the mouse position relative to the state's canvas
// If you wanna be super-correct this can be tricky,
// we have to worry about padding and borders
CanvasState.prototype.getMouse = function(e) {
  var element = this.canvas, offsetX = 0, offsetY = 0, mx, my;
 
  // Compute the total offset
  if (element.offsetParent !== undefined) {
    do {
      offsetX += element.offsetLeft;
      offsetY += element.offsetTop;
    } while ((element = element.offsetParent));
  }
 
  // Add padding and border style widths to offset
  // Also add the <html> offsets in case there's a position:fixed bar
  offsetX += this.stylePaddingLeft + this.styleBorderLeft + this.htmlLeft;
  offsetY += this.stylePaddingTop + this.styleBorderTop + this.htmlTop;
 
  mx = e.pageX - offsetX;
  my = e.pageY - offsetY;
 
  // We return a simple javascript object (a hash) with x and y defined
  return {x: mx, y: my};
}

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

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

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

 

Источник: http://simonsarris.com/blog/510-making-html5-canvas-useful