Tracking Canvas State Using Objects
Despite being a fairly simple animation to watch, the instructions for a fading grid of colors is actually very complex. By creating objects to track the state of the animation a lot of duplicate code can be avoided, and the concepts exposed by gridfader.js can be encapsulated for easier canvas manipulation.
Declaring gridfader.js
var GridFader = function(canvasId) { });
I chose to declare GridFader as a function under the global scope. Global scope is relative to the way I declared GridFader - under a browser environment, this would be the window object. I debated the merits of explicitly declaring GridFader as window.GridFader() but decided against it in case gridfader.js is included in a different JavaScript environment, such as node.js that would not contain a window object.
External Dependencies
Despite not being mentioned explicitly, the GridFader() object does rely on objects and methods built into the JavaScript DOM, including but not limited to the extensions created for the HTML5 canvas element. If no canvas object exists, it could be created earlier in the executing environment, similar to the way jQuery handles invoking a faux window object.
Speaking of jQuery, I chose not to include a it as a dependency since I would need it for fewer than even 5 lines of code. It would be an unnecessary dependency at that point that would become quickly outdated or become incompatible. Use of methods and objects native to the DOM should be much more stable over time.
Scoping
Due to JavaScript's object-oriented nature - every function is an object, and objects can be declared inline within the scope you assign them - GridFader acts as more than just a single object. GridFader is treated as an entire namespace. I use this to expose types that are reusable in other scopes, but only if GridFader has been instantiated. This would be similar to C#'s "public" class scope modifier where including another assembly gives you access to all classes declared public.
(function() { var fader = new GridFader('canvasId'); var color = new fader.ColorManagement.Color(); color.r = 32; color.g = 48; color.b = 51; fader.palette.push(color); })();
Likewise, there are many objects and functions that are only available within GridFader to encapsulate its behavior. These classes mimic C#'s "internal" class scope modifier.
The difference lies in how they are declared:
Public
var GridFader = function(canvasId) { ... this.ColorManagement = { Color: function(obj) { ... }, ... }; });
Internal/Private
var GridFader = function(canvasId) { ... var Events = { ... }; ... });
When creating any value inside of a function, you can set its scope to the equivalent of public by using the this keyword or use the var declaration of a value to mark it as private.
By encapsulating GridFader this way, all anyone else implementing GridFader has to understand is how to call the constructor, use the Color type, and call BindEvents when ready. They also have the option of adjusting any other public properties that are mostly optional.
Giving Canvas State
As mentioned in my previous article canvas does not contain layers, remove content before painting, nor have a way of tracking state on its own.
The first step was creating the object (GridFader) that retains state as long as the page containing it is active.
Next, it makes sense to track the state of each cell individually. This is where GridFader's CellManagement namespace comes in.
When GridFader is initialized and its events are bound, it makes a call to CellManagement.GetAllCells():
var GridFader = function() { .... var Events = { Init: function() { self.canvas = document.getElementById(canvasId); self.brush = self.canvas.getContext('2d'); Events.Resize(); }, Resize: function() { clearInterval(clock); Environment.SetCanvasSize(); self.DrawGrid(); self.CellManagement.GetAllCells(); clock = setInterval(Events.Tick, self.clockSpeed); }, ... }; ... };
This determines how many cells can fit on screen and stores a reference to each one in an array. The array's type is GridFader.CellManagement.Cell().
Each Cell object is responsible for tracking its coordinates, dimensions, color, animation state, and fade progress.
var GridFader = function() { ... this.CellManagement = { Cell: function() { this.Color = null; this.Length = 0; this.X = 0; this.Y = 0; this.State = null; this.Clear = function() { self.brush.clearRect(this.X, this.Y, this.Length, this.Length); }; this.Destroy = function() { this.Clear(); this.Color = null; this.State = null; }; this.Draw = function() { var brush = self.brush; var cellSize = self.cellSize; var grd = brush.createRadialGradient( this.X + this.Length / 2, this.Y + this.Length / 2, 0, this.X + this.Length / 2, this.Y + this.Length / 2, cellSize); this.Clear(); brush.beginPath(); grd.addColorStop(0, this.Color); grd.addColorStop(1, this.Color.Darken(64)); brush.fillStyle = grd; brush.fillRect(this.X, this.Y, this.Length, this.Length); brush.stroke(); brush.closePath(); brush.fillStyle = null; } this.FadeIn = function() { this.State = this.States.FadingIn; if(this.Color === null) { this.Color = self.ColorManagement.GetRandomColor(); this.Color.a = 0.0; } else if(this.Color.a >= self.maxCellOpacity) { this.State = this.States.Full; } else { this.Color.a += self.fadeStep; } }; this.FadeOut = function() { this.State = this.States.FadingOut; if(this.Color.a >= self.fadeStep) { this.Color.a -= self.fadeStep; } else { this.Destroy(); } }; this.States = { FadingIn: 'FadingIn', FadingOut: 'FadingOut', Full: 'Full' }; }, ... } ... };
Each cell is also responsible for drawing itself - GridFader enumerates over each Cell in the array and calls Cell.Draw() on each animation tick:
var GridFader = function() { ... var Events = { ... Tick: function() { if(clockCycle === self.cycleSize) { var cell = self.CellManagement.GetRandomCell(); if(cell.State === null) { cell.FadeIn(); } else if(cell.State === cell.States.Full) { cell.FadeOut(); } clockCycle = 0; } else { clockCycle++; } var i = 0; var cells = self.CellManagement.Cells; while(i < cells.length) { var cell = cells[i]; if(cell.State === cell.States.FadingIn) { cell.FadeIn(); cell.Draw(); } else if(cell.State === cell.States.FadingOut) { cell.FadeOut(); if(cell.State !== null) { cell.Draw(); } } i++; } } } ... };
Github
For more information, please feel free to view GridFader's entire source on Github!