@sky-foundry/two.js
Version:
A renderer agnostic two-dimensional drawing api for the web.
1,272 lines (918 loc) • 36.7 kB
JavaScript
(function(Two) {
/**
* Constants
*/
var root = Two.root,
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,
getComputedMatrix = Two.Utils.getComputedMatrix,
toFixed = Two.Utils.toFixed,
CanvasUtils = Two[Two.Types.canvas].Utils,
_ = Two.Utils;
var webgl = {
isHidden: /(none|transparent)/i,
canvas: (root.document ? root.document.createElement('canvas') : { getContext: _.identity }),
alignments: {
left: 'start',
middle: 'center',
right: 'end'
},
matrix: new Two.Matrix(),
uv: new Two.Array([
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1
]),
group: {
removeChild: function(child, gl) {
if (child.children) {
for (var i = 0; i < child.children.length; i++) {
webgl.group.removeChild(child.children[i], gl);
}
return;
}
// Deallocate texture to free up gl memory.
gl.deleteTexture(child._renderer.texture);
delete child._renderer.texture;
},
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);
if (this._flagSubtractions) {
for (var i = 0; i < this.subtractions.length; i++) {
webgl.group.removeChild(this.subtractions[i], gl);
}
}
for (var i = 0; i < this.children.length; i++) {
var child = this.children[i];
webgl[child._renderer.type].render.call(child, gl, program);
}
this.children.forEach(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();
}
},
path: {
updateCanvas: function(elem) {
var next, prev, a, c, ux, uy, vx, vy, ar, bl, br, cl, x, y;
var isOffset;
var commands = elem._renderer.vertices;
var canvas = this.canvas;
var ctx = this.ctx;
// Styles
var scale = elem._renderer.scale;
var stroke = elem._stroke;
var linewidth = elem._linewidth;
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 dashes = elem.dashes;
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;
var cy = centroid.y;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (fill) {
if (_.isString(fill)) {
ctx.fillStyle = fill;
} else {
webgl[fill._renderer.type].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (_.isString(stroke)) {
ctx.strokeStyle = stroke;
} else {
webgl[stroke._renderer.type].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
}
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;
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(dashes);
}
var d;
ctx.save();
ctx.scale(scale, scale);
ctx.translate(cx, cy);
ctx.beginPath();
for (var i = 0; i < commands.length; i++) {
var b = commands[i];
x = toFixed(b.x);
y = toFixed(b.y);
switch (b.command) {
case Two.Commands.close:
ctx.closePath();
break;
case Two.Commands.arc:
var rx = b.rx;
var ry = b.ry;
var xAxisRotation = b.xAxisRotation;
var largeArcFlag = b.largeArcFlag;
var sweepFlag = b.sweepFlag;
prev = closed ? mod(i - 1, length) : max(i - 1, 0);
a = commands[prev];
var ax = toFixed(a.x);
var ay = toFixed(a.y);
CanvasUtils.renderSvgArcCommand(ctx, ax, ay, rx, ry, largeArcFlag, sweepFlag, xAxisRotation, x, y);
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) || Two.Vector.zero;
bl = (b.controls && b.controls.left) || Two.Vector.zero;
if (a._relative) {
vx = toFixed((ar.x + a.x));
vy = toFixed((ar.y + a.y));
} else {
vx = toFixed(ar.x);
vy = toFixed(ar.y);
}
if (b._relative) {
ux = toFixed((bl.x + b.x));
uy = toFixed((bl.y + b.y));
} else {
ux = toFixed(bl.x);
uy = toFixed(bl.y);
}
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
if (i >= last && closed) {
c = d;
br = (b.controls && b.controls.right) || Two.Vector.zero;
cl = (c.controls && c.controls.left) || Two.Vector.zero;
if (b._relative) {
vx = toFixed((br.x + b.x));
vy = toFixed((br.y + b.y));
} else {
vx = toFixed(br.x);
vy = toFixed(br.y);
}
if (c._relative) {
ux = toFixed((cl.x + c.x));
uy = toFixed((cl.y + c.y));
} else {
ux = toFixed(cl.x);
uy = toFixed(cl.y);
}
x = toFixed(c.x);
y = toFixed(c.y);
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();
}
if (!webgl.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset
if (isOffset) {
ctx.save();
ctx.translate(
- fill._renderer.offset.x, - fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!webgl.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(
- stroke._renderer.offset.x, - stroke._renderer.offset.y);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
ctx.restore();
},
/**
* 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;
},
render: function(gl, program, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
this._update();
// 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._fill instanceof Two.LinearGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagEndPoints))
|| (this._fill instanceof Two.RadialGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagRadius || this._fill._flagCenter || this._fill._flagFocal))
|| (this._fill instanceof Two.Texture && (this._fill._flagLoaded && this._fill.loaded || this._fill._flagImage || this._fill._flagVideo || this._fill._flagRepeat || this._fill._flagOffset || this._fill._flagScale))
|| (this._stroke instanceof Two.LinearGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagEndPoints))
|| (this._stroke instanceof Two.RadialGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagRadius || this._stroke._flagCenter || this._stroke._flagFocal))
|| (this._stroke instanceof Two.Texture && (this._stroke._flagLoaded && this._stroke.loaded || this._stroke._flagImage || this._stroke._flagVideo || this._stroke._flagRepeat || this._stroke._flagOffset || this._fill._flagScale))
|| this._flagStroke || this._flagLinewidth || this._flagOpacity
|| parent._flagOpacity || this._flagVisible || this._flagCap
|| this._flagJoin || this._flagMiter || this._flagScale
|| (this.dashes && this.dashes.length > 0)
|| !this._renderer.texture;
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.path.getBoundingClientRect(this._renderer.vertices, this._linewidth, this._renderer.rect);
webgl.getTriangles(this._renderer.rect, this._renderer.triangles);
webgl.updateBuffer.call(webgl, gl, this, program);
webgl.updateTexture.call(webgl, gl, this);
} else {
// We still need to update child Two elements on the fill and
// stroke properties.
if (!_.isString(this._fill)) {
this._fill._update();
}
if (!_.isString(this._stroke)) {
this._stroke._update();
}
}
// 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();
}
},
text: {
updateCanvas: function(elem) {
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 dashes = elem.dashes;
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;
var cy = centroid.y;
var a, b, c, d, e, sx, sy;
var isOffset = fill._renderer && fill._renderer.offset
&& stroke._renderer && stroke._renderer.offset;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!isOffset) {
ctx.font = [elem._style, elem._weight, elem._size + 'px/' +
elem._leading + 'px', elem._family].join(' ');
}
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Styles
if (fill) {
if (_.isString(fill)) {
ctx.fillStyle = fill;
} else {
webgl[fill._renderer.type].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (_.isString(stroke)) {
ctx.strokeStyle = stroke;
} else {
webgl[stroke._renderer.type].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
}
if (linewidth) {
ctx.lineWidth = linewidth;
}
if (_.isNumber(opacity)) {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(dashes);
}
ctx.save();
ctx.scale(scale, scale);
ctx.translate(cx, cy);
if (!webgl.isHidden.test(fill)) {
if (fill._renderer && fill._renderer.offset) {
sx = toFixed(fill._renderer.scale.x);
sy = toFixed(fill._renderer.scale.y);
ctx.save();
ctx.translate( - toFixed(fill._renderer.offset.x),
- toFixed(fill._renderer.offset.y));
ctx.scale(sx, sy);
a = elem._size / fill._renderer.scale.y;
b = elem._leading / fill._renderer.scale.y;
ctx.font = [elem._style, elem._weight, toFixed(a) + 'px/',
toFixed(b) + 'px', elem._family].join(' ');
c = fill._renderer.offset.x / fill._renderer.scale.x;
d = fill._renderer.offset.y / fill._renderer.scale.y;
ctx.fillText(elem.value, toFixed(c), toFixed(d));
ctx.restore();
} else {
ctx.fillText(elem.value, 0, 0);
}
}
if (!webgl.isHidden.test(stroke)) {
if (stroke._renderer && stroke._renderer.offset) {
sx = toFixed(stroke._renderer.scale.x);
sy = toFixed(stroke._renderer.scale.y);
ctx.save();
ctx.translate(- toFixed(stroke._renderer.offset.x),
- toFixed(stroke._renderer.offset.y));
ctx.scale(sx, sy);
a = elem._size / stroke._renderer.scale.y;
b = elem._leading / stroke._renderer.scale.y;
ctx.font = [elem._style, elem._weight, toFixed(a) + 'px/',
toFixed(b) + 'px', elem._family].join(' ');
c = stroke._renderer.offset.x / stroke._renderer.scale.x;
d = stroke._renderer.offset.y / stroke._renderer.scale.y;
e = linewidth / stroke._renderer.scale.x;
ctx.lineWidth = toFixed(e);
ctx.strokeText(elem.value, toFixed(c), toFixed(d));
ctx.restore();
} else {
ctx.strokeText(elem.value, 0, 0);
}
}
ctx.restore();
},
getBoundingClientRect: function(elem, rect) {
var ctx = webgl.ctx;
ctx.font = [elem._style, elem._weight, elem._size + 'px/' +
elem._leading + 'px', elem._family].join(' ');
ctx.textAlign = 'center';
ctx.textBaseline = elem._baseline;
// TODO: Estimate this better
var width = ctx.measureText(elem._value).width;
var height = Math.max(elem._size || elem._leading);
if (this._linewidth && !webgl.isHidden.test(this._stroke)) {
// width += this._linewidth; // TODO: Not sure if the `measure` calcs this.
height += this._linewidth;
}
var w = width / 2;
var h = height / 2;
switch (webgl.alignments[elem._alignment] || elem._alignment) {
case webgl.alignments.left:
rect.left = 0;
rect.right = width;
break;
case webgl.alignments.right:
rect.left = - width;
rect.right = 0;
break;
default:
rect.left = - w;
rect.right = w;
}
// TODO: Gradients aren't inherited...
switch (elem._baseline) {
case 'bottom':
rect.top = - height;
rect.bottom = 0;
break;
case 'top':
rect.top = 0;
rect.bottom = height;
break;
default:
rect.top = - h;
rect.bottom = h;
}
rect.width = width;
rect.height = height;
if (!rect.centroid) {
rect.centroid = {};
}
// TODO:
rect.centroid.x = w;
rect.centroid.y = h;
},
render: function(gl, program, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
this._update();
// 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._fill instanceof Two.LinearGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagEndPoints))
|| (this._fill instanceof Two.RadialGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagRadius || this._fill._flagCenter || this._fill._flagFocal))
|| (this._fill instanceof Two.Texture && (this._fill._flagLoaded && this._fill.loaded || this._fill._flagImage || this._fill._flagVideo || this._fill._flagRepeat || this._fill._flagOffset || this._fill._flagScale))
|| (this._stroke instanceof Two.LinearGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagEndPoints))
|| (this._stroke instanceof Two.RadialGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagRadius || this._stroke._flagCenter || this._stroke._flagFocal))
|| (this._stroke instanceof Two.Texture && (this._stroke._flagLoaded && this._stroke.loaded || this._stroke._flagImage || this._stroke._flagVideo || this._stroke._flagRepeat || this._stroke._flagOffset || this._fill._flagScale))
|| this._flagStroke || this._flagLinewidth || this._flagOpacity
|| parent._flagOpacity || this._flagVisible || this._flagScale
|| this._flagValue || this._flagFamily || this._flagSize
|| this._flagLeading || this._flagAlignment || this._flagBaseline
|| this._flagStyle || this._flagWeight || this._flagDecoration
|| (this.dashes && this.dashes.length > 0)
|| !this._renderer.texture;
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.text.getBoundingClientRect(this, this._renderer.rect);
webgl.getTriangles(this._renderer.rect, this._renderer.triangles);
webgl.updateBuffer.call(webgl, gl, this, program);
webgl.updateTexture.call(webgl, gl, this);
} else {
// We still need to update child Two elements on the fill and
// stroke properties.
if (!_.isString(this._fill)) {
this._fill._update();
}
if (!_.isString(this._stroke)) {
this._stroke._update();
}
}
// 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();
}
},
'linear-gradient': {
render: function(ctx, elem) {
if (!ctx.canvas.getContext('2d')) {
return;
}
this._update();
if (!this._renderer.effect || this._flagEndPoints || this._flagStops) {
this._renderer.effect = ctx.createLinearGradient(
this.left._x, this.left._y,
this.right._x, this.right._y
);
for (var i = 0; i < this.stops.length; i++) {
var stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
return this.flagReset();
}
},
'radial-gradient': {
render: function(ctx, elem) {
if (!ctx.canvas.getContext('2d')) {
return;
}
this._update();
if (!this._renderer.effect || this._flagCenter || this._flagFocal
|| this._flagRadius || this._flagStops) {
this._renderer.effect = ctx.createRadialGradient(
this.center._x, this.center._y, 0,
this.focal._x, this.focal._y, this._radius
);
for (var i = 0; i < this.stops.length; i++) {
var stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
return this.flagReset();
}
},
texture: {
render: function(ctx, elem) {
if (!ctx.canvas.getContext('2d')) {
return;
}
this._update();
var image = this.image;
var repeat;
if (((this._flagLoaded || this._flagImage || this._flagVideo || this._flagRepeat) && this.loaded)) {
this._renderer.effect = ctx.createPattern(image, this._repeat);
} else if (!this._renderer.effect) {
return this.flagReset();
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
if (!(this._renderer.offset instanceof Two.Vector)) {
this._renderer.offset = new Two.Vector();
}
this._renderer.offset.x = - this._offset.x;
this._renderer.offset.y = - this._offset.y;
if (image) {
this._renderer.offset.x += image.width / 2;
this._renderer.offset.y += image.height / 2;
if (this._scale instanceof Two.Vector) {
this._renderer.offset.x *= this._scale.x;
this._renderer.offset.y *= this._scale.y;
} else {
this._renderer.offset.x *= this._scale;
this._renderer.offset.y *= this._scale;
}
}
}
if (this._flagScale || this._flagLoaded) {
if (!(this._renderer.scale instanceof Two.Vector)) {
this._renderer.scale = new Two.Vector();
}
if (this._scale instanceof Two.Vector) {
this._renderer.scale.copy(this._scale);
} else {
this._renderer.scale.set(this._scale, this._scale);
}
}
return this.flagReset();
}
},
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;
},
updateTexture: function(gl, elem) {
this[elem._renderer.type].updateCanvas.call(webgl, elem);
if (elem._renderer.texture) {
gl.deleteTexture(elem._renderer.texture);
}
gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.textureCoordsBuffer);
// TODO: Is this necessary every time or can we do once?
// TODO: Create a registry for textures
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')
},
TextureRegistry: new Two.Registry()
};
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, {
Utils: webgl
});
_.extend(Renderer.prototype, Two.Utils.Events, {
constructor: Renderer,
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.trigger(Two.Events.resize, width, height, ratio);
},
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;
}
});
})((typeof global !== 'undefined' ? global : (this || window)).Two);