/*### PATTERN | CANVAS.JS ###########################################################################*/ // Copyright (c) 2010 University of Antwerp, Belgium // Authors: Tom De Smedt // License: BSD (see LICENSE.txt for details). // Version: 1.4 // http://www.clips.ua.ac.be/pages/pattern-canvas // The NodeBox drawing API for the HTML5 element. // The commands are adopted from NodeBox for OpenGL (http://www.cityinabottle.org/nodebox), // including a (partial) port from nodebox.graphic.bezier, // nodebox.graphics.geometry and nodebox.graphics.shader. /*##################################################################################################*/ try { $; } catch(e) { function $(x, y) { return Array.copy(((y !== undefined)? y : document).querySelectorAll(x)); } } function attachEvent(element, name, f) { /* Cross-browser attachEvent(). * Ensures that "this" inside the function f refers to the given element . */ f = Function.closure(element, f); if (element.addEventListener) { element.addEventListener(name, f, false); } else if (element.attachEvent) { element.attachEvent("on"+name, f); } else { element["on"+name] = f; } element[name] = f; // So we can do element.name(), see widget(). } Function.closure = function(parent, f) { /* Returns the function f, where "this" inside the function refers to the given parent. */ return function() { return f.apply(parent, arguments); }; }; window.width = function() { return (window.innerWidth !== undefined)? window.innerWidth : document.documentElement.clientWidth; }; window.height = function() { return (window.innerHeight !== undefined)? window.innerHeight : document.documentElement.clientHeight; }; /*##################################################################################################*/ /*--- ARRAY FUNCTIONS ------------------------------------------------------------------------------*/ // JavaScript Array object: // var a = [1,2,3]; // 1 in [1,2,3] => true; // [1,2,3].indexOf(1) => 0 // [1,2,3].push(4) => [1,2,3,4] // [1,2,3].pop() => [1,2] 3 // [1,2,3].shift() => 1 [2,3] // [1,2,3].concat([4,5,6]) => [1,2,3,4,5,6] copy // [1,2,3].slice(1,2) => [2,3] // [1,2,3].splice(1,0,11,12) => [1,11,12,2,3] // [1,2,3].splice(1,2) => [1] // [1,2,3].join(",") => "1,2,3" // Additional array functions are invoked with Array.[function]. Array.instanceof = function(array) { return Object.prototype.toString.call(Array) === "[object Array]"; }; Array.get = function(array, index) { return (index >= 0)? array[index] : array[array.length+index]; } Array.index = function(array, v) { for (var i=0; i < array.length; i++) { if (array[i] === v) return i; } return -1; }; Array.contains = function(array, v) { for (var i=0; i < array.length; i++) { if (array[i] === v) return true; } return false; }; Array.find = function(array, match) { for (var i=0; i < array.length; i++) { if (match(array[i])) return i; } }; Array.sum = function(array) { for (var i=0, sum=0; i < array.length; sum+=array[i++]){}; return sum; }; Array.min = function(array, callback) { callback = callback || function(x) { return x; } var x = +Infinity; for (var i=0; i < array.length; i++) { x = Math.min(x, callback(array[i])); } return x; }; Array.max = function(array, callback) { callback = callback || function(x) { return x; } var x = -Infinity; for (var i=0; i < array.length; i++) { x = Math.max(x, callback(array[i])); } return x; }; Array.map = function(array, callback) { /* Returns a new array with callback(value) for each value in the given array. */ var a = []; for (var i=0; i < array.length; i++) { a.push(callback(array[i])); } return a; }; Array.filter = function(array, callback) { /* Returns a new array with values for which callback(value)==true. */ var a = []; for (var i=0; i < array.length; i++) { if (callback(array[i])) a.push(array[i]); } return a; }; Array.enumerate = function(array, callback, that) { /* Calls callback(index, value) for each value in the given array. */ callback = Function.closure(that || this, callback); for (var i=0; i < array.length; i++) { if (callback(i, array[i]) == false) return; } }; Array.forEach = function(array, callback, that) { /* Calls callback(value, index, array) for each value in the given array. */ callback = Function.closure(that || this, callback); for (var i=0; i < array.length; i++) { if (callback(array[i]) == false, i, array) return; } } Array.eq = function(array1, array2) { /* Returns true if both arrays contain the same values. */ if (!(array1 instanceof Array) || !(array2 instanceof Array) || array1.length != array2.length) { return false; } for (var i=0; i < array1.length; i++) { if (array1[i] !== array2[i]) return false; } return true; }; Array.sorted = function(array, reverse, key) { /* Returns a sorted copy of the given array. */ if (key instanceof Function) { var cmp = function(a, b) { return key(b) - key(a); } } else { var cmp = undefined } array = array.slice(); array = array.sort(cmp); if (reverse) array = array.reverse(); return array; }; Array.reversed = function(array) { /* Returns a reversed copy of the given array. */ array = array.slice(); array = array.reverse(); return array; }; Array.unique = function (array) { /* Returns a new array with unique items. */ var a = array.slice(); for (var i=a.length-1; i > 0; --i) { var v = a[i]; for (var j=i-1; j >= 0; --j) { if (a[j] === v) a.splice(j, 1); i--; } } return a; }; Array.copy = function(array) { /* Returns a shallow copy of the given array. */ var a = []; for (var i=0; i < array.length; i++) { a.push(array[i]); } return a; }; Array.choice = function(array) { /* Returns a random value from the given array (undefined if empty). */ return array[Math.round(Math.random() * (array.length-1))]; }; Array.shuffle = function(array) { /* Randomly shuffles the values in the given array. */ var n = array.length; var i = n; while (i--) { var p = parseInt(Math.random() * n); var x = array[i]; array[i] = array[p]; array[p] = x; } return array; }; Array.shuffled = function(array) { /* Returns a new array with randomly shuffled values. */ return Array.shuffle(array.slice()); }; Array.range = function(i, j) { /* Returns a new array with numeric values from i to j (not including j). */ if (j === undefined) { j = i; i = 0; } var a = []; for (var k=0; k 0) return +1; return 0; } Math.clamp = function(value, min, max) { if (max > min) { return Math.max(min, Math.min(value, max)); } else { return Math.max(max, Math.min(value, min)); } }; Math.normalize = function(value, min, max) { return (value - min) / (max - min); } Math.sum = function(a) { var n = 0; for (var i=0; i < a.length; i++) n += a[i]; return n; }; Math.dot = function(a, b) { var m = Math.min(a.length, b.length); var n = 0; for (var i = 0; i < m; i++) n += a[i] * b[i]; return n; }; Math.mix = function(a, b, t) { if (t < 0.0) return a; if (t > 1.0) return b; return a + (b-a)*t; }; Math.lerp = Math.mix; Math.smoothstep = function(a, b, x) { if (x < a) return 0.0; if (x >=b) return 1.0; x = (x-a) / (b-a); return x*x * (3-2*x); }; Math.smoothrange = function(a, b, n) { /* Returns an array of approximately n values v1, v2, ... vn, * so that v1 <= a, and vn >= b, and all values are multiples of 1, 2, 5 and 10. * For example: Math.smoothrange(1, 123) => [0, 20, 40, 60, 80, 100, 120, 140], */ function _multiple(v, round) { var e = Math.floor(Math.log(v) / Math.LN10); // exponent var m = Math.pow(10, e); // magnitude var f = v / m; // fraction if (round) { if (f < 1.5) return m * 1; if (f < 3.0) return m * 2; if (f < 7.0) return m * 5; } else { if (f <= 1.0) return m * 1; if (f <= 2.0) return m * 2; if (f <= 5.0) return m * 5; } return m * 10; } if (a === undefined && b === undefined) { a=0; b=1; } if (a === undefined) { a=0; b=b; } if (b === undefined) { a=0; b=a; } if (n === undefined) { n=10; } if (a === b) { return [a]; } var r = _multiple(b-a); var t = _multiple(r / (n-1), true); a = Math.floor(a/t) * t; b = Math.ceil( b/t) * t; var rng = []; for (var i=0; i < (b-a) / t + 1; i++) { rng.push(a + i*t); } return rng; }; /*--- GEOMETRY -------------------------------------------------------------------------------------*/ var Point = Class.extend({ init: function(x, y) { this.x = x; this.y = y; }, copy: function() { return new Point(this.x, this.y); } }); var Geometry = Class.extend({ // ROTATION: angle: function(x0, y0, x1, y1) { /* Returns the angle between two points. */ return Math.degrees(Math.atan2(y1-y0, x1-x0)); }, distance: function(x0, y0, x1, y1) { /* Returns the distance between two points. */ return Math.sqrt(Math.pow(x1-x0, 2) + Math.pow(y1-y0, 2)); }, coordinates: function(x0, y0, distance, angle) { /* Returns the location of a point by rotating around origin (x0,y0). */ var x1 = x0 + Math.cos(Math.radians(angle)) * distance; var y1 = y0 + Math.sin(Math.radians(angle)) * distance; return new Point(x1, y1); }, rotate: function(x, y, x0, y0, angle) { /* Returns the coordinates of (x,y) rotated clockwise around origin (x0,y0). */ x -= x0; y -= y0; var a = Math.cos(Math.radians(angle)); var b = Math.sin(Math.radians(angle)); return new Point( x*a - y*b + x0, y*a + x*b + y0 ); }, reflect: function(x0, y0, x1, y1, d, a) { // Returns the reflection of a point through origin (x0,y0). if (d === undefined ) d = 1.0; if (a === undefined) a = 180; d *= this.distance(x0, y0, x1, y1); a += this.angle(x0, y0, x1, y1); return this.coordinates(x0, y0, d, a); }, // INTERPOLATION: lerp: function(a, b, t) { /* Returns the linear interpolation between a and b for time t between 0.0-1.0. * For example: lerp(100, 200, 0.5) => 150. */ if (t < 0.0) return a; if (t > 1.0) return b; return a + (b-a)*t; }, smoothstep: function(a, b, x) { /* Returns a smooth transition between 0.0 and 1.0 using Hermite interpolation (cubic spline), * where x is a number between a and b. The return value will ease (slow down) as x nears a or b. * For x smaller than a, returns 0.0. For x bigger than b, returns 1.0. */ if (x < a) return 0.0; if (x >=b) return 1.0; x = (x-a) / (b-a); return x*x * (3-2*x); }, bounce: function(x) { /* Returns a bouncing value between 0.0 and 1.0 (e.g. Mac OS X Dock) for a value between 0.0-1.0. */ return Math.abs(Math.sin(2 * Math.PI * (x+1) * (x+1)) * (1-x)); }, // ELLIPSES: superformula: function(m, n1, n2, n3, phi) { /* A generalization of the superellipse (Gielis, 2003). * that can be used to describe many complex shapes and curves found in nature. */ if (n1 == 0) return (0, 0); var a = 1.0; var b = 1.0; var r = Math.pow(Math.pow(Math.abs(Math.cos(m * phi/4) / a), n2) + Math.pow(Math.abs(Math.sin(m * phi/4) / b), n3), 1/n1); if (Math.abs(r) == 0) return (0, 0); r = 1 / r; return [r * Math.cos(phi), r * Math.sin(phi)]; }, // INTERSECTION: lineIntersection: function(x1, y1, x2, y2, x3, y3, x4, y4, infinite) { /* Determines the intersection point of two lines, or two finite line segments if infinite=False. * When the lines do not intersect, returns null. */ // Based on: P. Bourke, 1989, http://paulbourke.net/geometry/pointlineplane/ if (infinite === undefined) infinite = false; var ua = (x4-x3) * (y1-y3) - (y4-y3) * (x1-x3); var ub = (x2-x1) * (y1-y3) - (y2-y1) * (x1-x3); var d = (y4-y3) * (x2-x1) - (x4-x3) * (y2-y1); // The lines are coincident if (ua == ub && ub == 0). // The lines are parallel otherwise. if (d == 0) return null; ua /= d; ub /= d; // Intersection point is within both finite line segments? if (!infinite && !(0 <= ua && ua <= 1 && 0 <= ub && ub <= 1)) return null; return new Point( x1 + ua * (x2-x1), y1 + ua * (y2-y1) ); }, pointInPolygon: function(points, x, y) { /* Ray casting algorithm. * Determines how many times a horizontal ray starting from the point * intersects with the sides of the polygon. * If it is an even number of times, the point is outside, if odd, inside. * The algorithm does not always report correctly when the point is very close to the boundary. * The polygon is passed as an array of Points. */ // Based on: W. Randolph Franklin, 1970, http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html var odd = false; var n = points.length; for (var i=0; i < n; i++) { var j = (i= y) || (y1 < y && y0 >= y)) { if (x0 + (y-y0) / (y1-y0) * (x1-x0) < x) { odd = !odd; } } } return odd; }, Bounds: Class.extend({ init: function(x, y, width, height) { /* Creates a bounding box. * The bounding box is an untransformed rectangle that encompasses a shape or group of shapes. */ if (width === undefined) width = Infinity; if (height === undefined) height = Infinity; // Normalize if width or height is negative: if (width < 0) { x+=width; width=-width; } if (height < 0) { y+=height; height=-height; } this.x = x; this.y = y; this.width = width ; this.height = height; }, copy: function() { return new Bounds(this.x, this.y, this.width, this.height); }, intersects: function(b) { /* Return True if a part of the two bounds overlaps. */ return Math.max(this.x, b.x) < Math.min(this.x + this.width, b.x + b.width) && Math.max(this.y, b.y) < Math.min(this.y + this.height, b.y + b.height); }, intersection: function(b) { /* Returns bounds that encompass the intersection of the two. * If there is no overlap between the two, null is returned. */ if (!this.intersects(b)) { return null; } var mx = Math.max(this.x, b.x); var my = Math.max(this.y, b.y); return new Bounds(mx, my, Math.min(this.x + this.width, b.x+b.width) - mx, Math.min(this.y + this.height, b.y+b.height) - my ); }, union: function(b) { /* Returns bounds that encompass the union of the two. */ var mx = Math.min(this.x, b.x); var my = Math.min(this.y, b.y); return new Bounds(mx, my, Math.max(this.x + this.width, b.x+b.width) - mx, Math.max(this.y + this.height, b.y+b.height) - my ); }, contains: function(pt) { /* Returns True if the given point or rectangle falls within the bounds. */ if (pt instanceof Point) { return pt.x >= this.x && pt.x <= this.x + this.width && pt.y >= this.y && pt.y <= this.y + this.height } if (pt instanceof Bounds) { return pt.x >= this.x && pt.x + pt.width <= this.x + this.width && pt.y >= this.y && pt.y + pt.height <= this.y + this.height } } }) }); var geometry = new Geometry(); var geo = geometry; Math.angle = geometry.angle; Math.distance = geometry.distance; Math.coordinates = geometry.coordinates; /*##################################################################################################*/ /*--- COLOR ----------------------------------------------------------------------------------------*/ var RGB = "RGB"; var HSB = "HSB"; var HCL = "HCL"; var HEX = "HEX"; var Color = Class.extend({ // init: function(r, g, b, a, {base: 1.0, colorspace: RGB}) init: function(r, g, b, a, options) { /* A color with R,G,B,A channels, with channel values ranging between 0.0-1.0. * Either takes four parameters (R,G,B,A), three parameters (R,G,B), * two parameters (grayscale and alpha) or one parameter (grayscale or Color object). * An optional {base: 1.0, colorspace: RGB} can be used with four parameters. */ var base = options && options.base || 1.0; var colorspace = options && options.colorspace || RGB; // One value, a hexadecimal string. if (r && r.match && r.match(/^#/)) { colorspace = HEX; // One value, another color object. } else if (r instanceof Color) { g=r.g; b=r.b; a=r.a; r=r.r; // One value, array with R,G,B,A values. } else if (r instanceof Array) { g=r[1]; b=r[2]; a=r[3] !== undefined ? r[3] : 1; r=r[0]; // No value or null, transparent black. } else if (r === undefined || r == null) { r=0; g=0; b=0; a=0; // One value, grayscale. } else if (g === undefined || g == null) { a=1; g=r; b=r; // Two values, grayscale and alpha. } else if (b === undefined || b == null) { a=g; g=r; b=r; // R, G and B. } else if (a === undefined || a == null) { a=1; } // Transform to base 1. if (base != 1) { r /= base; g /= base; b /= base; a /= base; } // Transform to base (0.0-1.0). if (colorspace != HEX) { r = (r < 0)? 0 : (r > 1)? 1 : r; g = (g < 0)? 0 : (g > 1)? 1 : g; b = (b < 0)? 0 : (b > 1)? 1 : b; a = (a < 0)? 0 : (a > 1)? 1 : a; } // Transform to RGB. if (colorspace != RGB) { if (colorspace == HEX) { var rgb = _hex2rgb(r); r=rgb[0]; g=rgb[1]; b=rgb[2]; a=1; } if (colorspace == HSB) { var rgb = _hsb2rgb(r, g, b); r=rgb[0]; g=rgb[1]; b=rgb[2]; } if (colorspace == HCL) { var rgb = _hcl2rgb(r, g, b); r=rgb[0]; g=rgb[1]; b=rgb[2]; } } this.r = r; this.g = g; this.b = b; this.a = a; }, rgb: function() { return [this.r, this.g, this.b]; }, rgba: function() { return [this.r, this.g, this.b, this.a]; }, _get: function() { var r = Math.round(this.r * 255); var g = Math.round(this.g * 255); var b = Math.round(this.b * 255); return "rgba("+r+", "+g+", "+b+", "+this.a+")"; }, copy: function() { return new Color(this); }, map: function(options) { /* Returns array [R,G,B,A] mapped to the given base, * e.g. 0-255 instead of 0.0-1.0 which is useful for setting image pixels. * Other values than RGBA can be obtained by setting the colorspace (RGB/HSB/HEX). */ var base = options && options.base || 1.0; var colorspace = options && options.colorspace || RGB; var r = this.r; var g = this.g; var b = this.b; var a = this.a; if (colorspace == HEX) { return _rgb2hex(r, g, b); } if (colorspace == HSB) { hsb = _rgb2hsb(r, g, b); r=hsb[0]; g=hsb[1]; b=hsb[2]; } if (colorspace == HCL) { hcl = _rgb2hcl(r, g, b); r=hcl[0]; g=hcl[1]; b=hcl[2]; } if (colorspace == "premultiplied") { rgb = _rgba2rgb(r, g, b, a); r=rgb[0]; g=rgb[1]; b=rgb[2]; a=1; } if (base != 1) { return [r * base, g * base, b * base, a * base]; } return [r, g, b, a]; }, rotate: function(angle) { /* Returns a new color with it's hue rotated on the RYB color wheel. */ var hsb = _rgb2hsb(this.r, this.g, this.b); var hsb = _rotateRYB(hsb[0], hsb[1], hsb[2], angle); return new Color(hsb[0], hsb[1], hsb[2], this.a, {"colorspace": HSB}); }, adjust: function(options) { /* Returns a new color transformed in HSB colorspace. * Hue is added, saturation and brightness are multiplied. */ var hsb = _rgb2hsb(this.r, this.g, this.b); hsb[0] += options.hue || 0; hsb[1] *= options.saturation || 1; hsb[2] *= options.brightness || 1; return new Color(Math.mod(hsb[0], 1), hsb[1], hsb[2], this.a, {"colorspace": HSB}); } }); function rgb(r, g, b) { /* Returns a new Color from R, G, B values (0-255). */ return new Color(r, g, b, 1 * 255, {"base": 255, "colorspace": RGB}); } function rgba(r, g, b, a) { /* Returns a new Color from R, G, B values (0-255) and alpha (0.0-1.0). */ return new Color(r, g, b, a * 255, {"base": 255, "colorspace": RGB}); } function color(r, g, b, a, options) { /* Returns a new Color from R, G, B, A values (0.0-1.0). */ return new Color(r, g, b, a, options); } function background(r, g, b, a) { /* Sets the current background color. */ if (r !== undefined) { var tf = _ctx.currentTransform; _ctx.state.background = (r instanceof Color)? new Color(r) : new Color(r, g, b, a); _ctx_fill(_ctx.state.background); _ctx.setTransform(1, 0, 0, 1, 0, 0); // Identity matrix. _ctx.fillRect(0, 0, _ctx._canvas.width, _ctx._canvas.height); _ctx.currenTransform = tf; } return _ctx.state.background; } function fill(r, g, b, a) { /* Sets the current fill color for drawing primitives and paths. */ if (r !== undefined) { _ctx.state.fill = (r instanceof Color || r instanceof Gradient)? r.copy() : new Color(r, g, b, a); } return _ctx.state.fill; } function stroke(r, g, b, a) { /* Sets the current stroke color. */ if (r !== undefined) { _ctx.state.stroke = (r instanceof Color)? r.copy() : new Color(r, g, b, a); } return _ctx.state.stroke; } function nofill() { /* No current fill color. */ _ctx.state.fill = null; } function nostroke() { /* No current stroke color. */ _ctx.state.stroke = null; } function strokewidth(width) { /* Sets the outline stroke width. */ if (width !== undefined) { _ctx.state.strokewidth = width; } return _ctx.state.strokewidth; } var SOLID = "solid"; var DOTTED = "dotted"; var DASHED = "dashed"; function strokestyle(style) { /* Sets the outline stroke style. */ if (style !== undefined) { _ctx.state.strokestyle = style; } return _ctx.state.strokestyle; } var BUTT = "butt"; var ROUND = "round"; var SQUARE = "square"; function linecap(cap) { /* Sets the outline line caps style. */ if (cap !== undefined) { _ctx.state.linecap = cap; } return _ctx.state.linecap; } var noFill = nofill; var noStroke = nostroke; var strokeWidth = strokewidth; var strokeStyle = strokestyle; var lineCap = linecap; // fill() and stroke() are heavy operations: // - they are called often, // - they copy a Color object, // - they change the state. // // This is the main reason that scripts will run (in overall) 2-8 fps slower than in processing.js. /*--- COLOR SPACE ----------------------------------------------------------------------------------*/ // Transformations between RGB, HSB, XYZ, LAB, LCH, HEX color spaces. // Based on: http://www.easyrgb.com/math.php function _rgb2hex(r, g, b) { /* Returns a hexadecimal color string from the given R,G,B values. */ parseHex = function(i) { var s = "00"; s = (i != 0)? i.toString(16).toUpperCase() : s; s = (s.length < 2)? "0" + s : s; return s; } return "#" + parseHex(Math.round(r * 255)) + parseHex(Math.round(g * 255)) + parseHex(Math.round(b * 255)); } function _hex2rgb(hex) { /* Returns an array [R,G,B] (0.0-1.0) from the given hexadecimal color string. */ hex = hex.replace(/^#/, ""); if (hex.length < 6) { // hex += hex[-1] * (6-hex.length); hex += (new Array(6-hex.length)).join(hex.substr(hex.length-1)); } var r = parseInt(hex.substr(0, 2), 16) / 255; var g = parseInt(hex.substr(2, 2), 16) / 255; var b = parseInt(hex.substr(4, 2), 16) / 255; return [r, g, b]; } function _rgb2hsb(r, g, b) { /* Returns an array [H,S,B] (0.0-1.0) from the given R,G,B values. */ var h = 0; var s = 0; var v = Math.max(r, g, b); var d = v - Math.min(r, g, b); if (v != 0) { s = d / v; } if (s != 0) { if (r == v) { h = 0 + (g-b) / d; } else if (g == v) { h = 2 + (b-r) / d; } else { h = 4 + (r-g) / d; } } h = Math.mod(h / 6.0, 1); return [h, s, v]; } function _hsb2rgb(h, s, v) { /* Returns an array [R,G,B] (0.0-1.0) from the given H,S,B values. */ if (s == 0) { return [v, v, v]; } h = Math.mod(h, 1) * 6.0; var i = Math.floor(h); var f = h - i; var x = v * (1-s); var y = v * (1-s * f); var z = v * (1-s * (1-f)); if (i > 4) { return [v, x, y]; } return [[v,z,x], [y,v,x], [x,v,z], [x,y,v], [z,x,v]][parseInt(i)]; } function _rgb2xyz(r, g, b) { /* Returns an array [X,Y,Z] (0-95.047, 0-100, 0-108.883) from the given R,G,B values. */ r = ((r > 0.04045)? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92) * 100; g = ((g > 0.04045)? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92) * 100; b = ((b > 0.04045)? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92) * 100; // Observer = 2°, Illuminant = D65. var x = r * 0.4124 + g * 0.3576 + b * 0.1805; var y = r * 0.2126 + g * 0.7152 + b * 0.0722; var z = r * 0.0193 + g * 0.1192 + b * 0.9505; return [x, y, z]; } function _xyz2rgb(x, y, z) { /* Returns an array [R,G,B] (0.0-1.0) from the given X,Y,Z values. */ x /= 100; y /= 100; z /= 100; var r = x * 3.2406 + y * -1.5372 + z * -0.4986; var g = x * -0.9689 + y * 1.8758 + z * 0.0415; var b = x * 0.0557 + y * -0.2040 + z * 1.0570; r = (r > 0.0031308)? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r; g = (g > 0.0031308)? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g; b = (b > 0.0031308)? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b; return [r, g, b]; } function _xyz2lab(x, y, z) { /* Returns an array [L,a,b] (0.0-1.0) from the given X,Y,Z values. */ // Observer = 2°, Illuminant = D65. x /= 95.047; y /= 100.000; z /= 108.883; x = (x > 0.008856)? Math.pow(x, 1 / 3) : 7.787 * x + (16 / 116); y = (y > 0.008856)? Math.pow(y, 1 / 3) : 7.787 * y + (16 / 116); z = (z > 0.008856)? Math.pow(z, 1 / 3) : 7.787 * z + (16 / 116); var l = 116 * y - 16; var a = 500 * (x - y); var b = 200 * (y - z); return [l, a, b]; } function _lab2xyz(l, a, b) { /* Returns an array [X,Y,Z] (0-95.047, 0-100, 0-108.883) from the given L,a,b values. */ var y = (l + 16 ) / 116; var x = a / 500 + y; var z = y - b / 200; x = (Math.pow(x, 3) > 0.008856)? Math.pow(x, 3) : (x - 16 / 116) / 7.787; y = (Math.pow(y, 3) > 0.008856)? Math.pow(y, 3) : (y - 16 / 116) / 7.787; z = (Math.pow(z, 3) > 0.008856)? Math.pow(z, 3) : (z - 16 / 116) / 7.787; // Observer = 2°, Illuminant = D65. x *= 95.047; y *= 100.000; z *= 108.883; return [x, y, z]; } function _lab2lch(l, a, b) { /* Returns an array [L,C,H] (0.0-1.0) from the given L,a,b values. */ var h = Math.atan2(b, a); h = (h > 0)? h / Math.PI * 180 : 360 - Math.abs(h) / Math.PI * 180; c = Math.sqrt(a * a + b * b); return [l, c, h]; } function _lch2lab(l, c, h) { /* Returns an array [L,a,b] (0.0-1.0) from the given L,C,H values. */ h = h / 180 * Math.PI; var a = Math.cos(h) * c; var b = Math.sin(h) * c; return [l, a, b]; } function _rgb2hcl(r, g, b) { /* Returns an array [H,C,L] (0.0-1.0) from the given R,G,B values. */ var xyz = _rgb2xyz(r, g, b); var lab = _xyz2lab(xyz[0], xyz[1], xyz[2]); var lch = _lab2lch(lab[0], lab[1], lab[2]); lch[0] /= 100; lch[1] /= 100; lch[2] /= 360; return [lch[2], lch[1], lch[0]]; } function _hcl2rgb(h, c, l) { /* Returns an array [R,G,B] (0.0-1.0) from the given H,C,L values. */ h *= 360; c *= 100; l *= 100; var lab = _lch2lab(l, c, h); var xyz = _lab2xyz(lab[0], lab[1], lab[2]); return _xyz2rgb(xyz[0], xyz[1], xyz[2]); } function _rgba2rgb(r, g, b, a, blend) { /* Returns an array [R,G,B,1] (0.0-1.0) from the given R,G,B,A values. */ blend = blend || [1,1,1]; r = r * a + (1 - a) * blend[0]; g = g * a + (1 - a) * blend[1]; b = b * a + (1 - a) * blend[2]; a = 1; return [r, g, b, a]; } function darker(clr, step) { /* Returns a copy of the color with a darker brightness. */ if (step === undefined) step = 0.2; var hsb = _rgb2hsb(clr.r, clr.g, clr.b); var rgb = _hsb2rgb(hsb[0], hsb[1], Math.max(0, hsb[2] - step)); return new Color(rgb[0], rgb[1], rgb[2], clr.a); } function lighter(clr, step) { /* Returns a copy of the color with a lighter brightness. */ if (step === undefined) step = 0.2; var hsb = _rgb2hsb(clr.r, clr.g, clr.b); var rgb = _hsb2rgb(hsb[0], hsb[1], Math.min(1, hsb[2] + step)); return new Color(rgb[0], rgb[1], rgb[2], clr.a); } var darken = darker; var lighten = lighter; /*--- COLOR INTERPOLATION --------------------------------------------------------------------------*/ function colors(colors, k, colorspace) { /* Returns a new array with k colors, by calculating intermediary colors in the given colorspace (by default, HCL). */ if (k === undefined) k = 100; if (colorspace === undefined) colorspace = HCL; var a = Array.map(colors, function(clr) { return clr.map({colorspace: colorspace}); }); var b = []; var x, y, t, n = a.length - 1; for (var i=0; i < k-1; i++) { x = Math.min(Math.floor(i / k * n), n); y = Math.min(x + 1, n); t = (i - x * k / n) / (k / n); b.push(new Color( Math.lerp(a[x][0], a[y][0], t), Math.lerp(a[x][1], a[y][1], t), Math.lerp(a[x][2], a[y][2], t), Math.lerp(a[x][3], a[y][3], t), {colorspace: colorspace} )); } if (k > 0) { b.push(new Color(a[n][0], a[n][1], a[n][2], a[n][3], {colorspace: colorspace})); } return b; } /*--- COLOR ROTATION -------------------------------------------------------------------------------*/ // Approximation of the RYB color wheel. // In HSB, colors hues range from 0 to 360, // but on the color wheel these values are not evenly distributed. // The second tuple value contains the actual value on the wheel (angle). var _COLORWHEEL = [ [ 0, 0], [ 15, 8], [ 30, 17], [ 45, 26], [ 60, 34], [ 75, 41], [ 90, 48], [105, 54], [120, 60], [135, 81], [150, 103], [165, 123], [180, 138], [195, 155], [210, 171], [225, 187], [240, 204], [255, 219], [270, 234], [285, 251], [300, 267], [315, 282], [330, 298], [345, 329], [360, 360] ]; function _rotateRYB(h, s, b, angle) { /* Rotates the given H,S,B color (0.0-1.0) on the RYB color wheel. * The RYB colorwheel is not mathematically precise, * but focuses on aesthetically pleasing complementary colors. */ if (angle === undefined) angle = 180; h = Math.mod(h * 360, 360); // Find the location (angle) of the hue on the RYB color wheel. var x0, y0, x1, y1, a; var wheel = _COLORWHEEL; for (var i=0; i < wheel.length-1; i++) { x0 = wheel[i][0]; x1 = wheel[i+1][0]; y0 = wheel[i][1]; y1 = wheel[i+1][1]; if (y0 <= h && h <= y1) { a = Math.lerp(x0, x1, (h-y0) / (y1-y0)); break; } } // Rotate the angle and retrieve the hue. a = Math.mod(a + angle, 360); for (var i=0; i < wheel.length-1; i++) { x0 = wheel[i][0]; x1 = wheel[i+1][0]; y0 = wheel[i][1]; y1 = wheel[i+1][1]; if (x0 <= a && a <= x1) { h = Math.lerp(y0, y1, (a-x0) / (x1-x0)); break; } } return [h/360.0, s, b]; } function complement(clr) { /* Returns the color opposite on the color wheel. * The complementary color contrasts with the given color. */ return clr.rotate(180); } function analog(clr, angle, d) { /* Returns a random adjacent color on the color wheel. * Analogous color schemes can often be found in nature. */ if (angle === undefined) angle = 20; if (d === undefined) d = 0.1; var hsb = _rgb2hsb(clr.r, clr.g, clr.b); var hsb = _rotateRYB(hsb[0], hsb[1], hsb[2], Math.random() * 2 * angle - angle); hsb[1] *= 1 - Math.random()*2*d-d; hsb[2] *= 1 - Math.random()*2*d-d; return new Color(hsb[0], hsb[1], hsb[2], clr.a, {"colorspace":HSB}); } /*--- COLOR MIXIN ----------------------------------------------------------------------------------*/ // Drawing commands like rect() have optional parameters fill and stroke to set the color directly. // function _colorMixin({fill: Color(), stroke: Color(), strokewidth: 1.0, strokestyle: SOLID}) function _colorMixin(options) { var s = _ctx.state; var o = options; if (o === undefined) { return [s.fill, s.stroke, s.strokewidth, s.strokestyle, s.linecap]; } else { return [ (o.fill !== undefined)? (o.fill instanceof Color || o.fill instanceof Gradient)? o.fill : new Color(o.fill) : s.fill, (o.stroke !== undefined)? (o.stroke instanceof Color)? o.stroke : new Color(o.stroke) : s.stroke, (o.strokewidth !== undefined)? o.strokewidth : (o.strokeWidth !== undefined)? o.strokeWidth : s.strokewidth, (o.strokestyle !== undefined)? o.strokestyle : (o.strokeStyle !== undefined)? o.strokeStyle : s.strokestyle, (o.linecap !== undefined)? o.linecap : (o.lineCap !== undefined)? o.lineCap : s.linecap ]; } } /*--------------------------------------------------------------------------------------------------*/ // Wrappers for _ctx.fill() and _ctx.stroke(), only calling them when necessary. function _ctx_fill(fill) { if (fill && (fill.a > 0 || fill.clr1)) { // Ignore transparent colors. // Avoid switching _ctx.fillStyle() - we can gain up to 5fps: var f = fill._get(); if (_ctx.state._fill != f) { _ctx.fillStyle = _ctx.state._fill = f; } _ctx.fill(); } } function _ctx_stroke(stroke, strokewidth, strokestyle, linecap) { if (stroke && stroke.a > 0 && strokewidth > 0) { var s = stroke._get(); if (_ctx.state._stroke != s) { _ctx.strokeStyle = _ctx.state._stroke = s; } if (_ctx.state._strokestyle != strokestyle && _ctx.setLineDash) { _ctx.state._strokestyle = strokestyle; switch(strokestyle) { case DOTTED: _ctx.setLineDash([1,3]); break; case DASHED: _ctx.setLineDash([3,3]); break; default: _ctx.setLineDash([]); } } _ctx.lineWidth = strokewidth; _ctx.lineCap = linecap; _ctx.stroke(); } } /*##################################################################################################*/ /*--- GRADIENT -------------------------------------------------------------------------------------*/ var LINEAR = "linear"; var RADIAL = "radial"; var Gradient = Class.extend({ // init: function(clr1, clr2, {type: LINEAR, x: 0, y: 0, spread: 100, angle: 0}) init: function(clr1, clr2, options) { /* A gradient with two colors. */ var o = options || {}; if (clr1 instanceof Gradient) { // One parameter, another gradient object. var g = clr1; clr1 = g.clr1.copy(); clr2 = g.clr2.copy(); o = {type: g.type, x: g.x, y: g.y, spread: g.spread, angle: g.angle}; } this.clr1 = (clr1 instanceof Color)? clr1 : new Color(clr1); this.clr2 = (clr2 instanceof Color)? clr2 : new Color(clr2); this.type = o.type || LINEAR; this.x = o.x || 0; this.y = o.y || 0; this.a = 1.0; // Shapes will only be drawn if color or gradient has alpha > 1.0. this.spread = Math.max((o.spread !== undefined)? o.spread : 100, 0); this.angle = o.angle || 0; }, _get: function(dx, dy) { // See also Path.draw() for dx and dy: // we use the first MOVETO of the path to make the gradient location relative. var x = this.x + (dx || 0); var y = this.y + (dy || 0); if (this.type == LINEAR) { var p = geometry.coordinates(x, y, this.spread, this.angle); var g = _ctx.createLinearGradient(x, y, p.x, p.y); } if (this.type == RADIAL) { var g = _ctx.createRadialGradient(x, y, 0, x, y, this.spread); } g.addColorStop(0.0, this.clr1._get()); g.addColorStop(1.0, this.clr2._get()); return g; }, copy: function() { return new Gradient(this); } }); function gradient(clr1, clr2, options) { return new Gradient(clr1, clr2, options); } /*--- SHADOW ---------------------------------------------------------------------------------------*/ function shadow(dx, dy, blur, alpha) { /* Sets the current dropshadow, used with all subsequent shapes. */ var s = _ctx.state; s.shadow = { "dx": (dx !== undefined)? dx : 5, "dy": (dy !== undefined)? dy : 5, "blur": (blur !== undefined)? blur : 5, "alpha": (alpha !== undefined)? alpha : 0.5 } _ctx.shadowOffsetX = s.shadow.dx; _ctx.shadowOffsetY = s.shadow.dy; _ctx.shadowBlur = s.shadow.blur; _ctx.shadowColor = "rgba(0,0,0," + s.shadow.alpha + ")"; return s.shadow; } function noshadow() { _ctx.state.shadow = null; _ctx.shadowColor = "transparent"; } var noShadow = noshadow; /*##################################################################################################*/ /*--- AFFINE TRANSFORM -----------------------------------------------------------------------------*/ // Based on: T. McCauley, 2009, http://www.senocular.com/flash/tutorials/transformmatrix/ var AffineTransform = Transform = Class.extend({ init: function(transform) { /* A geometric transformation in Euclidean space (i.e. 2D) * that preserves collinearity and ratio of distance between points. * Linear transformations include rotation, translation, scaling, shear. */ if (transform instanceof AffineTransform) { this.matrix = transform.matrix.copy(); } else { this.matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity matrix. } }, copy: function() { return new AffineTransform(this); }, prepend: function(transform) { this.matrix = this._mmult(this.matrix, transform.matrix); }, append: function(transform) { this.matrix = this._mmult(transform.matrix, this.matrix); }, _mmult: function(a, b) { /* Returns the 3x3 matrix multiplication of A and B. * Note that scale(), translate(), rotate() work with premultiplication, * e.g. the matrix A followed by B = BA and not AB. */ return [ a[0] * b[0] + a[1] * b[3], a[0] * b[1] + a[1] * b[4], 0, a[3] * b[0] + a[4] * b[3], a[3] * b[1] + a[4] * b[4], 0, a[6] * b[0] + a[7] * b[3] + b[6], a[6] * b[1] + a[7] * b[4] + b[7], 1 ]; }, invert: function() { /* Multiplying a matrix by its inverse produces the identity matrix. */ var m = this.matrix; var d = m[0] * m[4] - m[1] * m[3]; this.matrix = [ m[4] / d, -m[1] / d, 0, -m[3] / d, m[0] / d, 0, (m[3] * m[7] - m[4] * m[6]) / d, -(m[0] * m[7] - m[1] * m[6]) / d, 1 ]; }, inverse: function() { var m = this.copy(); m.invert(); return m; }, scale: function(x, y) { if (y === undefined) y = x; this.matrix = this._mmult([x, 0, 0, 0, y, 0, 0, 0, 1], this.matrix); }, translate: function(x, y) { this.matrix = this._mmult([1, 0, 0, 0, 1, 0, x, y, 1], this.matrix); }, rotate: function(angle) { var c = Math.cos(Math.radians(angle)); var s = Math.sin(Math.radians(angle)); this.matrix = this._mmult([c, s, 0, -s, c, 0, 0, 0, 1], this.matrix); }, rotation: function() { return Math.mod(Math.degrees(Math.atan2(this.matrix[1], this.matrix[0])) + 360, 360); }, apply: function(x, y) { return this.transform_point(x, y); }, transform_point: function(x, y) { /* Returns the new coordinates of the given point (x,y) after transformation. */ if (y === undefined) { y=x.y; x=x.x; } // One parameter, Point object. var m = this.matrix; return new Point( x * m[0] + y * m[3] + m[6], x * m[1] + y * m[4] + m[7] ); }, transform_path: function(path) { /* Returns a Path object with the transformation applied. */ var p = new Path(); for (var i=0; i < path.array.length; i++) { var pt = path.array[i]; if (pt.cmd == CLOSE) { p.closepath(); } else if (pt.cmd == MOVETO) { pt = this.apply(pt); p.moveto(pt.x, pt.y); } else if (pt.cmd == LINETO) { pt = this.apply(pt); p.lineto(pt.x, pt.y); } else if (pt.cmd == CURVETO) { var ctrl1 = this.apply(pt.ctrl1); var ctrl2 = this.apply(pt.ctrl2); pt = this.apply(pt); p.curveto(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, pt.x, pt.y); } } return p; }, transformPoint: function(x, y) { return this.transform_point(x, y); }, transformPath: function(path) { return this.transform_path(path); }, transform: function(path_x, y) { return (path_x instanceof Path)? this.transform_path(path_x) : this.transform_point(path_x, y); }, map: function(points) { return Array.map(points, function(pt) { if (pt instanceof Array) { return this.apply(pt[0], pt[1]); } else { return this.apply(pt.x, pt.y); } }); } }); /*--- TRANSFORMATIONS ------------------------------------------------------------------------------*/ function push() { /* Pushes the transformation state. * Subsequent transformations (translate, rotate, scale) remain in effect until pop() is called. */ _ctx.save(); } function pop() { /* Pops the transformation state. * This reverts the transformation to before the last push(). */ _ctx.restore(); // Do not reset the color state: if (_ctx.state._fill) { _ctx.fillStyle = _ctx.state._fill; } if (_ctx.state._stroke) { _ctx.strokeStyle = _ctx.state._stroke; } } function translate(x, y) { /* By default, the origin of the layer or canvas is at the bottom left. * This origin point will be moved by (x,y) pixels. */ _ctx.translate(x, y); } function rotate(degrees) { /* Rotates the transformation state, i.e. all subsequent drawing primitives are rotated. * Rotations work incrementally: * calling rotate(60) and rotate(30) sets the current rotation to 90. */ _ctx.rotate(degrees / 180 * Math.PI); } function scale(x, y) { /* Scales the transformation state. */ if (y === undefined) y = x; _ctx.scale(x, y); } function reset() { /* Resets the transform state of the canvas. */ _ctx.restore(); _ctx.save(); } /*##################################################################################################*/ /*--- BEZIER MATH ----------------------------------------------------------------------------------*/ // Thanks to Prof. F. De Smedt at the Vrije Universiteit Brussel, 2006. var Bezier = Class.extend({ // BEZIER MATH: linePoint: function(t, x0, y0, x1, y1) { /* Returns coordinates for the point at t (0.0-1.0) on the line. */ return [ x0 + t * (x1-x0), y0 + t * (y1-y0) ]; }, lineLength: function(x0, y0, x1, y1) { /* Returns the length of the line. */ var a = Math.pow(Math.abs(x0-x1), 2); var b = Math.pow(Math.abs(y0-y1), 2); return Math.sqrt(a + b); }, curvePoint: function(t, x0, y0, x1, y1, x2, y2, x3, y3, handles) { /* Returns coordinates for the point at t (0.0-1.0) on the curve * (de Casteljau interpolation algorithm). */ var dt = 1 - t; var x01 = x0*dt + x1*t; var y01 = y0*dt + y1*t; var x12 = x1*dt + x2*t; var y12 = y1*dt + y2*t; var x23 = x2*dt + x3*t; var y23 = y2*dt + y3*t; var h1x = x01*dt + x12*t; var h1y = y01*dt + y12*t; var h2x = x12*dt + x23*t; var h2y = y12*dt + y23*t; var x = h1x*dt + h2x*t; var y = h1y*dt + h2y*t; if (!handles) { return [x, y, h1x, h1y, h2x, h2y]; } else { // Include the new handles of pt0 and pt3 (see Bezier.insert_point()). return [x, y, h1x, h1y, h2x, h2y, x01, y01, x23, y23]; } }, curvelength: function(x0, y0, x1, y1, x2, y2, x3, y3, n) { /* Returns the length of the curve. * Integrates the estimated length of the cubic bezier spline defined by x0, y0, ... x3, y3, * by adding up the length of n linear lines along the curve. */ if (n === undefined) n = 20; var length = 0; var xi = x0; var yi = y0; for (var i=0; i < n; i++) { var t = (i+1) / n; var pt = this.curvePoint(t, x0, y0, x1, y1, x2, y2, x3, y3); length += Math.sqrt( Math.pow(Math.abs(xi-pt[0]), 2) + Math.pow(Math.abs(yi-pt[1]), 2) ); xi = pt[0]; yi = pt[1]; } return length; }, // BEZIER PATH LENGTH: segmentLengths: function(path, relative, n) { /* Returns an array with the length of each segment in the path. * With relative=true, the total length of all segments is 1.0. */ if (n === undefined) n = 20; var lengths = []; for (var i=0; i < path.array.length; i++) { var pt = path.array[i]; if (i == 0) { var close_x = pt.x; var close_y = pt.y; } else if (pt.cmd == MOVETO) { var close_x = pt.x; var close_y = pt.y; lengths.push(0.0); } else if (pt.cmd == CLOSE) { lengths.push(this.lineLength(x0, y0, close_x, close_y)); } else if (pt.cmd == LINETO) { lengths.push(this.lineLength(x0, y0, pt.x, pt.y)); } else if (pt.cmd == CURVETO) { lengths.push(this.curvelength(x0, y0, pt.ctrl1.x, pt.ctrl1.y, pt.ctrl2.x, pt.ctrl2.y, pt.x, pt.y, n)); } if (pt.cmd != CLOSE) { var x0 = pt.x; var y0 = pt.y; } } if (relative == true) { var s = Array.sum(lengths); if (s > 0) { return Array.map(lengths, function(v) { return v/s; }); } else { return Array.map(lengths, function(v) { return 0.0; }); } } return lengths; }, length: function(path, segmented, n) { /* Returns the approximate length of the path. * Calculates the length of each curve in the path using n linear samples. * With segmented=true, returns an array with the relative length of each segment (sum=1.0). */ if (n === undefined) n = 20; if (!segmented) { return sum(this.segmentLengths(path, false, n)); } else { return this.segmentLengths(path, true, n); } }, // BEZIER PATH POINT: _locate : function(path, t, segments) { /* For a given relative t on the path (0.0-1.0), returns an array [index, t, PathElement], * with the index of the PathElement before t, * the absolute time on this segment, * the last MOVETO or any subsequent CLOSETO after i. */ // Note: during iteration, supplying segmentLengths() yourself is 30x faster. if (segments === undefined) segments = this.segmentLengths(path, true); for (var i=0; i < path.array.length; i++) { var pt = path.array[i]; if (i == 0 || pt.cmd == MOVETO) { var closeto = new Point(pt.x, pt.y); } if (t <= segments[i] || i == segments.length-1) { break; } t -= segments[i]; } if (segments[i] != 0) t /= segments[i]; if (i == segments.length-1 && segments[i] == 0) i -= 1; return [i, t, closeto]; }, point: function(path, t, segments) { /* Returns the DynamicPathElement at time t on the path. * Note: in PathElement, ctrl1 is how the curve started, and ctrl2 how it arrives in this point. * Here, ctrl1 is how the curve arrives, and ctrl2 how it continues to the next point. */ var _, i, closeto; _=this._locate(path, t, segments); i=_[0]; t=_[1]; closeto=_[2]; var x0 = path.array[i].x; var y0 = path.array[i].y; var pt = path.array[i+1]; if (pt.cmd == LINETO || pt.cmd == CLOSE) { var _pt = (pt.cmd == CLOSE)? this.linePoint(t, x0, y0, closeto.x, closeto.y) : this.linePoint(t, x0, y0, pt.x, pt.y); pt = new DynamicPathElement(_pt[0], _pt[1], LINETO); pt.ctrl1 = new Point(pt.x, pt.y); pt.ctrl2 = new Point(pt.x, pt.y); } else if (pt.cmd == CURVETO) { var _pt = this.curvePoint(t, x0, y0, pt.ctrl1.x, pt.ctrl1.y, pt.ctrl2.x, pt.ctrl2.y, pt.x, pt.y); pt = new DynamicPathElement(_pt[0], _pt[1], CURVETO); pt.ctrl1 = new Point(_pt[2], _pt[3]); pt.ctrl2 = new Point(_pt[4], _pt[5]); } return pt; }, findPath: function(points, curvature) { /* Returns a smooth Path from the given list of points. */ if (curvature === undefined) curvature = 1.0; // Don't crash on something straightforward such as a list of [x,y]-arrays. if (points instanceof Path) { points = points.array; } for (var i=0; i < points.length; i++) { if (points[i] instanceof Array) { points[i] = new Point(points[i][0], points[i][1]); } } var path = new Path(); // No points: return nothing. // One point: return a path with a single MOVETO-point. // Two points: return a path with a single straight line. if (points.length == 0) { return null; } if (points.length == 1) { path.moveto(points[0].x, points[0].y); return path; } if (points.length == 2) { path.moveto(points[0].x, points[0].y); path.lineto(points[1].x, points[1].y); return path; } // Zero curvature means path with straight lines. if (curvature <= 0) { path.moveto(points[0].x, points[0].y) for (var i=1; i < points.length; i++) { path.lineto(points[i].x, points[i].y); } return path; } // Construct the path with curves. curvature = Math.min(1.0, curvature); curvature = 4 + (1.0 - curvature) * 40; // The first point's ctrl1 and ctrl2 and last point's ctrl2 // will be the same as that point's location; // we cannot infer how the path curvature started or will continue. var dx = {0: 0}; dx[points.length-1] = 0; var dy = {0: 0}; dy[points.length-1] = 0; var bi = {1: 1 / curvature}; var ax = {1: (points[2].x - points[0].x - dx[0]) * bi[1]}; var ay = {1: (points[2].y - points[0].y - dy[0]) * bi[1]}; for (var i=2; i < points.length-1; i++) { bi[i] = -1 / (curvature + bi[i-1]); ax[i] = -(points[i+1].x - points[i-1].x - ax[i-1]) * bi[i]; ay[i] = -(points[i+1].y - points[i-1].y - ay[i-1]) * bi[i]; } var r = Array.reversed(Array.range(1, points.length-1)); for (var i=points.length-2; i > 0; i--) { dx[i] = ax[i] + dx[i+1] * bi[i]; dy[i] = ay[i] + dy[i+1] * bi[i]; } path.moveto(points[0].x, points[0].y); for (var i=0; i < points.length-1; i++) { path.curveto( points[i].x + dx[i], points[i].y + dy[i], points[i+1].x - dx[i+1], points[i+1].y - dy[i+1], points[i+1].x, points[i+1].y ); } return path; }, arc: function(x1, y1, x2, y2, angle, extent) { /* Returns a list of [x1, y1, x2, y2, x3, y3, x4, y4] coordinates, * each a curve from (x1, x1) to (x4, y4) with (x2, y2) and (x3, y3) * as their respective Bezier control points. */ angle = -angle; extent = -extent; // clockwise var bounds = [x1, y1, x2, y2]; x1 = Math.min(bounds[0], bounds[2]); y1 = Math.min(bounds[1], bounds[3]); x2 = Math.max(bounds[0], bounds[2]); y2 = Math.max(bounds[1], bounds[3]); extent = Math.min(Math.max(extent, -360), +360); var n = Math.abs(extent) <= 90 && 1 || Math.ceil(Math.abs(extent) / 90.0); var a = extent / n; var cx = (x1 + x2) / 2; var cy = (y1 + y2) / 2; var rx = (x2 - x1) / 2; var ry = (y2 - y1) / 2; var a2 = Math.radians(a) / 2; var kappa = Math.abs(4.0 / 3 * (1 - Math.cos(a2)) / Math.sin(a2)); var points = [], theta0, theta1, c0, c1, s0, s1, k; for (var i=0; i < n; i++) { theta0 = Math.radians(angle + (i+0) * a); theta1 = Math.radians(angle + (i+1) * a); c0 = Math.cos(theta0); s0 = Math.sin(theta0); c1 = Math.cos(theta1); s1 = Math.sin(theta1); k = a > 0 && -kappa || kappa; points.push([ cx + rx * c0, cy - ry * s0, cx + rx * (c0 + k * s0), cy - ry * (s0 - k * c0), cx + rx * (c1 - k * s1), cy - ry * (s1 + k * c1), cx + rx * c1, cy - ry * s1 ]); } return points } }); _bezier = new Bezier(); /*--- BEZIER PATH ----------------------------------------------------------------------------------*/ // A Path class with lineto(), curveto() and moveto() methods. var MOVETO = "moveto"; var LINETO = "lineto"; var CURVETO = "curveto"; var CLOSE = "close"; var PathElement = Class.extend({ init: function(x, y, cmd) { /* A point in the path, optionally with control handles. */ this.x = x; this.y = y; this.ctrl1 = new Point(0, 0); this.ctrl2 = new Point(0, 0); this.radius = 0; this.cmd = cmd; }, copy: function() { var pt = new PathElement(this.x, this.y, this.cmd); pt.ctrl1 = this.ctrl1.copy(); pt.ctrl2 = this.ctrl2.copy(); return pt; } }); var DynamicPathElement = PathElement.extend({ // Not a "fixed" point in the Path, but calculated with Path.point(). }); var Path = BezierPath = Class.extend({ init: function(path) { /* A list of PathElements describing the curves and lines that make up the path. */ if (path === undefined) { this.array = []; // We can't subclass Array. } else if (path instanceof Path) { this.array = Array.map(path.array, function(pt) { return pt.copy(); }); } else if (path instanceof Array) { this.array = Array.map(path, function(pt) { return pt.copy(); }); } this._clip = false; this._update(); }, _update: function() { this._segments = null; this._polygon = null; }, copy: function() { return new Path(this); }, moveto: function(x, y) { /* Adds a new point to the path at x, y. */ var pt = new PathElement(x, y, MOVETO); pt.ctrl1 = new Point(x, y); pt.ctrl2 = new Point(x, y); this.array.push(pt); this._update(); }, lineto: function(x, y) { /* Adds a line from the previous point to x, y. */ var pt = new PathElement(x, y, LINETO); pt.ctrl1 = new Point(x, y); pt.ctrl2 = new Point(x, y); this.array.push(pt); this._update(); }, curveto: function(x1, y1, x2, y2, x3, y3) { /* Adds a Bezier-curve from the previous point to x3, y3. * The curvature is determined by control handles x1, y1 and x2, y2. */ var pt = new PathElement(x3, y3, CURVETO); pt.ctrl1 = new Point(x1, y1); pt.ctrl2 = new Point(x2, y2); this.array.push(pt); this._update(); }, moveTo: function(x, y) { this.moveto(x, y); }, lineTo: function(x, y) { this.lineto(x, y); }, curveTo: function(x1, y1, x2, y2, x3, y3) { this.curveto(x1, y1, x2, y2, x3, y3); }, closepath: function() { /* Adds a line from the previous point to the last MOVETO. */ this.array.push(new PathElement(0, 0, CLOSE)); this._update(); }, closePath: function() { this.closepath(); }, rect: function(x, y, width, height, options) { /* Adds a rectangle to the path. */ if (!options || options.roundness === undefined) { this.moveto(x, y); this.lineto(x+width, y); this.lineto(x+width, y+height); this.lineto(x, y+height); this.lineto(x, y); } else { var curve = Math.min(width * options.roundness, height * options.roundness); this.moveto(x, y+curve); this.curveto(x, y, x, y, x+curve, y); this.lineto(x+width-curve, y); this.curveto(x+width, y, x+width, y, x+width, y+curve); this.lineto(x+width, y+height-curve); this.curveto(x+width, y+height, x+width, y+height, x+width-curve, y+height); this.lineto(x+curve, y+height); this.curveto(x, y+height, x, y+height, x, y+height-curve); this.closepath(); } }, ellipse: function(x, y, width, height) { /* Adds an ellipse to the path. */ x -= 0.5 * width; // Center origin. y -= 0.5 * height; var k = 0.55; // kappa = (-1 + sqrt(2)) / 3 * 4 var dx = k * 0.5 * width; var dy = k * 0.5 * height; var x0 = x + 0.5 * width; var y0 = y + 0.5 * height; var x1 = x + width; var y1 = y + height; this.moveto(x, y0); this.curveto(x, y0-dy, x0-dx, y, x0, y); this.curveto(x0+dx, y, x1, y0-dy, x1, y0); this.curveto(x1, y0+dy, x0+dx, y1, x0, y1); this.curveto(x0-dx, y1, x, y0+dy, x, y0); this.closepath(); }, arc: function(x, y, radius, angle1, angle2, options) { /* Adds an arc to the path, * clockwise from angle1 to angle2 along circle (x, y, radius). */ var a1 = angle1 % 360; var a2 = angle2; if (a2 < a1) { a2 = a2 + 360; } if (options && options.clockwise === false) { a2 = a2 % 360 - 360; } var points = _bezier.arc(x-radius, y-radius, x+radius, y+radius, a1, a2-a1); for (var i=0; i < points.length; i++) { var pt = points[i]; if (i == 0) { this.moveto(pt[0], pt[1]); } this.curveto(pt[2], pt[3], pt[4], pt[5], pt[6], pt[7]); } }, // draw: function({fill: Color(), stroke: Color(), strokewidth: 1.0, strokestyle: SOLID}) draw: function(options) { /* Draws the path. */ _ctx.beginPath(); if (this.array.length > 0 && this.array[0].cmd != MOVETO) { throw "No current point for path (first point must be MOVETO)." } for (var i=0; i < this.array.length; i++) { var pt = this.array[i]; switch(pt.cmd) { case MOVETO: _ctx.moveTo(pt.x, pt.y); break; case LINETO: _ctx.lineTo(pt.x, pt.y); break; case CURVETO: _ctx.bezierCurveTo(pt.ctrl1.x, pt.ctrl1.y, pt.ctrl2.x, pt.ctrl2.y, pt.x, pt.y); break; case CLOSE: _ctx.closePath(); break; } } if (!this._clip) { var a = _colorMixin(options); // [fill, stroke, strokewidth, strokestyle, linecap] _ctx_fill(a[0]); _ctx_stroke(a[1], a[2], a[3], a[4]); } else { _ctx.clip(); } }, angle: function(t) { /* Returns the directional angle at time t (0.0-1.0) on the path. */ // The derive() enumerator is much faster but less precise. if (t == 0) { var pt0 = this.point(t); var pt1 = this.point(t+0.001); } else { var pt0 = this.point(t-0.001); var pt1 = this.point(t); } return geometry.angle(pt0.x, pt0.y, pt1.x, pt1.y); }, point: function(t) { /* Returns the DynamicPathElement at time t (0.0-1.0) on the path. */ if (this._segments == null) { // Cache the segment lengths for performace. this._segments = _bezier.length(this, true, 10); } return _bezier.point(this, t, this._segments); }, points: function(amount, options) { /* Returns an array of DynamicPathElements along the path. * To omit the last point on closed paths: {end: 1-1.0/amount} */ var start = (options && options.start !== undefined)? options.start : 0.0; var end = (options && options.end !== undefined)? options.end : 1.0; if (this.array.length == 0) { // Otherwise Bezier.point() will raise an error for empty paths. return []; } amount = Math.round(amount); // The delta value is divided by amount-1, because we also want the last point (t=1.0) // If we don't use amount-1, we fall one point short of the end. // If amount=4, we want the point at t 0.0, 0.33, 0.66 and 1.0. // If amount=2, we want the point at t 0.0 and 1.0. var d = (amount > 1)? (end-start) / (amount-1) : (end-start); var a = []; for (var i=0; i < amount; i++) { a.push(this.point(start + d*i)); } return a; }, length: function(precision) { /* Returns an approximation of the total length of the path. */ if (precision === undefined) precision = 10; return _bezier.length(this, false, precision); }, contains: function(x, y, precision) { /* Returns true when point (x,y) falls within the contours of the path. */ if (precision === undefined) precision = 100; if (this._polygon == null || this._polygon[1] != precision) { this._polygon = [this.points(precision), precision]; } return geometry.pointInPolygon(this._polygon[0], x, y); } }); function drawpath(path, options) { /* Draws the given Path (or list of PathElements). * The current stroke, strokewidth, strokestyle and fill color are applied. */ if (path instanceof Array) { path = new Path(path); } path.draw(options); } function autoclosepath(close) { /* Paths constructed with beginpath() and endpath() are automatically closed. */ if (close === undefined) close = true; _ctx.state.autoclosepath = close; } function beginpath(x, y) { /* Starts a new path at (x,y). * Functions moveto(), lineto(), curveto() and closepath() * can then be used between beginpath() and endpath() calls. */ _ctx.state.path = new Path(); _ctx.state.path.moveto(x, y); } function moveto(x, y) { /* Moves the current point in the current path to (x,y). */ _ctx.state.path.moveto(x, y); } function lineto(x, y) { /* Draws a line from the current point in the current path to (x,y). */ _ctx.state.path.lineto(x, y); } function curveto(x1, y1, x2, y2, x3, y3) { /* Draws a curve from the current point in the current path to (x3,y3). * The curvature is determined by control handles x1, y1 and x2, y2. */ _ctx.state.path.curveto(x1, y1, x2, y2, x3, y3); } function closepath() { /* Closes the current path with a straight line to the last MOVETO. */ _ctx.state.path.closepath(); } function endpath(options) { /* Draws and returns the current path. * With {draw:false}, only returns the path so it can be manipulated and drawn with drawpath(). */ var s = _ctx.state; if (s.autoclosepath) s.path.closepath(); if (!options || options.draw) { s.path.draw(options); } var p=s.path; s.path=null; return p; } function findpath(points, curvature) { /* Returns a smooth BezierPath from the given list of points. */ return _bezier.findPath(points, curvature); } var drawPath = drawpath; var autoClosePath = autoclosepath; var beginPath = beginpath; var moveTo = moveto; var lineTo = lineto; var curveTo = curveto; var closePath = closepath; var endPath = endpath; var findPath = findpath; /*--- POINT ANGLES ---------------------------------------------------------------------------------*/ function derive(points, callback) { /* Calls callback(angle, pt) for each point in the given path. * The angle represents the direction of the point on the path. * This works with Path, Path.points, [pt1, pt2, pt2, ...] * For example: * derive(path.points(30), function(angle, pt) { * push(); * translate(pt.x, pt.y); * rotate(angle); * arrow(0, 0, 10); * pop(); * }); * This is useful if you want to have shapes following a path. * To put text on a path, rotate the angle by +-90 to get the normal (i.e. perpendicular). */ var p = (points instanceof Path)? points.array : points; var n = p.length; for (var i=0; i 0) { _ctx.beginPath(); _ctx.moveTo(x0, y0); _ctx.lineTo(x1, y1); _ctx_stroke(a[1], a[2], a[3], a[4]); } } function arc(x, y, radius, angle1, angle2, options) { /* Draws an arc with the center at x, y, clockwise from angle1 to angle2. * The current stroke, strokewidth and strokestyle are applied. */ var a = _colorMixin(options); if (a[0] && a[0].a > 0 || a[1] && a[1].a > 0) { var a1 = Math.radians(angle1); var a2 = Math.radians(angle2); _ctx.beginPath(); _ctx.arc(x, y, radius, a1, a2, (options && options.clockwise === false)); _ctx_stroke(a[1], a[2], a[3], a[4]); } } function rect(x, y, width, height, options) { /* Draws a rectangle with the top left corner at x, y. * The current stroke, strokewidth, strokestyle and fill color are applied. */ var a = _colorMixin(options); if (a[0] && a[0].a > 0 || a[1] && a[1].a > 0) { if (!options || options.roundness === undefined) { _ctx.beginPath(); _ctx.rect(x, y, width, height); _ctx_fill(a[0]); _ctx_stroke(a[1], a[2], a[3], a[4]); } else { var p = new Path(); p.rect(x, y, width, height, options); p.draw(options); } } } function triangle(x1, y1, x2, y2, x3, y3, options) { /* Draws the triangle created by connecting the three given points. * The current stroke, strokewidth, strokestyle and fill color are applied. */ var a = _colorMixin(options); if (a[0] && a[0].a > 0 || a[1] && a[1].a > 0) { _ctx.beginPath(); _ctx.moveTo(x1, y1); _ctx.lineTo(x2, y2); _ctx.lineTo(x3, y3); _ctx.closePath(); _ctx_fill(a[0]); _ctx_stroke(a[1], a[2], a[3], a[4]); } } function ellipse(x, y, width, height, options) { /* Draws an ellipse with the center located at x, y. * The current stroke, strokewidth, strokestyle and fill color are applied. */ var p = new Path(); p.ellipse(x, y, width, height); p.draw(options); } var oval = ellipse; function arrow(x, y, width, options) { /* Draws an arrow with its tip located at x, y. * The current stroke, strokewidth, strokestyle and fill color are applied. */ var head = width * 0.4; var tail = width * 0.2; var p = new Path(); p.moveto(x, y); p.lineto(x-head, y+head); p.lineto(x-head, y+tail); p.lineto(x-width, y+tail); p.lineto(x-width, y-tail); p.lineto(x-head, y-tail); p.lineto(x-head, y-head); p.closepath(); p.draw(options); } function star(x, y, points, outer, inner, options) { /* Draws a star with the given points, outer radius and inner radius. * The current stroke, strokewidth, strokestyle and fill color are applied. */ if (points === undefined) points = 20; if (outer === undefined) outer = 100; if (inner === undefined) inner = 50; var p = new Path(); p.moveto(x, y+outer); for (var i=0; i < 2 * points + 1; i++) { var r = (i % 2 == 0)? outer : inner; var a = Math.PI * i / points; p.lineto( x + r * Math.sin(a), y + r * Math.cos(a) ); }; p.closepath(); p.draw(options); } // To draw crisp lines, you can use translate(0, 0.5) + line0(). // We call it "0" because floats are rounded to nearest int. function line0(x1, y1, x2, y2, options) { line( Math.round(x1), Math.round(y1), Math.round(x2), Math.round(y2), options ); } function rect0(x, y, width, height, options) { rect( Math.round(x), Math.round(y), Math.round(width), Math.round(height), options ); } /*##################################################################################################*/ /*--- CACHE ----------------------------------------------------------------------------------------*/ // The global cache is a collection of objects that are preloading asynchronously. var Cache = Class.extend({ init: function() { /* A collection of objects (images, scripts, data) that are preloading asynchronously. * The Canvas.draw() method is delayed until all objects in the global cache are ready. */ this.objects = {}; // Objects in cache, by id. this.busy = 0; // Objects still loading. }, preload: function(id, object, callback) { /* Attaches an onload() event to the object and caches it under id. */ object._preload = [id, _cache, callback]; object.onreadystatechange = function() { if (this.readyState == "complete") { // IE if (this._preload) { this._preload[1].busy--; this._preload[2](this); this._preload = null; } }}; object.onload = function() { if (this._preload) { this._preload[1].busy--; this._preload[2](this); this._preload = null; } }; object.onerror = function() { _ctx && _ctx._canvas.onerror("Can't load: " + this._preload[0]); if (this._preload) { this._preload[1].busy--; } }; if (id) { this.objects[id] = object; } this.busy++; }, script: function(js) { /* Caches the given JavaScript file. */ if (this.objects[js]) { return; } else if (js && !js.substr) { throw "Can't load script: " + js; } else if (js === undefined) { throw "Can't load script: " + js; } try { var script = document.createElement("script"); script.async = true; script.type = "text/javascript"; script.src = js; this.preload(js, script, function(){}); document.body.appendChild(script); } catch(e) { throw "Can't load script: " + js + e; } }, image: function(img) { /* Returns a cached element, * for the given URL path, URL data, Image, Pixels, Canvas or Buffer. */ if (img === undefined) { throw "Can't load image: " + img; // Cached. } else if (this.objects[img]) { return this.objects[img]; // From URL path ("http://"). } else if (img && img.substr && img.substr(0,5) == "http:") { var url = img; var src = img; // From URL data ("data:image/png"), e.g., Canvas.save(). } else if (img && img.substr && img.substr(0,5) == "data:") { var url = null var src = img; // From local path ("/g/image.png"). } else if (img && img.substr && img.substr(0,5) != "file:") { var url = img; var src = img; // From . } else if (img.src && img.complete) { var url = null; var src = img.src; // From Canvas + Buffer. } else if (img instanceof Canvas || img instanceof Buffer) { var url = null; var src = img.save(); // From HTML element. } else if (img .getContext) { img.complete = true; return img; // From Image. } else if (img instanceof Image) { return this.image(img._img); // From Pixels. } else if (img instanceof Pixels) { return img._img._img; } else { throw "Can't load image: " + img; } // Cache image source. // URL paths, URL data, Canvas and Buffer get an onload() event. img = document.createElement("img"); img._owners = []; this.preload(url, img, function(img) { // Set Image owner size. // There can be multiple owners displaying the same image. for (var i=0; i < img._owners.length; i++) { var o = img._owners[i]; if (o && o.width == null) o.width = img.width; if (o && o.height == null) o.height = img.height; } img._owners = []; // Remove reference. }); // A canvas with cross-domain images becomes tainted: // https://developer.mozilla.org/en/CORS_Enabled_Image // This disables ctx.toDataURL() and ctx.getImageData(). // This disables Pixels and Buffer. //img.crossOrigin = ""; img.src = src; return img; } }); // Global cache: var _cache = new Cache(); /*--- INCLUDE --------------------------------------------------------------------------------------*/ function include(url) { /* Imports the given JavaScript library, * from a local path (e.g., "graph.js") or a remote URL (e.g., "http://domain.com/graph.js"). */ _cache.script(url); } var require = require || include; /*--- ASYNCHRONOUS REQUEST -------------------------------------------------------------------------*/ var AsynchronousRequest = Class.extend({ init: function(url, data) { /* Loads the given URL in the background using XMLHTTPRequest. * AsynchronousRequest.done is False as long as it is busy. * AsynchronousRequest.value contains the response value once done. */ if (window.XMLHttpRequest) var q = new XMLHttpRequest(); if (window.ActiveXObject) var q = new ActiveXObject("Microsoft.XMLHTTP"); q.onreadystatechange = function() { if (q.readyState == 4 && q.status == 200) this._fetch(q); } if (data) { q.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); q.open("POST", url, true); q.send(data); } else { q.open("GET", url, true); q.send(); } this.value = null; this.done = false; }, _fetch: function(q) { this.value = q.responseText; this.done = true; } }); function asynchronous(url, data) { return new AsynchronousRequest(url, data); } /*##################################################################################################*/ /*--- IMAGE ----------------------------------------------------------------------------------------*/ var Image = Class.extend({ // init: function(url, {x: 0, y: 0, width: null, height: null, alpha: 1.0}) init: function(url, options) { /* A image that can be drawn at a given position. */ var o = options || {}; this._url = url; this._img = _cache.image(url); this.x = o.x || 0; this.y = o.y || 0; this.width = (o.width !== undefined)? o.width : null; this.height = (o.height !== undefined)? o.height : null; this.alpha = (o.alpha !== undefined)? o.alpha : 1.0; // If no width or height is given (undefined | null), // use the dimensions of the source . // If the is still loading, this happens with a delay (see Images.load()). if (this._img._owners && !this._img.complete) { this._img._owners.push(this); } if (this.width == null && this._img.complete) { this.width = this._img.width; } if (this.height == null && this._img.complete) { this.height = this._img.height; } }, copy: function() { return new Image(this._url, { x: this.x, y: this.y, width: this.width, height: this.height, alpha: this.alpha }); }, // draw: function(x, y, {width: null, height: null, alpha: 1.0}) draw: function(x, y, options) { /* Draws the image. * The given parameters (if any) override the image's attributes. */ var o = options || {}; var w = (o.width !== undefined && o.width != null)? o.width : this.width; var h = (o.height !== undefined && o.height != null)? o.height : this.height; var a = (o.alpha !== undefined)? o.alpha : this.alpha; x = (x || x === 0)? x : this.x; y = (y || y === 0)? y : this.y; if (this._img.complete && w && h && a > 0) { if (a >= 1.0) { _ctx.drawImage(this._img, x, y, w, h); } else { _ctx.globalAlpha = a; _ctx.drawImage(this._img, x, y, w, h); _ctx.globalAlpha = 1.0; } } }, busy: function() { return !this._img.complete; // Still loading? } }); function image(img, x, y, options) { /* Draws the image at (x,y), scaling it to the given width and height. * The image's transparency can be set with alpha (0.0-1.0). */ img = (img instanceof Image)? img : new Image(img); if (!options || options.draw != false) { img.draw(x, y, options); } return img; } function imagesize(img) { /* Returns an array [width, height] with the image dimensions. */ img = (img instanceof Image)? img : new Image(img); return [img._img.width, img._img.height]; } var imageSize = imagesize; /*##################################################################################################*/ /*--- PIXELS ---------------------------------------------------------------------------------------*/ var Pixels = Class.extend({ init: function(img) { /* An array of RGBA color values (0-255) for each pixel in the given image. * Pixels.update() must be called to reflect any changes to Pixels.array. * The Pixels object can be passed to the image() function. * The original image will not be modified. * Throws a security error for remote (cross-domain) images. */ img = (img instanceof Image)? img : new Image(img); this._img = img; this._element = document.createElement("canvas"); this._element.width = this.width = img._img.width; this._element.height = this.height = img._img.height; this._ctx = this._element.getContext("2d"); this._ctx.drawImage(img._img, 0, 0); this._data = this._ctx.getImageData(0, 0, this.width, this.height); this.array = this._data.data; }, copy: function() { return new Pixels(this._img); }, get: function(i) { /* Returns array [R,G,B,A] with channel values between 0-255 from pixel i. * var rgba = Pixels.get[i]; * var clr = new Color(rgba, {base:255}); */ i*= 4; return [this.array[i+0], this.array[i+1], this.array[i+2], this.array[i+3]] }, set: function(i, rgba) { /* Sets pixel i to the given array [R,G,B,A] with values 0-255. * var clr = new Color(0,0,0,1); * Pixels.set(i, clr.map({base:255})); */ i*= 4; this.array[i+0] = rgba[0]; this.array[i+1] = rgba[1]; this.array[i+2] = rgba[2]; this.array[i+3] = rgba[3]; }, map: function(callback) { /* Applies a function to each pixel. * Function takes a list of R,G,B,A channel values and must return a similar list. */ for (var i=0; i < this.width * this.height; i++) { this.set(i, callback(this.get(i))); } }, update: function() { /* Pixels.update() must be called to refresh the image. */ this._ctx.putImageData(this._data, 0, 0); this._img = new Image(this._element); }, image: function() { return this._img; } }); function pixels(img) { return new Pixels(img); } /*##################################################################################################*/ /*--- FONT -----------------------------------------------------------------------------------------*/ var NORMAL = "normal"; var BOLD = "bold"; var ITALIC = "italic" function font(fontname, fontsize, fontweight) { /* Sets the current font and/or fontsize. */ if (fontname !== undefined) _ctx.state.fontname = fontname; if (fontsize !== undefined) _ctx.state.fontsize = fontsize; if (fontweight !== undefined) _ctx.state.fontweight = fontweight; return _ctx.state.fontname; } function fontsize(fontsize) { /* Sets the current fontsize in points. */ if (fontsize !== undefined) _ctx.state.fontsize = fontsize; return _ctx.state.fontsize; } function fontweight(fontweight) { /* Sets the current font weight (BOLD, ITALIC, BOLD+ITALIC). */ if (fontweight !== undefined) _ctx.state.fontweight = fontweight; return _ctx.state.fontweight; } function lineheight(lineheight) { /* Sets the vertical spacing between lines of text. * The given size is a relative value: lineheight 1.2 for fontsize 10 means 12. */ if (lineheight !== undefined) _ctx.state.lineheight = lineheight; return _ctx.state.lineheight; } var fontSize = fontsize; var fontWeight = fontweight; var lineHeight = lineheight; /*--- FONT MIXIN -----------------------------------------------------------------------------------*/ // The text() function has optional parameters font, fontsize, fontweight, bold, italic, lineheight and align. function _fontMixin(options) { var s = _ctx.state; var o = options; if (options === undefined) { return [s.fontname, s.fontsize, s.fontweight, s.lineheight]; } else { return [ (o.font)? o.font : s.fontname, (o.fontsize !== undefined)? o.fontsize : (o.fontSize !== undefined)? o.fontSize : s.fontsize, (o.fontweight !== undefined)? o.fontweight : (o.fontWeight !== undefined)? o.fontWeight : s.fontweight, (o.lineheight !== undefined)? o.lineheight : (o.lineHeight !== undefined)? o.lineHeight : s.lineheight ]; } } /*--- TEXT -----------------------------------------------------------------------------------------*/ function _ctx_font(fontname, fontsize, fontweight) { // Wrappers for _ctx.font, only calling it when necessary. if (fontweight.length > ITALIC.length && fontweight == BOLD+ITALIC || fontweight == ITALIC+BOLD) { fontweight = ITALIC + " " + BOLD; } _ctx.font = fontweight + " " + fontsize + "pt " + fontname; } function str(v) { return v.toString(); } function text(str, x, y, options) { /* Draws the string at the given position. * Lines of text will be split at \n. * The text will be displayed with the current state font(), fontsize(), fontweight(). */ var a1 = _colorMixin(options); var a2 = _fontMixin(options); if (a1[0] && a1[0].a > 0) { var f = a1[0]._get(); if (_ctx.state._fill != f) { _ctx.fillStyle = _ctx.state._fill = f; } _ctx_font(a2[0], a2[1], a2[2]); var lines = str.toString().split("\n"); for (var i=0; i index in the _map curve. var n = _RANDOM_MAP[i]; // If bias is 0.3, rnd()**2.33 will average 0.3. if (bias < 10) { n += (_RANDOM_MAP[i+1]-n) * (bias-i); } return n; } function random(v1, v2, bias) { /* Returns a number between v1 and v2, including v1 but not v2. * The bias (0.0-1.0) represents preference towards lower or higher numbers. */ if (v1 === undefined) v1 = 1.0; if (v2 === undefined) { v2=v1; v1=0; } if (bias === undefined) { var r = Math.random(); } else { var r = Math.pow(Math.random(), _rndExp(bias)); } return r * (v2-v1) + v1; } var _NOISE_P = []; // Noise permutation table. var _NOISE_G = []; // Noise gradient (2D = 8 directions). function noise(x, y, m) { /* Returns a smooth value between -1.0 and 1.0. * Since the 2D space is infinite, the actual (x,y) coordinates do not matter. * Smaller steps between successive coordinates yield smoother noise (e.g., 0.01-0.1). */ // Based on: http://www.angelcode.com/dev/perlin/perlin.html if (m === undefined) m = 1.5; if (_NOISE_P.length == 0) { _NOISE_P = Array.shuffle(Array.range(256)); _NOISE_P = _NOISE_P.concat(_NOISE_P); _NOISE_G = [[-1, +1], [+0, +1], [+1, +1], [-1, +0], [+1, +0], [-1, -1], [+0, -1], [+1, -1]]; } var X = Math.floor(x) & 255; var Y = Math.floor(y) & 255; x -= Math.floor(x); y -= Math.floor(y); var u = x * x * x * (x * (x * 6 - 15) + 10); // fade var v = y * y * y * (y * (y * 6 - 15) + 10); var n = Math.mix( Math.mix(Math.dot(_NOISE_G[_NOISE_P[X + _NOISE_P[Y ]] % 8], [x , y ]), Math.dot(_NOISE_G[_NOISE_P[X+1 + _NOISE_P[Y ]] % 8], [x-1, y ]), u), Math.mix(Math.dot(_NOISE_G[_NOISE_P[X + _NOISE_P[Y+1]] % 8], [x , y-1]), Math.dot(_NOISE_G[_NOISE_P[X+1 + _NOISE_P[Y+1]] % 8], [x-1, y-1]), u), v); return (Math.clamp(n * m, -1, +1) + 1) * 0.5; // 0.0-1.0 } function grid(cols, rows, colWidth, rowHeight, shuffled) { /* Returns an array of Points for the given number of rows and columns. * The space between each point is determined by colwidth and colheight. */ if (colWidth === undefined) colWidth = 1; if (rowHeight === undefined) rowHeight = 1; rows = Array.range(parseInt(rows)); cols = Array.range(parseInt(cols)); if (shuffled) { Array.shuffle(rows); Array.shuffle(cols); } var a = []; for (var y=0; y < rows.length; y++) { for (var x=0; x < cols.length; x++) { a.push(new Point(x*colWidth, y*rowHeight)); } } return a; } /*##################################################################################################*/ /*--- MOUSE ----------------------------------------------------------------------------------------*/ function absOffset(element) { /* Returns the absolute position of the given element in the browser. */ var x = y = 0; if (element.offsetParent) { do { x += element.offsetLeft; y += element.offsetTop; } while (element = element.offsetParent); } return [x,y]; } // Mouse cursors: var DEFAULT = "default"; var HIDDEN = "none"; var CROSS = "crosshair"; var HAND = "pointer"; var POINTER = "pointer"; var DRAG = "move"; var TEXT = "text"; var WAIT = "wait"; var Mouse = Class.extend({ init: function(element) { /* Keeps track of the mouse position on the given element. */ this.parent = element; element._mouse=this; this.x = 0; this.y = 0; this.relative_x = this.relativeX = 0; this.relative_y = this.relativeY = 0; this.pressed = false; this.dragged = false; this.drag = { "x": 0, "y": 0 }; this.scroll = 0; this._out = function() { // Returns true if not inside Mouse.parent. return !(0 <= this.x && this.x <= this.parent.offsetWidth && 0 <= this.y && this.y <= this.parent.offsetHeight); } var eventScroll = function(e) { // Create parent onmousewheel event (set Mouse.scroll). // Fire Mouse.onscroll(). var m = this._mouse; m.scroll += Math.sign(e.wheelDelta); if (m.onscroll && m.onscroll(m) === false) { e.preventDefault(); e.stopPropagation(); } }; var eventDown = function(e) { // Create parent onmousedown event (set Mouse.pressed). // Fire Mouse.onpress(). var m = this._mouse; if (e.touches !== undefined) { // TouchStart (iPad). e.preventDefault(); } m.pressed = true; m._x0 = m.x; m._y0 = m.y; m.onpress(m); }; var eventUp = function(e) { // Create parent onmouseup event (reset Mouse state). // Fire Mouse.onrelease() if inside parent bounds. var m = this._mouse; m.pressed = false; m.dragged = false; m.drag.x = 0; m.drag.y = 0; m.scroll = 0; m.onrelease(m, m._out()); }; var eventMove = function(e) { // Create parent onmousemove event (set Mouse position & drag). // Fire Mouse.onmove() if mouse pressed and inside parent bounds. // Fire Mouse.ondrag() if mouse pressed. var m = this._mouse; var o1 = document.documentElement || document.body; var o2 = absOffset(this); if (e.touches !== undefined) { // TouchEvent (iPad). m.x = e.touches[0].pageX; m.y = e.touches[0].pageY; } else { // MouseEvent. m.x = (e.pageX || (e.clientX + o1.scrollLeft)) - o2[0]; m.y = (e.pageY || (e.clientY + o1.scrollTop)) - o2[1]; } if (m.pressed) { m.dragged = true; m.drag.x = m.x - m._x0; m.drag.y = m.y - m._y0; } m.relative_x = m.relativeX = m.x / m.parent.offsetWidth; m.relative_y = m.relativeY = m.y / m.parent.offsetHeight; if (m.pressed) { m.ondrag(m); } else if (!m._out()) { m.onmove(m); } }; // Bind mouse and multi-touch events: attachEvent(element, "mousewheel", eventScroll); attachEvent(element, "mousedown" , eventDown); attachEvent(element, "touchstart", eventDown); attachEvent(window, "mouseup" , Function.closure(this.parent, eventUp)); attachEvent(window, "touchend" , Function.closure(this.parent, eventUp)); attachEvent(window, "mousemove" , Function.closure(this.parent, eventMove)); attachEvent(window, "touchmove" , Function.closure(this.parent, eventMove)); }, // These can be patched with a custom function: onscroll: function(mouse) {}, onpress: function(mouse) {}, onrelease: function(mouse, out) {}, // With out=true if mouse outside canvas. onmove: function(mouse) {}, ondrag: function(mouse) {}, cursor: function(mode) { /* Sets the mouse cursor (DEFAULT, HIDDEN, CROSS, POINTER, TEXT or WAIT). */ if (mode !== undefined) { this.parent.style.cursor = mode; } return this.parent.style.cursor; } }); /*--- CANVAS ---------------------------------------------------------------------------------------*/ function _uid() { // Returns a unique number. if (_uid.i === undefined) _uid.i=0; return ++_uid.i; } function _unselectable(element) { // Disables text dragging on the given element. element.onselectstart = function() { return false; }; element.unselectable = "on"; element.style.MozUserSelect = "none"; element.style.cursor = "default"; } window._requestFrame = function(callback, canvas, fps) { var f = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || function(callback, element) { return window.setTimeout(callback, 1000 / (fps || 60)); }; // When requestFrame() calls Canvas._draw() directly, the "this" keyword will be detached. // Make "this" available inside Canvas._draw() by binding it: return f(Function.closure(canvas, callback), canvas.element); }; window._clearFrame = function(id) { var f = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.webkitCancelRequestAnimationFrame || window.mozCancelAnimationFrame || window.mozCancelRequestAnimationFrame || window.msCancelAnimationFrame || window.msCancelRequestAnimationFrame || window.oCancelAnimationFrame || window.oCancelRequestAnimationFrame || window.clearTimeout; return f(id); }; // Current graphics context. // It is the Canvas that was last created, OR // it is the Canvas that is preparing to call Canvas.draw(), OR // it is the Buffer that has called Buffer.push(). var _ctx = null; var G_vmlCanvasManager; // Needed with excanvas.js var Canvas = Class.extend({ init: function(element, width, height, options) { /* Interface to the HTML5 element. * Drawing starts when Canvas.run() is called. * The draw() method must be overridden with your own drawing commands, which will be executed each frame. */ if (options === undefined) { options = {}; } if (!element) { element = document.createElement("canvas"); } if (!element.getContext && typeof(G_vmlCanvasManager) != "undefined") { element = G_vmlCanvasManager.initElement(element); } if (width !== undefined && width !== null) { element.width = width; } if (height !== undefined && height !== null) { element.height = height; } _unselectable(element); this.id = "canvas" + _uid(); this.element = element; this.element.style["-webkit-tap-highlight-color"] = "rgba(0,0,0,0)"; this.element.canvas = this; this._ctx = this.element.getContext("2d"); this._ctx._canvas = this; this.focus(); this.mouse = (options.mouse != false)? new Mouse(this.element) : null; this.width = this.element.width; this.height = this.element.height; this.frame = 0; this.fps = 0; this.dt = 0; this._time = {start: null, current: null}; this._step = 0; this._active = false; this._widgets = []; this.variables = []; this._resetState(); }, _resetState: function() { // Initialize color state: current background, current fill, ... // The state is applied to each shape when drawn - see _ctx_fill(), _ctx_stroke() and _ctx_font(). // Drawing commands such as rect() have optional parameters // that can override the state - see _colorMixin(). _ctx.state = { "path": null, "autoclosepath": false, "background": null, "fill": new Color(0,0,0,1), "stroke": null, "strokewidth": 1.0, "strokestyle": SOLID, "shadow": null, "fontname": "sans-serif", "fontsize": 12, "fontweight": NORMAL, "lineheight": 1.2 } }, _resetWidgets: function() { // Resets canvas widgets, removing the variables and elements. for (var i=0; i < this._widgets.length; i++) { var w = this._widgets[i]; var e = w.element.parentNode; e.parentNode.removeChild(e); delete this[w.name]; } var p = document.getElementById(this.id + "_widgets"); if (p) p.parentNode.removeChild(p); this._widgets = []; this.variables = []; }, focus: function() { _ctx = this._ctx; // Set the current graphics context. }, size: function(width, height) { this.width = this.element.width = width; this.height = this.element.height = height; }, setup: function() { }, draw: function() { this.clear(); }, stop: function() { this._stop(); }, clear: function() { /* Clears the previous frame from the canvas. */ this._ctx.clearRect(0, 0, this.width, this.height); }, _setup: function() { this._resetState(); this.focus(); push(); try { this.setup(this); } catch(e) { this.onerror(e); throw e; } pop(); }, _draw: function() { if (this._active == false && !(this._step > this.frame)) { // Drawing halts after stop(), pause() or step(). // If step() is called, we first need to draw one more frame. return; } var t = new Date; this.fps = this.frame * 1000 / (t - this._time.start) || 1; this.fps = Math.round(this.fps * 100) / 100; this.dt = (t - this._time.current) / 1000; this._time.current = t; this._step = 0; this.frame++; this.focus() push(); this._resetState(); try { this.draw(this); } catch(e) { this.onerror(e); throw e; } pop(); this._scheduled = window._requestFrame(this._draw, this); }, _stop: function() { /* Stops the animation. When run() is called subsequently, the animation will restart from the first frame. */ if (this._scheduled !== undefined) { window._clearFrame(this._scheduled); } this._active = false; this._resetWidgets(); this.frame = 0; }, run: function() { /* Starts drawing the canvas. * Canvas.setup() will be called once during initialization. * Canvas.draw() will be called each frame. * Canvas.clear() needs to be called explicitly to clear the previous frame drawing. * Canvas.stop() stops the animation, but doesn't clear the canvas. */ this._active = true; this._time.start = new Date(); this._time.current = this._time.start; if (this.frame == 0) { this._setup(); } // Delay Canvas.draw() until the cached images are done loading // (for example, Image objects created during Canvas.setup()). var _preload = function() { if (_cache.busy > 0) { setTimeout(Function.closure(this, _preload), 10); return; } this._draw(); } _preload.apply(this); }, pause: function() { /* Pauses the animation at the current frame. */ if (this._scheduled !== undefined) { window._clearFrame(this._scheduled); } this._active = false; }, step: function() { /* Draws one frame and pauses. */ this._step = this.frame+1; this.run(); this.pause(); }, active: function() { /* Returns true if the animation is running. */ return this._active; }, image: function() { /* Returns a new Image with the contents of the canvas. * Throws a security error if the canvas contains remote (cross-domain) images. */ return new Image(this.element, {width: this.width, height: this.height}); }, save: function() { /* Returns a data:image/png base64-encoded string. * Throws a security error if the canvas contains remote (cross-domain) images. */ //var w = window.open(); //w.document.body.innerHTML = ""; return this.element.toDataURL("image/png"); }, widget: function(variable, type, options) { widget(this, variable, type, options); }, onerror: function(error) { // Called when an error occurs in Canvas.draw() or Canvas.setup(). }, onprint: function(string) { // Called when the print() function is called. } }); function size(width, height) { /* Sets the width and the height of the canvas. */ _ctx._canvas.size(width, height); } function print() { /* Calls Canvas.onprint() with the given arguments joined as a string. */ if (_ctx) _ctx._canvas.onprint(Array.prototype.slice.call(arguments).join(" ")); } /*##################################################################################################*/ /*--- OFFSCREEN BUFFER -----------------------------------------------------------------------------*/ var OffscreenBuffer = Buffer = Canvas.extend({ init: function(width, height) { /* A hidden canvas, useful for preparing procedural images. */ this._ctx_stack = [_ctx]; this._super(document.createElement("canvas"), width, height, {mouse:false}); // Do not set the Buffer as current graphics context // (call Buffer.push() explicitly to do this): _ctx = this._ctx_stack[0]; }, _setup: function() { this.push(); this._super(); this.pop(); }, _draw: function() { this.push(); this._super(); this.pop(); }, push: function() { /* Between push() and pop(), all drawing is done offscreen in OffscreenBuffer.image(). * The offscreen buffer has its own transformation state, * so any translate(), rotate() etc. does not affect the onscreen canvas. */ this._ctx_stack.push(_ctx); _ctx=this._ctx; }, pop: function() { /* Reverts to the onscreen canvas. * The contents of the offscreen buffer can be retrieved with OffscreenBuffer.image(). */ _ctx = this._ctx_stack.pop(); }, render: function() { /* Executes the drawing commands in OffscreenBuffer.draw() offscreen and returns image. */ this.run(); this._stop(); return this.image(); }, reset: function(width, height) { if (width !== undefined && height !== undefined) { this.clear(); this.size(width, height); } } }); function render(callback, width, height) { /* Returns an Image object from a function containing drawing commands (i.e. a procedural image). * This is useful when, for example, you need to render filters on paths. */ var buffer = new OffscreenBuffer(width, height); buffer.draw = callback; return buffer.render(); } function filter(img, callback) { /* Returns a new Image object with the given pixel function applied to it. * The function takes an array of RGBA-values (base 255) and returns a new array. */ var pixels = new Pixels(img); pixels.map(callback); pixels.update(); return pixels.image(); } /*##################################################################################################*/ /*--- IMAGE FILTERS | GENERATORS -------------------------------------------------------------------*/ function solid(width, height, clr) { /* Returns an image with a solid fill color. */ return render(function(canvas) { rect(0, 0, width, height, {fill: clr || [0,0,0,0]}); }, width, height); } /*--- IMAGE FILTERS | COLOR ------------------------------------------------------------------------*/ var LUMINANCE = [0.2125 / 255, 0.7154 / 255, 0.0721 / 255]; function invert(img) { /* Returns an image with inverted colors (e.g. white becomes black). */ return filter(img, function(p) { return [255-p[0], 255-p[1], 255-p[2], p[3]]; }); } function colorize(img, color, bias) { /* Returns a colorized image. * - color: a Color (or array) of RGBA-values to multiply with each image pixel. * - bias : a Color (or array) of RGBA-values to add to each image pixel. */ var m1 = new Color(color || [1,1,1,1]).rgba(); var m2 = new Color(bias || [0,0,0,0]).map({base: 255}); return filter(img, function(p) { return [ p[0] * m1[0] + m2[0], p[1] * m1[1] + m2[1], p[2] * m1[2] + m2[2], p[3] * m1[3] + m2[3] ]; }); } // function adjust(img, {hue:0, saturation:1.0, brightness:1.0, contrast:1.0}) function adjust(img, options) { /* Applies color adjustment filters to the image and returns the adjusted image. * - hue : the shift in hue (1.0 is 360 degrees on the color wheel). * - saturation : the intensity of the colors (0.0 is a grayscale image). * - brightness : the overall lightness or darkness (0.0 is a black image). * - contrast : the difference in brightness between regions. */ var pixels = new Pixels(img); var adjust_hue = function(pixels, m) { pixels.map(function(p) { var hsb = _rgb2hsb(p[0]/255, p[1]/255, p[2]/255); var rgb = _hsb2rgb(Math.clamp(Math.mod(hsb[0] + m, 1), 0, 1), hsb[1], hsb[2]); return [rgb[0]*255, rgb[1]*255, rgb[2]*255, p[3]]; }); } var adjust_saturation = function(pixels, m) { pixels.map(function(p) { var i = (0.3*p[0] + 0.59*p[1] + 0.11*p[2]) * (1-m); return [p[0]*m + i, p[1]*m + i, p[2]*m + i, p[3]]; }); } var adjust_brightness = function(pixels, m) { pixels.map(function(p) { return [p[0] + m, p[1] + m, p[2] + m, p[3]]; }); } var adjust_contrast = function(pixels, m) { pixels.map(function(p) { return [(p[0]-128)*m + 128, (p[1]-128)*m + 128, (p[2]-128)*m + 128, p[3]]; }); } var o = options || {}; if (o.hue !== undefined) adjust_hue(pixels, o.hue); if (o.saturation !== undefined && o.saturation != 1) adjust_saturation(pixels, o.saturation); if (o.brightness !== undefined && o.brightness != 1) adjust_brightness(pixels, 255 * (o.brightness-1)); if (o.contrast !== undefined && o.contrast != 1) adjust_contrast(pixels, o.contrast); pixels.update(); return pixels.image(); } function desaturate(img) { /* Returns a grayscale version of the image. */ return adjust(img, {saturation: 0}); } function brightpass(img, threshold) { /* Returns a new image where pixels whose luminance fall below the threshold are black. */ return filter(img, function(p) { return (Math.dot(p, LUMINANCE) > ((threshold || threshold == 0)? threshold : 0.5))? p : [0,0,0, p[3]]; }); } function blur(img, radius) { /* Applies a stack blur filter to the image and returns the blurred image. * - radius: the radius of the blur effect in pixels (0-175). */ radius = (radius === undefined)? 10 : radius; radius = Math.min(radius, 175); var buffer = new OffscreenBuffer(img._img.width, img._img.height); return _stackblur(img, buffer, radius); } /*--- IMAGE FILTERS | ALPHA COMPOSITING ------------------------------------------------------------*/ // Based on: R. Dura, 2009, http://mouaif.wordpress.com/2009/01/05/photoshop-math-with-glsl-shaders/ function composite(img1, img2, dx, dy, operator) { /* Returns a new Image by mixing img1 (the destination) with blend image img2 (the source). * The given operator is a function(pixel1, pixel2) that returns a new pixel (RGBA-array). * This is used to implement alpha compositing and blend modes. * - dx: horizontal offset (in pixels) of the blend layer. * - dy: vertical offset (in pixels) of the blend layer. */ //var t = new Date().getTime(); dx = dx || 0; dy = dy || 0; var pixels1 = new Pixels(img1); var pixels2 = new Pixels(img2); for (var j=0; j < pixels1.height; j++) { for (var i=0; i < pixels1.width; i++) { if (0 <= i-dx && i-dx < pixels2.width) { if (0 <= j-dy && j-dy < pixels2.height) { var p1 = pixels1.get(i + j * pixels1.width); var p2 = pixels2.get((i-dx) + (j-dy) * pixels2.width); pixels1.set(i + j * pixels1.width, operator(p1, p2)); } } } } pixels1.update(); //console.log(new Date().getTime() - t); return pixels1.image(); } function transparent(img, alpha) { /* Returns a transparent version of the image. */ return render(function(canvas) { image(img, {alpha: alpha}); }, img.width, img.height); } function mask(img1, img2, dx, dy, alpha) { /* Applies the second image as an alpha mask to the first image. * The second image must be a grayscale image, where the black areas * make the first image transparent (e.g. punch holes in it). * - dx: horizontal offset (in pixels) of the blend layer. * - dy: vertical offset (in pixels) of the blend layer. */ alpha = (alpha || alpha == 0)? alpha : 1; return composite(img1, img2, dx, dy, function(p1, p2) { p1[3] = p1[3] * p2[0]/255 * p2[3]/255 * alpha; return p1; }); } var ADD = "add"; // Pixels are added. var SUBTRACT = "subtract"; // Pixels are subtracted. var LIGHTEN = "lighten"; // Lightest value for each pixel. var DARKEN = "darken"; // Darkest value for each pixel. var MULTIPLY = "multiply"; // Pixels are multiplied, resulting in a darker image. var SCREEN = "screen"; // Pixels are inverted/multiplied/inverted, resulting in a brighter picture. var OVERLAY = "overlay"; // Combines multiply and screen: light parts become ligher, dark parts darker. var HARDLIGHT = "hardlight"; // Same as overlay, but uses the blend instead of base image for luminance. var HUE = "hue"; // Hue from the blend image, brightness and saturation from the base image. function blend(mode, img1, img2, dx, dy, alpha) { /* Applies the second image as a blend layer with the first image. * - dx: horizontal offset (in pixels) of the blend layer. * - dy: vertical offset (in pixels) of the blend layer. */ alpha = (alpha || alpha == 0)? alpha : 1; switch(mode) { case ADD : op = function(x, y) { return x + y; }; break; case SUBTRACT : op = function(x, y) { return x + y - 255; }; break; case LIGHTEN : op = function(x, y) { return Math.max(x, y); }; break; case DARKEN : op = function(x, y) { return Math.min(x, y); }; break; case MULTIPLY : op = function(x, y) { return x * y / 255; }; break; case SCREEN : op = function(x, y) { return 255 - (255-x) * (255-y) / 255; }; break; case OVERLAY : op = function(x, y, luminance) { }; case HARDLIGHT: op = function(x, y, luminance) { var a = 2 * x * y / 255; var b = 255 - 2 * (255-x) * (255-y) / 255; return (luminance < 0.45)? a : (luminance > 0.55)? b : Math.lerp(a, b, (luminance - 0.45) * 255); }; break; default: op = function(x, y) { return 0; }; } function mix(p1, p2, op, luminance) { var p = [0,0,0,0]; var a = p2[3] / 255 * alpha; p[0] = Math.lerp(p1[0], op(p1[0], p2[0], luminance), a); p[1] = Math.lerp(p1[1], op(p1[1], p2[1], luminance), a); p[2] = Math.lerp(p1[2], op(p1[2], p2[2], luminance), a); p[3] = Math.lerp(p1[3], 255, a); return p; } // Some blend modes swap opaque blend (alpha=255) with base layer to mimic Photoshop. // Some blend modes use luminace (overlay & hard light). if (mode == ADD|| mode == SUBTRACT) { return composite(img1, img2, dx, dy, function(p1, p2) { return mix(p1, p2, op); }); } if (mode == LIGHTEN || mode == DARKEN || mode == MULTIPLY || mode == SCREEN) { return composite(img1, img2, dx, dy, function(p1, p2) { return (p2[3] == 255)? mix(p2, p1, op) : mix(p1, p2, op); }); } if (mode == OVERLAY) { return composite(img1, img2, dx, dy, function(p1, p2) { return mix(p1, p2, op, Math.dot(p1.slice(0, 3), LUMINANCE)); }); } if (mode == HARDLIGHT) { return composite(img1, img2, dx, dy, function(p1, p2) { return mix(p1, p2, op, Math.dot(p2.slice(0, 3), LUMINANCE)); }); } if (mode == HUE) { return composite(img1, img2, dx, dy, function(p1, p2) { var p, a=p1[3]; p1 = _rgb2hsb(p1[0], p1[1], p1[2]); p2 = _rgb2hsb(p2[0], p2[1], p2[2]); p = _hsb2rgb(p2[0], p1[1], p1[2]); p.push(a); return p; }); } } function add(img1, img2, dx, dy, alpha) { return blend(ADD, img1, img2, dx, dy, alpha); } function subtract(img1, img2, dx, dy, alpha) { return blend(SUBTRACT, img1, img2, dx, dy, alpha); } function lighten(img1, img2, dx, dy, alpha) { return blend(LIGHTEN, img1, img2, dx, dy, alpha); } function darken(img1, img2, dx, dy, alpha) { return blend(DARKEN, img1, img2, dx, dy, alpha); } function multiply(img1, img2, dx, dy, alpha) { return blend(MULTIPLY, img1, img2, dx, dy, alpha); } function screen_(img1, img2, dx, dy, alpha) { return blend(SCREEN, img1, img2, dx, dy, alpha); } function overlay(img1, img2, dx, dy, alpha) { return blend(OVERLAY, img1, img2, dx, dy, alpha); } function hardlight(img1, img2, dx, dy, alpha) { return blend(HARDLIGHT, img1, img2, dx, dy, alpha); } function hue(img1, img2, dx, dy, alpha) { return blend(HUE, img1, img2, dx, dy, alpha); } try { var s = screen; var p = [s.width, s.height, s.availWidth, s.availHeight, s.colorDepth, s.pixelDepth]; screen = screen_; screen.width = p[0]; screen.height = p[1]; screen.availWidth = p[2]; screen.availHeight = p[3]; screen.colorDepth = p[4]; screen.pixelDepth = p[5]; } catch(e) { } /*--- IMAGE FILTERS | LIGHT ------------------------------------------------------------------------*/ function glow(img, intensity, radius) { /* Returns the image blended with a blurred version, yielding a glowing effect. * - intensity: the opacity of the blur (0.0-1.0). * - radius : the blur radius. */ if (intensity === undefined) intensity = 0.5; var b = blur(img, radius); return blend(ADD, img, b, 0, 0, intensity); } function bloom(img, intensity, radius, threshold) { /* Returns the image blended with a blurred brightpass version, yielding a "magic glow" effect. * - intensity: the opacity of the blur (0.0-1.0). * - radius : the blur radius. * - threshold: the luminance threshold of pixels that light up. */ if (intensity === undefined) intensity = 0.5; if (threshold === undefined) threshold = 0.3; var b = blur(brightpass(img, threshold), radius); return blend(ADD, img, b, 0, 0, intensity); } /*--- IMAGE FILTERS | DISTORTION -------------------------------------------------------------------*/ // Based on: L. Spagnolini, 2007, http://dem.ocracy.org/libero/photobooth/ function polar(img, x0, y0, callback) { /* Returns a new Image based on a polar coordinates filter. * The given callback is a function(distance, angle) that returns new [distance, angle]. */ x0 = img.width / 2 + (x0 || 0); y0 = img.height / 2 + (y0 || 0); var p1 = new Pixels(img); var p2 = new Pixels(img); for (var y1=0; y1 < p1.height; y1++) { for (var x1=0; x1 < p1.width; x1++) { var x = x1 - x0; var y = y1 - y0; var d = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); var a = Math.atan2(y, x); var v = callback(d, a); d=v[0]; a=v[1]; p2.set(x1 + y1 * p1.width, p1.get( Math.round(x0 + Math.cos(a) * d) + Math.round(y0 + Math.sin(a) * d) * p1.width )); } } p2.update(); return p2.image(); } function bump(img, dx, dy, radius, zoom) { /* Returns the image with a dent distortion applied to it. * - dx: horizontal offset (in pixels) of the effect. * - dy: vertical offset (in pixels) of the effect. * - radius: the radius of the effect in pixels. * - zoom: the amount of bulge (0.0-1.0). */ var m1 = radius || 0; var m2 = Math.clamp(zoom || 0, 0, 1); return polar(img, dx, dy, function(d, a) { return [d * geometry.smoothstep(0, m2, d/m1), a]; }); } function dent(img, dx, dy, radius, zoom) { /* Returns the image with a dent distortion applied to it. * - dx: horizontal offset (in pixels) of the effect. * - dy: vertical offset (in pixels) of the effect. * - radius: the radius of the effect in pixels. * - zoom: the amount of pinch (0.0-1.0). */ var m1 = radius || 0; var m2 = Math.clamp(zoom || 0, 0, 1); return polar(img, dx, dy, function(d, a) { return [2 * d - d * geometry.smoothstep(0, m2, d/m1), a]; }); } function pinch(img, dx, dy, zoom) { /* Returns the image with a pinch distortion applied to it. * - dx: horizontal offset (in pixels) of the effect. * - dy: vertical offset (in pixels) of the effect. * - zoom: the amount of bulge or pinch (-1.0-1.0): */ var m1 = geometry.distance(0, 0, img.width, img.height); var m2 = Math.clamp(zoom || 0 * 0.75, -0.75, 0.75); return polar(img, dx, dy, function(d, a) { return [d * Math.pow(m1/d, m2) * (1-m2), a]; }); } function splash(img, dx, dy, radius) { /* Returns the image with a light-tunnel distortion applied to it. * - dx: horizontal offset (in pixels) of the effect. * - dy: vertical offset (in pixels) of the effect. * - radius: the radius of the unaffected area in pixels. */ var m = radius || 0; return polar(img, dx, dy, function(d, a) { return [(d > m)? m : d, a]; }); } function twirl(img, dx, dy, radius, angle) { /* Returns the image with a twirl distortion applied to it. * - dx: horizontal offset (in pixels) of the effect. * - dy: vertical offset (in pixels) of the effect. * - radius: the radius of the effect in pixels. * - angle: the amount of rotation in degrees. */ var m1 = Math.radians(angle || 0); var m2 = radius || 0; return polar(img, dx, dy, function(d, a) { return [d, a + (1 - geometry.smoothstep(-m2, m2, d)) * m1]; }); } var BUMP = "bump"; var DENT = "dent"; var PINCH = "pinch"; var SPLASH = "splash"; var TWIRL = "twirl"; function distort(mode, img, options) { /* Applies the given distortion and returns a new image. * Optional parameters are dx, dy, radius, zoom and angle. */ var o = options || {}; switch (mode) { case BUMP: return bump(img, o.dx, o.dy, o.radius, o.zoom); case DENT: return dent(img, o.dx, o.dy, o.radius, o.zoom); case PINCH: return pinch(img, o.dx, o.dy, o.zoom); case SPLASH: return splash(img, o.dx, o.dy, o.radius); case TWIRL: return twirl(img, o.dx, o.dy, o.radius, o.angle); default: return img; } } /*##################################################################################################*/ /*--- WIDGET ---------------------------------------------------------------------------------------*/ var STRING = "string"; var NUMBER = "number"; var BOOLEAN = "boolean"; var RANGE = "range"; var LIST = "list"; var ARRAY = "array"; var FUNCTION = "function"; // function widget(canvas, variable, type, {parent: null, value: null, min: 0, max: 1, step: 0.01, index: 1, callback: function(e){}}) function widget(canvas, variable, type, options) { /* Creates a widget linked to the given canvas. * The type of the widget can be STRING or NUMBER (field), BOOLEAN (checkbox), * RANGE (slider), LIST (dropdown list), or FUNCTION (button). * The value of the widget can be retrieved as canvas.variables[variable] (or canvas.variables.variable). * Optionally, a default value can be given. * For lists, this is an array and optionally an index of the selected value. * For sliders, you can also set min, max and step. * For functions, an optional callback(event){} must be given. * To get at the current canvas from inside the callback, use event.target.canvas. * Use event.target.canvas.focus() if drawing occurs in the callback. */ var v = variable; var o = options || {}; if (canvas.variables[v] === undefined) { var parent = (o && o.parent)? o.parent : document.getElementById(canvas.id + "_widgets"); if (!parent) { // No widget container is given, or exists. // Insert a
after the element. parent = document.createElement("div"); parent.id = (canvas.id + "_widgets"); parent.className = "widgets"; canvas.element.parentNode.insertBefore(parent, canvas.element.nextSibling); } // Create element with id [canvas.id]_[variable]. // Create an onchange() that will set the variable to the value of the widget. // For FUNCTION, it is an onclick() that will call options.callback(e). var id = canvas.id + "_" + v; // Fix callback event and event.target on IE8 and Safari2. // Function does nothing when propagate=false. var cb = function(e, propagate) { if (!e) { e = window.event; } if (!e && canvas.element.dispatchEvent) { e = new Event(); canvas.element.dispatchEvent(e) } if (e && !e.target) { e.target = e.srcElement || document; } if (e && e.target && e.target.nodeType === 3) { e.target = e.target.parentNode; } if (e && e.target && o.callback && propagate != false) { o.callback(e); } } // if (type == STRING || type == TEXT) { var s = ""; var f = function(e,p) { canvas.variables[this.id] = this.value; cb(e,p); }; // } else if (type == NUMBER) { var s = ""; var f = function(e,p) { canvas.variables[this.id] = parseFloat(this.value); cb(e,p); }; // } else if (type == BOOLEAN) { var s = ""; var f = function(e,p) { canvas.variables[this.id] = this.checked; cb(e,p); }; // } else if (type == RANGE) { var s = ""; var f = function(e,p) { canvas.variables[this.id] = parseFloat(this.value); cb(e,p); }; // } else if (type == LIST || type == ARRAY) { var s = ""; var a = o.value || [""]; for (var i=0; i < a.length; i++) { s += ""; } s = ""; f = function(e,p) { canvas.variables[this.id] = this.options[this.selectedIndex].value; cb(e,p); }; // } else if (type == FUNCTION) { var s = ""; var f = function(e,p) { cb(e,p); }; } else { throw "Variable type can be STRING, NUMBER, BOOLEAN, RANGE, LIST or FUNCTION, not '"+type+"'"; } // Wrap the widget in a . // Prepend a (except for buttons). // Attach the onchange() event. // Append to parent container. // Append to Canvas._widgets. var e = document.createElement("span"); e.innerHTML = "" + ((type == FUNCTION)? " " : v.replace("_"," ")) + "" + s; e.className = "widget" e.lastChild.canvas = canvas; if (type != FUNCTION) { attachEvent(e.lastChild, "change", f); e.lastChild.change(null, false); } else { attachEvent(e.lastChild, "click", f); } parent.appendChild(e); canvas._widgets.push({ "name": variable, "type": type, "element": e.lastChild }); } } /*##################################################################################################*/ /*---