Javascript Canvas - Animating

In this tutorial we are going to experiment rendering simple 3d particle system on the canvas and allowing the user to rotate the system

Click on Canvas below to allow movement, click again to stop, arrow keys work better on second example.


external version (has dead code removed)


Later we will add a rollover and allow using the arrow keys, which can be seen here Javascipt Canvas Animating with color rollover


This tutorial will show the basic concept of how can use canvas drawing to create very simple animations. Along the way we are going to explore the named colors supported by the browser and get used to some features of Haxe language.

First complete the tutorial on drawing a Red circle on the Canvas. I will assume you have setup your code and directory system following this tutorial. Change the html to give you a black background it looks better for this experiment. You also need to change the project to use the net.justinfront.javascript_canvas_animating package.


Getting used to Using and Typedef

Taking the current code we are going to start using static functions, we can move the position setup of the canvas to a static class method to get used to the idea of using and of typdef. I have removed all the circle drawing code for the moment. Our main class with some new code I will explain looks like..

package net.justinfront.javascript_canvas_animating;
import js.Lib;
import js.Dom;
typedef Point2D = 
{
    var x: Float;
    var y: Float;
}
import net.justinfront.javascript_canvas_animating.Main;
using net.justinfront.javascript_canvas_animating.Main;
class Main
{
    

    var root:               Body;
    var surface:            CanvasRenderingContext2D;    
   
    static function main(){ new Main(); }
    public function new()
    {
        
        root                        = Lib.document.body;
        var dom:        HtmlDom     = Lib.document.createElement('Canvas');
        var canvas:     Canvas      = cast dom;
        surface                     = untyped canvas.getContext('2d');
        var style                   = dom.style;
        
        root.appendChild( dom );
        canvas.width                = 1024;
        canvas.height               = 768;
        
        style.setPosition( { x: 0, y: 0 } );

    }
    
    static inline public function setPosition( style: Style, position: Point2D ): Point2D
    { 
        style.paddingLeft   = "0px";
        style.paddingTop    = "0px";
        style.left          = Std.string( position.x + 'px' );
        style.top           = Std.string( position.y + 'px' );
        style.position      = "absolute";
        return position;
    }

}

So we are setting up some of the position style attributes for our Canvas element using a static method, and a typedef.
We have defined a Point2D as a typedef.

typedef Point2D = 
{
    var x: Float;
    var y: Float;
}

This is like a class without any methods.
We have created a static method to handle all the messy parts of setting the Canvas Div's position.
    static inline public function setPosition( style: Style, position: Point2D ): Point2D
    { 
        style.paddingLeft   = "0px";
        style.paddingTop    = "0px";
        style.left          = Std.string( position.x + 'px' );
        style.top           = Std.string( position.y + 'px' );
        style.position      = "absolute";
        return position;
    }

Notice how we can use the new Point2D typedef to pass the position into the class.

notice these lines...

import net.justinfront.javascript_canvas_animating.Main;
using net.justinfront.javascript_canvas_animating.Main;

They setup using this is like a trick that allows us to use class methods almost like methods of some other class. The first parameter of a static method can now be moved to the front of the calling expression, so instead of...

Main.setPosition( style,point );

The using allows us to now write
style.setPosition( point );

So in our code we can put the point typedef directly in our call as a Point2D:
style.setPosition( { x: 0, y: 0 } );

More Using and Typedef's adding a red circle


Ok lets repeat this process for drawing a red circle so we really get the hang of typedef and using. We can create a typedef that holds information about the colors. Colors used with the javascript canvas drawing API we add any typedef to top of our class and an enum with some colors in, for the moment I will assume you only need the Red and Black color ( javascript has about 147 string named colors you can use instead of a hash version "#ff0000" which is still red but not as obvious.
enum Color =
{
    Red;
    Black;
}
typedef Pen =
{
    var fill:       Color;
    var lineColor:  Color;
    var thickness:  Int;
    var hasEdge:    Bool;
    var hasFill:    Bool;
}

Then we can add a static method to draw the circle, I am using arc but I think you can drawCircle or similar.
    static inline public function drawCircle(   surface:      CanvasRenderingContext2D
                                            ,   position:   Point2D
                                            ,   radius:     Float
                                            ,   pen:        Pen
                                            )
    {
        if( pen.hasEdge )
        {
            surface.strokeStyle = Std.string( pen.lineColor);
            surface.lineWidth = pen.thickness;
        }
        if( pen.hasFill ) surface.fillStyle = Std.string( pen.fill );
        surface.beginPath();
        surface.arc( position.x + radius, position.y + radius, radius, 0, 2*Math.PI, false );
        if( pen.hasEdge ) surface.stroke();
        surface.closePath();
        if( pen.hasFill ) surface.fill();
    }

You will notice I am converting the Red and Black enum into strings to provide the fill and stroke colors for the Canvas drawing API.
For continence we can create a default pen and put its creation in a little helper method, I will use a static method since I will move it to another class later.
    public static function redFill()
    {
        return { fill: Red, lineColor: Black, thickness: 1, hasEdge: false, hasFill: true };
    }

I want to keep a copy of the pen to modify the fill later so we add a pen property to our class.
    var pen:  Pen;

So now to draw a circle on screen we simply create the pen and then draw the circle, we are using again see the setPosition method as comparison.
        pen                         = Main.redFill();
        surface.drawCircle( { x: 20, y: 20 }, 20, pen );

Creating More Color


In Javascript as I said there are 147 predefined colors older browsers have less defined about 20. We will assume a modern browser. Obviously you can use any RGB value but for this experiment let just look at using named colors. Putting all the named colors in a class as a enum is a good way to make sure we always spell them correctly and we can make some methods to quickly get the Red, Green and Blue component values from them without doing any code work. Sometimes it is better to create hardcode values rather than calculate them, so I have added a method that switches on the enum value and provides an array of the Red, Green and Blue color quantities.
package net.justinfront.canvas_animating.Main;

typedef Pen =
{
    var fill:       ColorHtml5;
    var lineColor:  ColorHtml5;
    var thickness:  Int;
    var hasEdge:    Bool;
    var hasFill:    Bool;
}

enum ColorHtml5
{
    AliceBlue; AntiqueWhite; Aqua; Aquamarine; Azure;
    Beige; Bisque; Black; BlanchedAlmond; Blue; BlueViolet; Brown; BurlyWood;
    CadetBlue; Chartreuse; Chocolate; Coral; CornflowerBlue; Cornsilk; Crimson; Cyan;
    DarkBlue; DarkCyan; DarkGoldenRod; DarkGray; DarkGrey; DarkGreen; DarkKhaki; 
    DarkMagenta; DarkOliveGreen; Darkorange; DarkOrchid; DarkRed; DarkSalmon;
    DarkSeaGreen; DarkSlateBlue; DarkSlateGray; DarkSlateGrey; DarkTurquoise; DarkViolet;
    DeepPink; DeepSkyBlue; DimGray; DimGrey; DodgerBlue;
    FireBrick; FloralWhite; ForestGreen; Fuchsia;
    Gainsboro; GhostWhite; Gold; GoldenRod; Gray; Grey; Green; GreenYellow;
    HoneyDew; HotPink;
    IndianRed; Indigo; Ivory;
    Khaki;
    Lavender; LavenderBlush; LawnGreen; LemonChiffon;
    LightBlue; LightCoral; LightCyan; LightGoldenRodYellow; LightGray; LightGrey; 
    LightGreen; LightPink; LightSalmon; LightSeaGreen; LightSkyBlue;
    LightSlateGray; LightSlateGrey; LightSteelBlue; LightYellow;
    Lime; LimeGreen; Linen;
    Magenta; Maroon; 
    MediumAquaMarine; MediumBlue; MediumOrchid; MediumPurple; MediumSeaGreen;
    MediumSlateBlue; MediumSpringGreen; MediumTurquoise; MediumVioletRed;
    MidnightBlue; MintCream; MistyRose; Moccasin; 
    NavajoWhite; Navy;
    OldLace; Olive; OliveDrab; Orange; OrangeRed; Orchid; 
    PaleGoldenRod; PaleGreen; PaleTurquoise; PaleVioletRed; PapayaWhip; 
    PeachPuff; Peru; Pink; Plum; PowderBlue; Purple;
    Red; RosyBrown; RoyalBlue;
    SaddleBrown; Salmon; SandyBrown; SeaGreen; SeaShell; Sienna; Silver;
    SkyBlue; SlateBlue; SlateGray; SlateGrey; Snow; SpringGreen; SteelBlue;
    Tan; Teal; Thistle; Tomato; Turquoise; Violet; Wheat; White; WhiteSmoke;
    Yellow; YellowGreen;
}
/*
enum ColorHtml
{
    Aqua; Black; Blue; 
    Brown; Chartreuse; Fuchsia; 
    Gray; Green; Lime; 
    Maroon; Navy; Olive; 
    Orange; Purple; Red; 
    Silver; Teal; Violet; 
    White; Yellow;
}
*/
class Colorjs
{
    public function new()
    {
        
    }
    
    public static function penClone( p: Pen ): Pen
    {
        return { fill: p.fill, lineColor: p.lineColor, thickness: p.thickness, hasEdge: p.hasEdge, hasFill: p.hasFill };
    }
    
    public static function redFill()
    {
        return { fill: Red, lineColor: Black, thickness: 1, hasEdge: false, hasFill: true };
    }
/*    
    static public function colorHtmlAsHexString( color: ColorHtml )
    {
        switch( color )
        {
            case Aqua:          "#00FFFF"; 
            case Black:         "#000000";
            case Blue:          "#0000FF";
            case Brown:         "#A02820";
            case Chartreuse:    "#80FF00"; 
            case Fuchsia:       "#FF00FF"; 
            case Gray:          "#808080"; 
            case Green:         "#008000"; 
            case Lime:          "#00FF00";
            case Maroon:        "#800000";
            case Navy:          "#000080"; 
            case Olive:         "#808000"; 
            case Orange:        "#FFA000"; 
            case Purple:        "#800080"; 
            case Red:           "FF0000"; 
            case Silver:        "#C0C0C0";
            case Teal:          "#008080"; 
            case Violet:        "#F080F0"; 
            case White:         "#FFFFFF";
            case Yellow:        "#FFFF00" ; 
        }
    }
*/    
    static inline public function str( color: ColorHtml5 ): String
    {
        return switch( color )
        {
            case AliceBlue:              '#F0F8FF';
            case AntiqueWhite:          '#FAEBD7';
            case Aqua:                  '#00FFFF';
            case Aquamarine:            '#7FFFD4';
            case Azure:                  '#F0FFFF';
            case Beige:                 '#F5F5DC';
            case Bisque:                  '#FFE4C4';
            case Black:                 '#000000';
            case BlanchedAlmond:        '#FFEBCD';
            case Blue:                  '#0000FF';
            case BlueViolet:               '#8A2BE2';
            case Brown:                   '#A52A2A';
            case BurlyWood:               '#DEB887';
            case CadetBlue:               '#5F9EA0';
            case Chartreuse:              '#7FFF00';
            case Chocolate:               '#D2691E';
            case Coral:                   '#FF7F50';
            case CornflowerBlue:          '#6495ED';
            case Cornsilk:              '#FFF8DC';
            case Crimson:                 '#DC143C';
            case Cyan:                    '#00FFFF';
            case DarkBlue:                '#00008B';
            case DarkCyan:                '#008B8B';
            case DarkGoldenRod:          '#B8860B';
            case DarkGray:              '#A9A9A9';
            case DarkGrey:              '#A9A9A9';
            case DarkGreen:             '#006400';
            case DarkKhaki:               '#BDB76B';
            case DarkMagenta:             '#8B008B';
            case DarkOliveGreen:         '#556B2F';
            case Darkorange:             '#FF8C00';
            case DarkOrchid:              '#9932CC';
            case DarkRed:                 '#8B0000';
            case DarkSalmon:              '#E9967A';
            case DarkSeaGreen:            '#8FBC8F';
            case DarkSlateBlue:          '#483D8B';
            case DarkSlateGray:          '#2F4F4F';
            case DarkSlateGrey:          '#2F4F4F';
            case DarkTurquoise:          '#00CED1';
            case DarkViolet:              '#9400D3';
            case DeepPink:                '#FF1493';
            case DeepSkyBlue:             '#00BFFF';
            case DimGray:                 '#696969';
            case DimGrey:                 '#696969';
            case DodgerBlue:              '#1E90FF';
            case FireBrick:               '#B22222';
            case FloralWhite:             '#FFFAF0';
            case ForestGreen:             '#228B22';
            case Fuchsia:                 '#FF00FF';
            case Gainsboro:               '#DCDCDC';
            case GhostWhite:              '#F8F8FF';
            case Gold:                    '#FFD700';
            case GoldenRod:               '#DAA520';
            case Gray:                  '#808080';
            case Grey:                  '#808080';
            case Green:                  '#008000';
            case GreenYellow:              '#ADFF2F';
            case HoneyDew:              '#F0FFF0';
            case HotPink:                  '#FF69B4';
            case IndianRed:               '#CD5C5C';
            case Indigo:                   '#4B0082';
            case Ivory:                  '#FFFFF0';
            case Khaki:                  '#F0E68C';
            case Lavender:              '#E6E6FA';
            case LavenderBlush:          '#FFF0F5';
            case LawnGreen:              '#7CFC00';
            case LemonChiffon:          '#FFFACD';
            case LightBlue:              '#ADD8E6';
            case LightCoral:              '#F08080';
            case LightCyan:              '#E0FFFF';
            case LightGoldenRodYellow:  '#FAFAD2';
            case LightGray:              '#D3D3D3';
            case LightGrey:              '#D3D3D3';
            case LightGreen:              '#90EE90';
            case LightPink:              '#FFB6C1';
            case LightSalmon:              '#FFA07A';
            case LightSeaGreen:          '#20B2AA';
            case LightSkyBlue:          '#87CEFA';
            case LightSlateGray:          '#778899';
            case LightSlateGrey:          '#778899';
            case LightSteelBlue:          '#B0C4DE';
            case LightYellow:              '#FFFFE0';
            case Lime:                   '#00FF00';
            case LimeGreen:             '#32CD32';
            case Linen:                   '#FAF0E6';
            case Magenta:                 '#FF00FF';
            case Maroon:                  '#800000';
            case MediumAquaMarine:        '#66CDAA';
            case MediumBlue:              '#0000CD';
            case MediumOrchid:            '#BA55D3';
            case MediumPurple:            '#9370DB';
            case MediumSeaGreen:          '#3CB371';
            case MediumSlateBlue:         '#7B68EE';
            case MediumSpringGreen:      '#00FA9A';
            case MediumTurquoise:         '#48D1CC';
            case MediumVioletRed:         '#C71585';
            case MidnightBlue:            '#191970';
            case MintCream:               '#F5FFFA';
            case MistyRose:               '#FFE4E1';
            case Moccasin:                '#FFE4B5';
            case NavajoWhite:             '#FFDEAD';
            case Navy:                    '#000080';
            case OldLace:                 '#FDF5E6';
            case Olive:                   '#808000';
            case OliveDrab:               '#6B8E23';
            case Orange:                  '#FFA500';
            case OrangeRed:               '#FF4500';
            case Orchid:                  '#DA70D6';
            case PaleGoldenRod:           '#EEE8AA';
            case PaleGreen:               '#98FB98';
            case PaleTurquoise:           '#AFEEEE';
            case PaleVioletRed:           '#DB7093';
            case PapayaWhip:              '#FFEFD5';
            case PeachPuff:               '#FFDAB9';
            case Peru:                    '#CD853F';
            case Pink:                    '#FFC0CB';
            case Plum:                    '#DDA0DD';
            case PowderBlue:              '#B0E0E6';
            case Purple:                  '#800080';
            case Red:                     '#FF0000';
            case RosyBrown:               '#BC8F8F';
            case RoyalBlue:               '#4169E1';
            case SaddleBrown:             '#8B4513';
            case Salmon:                  '#FA8072';
            case SandyBrown:              '#F4A460';
            case SeaGreen:                '#2E8B57';
            case SeaShell:                '#FFF5EE';
            case Sienna:                  '#A0522D';
            case Silver:                  '#C0C0C0';
            case SkyBlue:                 '#87CEEB';
            case SlateBlue:               '#6A5ACD';
            case SlateGray:               '#708090';
            case SlateGrey:               '#708090';
            case Snow:                    '#FFFAFA';
            case SpringGreen:             '#00FF7F';
            case SteelBlue:               '#4682B4';
            case Tan:                     '#D2B48C';
            case Teal:                    '#008080';
            case Thistle:                 '#D8BFD8';
            case Tomato:                  '#FF6347';
            case Turquoise:               '#40E0D0';
            case Violet:                  '#EE82EE';
            case Wheat:                   '#F5DEB3';
            case White:                   '#FFFFFF';
            case WhiteSmoke:              '#F5F5F5';
            case Yellow:                  '#FFFF00';
            case YellowGreen:             '#9ACD32';
            
        }
    }
    static inline public function rgb( color: ColorHtml5 ): Array<Int>
    {
        return switch( color )
        {
            case AliceBlue:              [ 0xF0, 0xF8, 0xFF ];
            case AntiqueWhite:          [ 0xFA, 0xEB, 0xD7 ];
            case Aqua:                  [ 0x00, 0xFF, 0xFF ];
            case Aquamarine:            [ 0x7F, 0xFF, 0xD4 ];
            case Azure:                  [ 0xF0, 0xFF, 0xFF ];
            case Beige:                 [ 0xF5, 0xF5, 0xDC ];
            case Bisque:                  [ 0xFF, 0xE4, 0xC4 ];
            case Black:                 [ 0x00, 0x00, 0x00 ];
            case BlanchedAlmond:        [ 0xFF, 0xEB, 0xCD ];
            case Blue:                  [ 0x00, 0x00, 0xFF ];
            case BlueViolet:               [ 0x8A, 0x2B, 0xE2 ];
            case Brown:                   [ 0xA5, 0x2A, 0x2A ];
            case BurlyWood:               [ 0xDE, 0xB8, 0x87 ];
            case CadetBlue:               [ 0x5F, 0x9E, 0xA0 ];
            case Chartreuse:              [ 0x7F, 0xFF, 0x00 ];
            case Chocolate:               [ 0xD2, 0x69, 0x1E ];
            case Coral:                   [ 0xFF, 0x7F, 0x50 ];
            case CornflowerBlue:          [ 0x64, 0x95, 0xED ];
            case Cornsilk:              [ 0xFF, 0xF8, 0xDC ];
            case Crimson:                 [ 0xDC, 0x14, 0x3C ];
            case Cyan:                    [ 0x00, 0xFF, 0xFF ];
            case DarkBlue:                [ 0x00, 0x00, 0x8B ];
            case DarkCyan:                [ 0x00, 0x8B, 0x8B ];
            case DarkGoldenRod:          [ 0xB8, 0x86, 0x0B ];
            case DarkGray:              [ 0xA9, 0xA9, 0xA9 ];
            case DarkGrey:              [ 0xA9, 0xA9, 0xA9 ];
            case DarkGreen:             [ 0x00, 0x64, 0x00 ];
            case DarkKhaki:               [ 0xBD, 0xB7, 0x6B ];
            case DarkMagenta:             [ 0x8B, 0x00, 0x8B ];
            case DarkOliveGreen:         [ 0x55, 0x6B, 0x2F ];
            case Darkorange:             [ 0xFF, 0x8C, 0x00 ];
            case DarkOrchid:              [ 0x99, 0x32, 0xCC ];
            case DarkRed:                 [ 0x8B, 0x00, 0x00 ];
            case DarkSalmon:              [ 0xE9, 0x96, 0x7A ];
            case DarkSeaGreen:            [ 0x8F, 0xBC, 0x8F ];
            case DarkSlateBlue:          [ 0x48, 0x3D, 0x8B ];
            case DarkSlateGray:          [ 0x2F, 0x4F, 0x4F ];
            case DarkSlateGrey:          [ 0x2F, 0x4F, 0x4F ];
            case DarkTurquoise:          [ 0x00, 0xCE, 0xD1 ];
            case DarkViolet:              [ 0x94, 0x00, 0xD3 ];
            case DeepPink:                [ 0xFF, 0x14, 0x93 ];
            case DeepSkyBlue:             [ 0x00, 0xBF, 0xFF ];
            case DimGray:                 [ 0x69, 0x69, 0x69 ];
            case DimGrey:                 [ 0x69, 0x69, 0x69 ];
            case DodgerBlue:              [ 0x1E, 0x90, 0xFF ];
            case FireBrick:               [ 0xB2, 0x22, 0x22 ];
            case FloralWhite:             [ 0xFF, 0xFA, 0xF0 ];
            case ForestGreen:             [ 0x22, 0x8B, 0x22 ];
            case Fuchsia:                 [ 0xFF, 0x00, 0xFF ];
            case Gainsboro:               [ 0xDC, 0xDC, 0xDC ];
            case GhostWhite:              [ 0xF8, 0xF8, 0xFF ];
            case Gold:                    [ 0xFF, 0xD7, 0x00 ];
            case GoldenRod:               [ 0xDA, 0xA5, 0x20 ];
            case Gray:                  [ 0x80, 0x80, 0x80 ];
            case Grey:                  [ 0x80, 0x80, 0x80 ];
            case Green:                  [ 0x00, 0x80, 0x00 ];
            case GreenYellow:              [ 0xAD, 0xFF, 0x2F ];
            case HoneyDew:              [ 0xF0, 0xFF, 0xF0 ];
            case HotPink:                  [ 0xFF, 0x69, 0xB4 ];
            case IndianRed:               [ 0xCD, 0x5C, 0x5C ];
            case Indigo:                   [ 0x4B, 0x00, 0x82 ];
            case Ivory:                  [ 0xFF, 0xFF, 0xF0 ];
            case Khaki:                  [ 0xF0, 0xE6, 0x8C ];
            case Lavender:              [ 0xE6, 0xE6, 0xFA ];
            case LavenderBlush:          [ 0xFF, 0xF0, 0xF5 ];
            case LawnGreen:              [ 0x7C, 0xFC, 0x00 ];
            case LemonChiffon:          [ 0xFF, 0xFA, 0xCD ];
            case LightBlue:              [ 0xAD, 0xD8, 0xE6 ];
            case LightCoral:              [ 0xF0, 0x80, 0x80 ];
            case LightCyan:              [ 0xE0, 0xFF, 0xFF ];
            case LightGoldenRodYellow:  [ 0xFA, 0xFA, 0xD2 ];
            case LightGray:              [ 0xD3, 0xD3, 0xD3 ];
            case LightGrey:              [ 0xD3, 0xD3, 0xD3 ];
            case LightGreen:              [ 0x90, 0xEE, 0x90 ];
            case LightPink:              [ 0xFF, 0xB6, 0xC1 ];
            case LightSalmon:              [ 0xFF, 0xA0, 0x7A ];
            case LightSeaGreen:          [ 0x20, 0xB2, 0xAA ];
            case LightSkyBlue:          [ 0x87, 0xCE, 0xFA ];
            case LightSlateGray:          [ 0x77, 0x88, 0x99 ];
            case LightSlateGrey:          [ 0x77, 0x88, 0x99 ];
            case LightSteelBlue:          [ 0xB0, 0xC4, 0xDE ];
            case LightYellow:              [ 0xFF, 0xFF, 0xE0 ];
            case Lime:                   [ 0x00, 0xFF, 0x00 ];
            case LimeGreen:             [ 0x32, 0xCD, 0x32 ];
            case Linen:                   [ 0xFA, 0xF0, 0xE6 ];
            case Magenta:                 [ 0xFF, 0x00, 0xFF ];
            case Maroon:                  [ 0x80, 0x00, 0x00 ];
            case MediumAquaMarine:        [ 0x66, 0xCD, 0xAA ];
            case MediumBlue:              [ 0x00, 0x00, 0xCD ];
            case MediumOrchid:            [ 0xBA, 0x55, 0xD3 ];
            case MediumPurple:            [ 0x93, 0x70, 0xDB ];
            case MediumSeaGreen:          [ 0x3C, 0xB3, 0x71 ];
            case MediumSlateBlue:         [ 0x7B, 0x68, 0xEE ];
            case MediumSpringGreen:      [ 0x00, 0xFA, 0x9A ];
            case MediumTurquoise:         [ 0x48, 0xD1, 0xCC ];
            case MediumVioletRed:         [ 0xC7, 0x15, 0x85 ];
            case MidnightBlue:            [ 0x19, 0x19, 0x70 ];
            case MintCream:               [ 0xF5, 0xFF, 0xFA ];
            case MistyRose:               [ 0xFF, 0xE4, 0xE1 ];
            case Moccasin:                [ 0xFF, 0xE4, 0xB5 ];
            case NavajoWhite:             [ 0xFF, 0xDE, 0xAD ];
            case Navy:                    [ 0x00, 0x00, 0x80 ];
            case OldLace:                 [ 0xFD, 0xF5, 0xE6 ];
            case Olive:                   [ 0x80, 0x80, 0x00 ];
            case OliveDrab:               [ 0x6B, 0x8E, 0x23 ];
            case Orange:                  [ 0xFF, 0xA5, 0x00 ];
            case OrangeRed:               [ 0xFF, 0x45, 0x00 ];
            case Orchid:                  [ 0xDA, 0x70, 0xD6 ];
            case PaleGoldenRod:           [ 0xEE, 0xE8, 0xAA ];
            case PaleGreen:               [ 0x98, 0xFB, 0x98 ];
            case PaleTurquoise:           [ 0xAF, 0xEE, 0xEE ];
            case PaleVioletRed:           [ 0xDB, 0x70, 0x93 ];
            case PapayaWhip:              [ 0xFF, 0xEF, 0xD5 ];
            case PeachPuff:               [ 0xFF, 0xDA, 0xB9 ];
            case Peru:                    [ 0xCD, 0x85, 0x3F ];
            case Pink:                    [ 0xFF, 0xC0, 0xCB ];
            case Plum:                    [ 0xDD, 0xA0, 0xDD ];
            case PowderBlue:              [ 0xB0, 0xE0, 0xE6 ];
            case Purple:                  [ 0x80, 0x00, 0x80 ];
            case Red:                     [ 0xFF, 0x00, 0x00 ];
            case RosyBrown:               [ 0xBC, 0x8F, 0x8F ];
            case RoyalBlue:               [ 0x41, 0x69, 0xE1 ];
            case SaddleBrown:             [ 0x8B, 0x45, 0x13 ];
            case Salmon:                  [ 0xFA, 0x80, 0x72 ];
            case SandyBrown:              [ 0xF4, 0xA4, 0x60 ];
            case SeaGreen:                [ 0x2E, 0x8B, 0x57 ];
            case SeaShell:                [ 0xFF, 0xF5, 0xEE ];
            case Sienna:                  [ 0xA0, 0x52, 0x2D ];
            case Silver:                  [ 0xC0, 0xC0, 0xC0 ];
            case SkyBlue:                 [ 0x87, 0xCE, 0xEB ];
            case SlateBlue:               [ 0x6A, 0x5A, 0xCD ];
            case SlateGray:               [ 0x70, 0x80, 0x90 ];
            case SlateGrey:               [ 0x70, 0x80, 0x90 ];
            case Snow:                    [ 0xFF, 0xFA, 0xFA ];
            case SpringGreen:             [ 0x00, 0xFF, 0x7F ];
            case SteelBlue:               [ 0x46, 0x82, 0xB4 ];
            case Tan:                     [ 0xD2, 0xB4, 0x8C ];
            case Teal:                    [ 0x00, 0x80, 0x80 ];
            case Thistle:                 [ 0xD8, 0xBF, 0xD8 ];
            case Tomato:                  [ 0xFF, 0x63, 0x47 ];
            case Turquoise:               [ 0x40, 0xE0, 0xD0 ];
            case Violet:                  [ 0xEE, 0x82, 0xEE ];
            case Wheat:                   [ 0xF5, 0xDE, 0xB3 ];
            case White:                   [ 0xFF, 0xFF, 0xFF ];
            case WhiteSmoke:              [ 0xF5, 0xF5, 0xF5 ];
            case Yellow:                  [ 0xFF, 0xFF, 0x00 ];
            case YellowGreen:             [ 0x9A, 0xCD, 0x32 ];
            
        }
    }
}

We can now easily import this into our class.

import net.justinfront.javascript_canvas_animating.Colorjs;

and with using
import net.justinfront.javascript_canvas_animating.Colorjs;

we can easily convert a named color into an array of Red, Green and Blue pixel color quantities. For instance taking any color - we can get the amount of each component color.
var rgb =  MidnightBlue.val();
var amountOfRed: Int = rgb[0];
var amountOfGreen: Int = rgb[1];
var amountOfBlue: Int = rgb[2];

Drawing circles in 3d on canvas


With three color components it would be fun to render a colored circle in 3d to see how named colors are distributed across the color space.
We can look at the perspective tutorial at Bit-101 I also looked at some of my old as1 layer51 prototypes for simple 3d drawing code.
And create a simple 3D class all it does is help me convert a 3d position to a 2d position.
package net.justinfront.javascript_canvas_animating;
typedef Point2D = 
{
    var x: Float;
    var y: Float;
}
typedef Point3D = 
{
    > Point2D,
    var z : Float;
}

class Simple3D
{
    public static inline var fl: Float = 420;
    
    public function new()
    {
        
    }
    
    public static inline function scale( z: Float )
    {
        return 1-(-z)/fl;
    }
    
    public static inline function twoD( p: Point3D ): Point2D
    {
        var s = scale( p.z );
        return { x: p.x/s, y: p.y/s };
    }
    
    public static inline function rgbTwoD( rgb: Array<Float>, offSet: Point2D ): Point2D
    {
        var s = scale( rgb[2] );
        return { x: rgb[0]/s + offSet.x, y: rgb[1]/s + offSet.y };
    }
    
}

So again lets import and setup using this class in our Main class.
import net.justinfront.javascript_canvas_animating;
using net.justinfront.javascript_canvas_animating;

Now we can get modify the code to draw a Red circle to now draw a MidnightBlue circle in relative 3d. So this code:
        pen = Main.redFill();
        surface.drawCircle( { x: 20, y: 20 }, 20, pen );

becomes...
    // create default pen typedef ( the redFill has now been moved to Colorjs class. )
        pen = Colorjs.redFill();
    // modify the fill to the correct color
    pen.fill = Std.string( MidnightBlue );
    // get the component parts of the color
    rgb = MidnightBlue.val();
    // use Blue value as our z in 3d.
    var z = rgb[0];
    // calculate the size of our circle.
    var scale = z.scale();
    var radius = 10*scale;
    // translate 3d values ( Red Green Blue or xyz ) into 2d render values. 
    var xy = rgb.rgbTwoD();
    // draw circle on the screen.
        surface.drawCircle( xy., radius, pen );

Ok essentially we can now create a 3D render of named html colors but if we want to move them in 3D and depth sort the drawing order then we need a little more work. But before doing this we need to get all the colors.

Looping through Enum's


To loop through all enum values of our Html colors we can use Type.allEnums to help.
        for( col in Type.allEnums(ColorHtml5) )
        {
        
    }

I want to store the information as an Array of Pen and as RGB ( xyz ) values. So lets add a typedef to store this raw data.
typedef RawData =
{
    var col: Array<Float>;
    var pen: Pen;
}

So our method for storing the color dot information...
    public function createColoredDots()
    {
        
        pen = Colorjs.redFill();
        coloredDots = new Array<RawData>();
        
        for( col in Type.allEnums(ColorHtml5) )
        {
            pen.fill = col;
            var p = col.rgb();
            var pFloat: Array<Float> = [ p[0], p[1], p[2] ];// from Array<Int> to Array<Float> it can be done with **cast p** but later we need to adjust z.
            coloredDots.push( { pen: pen.penClone(), col: pFloat } );
        }
        
    }

I have had to add a penClone so that they don't all share the same pen in Colorjs class.
    public static function penClone( p: Pen ): Pen
    {
        return { fill: p.fill, lineColor: p.lineColor, thickness: p.thickness, hasEdge: p.hasEdge, hasFill: p.hasFill };
    }

Structuring the data for drawing and depth sorting.


Now the rawdata has xyz information so it can be manipulated in 3d but once any manipulations are done we are ready to structure it for rendering and depth sorting. So we add a typedef for this information...
typedef RenderData =
{
    var xy: Point2D;
    var depth: Float;
    var scale: Float;
    var pen: Pen;
}

We can define a sort function to sort an array of these.
    public function depth( a: RenderData, b: RenderData )
    {
        return ( a.depth < b.depth )? 1: -1; 
    }

So the method to prepare an array of this RenderData...
    public function depthSortStore(): Array<RenderData>
    {
        
        var store                   = new Array<RenderData>();
        
        for( i in 0...coloredDots.length )
        {
            var dot         = coloredDots[ i ];
            var z: Float    = dot.col[ 2 ];
            var scale       = (-z).scale();
            store.push( { xy: ( dot.col ).rgbTwoD( offset ), depth: z, scale: scale, pen: dot.pen });
        }
        
        store.sort( depth );
        return store;
        
    }

Notice how once we have created the Array of render data we can sort it with the sort function very easily.
store.sort( depth ); 

Render the dots to screen


Now we have everything setup this is very simple.
    public function render2D( store: Array<RenderData> )
    {
        var w = 10./.8;
        for( i in 0...store.length )
        {
            var item = store[ i ];
            surface.drawCircle( item.xy, w*item.scale, item.pen );
        }
    }

but before we draw to screen we must clear the part of the Canvas we are drawing on, this assumes an offset Point2D has been set up for drawing so that it's not so tight to the top right.
]
    public function clearScreen()
    {
        surface.clearRect( Std.int( offset.x ) - 100, Std.int( offset.y ) - 100 , 255 + 200, 255 + 200 );
    }

Rotation


To rotate all the colored dots we can take the array of RawData and apply trig based on the rotation axis. One approach to 3d rotation around an arbitary point or centre is to translate the centre ( arbitrary point ) to the origin, rotate, and then translate back. So in vector form we can think of the rotation as...
point = point - centre
rotate( point )
point = point + centre 

Without going into all the detailed trig something like this seems to work...
    public function rotateColors()
    {
        
        var rX = rotate.x;
        var rY = rotate.y;
        var rZ = rotate.z;
        var sX = Math.sin( rX ); 
        var sY = Math.sin( rY ); 
        var sZ = Math.sin( rZ );
        var cX = Math.cos( rX ); 
        var cY = Math.cos( rY ); 
        var cZ = Math.cos( rZ );
        var ox = centre.x;
        var oy = centre.y;
        var oz = centre.z;
        var x: Float;
        var y: Float;
        var z: Float;
        
        for( i in 0...coloredDots.length )
        {
            var dot     = coloredDots[i].col;
            dot[ 0 ]    -= ox;
            dot[ 1 ]    -= oy;
            dot[ 2 ]    -= oz;
            var x       = dot[ 0 ]; 
            var y       = dot[ 1 ]; 
            var z       = dot[ 2 ];
            dot[ 0 ]    = x*cY*cZ + z*sY - y*sZ + ox;
            dot[ 1 ]    = y*cX*cZ -z*sX + x*sZ + oy;
            dot[ 2 ]    = y*sX + z*cX*cY - x*sY + oz;
        }
        
    }

//TODO: add external link explaining rotation in 3d

Frame animation with haxe.Timer


Ok we can Test the rotation by modifying the position every frame like we would in flash, but there does not seem to be a frame concept in Javascript so instead we can use haxe.Timer. Say we want 30 frames per second ( a fairly low frame rate for games which are often 60, but common for flash ). haxe.Timer uses milliseconds so we can setup a Timer frequency in our constructor ( bottom of the new method/function of the Main class ).
        var frequency = Std.int( 1000/30 );

and then below that setup the timer.
        var t = new haxe.Timer( frequency );

and lastly we setup a function to be called every frame...
        t.run = someRotation;

Then add this method to our main class.
    public function rotateZ()
    {
        rotate.z = Math.PI/180/10;
        rotate.x = Math.PI/180/5;
        rotate.y = Math.PI/180/8;
        render();
    }

Ok if you test this you will see your points rotating, for clarity lets remove the timer code and instead try some keyboard interaction.

Keyboard Interaction


To get key presses we can just add a listener to the body...
root.onkeypress = spin;

And the arrow keys are 37 to 40, so we can just check the key and set the rotation depending on the value and then render using some of the methods above.
    private function spin( e: js.Event )
    {
        
        var angle = Math.PI/180;
        switch( e.keyCode )
        {
            case 37: rotate.y = -angle;
            case 38: rotate.x = angle;
            case 39: rotate.y = angle;
            case 40: rotate.x = -angle;
        }
        
        render();
        rotate.x = 0;
        rotate.y = 0;
    }

So the final code for Main looks like..


package net.justinfront.javascript_canvas_animating;

import net.justinfront.javascript_canvas_animating.Colorjs;
import net.justinfront.javascript_canvas_animating.Simple3d;
import net.justinfront.javascript_canvas_animating.Main;

import js.Lib;
import js.Dom;

typedef RenderData =
{
    var xy: Point2D;
    var depth: Float;
    var scale: Float;
    var pen: Pen;
}
typedef RawData =
{
    var col: Array<Float>;
    var pen: Pen;
}

using net.justinfront.javascript_canvas_animating.Main;
using net.justinfront.javascript_canvas_animating.Colorjs;
using net.justinfront.javascript_canvas_animating.Simple3d;
class Main
{
    

    var root:               Body;
    var surface:            CanvasRenderingContext2D;    
    var pen:                Pen;
    
    var centre:             Point3D;
    var coloredDots:        Array<RawData>;
    var rotate:             Point3D;
    var offset:             Point2D;
    
    static function main(){ new Main(); }
    public function new()
    {
        
        root                        = Lib.document.body;
        var dom:        HtmlDom     = Lib.document.createElement('Canvas');
        var canvas:     Canvas      = cast dom;
        surface                     = untyped canvas.getContext('2d');
        var style                   = dom.style;
        
        root.appendChild( dom );
        canvas.width                = 1024;
        canvas.height               = 768;
        
        style.setPosition( { x: 0, y: 0 } );
        
        offset                      = { x: 100, y: 100 };
        centre                      = { x: 255/2, y: 255/2, z:255/2 + 50  };
        rotate                      = { x: 0, y: 0, z: 0 };
        createColoredDots();
        render();
        root.onkeypress = spin;
        
    }
    
    public static function redFill()
    {
        return { fill: Red, lineColor: Black, thickness: 1, hasEdge: false, hasFill: true };
    }
    
    static inline public function setPosition( style: Style, position: Point2D ): Point2D
    { 
        style.paddingLeft   = "0px";
        style.paddingTop    = "0px";
        style.left          = Std.string( position.x + 'px' );
        style.top           = Std.string( position.y + 'px' );
        style.position      = "absolute";
        return position;
    }
    
    static inline public function drawCircle(   surface:      CanvasRenderingContext2D
                                            ,   position:   Point2D
                                            ,   radius:     Float
                                            ,   pen:        Pen
                                            )
    {
        if( pen.hasEdge )
        {
            surface.strokeStyle = Std.string( pen.lineColor);
            surface.lineWidth = pen.thickness;
        }
        if( pen.hasFill ) surface.fillStyle = Std.string( pen.fill );
        surface.beginPath();
        surface.arc( position.x + radius, position.y + radius, radius, 0, 2*Math.PI, false );
        if( pen.hasEdge ) surface.stroke();
        surface.closePath();
        if( pen.hasFill ) surface.fill();
    }
    
    public function createColoredDots()
    {
        
        pen = Colorjs.redFill();
        coloredDots = new Array<RawData>();
        
        for( col in Type.allEnums(ColorHtml5) )
        {
            pen.fill = col;
            var p = col.rgb();
            var pFloat: Array<Float> = [ p[0], p[1], p[2] + 50 ];
            coloredDots.push( { pen: pen.penClone(), col: pFloat } );
        }
        
    }
    
    public function render()
    {
        rotateColors();
        clearScreen();
        render2D( depthSortStore() );
    }
    
    public function rotateColors()
    {
        
        var rX = rotate.x;
        var rY = rotate.y;
        var rZ = rotate.z;
        var sX = Math.sin( rX ); 
        var sY = Math.sin( rY ); 
        var sZ = Math.sin( rZ );
        var cX = Math.cos( rX ); 
        var cY = Math.cos( rY ); 
        var cZ = Math.cos( rZ );
        var ox = centre.x;
        var oy = centre.y;
        var oz = centre.z;
        var x: Float;
        var y: Float;
        var z: Float;
        
        for( i in 0...coloredDots.length )
        {
            var dot     = coloredDots[i].col;
            dot[ 0 ]    -= ox;
            dot[ 1 ]    -= oy;
            dot[ 2 ]    -= oz;
            var x       = dot[ 0 ]; 
            var y       = dot[ 1 ]; 
            var z       = dot[ 2 ];
            dot[ 0 ]    = x*cY*cZ + z*sY - y*sZ + ox;
            dot[ 1 ]    = y*cX*cZ -z*sX + x*sZ + oy;
            dot[ 2 ]    = y*sX + z*cX*cY - x*sY + oz;
        }
        
    }
    
    public function depthSortStore(): Array<RenderData>
    {
        
        var store                   = new Array<RenderData>();
        
        for( i in 0...coloredDots.length )
        {
            var dot         = coloredDots[ i ];
            var z: Float    = dot.col[ 2 ];
            var scale       = dot.col[ 2 ].scale();
            store.push( { xy: ( dot.col ).rgbTwoD( offset ), depth: z, scale: scale, pen: dot.pen });
        }
        
        store.sort( depth );
        return store;
        
    }
    
    public function clearScreen()
    {
        surface.clearRect( Std.int( offset.x ) - 100, Std.int( offset.y ) - 100 , 255 + 200, 255 + 200 );
    }
    
    public function render2D( store: Array<RenderData> )
    {
        var w = 10./1.5;
        for( i in 0...store.length )
        {
            var item = store[ i ];
            surface.drawCircle( item.xy, w*item.scale, item.pen );
        }
    }
    
    public function depth( a: RenderData, b: RenderData )
    {
        return ( a.depth < b.depth )? -1: 1; 
    }
    
    private function spin( e: js.Event )
    {
        
        var angle = Math.PI/180;
        switch( e.keyCode )
        {
            case 37: rotate.y = -angle;
            case 38: rotate.x = angle;
            case 39: rotate.y = angle;
            case 40: rotate.x = -angle;
        }
        
        render();
        rotate.x = 0;
        rotate.y = 0;
    }
    
}

Adding a rollover and detecting the color.


Since every circle is a different color we can use a pixel color test to create a click or rollover one each circle. Since we are not drawing the stroke I can set a stroke color and using a rollover I can change the pen hasEdge property. If we use a white stroke/circle edge then it works well for dark colors but fails for bright colors. So instead we can check the color and use a Red edge for bright colors. We can set the edge here...
    public function createColoredDots()
    {
        
        pen = Colorjs.redFill();
        coloredDots = new Array<RawData>();
        
        for( col in Type.allEnums(ColorHtml5) )
        {
            pen.fill = col;
            
            var p = col.rgb();
            // if bright fill use a Red edge.
            if( p[0] + p[1] + p[2] > 255*2 )
            {
                pen.lineColor = Red;
            }
            else
            {
                pen.lineColor = White;
            }
            
            var pFloat: Array<Float> = [ p[0], p[1], p[2] + 50 ];
            coloredDots.push( { pen: pen.penClone(), col: pFloat } );
            
        }
        
    }

Now in the Main's new method/function ( the classes constructor ) we can add some simple code for a rollover, in this case we will use the mousemove and do a check every frame... it's quite heavy with so many colors, we can use dom.onmousedown if we want only a click effect, but lets test our processor!
        dom.onmousemove = checkPixel;
        dom.style.cursor = "pointer";

So we need to also add a check method that will check the pixel under the mouse and then see which ColorHtml5 it matches. We don't do the heavy check if the pixel is Black since our background is black, and since I am using Red and White for edges we also have to not check for them. This obviously means the rollover will not work for the Black, Red and White dots.
    public function checkPixel( e )
    {
        var imageData = surface.getImageData( e.clientX, e.clientY, 1, 1 );
        var rgba: CanvasPixelArray = imageData.data;
        // if the color is not Black background. 
        // and not white or red ( because using them for the line round )
        // so we can't have detection on them either
        // then don't do the pixel color check.
        var sum = rgba[0] + rgba[1] + rgba[2];
        var r = rgba[0];
        var isRed = ( r == 0xff && sum == 0xff );
        if( sum != 0 && sum != 3*0xff && !isRed )
        {
            for( i in 0...store.length )
            {
                var col: ColorHtml5 = store[ i ].pen.fill;
                var dot = col.rgb();
                if(     dot[ 0 ] == rgba[ 0 ] 
                    &&  dot[ 1 ] == rgba[ 1 ] 
                    &&  dot[ 2 ] == rgba[ 2 ] 
                    )
                {
                    if( lastDotIndex != i )
                    {
                        store[ i ].pen.hasEdge = true;
                        if( lastDotIndex != null ) store[ lastDotIndex ].pen.hasEdge = false;
                        lastDotIndex = i;        
                        clearScreen();
                        render2D(store);
                    }
                    break;
                }
            }
        }

    }

Notice we have added a lastDotIndex property to our Main, this is so that we only modify the dot if it is not currently already have a highlight edge.
So
store[ i ].pen.hasEdge = true;

Is the code that actually changes the dot to show the edge, after changing this we need to re-render. To re-render we don't need a depth sort so we can just clear the screen and redraw using the currently calculated array of RenderData.

As an extension for other projects we could use a hidden Canvas that uses this Pixel trick to do accurate hit or click checks, but it is likely that in most instances it is easier to work with virtual bounding boxes.

version #15683, modified 2012-11-09 05:51:25 by JLM