Load an Image from an Arbitrary URL into HTML Canvas

While working on Pixie we wanted to be able to import any image from the web into the editor. Ideally this would all occur in client-side JavaScript, but due to a “security” restriction* I believe that it is not possible without extensive workarounds. Fortunately loading an image on the server is actually much easier than working with the Canvas ImageData API (no joke), so though a loss from an efficiency standpoint, from a simplicity standpoint it may be win.

This particular implementation requires RMagick, though any image library should be about as easy. The first step is to read the image from the user-supplied URL, next gather the width and height meta-data.

  def data_from_url(url)
    image_data = Magick::Image.read(url).first
    width = image_data.columns
    height = image_data.rows
 
    data = image_data.get_pixels(0, 0, width, height).map do |pixel|
      hex_color_to_rgba(pixel.to_color(Magick::AllCompliance, false, 8, true), pixel.opacity)
    end
 
    return {
      :width => width,
      :height => height,
      :data => data,
    }
  end

The only tricky part is converting all the pixel data into a format that can be used by JavaScritpt. I decided on rgba format as that is simple to read, implement, and test. The one downside that I can see is that for large images it will be somewhat inefficient to convert each pixel into a large text string, but since I mostly plan on dealing with images < 100×100 it shouldn’t be a big deal. The hex_color_to_rgba helper method takes care of all the dirty work of converting the moderately unwieldy output from RMagick. It turns "#FAFAFA", 65535 into "rgba(250, 250, 25, 1)" a format that browsers respect.

  def hex_color_to_rgba(color, opacity)
    int_opacity = (Magick::QuantumRange - opacity) / Magick::QuantumRange.to_f
 
    match_data = /^#([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})/.match(color)[1..3].map(&amp;:hex)
 
    "rgba(#{match_data.join(',')},#{int_opacity})"
  end

In the end we return a hash containing the the width, height, and pixel data. This can easily be converted to json via the to_json method, or whatever other format tickles your fancy.

The JavaScript code is pretty dumb, it reads the width and height, then iterates over the data and paints each pixel individually. Not super exciting, but simple and effective. That’s that for loading arbitrary images into HTML Canvas, though I hope one day to develop an entirely JS solution.

* I do not understand the reasoning why you are allowed to load an image from an arbitrary url and display that image, but not have access to the pixel data from it. It sounds pretty silly, especially considering that you can load scripts from an arbitrary URL and run them, or load json from an arbitrary URL. I have no idea what “security” that this restriction provides, other than the “security” of having to load images through your server and hinder the user experience. Here’s a related discussion, but I still don’t get it. This is a hole in my personal understanding, so if anyone knows the reasoning behind the restriction I’d love to hear it.

jQuery style events

jQuery events are cool. But what if you want to trigger some events but not extend all your JavaScript objects to jQuery ones? If you are drawing thousands and thousands of these objects to screen you probably don’t want to worry about the overhead of making them jQuery objects.

Here’s how you can write your own simple jQuery style events:

function Meteor() {
  var eventCallbacks = {
    'destroy': alert('destroyed')
  };
 
  var destroyed = false;
 
  var self = {
    bind: function(event, callback) {
      eventCallbacks[event] = callback;
    },
    destroy: function() {
      if (!destroyed) {
        destroyed = true;
        self.trigger('destroy');
      }
    },
    explode: function() {
      // Kaboom
    },
    trigger: function(event) {
      eventCallbacks[event](self);
    },        
  };
  return self;
}

Here’s how you use it

var meteor = Meteor();
meteor.bind('destroy', function() { 
  meteor.explode();
});
 
...
 
meteor.destroy();

matrix.js Demo

Excerpts, source code and images are great, but a working example is even greater. That’s why I’ve cobbled together a working demo just for you! View it here: Matrix Demo. I’ve extracted it to just the minimum that you need to work with. Jump Straight to the guided tour.

Some Background

Though matrix.js has no dependencies, I depend on jQuery to code in JavaScript without sobbing uncontrollably. This code makes use of jQuery, though you should be fine if you are familiar with any major JS framework.

There are two main components that need to be brought together to make a decent demonstration: object(s) with nested component(s) and a canvas that can understand matrices.

Canvas

The canvas implementation of matrix transformations is mega-clunky, but that won’t bother us any longer.

        var canvas = $('#gameCanvas').get(0).getContext('2d');
 
        $.extend(canvas, {
          withTransform: function(matrix, block) {
            this.save();
 
            this.transform(
              matrix.a,
              matrix.b,
              matrix.c,
              matrix.d,
              matrix.tx,
              matrix.ty
            );
 
            try {
              block();
            } finally {
              this.restore();
            }
          }
        });

The withTransform method takes a matrix and a function (code block). It handles saving the context, applying the elements in the matrix to the correct parameters, calling the code block, and restoring the saved context no matter what, even if the code block throws an exception up in its face after eating bad seafood. It will stop at nothing to make your programming dreams come true.

Rather than handle all that junk ourselves every time want to draw a rotated top hat, we can instead do something like:

        var matrix = Matrix.rotation(Math.PI/2);
        canvas.withTransform(matrix, function() {
          // I'm so carefree, I can draw and draw without worrying about
          // saving or restoring the context, or what order those 4 trig
          // dealies go in. Thank you matrix.js for saving my life and
          // becoming my new best friend. <3<3<3 XOXO !!!1
        });

Hand-crafted matrix elements do not have more value than those forged in the heart of a machine.

Objects

Ok, so the canvas can handle matrix transforms easily, big deal. I just want to put a top hat on a dinosaur, make him dance, and laugh on into the early morning light.

  var hat = GameObject("images/accessories/tophat.png", Matrix.translation(30, -32));
 
  var dino = GameObject("images/levels/dino1.png", Matrix.translation(320, 240), [
    hat
  ]);

Done!

I’ve called my objects GameObject because sometimes I make games. The constructor takes 3 arguments, a url for an image, the transformation matrix of the object, and a list of component objects to draw inside it. I’ve got a dinosaur and the dinosaur has a hat.

To actually draw the dino we have a classic game loop.

  setInterval(function() {
    canvas.fill('#A2EEFF');
 
    dino.draw(canvas);
  }, 33);

Only the dino needs to be drawn because he’ll take care of drawing his own hat.


Making him dance

This is the part where you open up the demo page, fire up your JS console, and play along.

We want to move the dino up a little bit. Remember, up is negative.

  dino.translate(0, -50);

Now we want to rotate him.

  dino.rotate(Math.PI / 3);

Now we want him to walk warily down the street with his brim pulled way down low.

  // Brim down
  hat.translate(0, 10);
 
  // Walk warily...?
  (function() {
    var i = 0;
    setInterval(function() {
      dino.rotate(Math.sin(i / 8) * (Math.PI / 6));
      i++;
    }, 33);
  }());

Well you get the idea. If things get too nuts just refresh and start again.

Images Extras (Bonus Section!)

Even assuming you already have an image and HTML5 Canvas all set up, it is still a giant pain to draw it on there. Not to mention loading an image or waiting for it to load. So step one is to remove the tedium from drawing images onto the canvas forever.

  var sprite = Sprite.load(imageUrl);
  sprite.draw(canvas);

Wow, that was easy! Take a look at the Sprite class in the source for more on this miraculous occurrence.

Introducing matrix.js

When using the HTML5 canvas element it would be nice to have access to the one thing Flash does well: matrix transformations. This short utility adds the classic matrix operations: rotation, translation, scaling, concatenation, and inverting. It also provides the very useful transformPoint to transform points on an object in the same way that canvas transforms images.

Working with HTML5 Canvas

You’ll most likely want to use the following function to make using matrix transformations super easy with your HTML5 Canvas.

    function withTransformation(transformation, block) {
      context.save();
 
      context.transform(
        transformation.a,
        transformation.b,
        transformation.c,
        transformation.d,
        transformation.tx,
        transformation.ty
      );
 
      try {
        block();
      } finally {
        context.restore();
      }
    }

This is great for objects that have components, such as, completely hypothetically, a dinosaur flying and rotating through the air crazily swinging a chainsaw while wearing a jetpack.

Working with collision detection

A big advantage of circular collision detection comes when used with Euclidean Transformations, isometries of the Euclidean plane that preserve geometric properties such as length. Rotation, translation and reflection are the subset of affine transformations that maintain these properties.

If the collision area of an object is represented by n circles, then you can transform the points representing the centers of each circle with the same transformation as the one applied to the object. Because circles are the same across translation, rotation, and reflection, the logic to detect collisions remains the same, using the new center-points.


Scaling by the same factor across x and y could also work, just multiply the radius by the scale factor. Skewing would affect the shape of the circles, and therefore cannot be used without significant modification to the collision detection algorithm.

The Code

On github

/**
* Matrix.js v1.1.0
* 
* Copyright (c) 2010 STRd6
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Loosely based on flash:
* http://www.adobe.com/livedocs/flash/9.0/ActionScriptLangRefV3/flash/geom/Matrix.html
*/
(function() {
  /**
   * Create a new point with given x and y coordinates. If no arguments are given
   * defaults to (0, 0).
   * @name Point
   * @param {Number} [x]
   * @param {Number} [y]
   * @constructor
   */
  function Point(x, y) {
    return {
      /**
       * The x coordinate of this point.
       * @name x
       * @fieldOf Point#
       */
      x: x || 0,
      /**
       * The y coordinate of this point.
       * @name y
       * @fieldOf Point#
       */
      y: y || 0,
      /**
       * Adds a point to this one and returns the new point.
       * @name add
       * @methodOf Point#
       *
       * @param {Point} other The point to add this point to.
       * @returns A new point, the sum of both.
       * @type Point
       */
      add: function(other) {
        return Point(this.x + other.x, this.y + other.y);
      }
    }
  }
 
  /**
   * @param {Point} p1
   * @param {Point} p2
   * @returns The Euclidean distance between two points.
   */
  Point.distance = function(p1, p2) {
    return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
  };
 
  /**
   * If you have two dudes, one standing at point p1, and the other
   * standing at point p2, then this method will return the direction
   * that the dude standing at p1 will need to face to look at p2.
   * @param {Point} p1 The starting point.
   * @param {Point} p2 The ending point.
   * @returns The direction from p1 to p2 in radians.
   */
  Point.direction = function(p1, p2) {
    return Math.atan2(
      p2.y - p1.y,
      p2.x - p1.x
    );
  }
 
  /**
   *  _        _
   * | a  c tx  |
   * | b  d ty  |
   * |_0  0  1 _|
   * Creates a matrix for 2d affine transformations.
   *
   * concat, inverse, rotate, scale and translate return new matrices with the
   * transformations applied. The matrix is not modified in place.
   *
   * Returns the identity matrix when called with no arguments.
   * @name Matrix
   * @param {Number} [a]
   * @param {Number} [b]
   * @param {Number} [c]
   * @param {Number} [d]
   * @param {Number} [tx]
   * @param {Number} [ty]
   * @constructor
   */
  function Matrix(a, b, c, d, tx, ty) {
    a = a !== undefined ? a : 1;
    d = d !== undefined ? d : 1;
 
    return {
      /**
       * @name a
       * @fieldOf Matrix#
       */
      a: a,
      /**
       * @name b
       * @fieldOf Matrix#
       */
      b: b || 0,
      /**
       * @name c
       * @fieldOf Matrix#
       */
      c: c || 0,
      /**
       * @name d
       * @fieldOf Matrix#
       */
      d: d,
      /**
       * @name tx
       * @fieldOf Matrix#
       */
      tx: tx || 0,
      /**
       * @name ty
       * @fieldOf Matrix#
       */
      ty: ty || 0,
 
      /**
       * Returns the result of this matrix multiplied by another matrix
       * combining the geometric effects of the two. In mathematical terms, 
       * concatenating two matrixes is the same as combining them using matrix multiplication.
       * If this matrix is A and the matrix passed in is B, the resulting matrix is A x B
       * http://mathworld.wolfram.com/MatrixMultiplication.html
       * @name concat
       * @methodOf Matrix#
       *
       * @param {Matrix} matrix The matrix to multiply this matrix by.
       * @returns The result of the matrix multiplication, a new matrix.
       * @type Matrix
       */
      concat: function(matrix) {
        return Matrix(
          this.a * matrix.a + this.c * matrix.b,
          this.b * matrix.a + this.d * matrix.b,
          this.a * matrix.c + this.c * matrix.d,
          this.b * matrix.c + this.d * matrix.d,
          this.a * matrix.tx + this.c * matrix.ty + this.tx,
          this.b * matrix.tx + this.d * matrix.ty + this.ty
        );
      },
 
      /**
       * Given a point in the pretransform coordinate space, returns the coordinates of 
       * that point after the transformation occurs. Unlike the standard transformation 
       * applied using the transformPoint() method, the deltaTransformPoint() method's 
       * transformation does not consider the translation parameters tx and ty.
       * @name deltaTransformPoint
       * @methodOf Matrix#
       * @see #transformPoint
       *
       * @return A new point transformed by this matrix ignoring tx and ty.
       * @type Point
       */
      deltaTransformPoint: function(point) {
        return Point(
          this.a * point.x + this.c * point.y,
          this.b * point.x + this.d * point.y
        );
      },
 
      /**
       * Returns the inverse of the matrix.
       * http://mathworld.wolfram.com/MatrixInverse.html
       * @name inverse
       * @methodOf Matrix#
       *
       * @returns A new matrix that is the inverse of this matrix.
       * @type Matrix
       */
      inverse: function() {
        var determinant = this.a * this.d - this.b * this.c;
        return Matrix(
          this.d / determinant,
          -this.b / determinant,
          -this.c / determinant,
          this.a / determinant,
          (this.c * this.ty - this.d * this.tx) / determinant,
          (this.b * this.tx - this.a * this.ty) / determinant
        );
      },
 
      /**
       * Returns a new matrix that corresponds this matrix multiplied by a
       * a rotation matrix.
       * @name rotate
       * @methodOf Matrix#
       * @see Matrix.rotation
       *
       * @param {Number} theta Amount to rotate in radians.
       * @param {Point} [aboutPoint] The point about which this rotation occurs. Defaults to (0,0).
       * @returns A new matrix, rotated by the specified amount.
       * @type Matrix
       */
      rotate: function(theta, aboutPoint) {
        return this.concat(Matrix.rotation(theta, aboutPoint));
      },
 
      /**
       * Returns a new matrix that corresponds this matrix multiplied by a
       * a scaling matrix.
       * @name scale
       * @methodOf Matrix#
       * @see Matrix.scale
       *
       * @param {Number} sx
       * @param {Number} [sy]
       * @param {Point} [aboutPoint] The point that remains fixed during the scaling
       * @type Matrix
       */
      scale: function(sx, sy, aboutPoint) {
        return this.concat(Matrix.scale(sx, sy, aboutPoint));
      },
 
      /**
       * Returns the result of applying the geometric transformation represented by the 
       * Matrix object to the specified point.
       * @name transformPoint
       * @methodOf Matrix#
       * @see #deltaTransformPoint
       *
       * @returns A new point with the transformation applied.
       * @type Point
       */
      transformPoint: function(point) {
        return Point(
          this.a * point.x + this.c * point.y + this.tx,
          this.b * point.x + this.d * point.y + this.ty
        );
      },
 
      /**
       * Translates the matrix along the x and y axes, as specified by the tx and ty parameters.
       * @name translate
       * @methodOf Matrix#
       * @see Matrix.translation
       *
       * @param {Number} tx The translation along the x axis.
       * @param {Number} ty The translation along the y axis.
       * @returns A new matrix with the translation applied.
       * @type Matrix
       */
      translate: function(tx, ty) {
        return this.concat(Matrix.translation(tx, ty));
      }
    }
  }
 
  /**
   * Creates a matrix transformation that corresponds to the given rotation,
   * around (0,0) or the specified point.
   * @see Matrix#rotate
   *
   * @param {Number} theta Rotation in radians.
   * @param {Point} [aboutPoint] The point about which this rotation occurs. Defaults to (0,0).
   * @returns 
   * @type Matrix
   */
  Matrix.rotation = function(theta, aboutPoint) {
    var rotationMatrix = Matrix(
      Math.cos(theta),
      Math.sin(theta),
      -Math.sin(theta),
      Math.cos(theta)
    );
 
    if(aboutPoint) {
      rotationMatrix =
        Matrix.translation(aboutPoint.x, aboutPoint.y).concat(
          rotationMatrix
        ).concat(
          Matrix.translation(-aboutPoint.x, -aboutPoint.y)
        );
    }
 
    return rotationMatrix;
  };
 
  /**
   * Returns a matrix that corresponds to scaling by factors of sx, sy along
   * the x and y axis respectively.
   * If only one parameter is given the matrix is scaled uniformly along both axis.
   * If the optional aboutPoint parameter is given the scaling takes place
   * about the given point.
   * @see Matrix#scale
   *
   * @param {Number} sx The amount to scale by along the x axis or uniformly if no sy is given.
   * @param {Number} [sy] The amount to scale by along the y axis.
   * @param {Point} [aboutPoint] The point about which the scaling occurs. Defaults to (0,0).
   * @returns A matrix transformation representing scaling by sx and sy.
   * @type Matrix
   */
  Matrix.scale = function(sx, sy, aboutPoint) {
    sy = sy || sx;
 
    var scaleMatrix = Matrix(sx, 0, 0, sy);
 
    if(aboutPoint) {
      scaleMatrix =
        Matrix.translation(aboutPoint.x, aboutPoint.y).concat(
          scaleMatrix
        ).concat(
          Matrix.translation(-aboutPoint.x, -aboutPoint.y)
        );
    }
 
    return scaleMatrix;
  };
 
  /**
   * Returns a matrix that corresponds to a translation of tx, ty.
   * @see Matrix#translate
   *
   * @param {Number} tx The amount to translate in the x direction.
   * @param {Number} ty The amount to translate in the y direction.
   * @return A matrix transformation representing a translation by tx and ty.
   * @type Matrix
   */
  Matrix.translation = function(tx, ty) {
    return Matrix(1, 0, 0, 1, tx, ty);
  };
 
  /**
   * A constant representing the identity matrix.
   * @name IDENTITY
   * @fieldOf Matrix
   */
  Matrix.IDENTITY = Matrix();
  /**
   * A constant representing the horizontal flip transformation matrix.
   * @name HORIZONTAL_FLIP
   * @fieldOf Matrix
   */
  Matrix.HORIZONTAL_FLIP = Matrix(-1, 0, 0, 1);
  /**
   * A constant representing the vertical flip transformation matrix.
   * @name VERTICAL_FLIP
   * @fieldOf Matrix
   */
  Matrix.VERTICAL_FLIP = Matrix(1, 0, 0, -1);
 
  // Export to window
  window["Point"] = Point;
  window["Matrix"] = Matrix;
}());

Circular Collision Detection in JavaScript

Collision detection is the core of many games and simulations. It is therefore important to have a simple and efficient collision detection algorithm.

This algorithm uses circles as the basis for colliding elements.

function collision(c1, c2) {
  var dx = c1.x - c2.x;
  var dy = c1.y - c2.y;
  var dist = c1.radius + c2.radius;
 
  return (dx * dx + dy * dy <= dist * dist)
}

Envisioning it graphically shows how the inequality relates to the Pythagorean Theorem.