UNPKG

cupiditatea

Version:

A two-dimensional drawing api meant for modern browsers.

708 lines (501 loc) 18.6 kB
(function(Two) { /** * Constants */ var multiplyMatrix = Two.Matrix.Multiply, mod = Two.Utils.mod, identity = [1, 0, 0, 0, 1, 0, 0, 0, 1], transformation = new Two.Array(9), getRatio = Two.Utils.getRatio, toFixed = Two.Utils.toFixed; var webgl = { canvas: document.createElement('canvas'), uv: new Two.Array([ 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1 ]), group: { renderChild: function(child) { webgl[child._renderer.type].render.call(child, this.gl, this.program); }, render: function(gl, program) { this._update(); var parent = this.parent; var flagParentMatrix = (parent._matrix && parent._matrix.manual) || parent._flagMatrix; var flagMatrix = this._matrix.manual || this._flagMatrix; if (flagParentMatrix || flagMatrix) { if (!this._renderer.matrix) { this._renderer.matrix = new Two.Array(9); } // Reduce amount of object / array creation / deletion this._matrix.toArray(true, transformation); multiplyMatrix(transformation, parent._renderer.matrix, this._renderer.matrix); this._renderer.scale = this._scale * parent._renderer.scale; if (flagParentMatrix) { this._flagMatrix = true; } } if (this._mask) { gl.enable(gl.STENCIL_TEST); gl.stencilFunc(gl.ALWAYS, 1, 1); gl.colorMask(false, false, false, true); gl.stencilOp(gl.KEEP, gl.KEEP, gl.INCR); webgl[this._mask._renderer.type].render.call(this._mask, gl, program, this); gl.colorMask(true, true, true, true); gl.stencilFunc(gl.NOTEQUAL, 0, 1); gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); } this._flagOpacity = parent._flagOpacity || this._flagOpacity; this._renderer.opacity = this._opacity * (parent && parent._renderer ? parent._renderer.opacity : 1); _.each(this.children, webgl.group.renderChild, { gl: gl, program: program }); if (this._mask) { gl.colorMask(false, false, false, false); gl.stencilOp(gl.KEEP, gl.KEEP, gl.DECR); webgl[this._mask._renderer.type].render.call(this._mask, gl, program, this); gl.colorMask(true, true, true, true); gl.stencilFunc(gl.NOTEQUAL, 0, 1); gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); gl.disable(gl.STENCIL_TEST); } return this.flagReset(); } }, polygon: { render: function(gl, program, forcedParent) { if (!this._visible || !this._opacity) { return this; } // Calculate what changed var parent = this.parent; var flagParentMatrix = parent._matrix.manual || parent._flagMatrix; var flagMatrix = this._matrix.manual || this._flagMatrix; var flagTexture = this._flagVertices || this._flagFill || this._flagStroke || this._flagLinewidth || this._flagOpacity || parent._flagOpacity || this._flagVisible || this._flagCap || this._flagJoin || this._flagMiter || this._flagScale; this._update(); if (flagParentMatrix || flagMatrix) { if (!this._renderer.matrix) { this._renderer.matrix = new Two.Array(9); } // Reduce amount of object / array creation / deletion this._matrix.toArray(true, transformation); multiplyMatrix(transformation, parent._renderer.matrix, this._renderer.matrix); this._renderer.scale = this._scale * parent._renderer.scale; } if (flagTexture) { if (!this._renderer.rect) { this._renderer.rect = {}; } if (!this._renderer.triangles) { this._renderer.triangles = new Two.Array(12); } this._renderer.opacity = this._opacity * parent._renderer.opacity; webgl.getBoundingClientRect(this._vertices, this._linewidth, this._renderer.rect); webgl.getTriangles(this._renderer.rect, this._renderer.triangles); webgl.updateBuffer(gl, this, program); webgl.updateTexture(gl, this); } // if (this._mask) { // webgl[this._mask._renderer.type].render.call(mask, gl, program, this); // } if (this._clip && !forcedParent) { return; } // Draw Texture gl.bindBuffer(gl.ARRAY_BUFFER, this._renderer.textureCoordsBuffer); gl.vertexAttribPointer(program.textureCoords, 2, gl.FLOAT, false, 0, 0); gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture); // Draw Rect gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix); gl.bindBuffer(gl.ARRAY_BUFFER, this._renderer.buffer); gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLES, 0, 6); return this.flagReset(); } }, /** * Returns the rect of a set of verts. Typically takes vertices that are * "centered" around 0 and returns them to be anchored upper-left. */ getBoundingClientRect: function(vertices, border, rect) { var left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity, width, height; vertices.forEach(function(v) { var x = v.x, y = v.y, controls = v.controls; var a, b, c, d, cl, cr; top = Math.min(y, top); left = Math.min(x, left); right = Math.max(x, right); bottom = Math.max(y, bottom); if (!v.controls) { return; } cl = controls.left; cr = controls.right; if (!cl || !cr) { return; } a = v._relative ? cl.x + x : cl.x; b = v._relative ? cl.y + y : cl.y; c = v._relative ? cr.x + x : cr.x; d = v._relative ? cr.y + y : cr.y; if (!a || !b || !c || !d) { return; } top = Math.min(b, d, top); left = Math.min(a, c, left); right = Math.max(a, c, right); bottom = Math.max(b, d, bottom); }); // Expand borders if (_.isNumber(border)) { top -= border; left -= border; right += border; bottom += border; } width = right - left; height = bottom - top; rect.top = top; rect.left = left; rect.right = right; rect.bottom = bottom; rect.width = width; rect.height = height; if (!rect.centroid) { rect.centroid = {}; } rect.centroid.x = - left; rect.centroid.y = - top; }, getTriangles: function(rect, triangles) { var top = rect.top, left = rect.left, right = rect.right, bottom = rect.bottom; // First Triangle triangles[0] = left; triangles[1] = top; triangles[2] = right; triangles[3] = top; triangles[4] = left; triangles[5] = bottom; // Second Triangle triangles[6] = left; triangles[7] = bottom; triangles[8] = right; triangles[9] = top; triangles[10] = right; triangles[11] = bottom; }, updateCanvas: function(elem) { var commands = elem._vertices; var canvas = this.canvas; var ctx = this.ctx; // Styles var scale = elem._renderer.scale; var stroke = elem._stroke; var linewidth = elem._linewidth * scale; var fill = elem._fill; var opacity = elem._renderer.opacity || elem._opacity; var cap = elem._cap; var join = elem._join; var miter = elem._miter; var closed = elem._closed; var length = commands.length; var last = length - 1; canvas.width = Math.max(Math.ceil(elem._renderer.rect.width * scale), 1); canvas.height = Math.max(Math.ceil(elem._renderer.rect.height * scale), 1); var centroid = elem._renderer.rect.centroid; var cx = centroid.x * scale; var cy = centroid.y * scale; ctx.clearRect(0, 0, canvas.width, canvas.height); if (fill) { ctx.fillStyle = fill; } if (stroke) { ctx.strokeStyle = stroke; } if (linewidth) { ctx.lineWidth = linewidth; } if (miter) { ctx.miterLimit = miter; } if (join) { ctx.lineJoin = join; } if (cap) { ctx.lineCap = cap; } if (_.isNumber(opacity)) { ctx.globalAlpha = opacity; } var d; ctx.beginPath(); commands.forEach(function(b, i) { var next, prev, a, c, ux, uy, vx, vy, ar, bl, br, cl, x, y; x = toFixed(b.x * scale + cx); y = toFixed(b.y * scale + cy); switch (b._command) { case Two.Commands.close: ctx.closePath(); break; case Two.Commands.curve: prev = closed ? mod(i - 1, length) : Math.max(i - 1, 0); next = closed ? mod(i + 1, length) : Math.min(i + 1, last); a = commands[prev]; c = commands[next]; ar = (a.controls && a.controls.right) || a; bl = (b.controls && b.controls.left) || b; if (a._relative) { vx = toFixed((ar.x + a.x) * scale + cx); vy = toFixed((ar.y + a.y) * scale + cy); } else { vx = toFixed(ar.x * scale + cx); vy = toFixed(ar.y * scale + cy); } if (b._relative) { ux = toFixed((bl.x + b.x) * scale + cx); uy = toFixed((bl.y + b.y) * scale + cy); } else { ux = toFixed(bl.x * scale + cx); uy = toFixed(bl.y * scale + cy); } ctx.bezierCurveTo(vx, vy, ux, uy, x, y); if (i >= last && closed) { c = d; br = (b.controls && b.controls.right) || b; cl = (c.controls && c.controls.left) || c; if (b._relative) { vx = toFixed((br.x + b.x) * scale + cx); vy = toFixed((br.y + b.y) * scale + cy); } else { vx = toFixed(br.x * scale + cx); vy = toFixed(br.y * scale + cy); } if (c._relative) { ux = toFixed((cl.x + c.x) * scale + cx); uy = toFixed((cl.y + c.y) * scale + cy); } else { ux = toFixed(cl.x * scale + cx); uy = toFixed(cl.y * scale + cy); } x = toFixed(c.x * scale + cx); y = toFixed(c.y * scale + cy); ctx.bezierCurveTo(vx, vy, ux, uy, x, y); } break; case Two.Commands.line: ctx.lineTo(x, y); break; case Two.Commands.move: d = b; ctx.moveTo(x, y); break; } }); // Loose ends if (closed) { ctx.closePath(); } ctx.fill(); ctx.stroke(); }, updateTexture: function(gl, elem) { this.updateCanvas(elem); if (elem._renderer.texture) { gl.deleteTexture(elem._renderer.texture); } gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.textureCoordsBuffer); elem._renderer.texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, elem._renderer.texture); // Set the parameters so we can render any size image. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); if (this.canvas.width <= 0 || this.canvas.height <= 0) { return; } // Upload the image into the texture. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.canvas); }, updateBuffer: function(gl, elem, program) { if (_.isObject(elem._renderer.buffer)) { gl.deleteBuffer(elem._renderer.buffer); } elem._renderer.buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.buffer); gl.enableVertexAttribArray(program.position); gl.bufferData(gl.ARRAY_BUFFER, elem._renderer.triangles, gl.STATIC_DRAW); if (_.isObject(elem._renderer.textureCoordsBuffer)) { gl.deleteBuffer(elem._renderer.textureCoordsBuffer); } elem._renderer.textureCoordsBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.textureCoordsBuffer); gl.enableVertexAttribArray(program.textureCoords); gl.bufferData(gl.ARRAY_BUFFER, this.uv, gl.STATIC_DRAW); }, program: { create: function(gl, shaders) { var program, linked, error; program = gl.createProgram(); _.each(shaders, function(s) { gl.attachShader(program, s); }); gl.linkProgram(program); linked = gl.getProgramParameter(program, gl.LINK_STATUS); if (!linked) { error = gl.getProgramInfoLog(program); gl.deleteProgram(program); throw new Two.Utils.Error('unable to link program: ' + error); } return program; } }, shaders: { create: function(gl, source, type) { var shader, compiled, error; shader = gl.createShader(gl[type]); gl.shaderSource(shader, source); gl.compileShader(shader); compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); if (!compiled) { error = gl.getShaderInfoLog(shader); gl.deleteShader(shader); throw new Two.Utils.Error('unable to compile shader ' + shader + ': ' + error); } return shader; }, types: { vertex: 'VERTEX_SHADER', fragment: 'FRAGMENT_SHADER' }, vertex: [ 'attribute vec2 a_position;', 'attribute vec2 a_textureCoords;', '', 'uniform mat3 u_matrix;', 'uniform vec2 u_resolution;', '', 'varying vec2 v_textureCoords;', '', 'void main() {', ' vec2 projected = (u_matrix * vec3(a_position, 1.0)).xy;', ' vec2 normal = projected / u_resolution;', ' vec2 clipspace = (normal * 2.0) - 1.0;', '', ' gl_Position = vec4(clipspace * vec2(1.0, -1.0), 0.0, 1.0);', ' v_textureCoords = a_textureCoords;', '}' ].join('\n'), fragment: [ 'precision mediump float;', '', 'uniform sampler2D u_image;', 'varying vec2 v_textureCoords;', '', 'void main() {', ' gl_FragColor = texture2D(u_image, v_textureCoords);', '}' ].join('\n') } }; webgl.ctx = webgl.canvas.getContext('2d'); var Renderer = Two[Two.Types.webgl] = function(options) { var params, gl, vs, fs; this.domElement = options.domElement || document.createElement('canvas'); // Everything drawn on the canvas needs to come from the stage. this.scene = new Two.Group(); this.scene.parent = this; this._renderer = { matrix: new Two.Array(identity), scale: 1, opacity: 1 }; this._flagMatrix = true; // http://games.greggman.com/game/webgl-and-alpha/ // http://www.khronos.org/registry/webgl/specs/latest/#5.2 params = _.defaults(options || {}, { antialias: false, alpha: true, premultipliedAlpha: true, stencil: true, preserveDrawingBuffer: true, overdraw: false }); this.overdraw = params.overdraw; gl = this.ctx = this.domElement.getContext('webgl', params) || this.domElement.getContext('experimental-webgl', params); if (!this.ctx) { throw new Two.Utils.Error( 'unable to create a webgl context. Try using another renderer.'); } // Compile Base Shaders to draw in pixel space. vs = webgl.shaders.create( gl, webgl.shaders.vertex, webgl.shaders.types.vertex); fs = webgl.shaders.create( gl, webgl.shaders.fragment, webgl.shaders.types.fragment); this.program = webgl.program.create(gl, [vs, fs]); gl.useProgram(this.program); // Create and bind the drawing buffer // look up where the vertex data needs to go. this.program.position = gl.getAttribLocation(this.program, 'a_position'); this.program.matrix = gl.getUniformLocation(this.program, 'u_matrix'); this.program.textureCoords = gl.getAttribLocation(this.program, 'a_textureCoords'); // Copied from Three.js WebGLRenderer gl.disable(gl.DEPTH_TEST); // Setup some initial statements of the gl context gl.enable(gl.BLEND); // https://code.google.com/p/chromium/issues/detail?id=316393 // gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, gl.TRUE); gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA ); }; _.extend(Renderer.prototype, Backbone.Events, { setSize: function(width, height, ratio) { this.width = width; this.height = height; this.ratio = _.isUndefined(ratio) ? getRatio(this.ctx) : ratio; this.domElement.width = width * this.ratio; this.domElement.height = height * this.ratio; _.extend(this.domElement.style, { width: width + 'px', height: height + 'px' }); width *= this.ratio; height *= this.ratio; // Set for this.stage parent scaling to account for HDPI this._renderer.matrix[0] = this._renderer.matrix[4] = this._renderer.scale = this.ratio; this._flagMatrix = true; this.ctx.viewport(0, 0, width, height); var resolutionLocation = this.ctx.getUniformLocation( this.program, 'u_resolution'); this.ctx.uniform2f(resolutionLocation, width, height); return this; }, render: function() { var gl = this.ctx; if (!this.overdraw) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } webgl.group.render.call(this.scene, gl, this.program); this._flagMatrix = false; return this; } }); })(Two);