UNPKG

snapsvg

Version:
761 lines (701 loc) 19.7 kB
//============================================================ // // Copyright (C) 2013 Matthew Wagerfield // // Twitter: https://twitter.com/mwagerfield // // Permission is hereby granted, free of charge, to any // person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the // Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, // sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice // shall be included in all copies or substantial portions // of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY // OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT // LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO // EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN // AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE // OR OTHER DEALINGS IN THE SOFTWARE. // //============================================================ /** * Defines the Flat Surface Shader namespace for all the awesomeness to exist upon. * @author Matthew Wagerfield */ FSS = { FRONT : 0, BACK : 1, DOUBLE : 2, SVGNS : 'http://www.w3.org/2000/svg' }; /** * @class Array * @author Matthew Wagerfield */ FSS.Array = typeof Float32Array === 'function' ? Float32Array : Array; /** * @class Utils * @author Matthew Wagerfield */ FSS.Utils = { isNumber: function(value) { return !isNaN(parseFloat(value)) && isFinite(value); } }; /** * Request Animation Frame Polyfill. * @author Paul Irish * @see https://gist.github.com/paulirish/1579671 */ (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) { window.requestAnimationFrame = function(callback, element) { var currentTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currentTime - lastTime)); var id = window.setTimeout(function() { callback(currentTime + timeToCall); }, timeToCall); lastTime = currentTime + timeToCall; return id; }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } }()); /** * @object Math Augmentation * @author Matthew Wagerfield */ Math.PIM2 = Math.PI*2; Math.PID2 = Math.PI/2; Math.randomInRange = function(min, max) { return min + (max - min) * Math.random(); }; Math.clamp = function(value, min, max) { value = Math.max(value, min); value = Math.min(value, max); return value; }; /** * @object Vector3 * @author Matthew Wagerfield */ FSS.Vector3 = { create: function(x, y, z) { var vector = new FSS.Array(3); this.set(vector, x, y, z); return vector; }, clone: function(a) { var vector = this.create(); this.copy(vector, a); return vector; }, set: function(target, x, y, z) { target[0] = x || 0; target[1] = y || 0; target[2] = z || 0; return this; }, setX: function(target, x) { target[0] = x || 0; return this; }, setY: function(target, y) { target[1] = y || 0; return this; }, setZ: function(target, z) { target[2] = z || 0; return this; }, copy: function(target, a) { target[0] = a[0]; target[1] = a[1]; target[2] = a[2]; return this; }, add: function(target, a) { target[0] += a[0]; target[1] += a[1]; target[2] += a[2]; return this; }, addVectors: function(target, a, b) { target[0] = a[0] + b[0]; target[1] = a[1] + b[1]; target[2] = a[2] + b[2]; return this; }, addScalar: function(target, s) { target[0] += s; target[1] += s; target[2] += s; return this; }, subtract: function(target, a) { target[0] -= a[0]; target[1] -= a[1]; target[2] -= a[2]; return this; }, subtractVectors: function(target, a, b) { target[0] = a[0] - b[0]; target[1] = a[1] - b[1]; target[2] = a[2] - b[2]; return this; }, subtractScalar: function(target, s) { target[0] -= s; target[1] -= s; target[2] -= s; return this; }, multiply: function(target, a) { target[0] *= a[0]; target[1] *= a[1]; target[2] *= a[2]; return this; }, multiplyVectors: function(target, a, b) { target[0] = a[0] * b[0]; target[1] = a[1] * b[1]; target[2] = a[2] * b[2]; return this; }, multiplyScalar: function(target, s) { target[0] *= s; target[1] *= s; target[2] *= s; return this; }, divide: function(target, a) { target[0] /= a[0]; target[1] /= a[1]; target[2] /= a[2]; return this; }, divideVectors: function(target, a, b) { target[0] = a[0] / b[0]; target[1] = a[1] / b[1]; target[2] = a[2] / b[2]; return this; }, divideScalar: function(target, s) { if (s !== 0) { target[0] /= s; target[1] /= s; target[2] /= s; } else { target[0] = 0; target[1] = 0; target[2] = 0; } return this; }, cross: function(target, a) { var x = target[0]; var y = target[1]; var z = target[2]; target[0] = y*a[2] - z*a[1]; target[1] = z*a[0] - x*a[2]; target[2] = x*a[1] - y*a[0]; return this; }, crossVectors: function(target, a, b) { target[0] = a[1]*b[2] - a[2]*b[1]; target[1] = a[2]*b[0] - a[0]*b[2]; target[2] = a[0]*b[1] - a[1]*b[0]; return this; }, min: function(target, value) { if (target[0] < value) { target[0] = value; } if (target[1] < value) { target[1] = value; } if (target[2] < value) { target[2] = value; } return this; }, max: function(target, value) { if (target[0] > value) { target[0] = value; } if (target[1] > value) { target[1] = value; } if (target[2] > value) { target[2] = value; } return this; }, clamp: function(target, min, max) { this.min(target, min); this.max(target, max); return this; }, limit: function(target, min, max) { var length = this.length(target); if (min !== null && length < min) { this.setLength(target, min); } else if (max !== null && length > max) { this.setLength(target, max); } return this; }, dot: function(a, b) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }, normalise: function(target) { return this.divideScalar(target, this.length(target)); }, negate: function(target) { return this.multiplyScalar(target, -1); }, distanceSquared: function(a, b) { var dx = a[0] - b[0]; var dy = a[1] - b[1]; var dz = a[2] - b[2]; return dx*dx + dy*dy + dz*dz; }, distance: function(a, b) { return Math.sqrt(this.distanceSquared(a, b)); }, lengthSquared: function(a) { return a[0]*a[0] + a[1]*a[1] + a[2]*a[2]; }, length: function(a) { return Math.sqrt(this.lengthSquared(a)); }, setLength: function(target, l) { var length = this.length(target); if (length !== 0 && l !== length) { this.multiplyScalar(target, l / length); } return this; } }; /** * @object Vector4 * @author Matthew Wagerfield */ FSS.Vector4 = { create: function(x, y, z, w) { var vector = new FSS.Array(4); this.set(vector, x, y, z); return vector; }, set: function(target, x, y, z, w) { target[0] = x || 0; target[1] = y || 0; target[2] = z || 0; target[3] = w || 0; return this; }, setX: function(target, x) { target[0] = x || 0; return this; }, setY: function(target, y) { target[1] = y || 0; return this; }, setZ: function(target, z) { target[2] = z || 0; return this; }, setW: function(target, w) { target[3] = w || 0; return this; }, add: function(target, a) { target[0] += a[0]; target[1] += a[1]; target[2] += a[2]; target[3] += a[3]; return this; }, multiplyVectors: function(target, a, b) { target[0] = a[0] * b[0]; target[1] = a[1] * b[1]; target[2] = a[2] * b[2]; target[3] = a[3] * b[3]; return this; }, multiplyScalar: function(target, s) { target[0] *= s; target[1] *= s; target[2] *= s; target[3] *= s; return this; }, min: function(target, value) { if (target[0] < value) { target[0] = value; } if (target[1] < value) { target[1] = value; } if (target[2] < value) { target[2] = value; } if (target[3] < value) { target[3] = value; } return this; }, max: function(target, value) { if (target[0] > value) { target[0] = value; } if (target[1] > value) { target[1] = value; } if (target[2] > value) { target[2] = value; } if (target[3] > value) { target[3] = value; } return this; }, clamp: function(target, min, max) { this.min(target, min); this.max(target, max); return this; } }; /** * @class Color * @author Matthew Wagerfield */ FSS.Color = function(hex, opacity) { this.rgba = FSS.Vector4.create(); this.hex = hex || '#000000'; this.opacity = FSS.Utils.isNumber(opacity) ? opacity : 1; this.set(this.hex, this.opacity); }; FSS.Color.prototype = { set: function(hex, opacity) { hex = hex.replace('#', ''); var size = hex.length / 3; this.rgba[0] = parseInt(hex.substring(size*0, size*1), 16) / 255; this.rgba[1] = parseInt(hex.substring(size*1, size*2), 16) / 255; this.rgba[2] = parseInt(hex.substring(size*2, size*3), 16) / 255; this.rgba[3] = FSS.Utils.isNumber(opacity) ? opacity : this.rgba[3]; return this; }, hexify: function(channel) { var hex = Math.ceil(channel*255).toString(16); if (hex.length === 1) { hex = '0' + hex; } return hex; }, format: function() { var r = this.hexify(this.rgba[0]); var g = this.hexify(this.rgba[1]); var b = this.hexify(this.rgba[2]); this.hex = '#' + r + g + b; return this.hex; } }; /** * @class Object * @author Matthew Wagerfield */ FSS.Object = function() { this.position = FSS.Vector3.create(); }; FSS.Object.prototype = { setPosition: function(x, y, z) { FSS.Vector3.set(this.position, x, y, z); return this; } }; /** * @class Light * @author Matthew Wagerfield */ FSS.Light = function(ambient, diffuse) { FSS.Object.call(this); this.ambient = new FSS.Color(ambient || '#FFFFFF'); this.diffuse = new FSS.Color(diffuse || '#FFFFFF'); this.ray = FSS.Vector3.create(); }; FSS.Light.prototype = Object.create(FSS.Object.prototype); /** * @class Vertex * @author Matthew Wagerfield */ FSS.Vertex = function(x, y, z) { this.position = FSS.Vector3.create(x, y, z); }; FSS.Vertex.prototype = { setPosition: function(x, y, z) { FSS.Vector3.set(this.position, x, y, z); return this; } }; /** * @class Triangle * @author Matthew Wagerfield */ FSS.Triangle = function(a, b, c, s, material) { this.a = a || new FSS.Vertex(); this.b = b || new FSS.Vertex(); this.c = c || new FSS.Vertex(); this.vertices = [this.a, this.b, this.c]; this.u = FSS.Vector3.create(); this.v = FSS.Vector3.create(); this.centroid = FSS.Vector3.create(); this.normal = FSS.Vector3.create(); this.material = material || new FSS.Material(); this.color = new FSS.Color(); this.polygon = s.polygon(); this.polygon.attr({ 'stroke-linejoin': 'round', 'stroke-miterlimit': 1, 'stroke-width': 1 }); this.computeCentroid(); this.computeNormal(); }; FSS.Triangle.prototype = { computeCentroid: function() { this.centroid[0] = this.a.position[0] + this.b.position[0] + this.c.position[0]; this.centroid[1] = this.a.position[1] + this.b.position[1] + this.c.position[1]; this.centroid[2] = this.a.position[2] + this.b.position[2] + this.c.position[2]; FSS.Vector3.divideScalar(this.centroid, 3); return this; }, computeNormal: function() { FSS.Vector3.subtractVectors(this.u, this.b.position, this.a.position); FSS.Vector3.subtractVectors(this.v, this.c.position, this.a.position); FSS.Vector3.crossVectors(this.normal, this.u, this.v); FSS.Vector3.normalise(this.normal); return this; } }; /** * @class Geometry * @author Matthew Wagerfield */ FSS.Geometry = function() { this.vertices = []; this.triangles = []; this.dirty = false; }; FSS.Geometry.prototype = { update: function() { if (this.dirty) { var t,triangle; for (t = this.triangles.length - 1; t >= 0; t--) { triangle = this.triangles[t]; triangle.computeCentroid(); triangle.computeNormal(); } this.dirty = false; } return this; } }; /** * @class Plane * @author Matthew Wagerfield */ FSS.Plane = function(width, height, segments, slices, s, material) { FSS.Geometry.call(this); this.width = width || 100; this.height = height || 100; this.segments = segments || 4; this.slices = slices || 4; this.segmentWidth = this.width / this.segments; this.sliceHeight = this.height / this.slices; // Cache Variables var x, y, v0, v1, v2, v3, vertex, triangle, vertices = [], offsetX = this.width * -0.5, offsetY = this.height * 0.5; // Add Vertices for (x = 0; x <= this.segments; x++) { vertices.push([]); for (y = 0; y <= this.slices; y++) { vertex = new FSS.Vertex(offsetX + x*this.segmentWidth, offsetY - y*this.sliceHeight); vertices[x].push(vertex); this.vertices.push(vertex); } } // Add Triangles for (x = 0; x < this.segments; x++) { for (y = 0; y < this.slices; y++) { v0 = vertices[x+0][y+0]; v1 = vertices[x+0][y+1]; v2 = vertices[x+1][y+0]; v3 = vertices[x+1][y+1]; t0 = new FSS.Triangle(v0, v1, v2, s, material); t1 = new FSS.Triangle(v2, v1, v3, s, material); this.triangles.push(t0, t1); } } }; FSS.Plane.prototype = Object.create(FSS.Geometry.prototype); /** * @class Material * @author Matthew Wagerfield */ FSS.Material = function(ambient, diffuse) { this.ambient = new FSS.Color(ambient || '#444444'); this.diffuse = new FSS.Color(diffuse || '#FFFFFF'); this.slave = new FSS.Color(); }; /** * @class Mesh * @author Matthew Wagerfield */ FSS.Mesh = function(geometry, material) { FSS.Object.call(this); this.geometry = geometry || new FSS.Geometry(); this.material = material || new FSS.Material(); this.side = FSS.FRONT; this.visible = true; }; FSS.Mesh.prototype = Object.create(FSS.Object.prototype); FSS.Mesh.prototype.update = function(lights, calculate) { var t,triangle, l,light, illuminance; // Update Geometry this.geometry.update(); // Calculate the triangle colors if (calculate) { // Iterate through Triangles for (t = this.geometry.triangles.length - 1; t >= 0; t--) { triangle = this.geometry.triangles[t]; // Reset Triangle Color FSS.Vector4.set(triangle.color.rgba); // Iterate through Lights for (l = lights.length - 1; l >= 0; l--) { light = lights[l]; // Calculate Illuminance FSS.Vector3.subtractVectors(light.ray, light.position, triangle.centroid); FSS.Vector3.normalise(light.ray); illuminance = FSS.Vector3.dot(triangle.normal, light.ray); if (this.side === FSS.FRONT) { illuminance = Math.max(illuminance, 0); } else if (this.side === FSS.BACK) { illuminance = Math.abs(Math.min(illuminance, 0)); } else if (this.side === FSS.DOUBLE) { illuminance = Math.max(Math.abs(illuminance), 0); } // Calculate Ambient Light FSS.Vector4.multiplyVectors(triangle.material.slave.rgba, triangle.material.ambient.rgba, light.ambient.rgba); FSS.Vector4.add(triangle.color.rgba, triangle.material.slave.rgba); // Calculate Diffuse Light FSS.Vector4.multiplyVectors(triangle.material.slave.rgba, triangle.material.diffuse.rgba, light.diffuse.rgba); FSS.Vector4.multiplyScalar(triangle.material.slave.rgba, illuminance); FSS.Vector4.add(triangle.color.rgba, triangle.material.slave.rgba); } // Clamp & Format Color FSS.Vector4.clamp(triangle.color.rgba, 0, 1); } } return this; }; /** * @class Scene * @author Matthew Wagerfield */ FSS.Scene = function() { this.meshes = []; this.lights = []; }; FSS.Scene.prototype = { add: function(object) { if (object instanceof FSS.Mesh && !~this.meshes.indexOf(object)) { this.meshes.push(object); } else if (object instanceof FSS.Light && !~this.lights.indexOf(object)) { this.lights.push(object); } return this; }, remove: function(object) { if (object instanceof FSS.Mesh && ~this.meshes.indexOf(object)) { this.meshes.splice(this.meshes.indexOf(object), 1); } else if (object instanceof FSS.Light && ~this.lights.indexOf(object)) { this.lights.splice(this.lights.indexOf(object), 1); } return this; } }; /** * @class Renderer * @author Matthew Wagerfield */ FSS.Renderer = function() { this.width = 0; this.height = 0; this.halfWidth = 0; this.halfHeight = 0; }; FSS.Renderer.prototype = { setSize: function(width, height) { if (this.width === width && this.height === height) return; this.width = width; this.height = height; this.halfWidth = this.width * 0.5; this.halfHeight = this.height * 0.5; return this; }, clear: function() { return this; }, render: function(scene) { return this; } }; /** * @class SVG Renderer * @author Matthew Wagerfield */ FSS.SVGRenderer = function(s) { FSS.Renderer.call(this); this.element = s.g(); }; FSS.SVGRenderer.prototype = Object.create(FSS.Renderer.prototype); FSS.SVGRenderer.prototype.setSize = function(width, height) { FSS.Renderer.prototype.setSize.call(this, width, height); return this; }; FSS.SVGRenderer.prototype.clear = function() { FSS.Renderer.prototype.clear.call(this); for (var i = this.element.childNodes.length - 1; i >= 0; i--) { this.element.removeChild(this.element.childNodes[i]); } return this; }; FSS.SVGRenderer.prototype.render = function(scene) { FSS.Renderer.prototype.render.call(this, scene); var m,mesh, t,triangle, points, style; // Update Meshes for (m = scene.meshes.length - 1; m >= 0; m--) { mesh = scene.meshes[m]; if (mesh.visible) { mesh.update(scene.lights, true); // Render Triangles for (t = mesh.geometry.triangles.length - 1; t >= 0; t--) { triangle = mesh.geometry.triangles[t]; if (triangle.polygon.parent() !== this.element) { this.element.append(triangle.polygon); } points = this.formatPoint(triangle.a)+' '; points += this.formatPoint(triangle.b)+' '; points += this.formatPoint(triangle.c); style = this.formatStyle(triangle.color.format()); triangle.polygon.attr({ points: points, style: style }); } } } return this; }; FSS.SVGRenderer.prototype.formatPoint = function(vertex) { return (this.halfWidth+vertex.position[0])+','+(this.halfHeight-vertex.position[1]); }; FSS.SVGRenderer.prototype.formatStyle = function(color) { var style = 'fill:'+color+';'; style += 'stroke:'+color+';'; return style; };