@sky-foundry/two.js
Version:
A renderer agnostic two-dimensional drawing api for the web.
844 lines (618 loc) • 22.3 kB
JavaScript
(function(Two) {
/**
* Constants
*/
var mod = Two.Utils.mod, toFixed = Two.Utils.toFixed;
var getRatio = Two.Utils.getRatio;
var _ = Two.Utils;
var emptyArray = [];
var TWO_PI = Math.PI * 2,
max = Math.max,
min = Math.min,
abs = Math.abs,
sin = Math.sin,
cos = Math.cos,
acos = Math.acos,
sqrt = Math.sqrt;
// Returns true if this is a non-transforming matrix
var isDefaultMatrix = function (m) {
return (m[0] == 1 && m[3] == 0 && m[1] == 0 && m[4] == 1 && m[2] == 0 && m[5] == 0);
};
var canvas = {
isHidden: /(none|transparent)/i,
alignments: {
left: 'start',
middle: 'center',
right: 'end'
},
shim: function(elem, name) {
elem.tagName = elem.nodeName = name || 'canvas';
elem.nodeType = 1;
elem.getAttribute = function(prop) {
return this[prop];
};
elem.setAttribute = function(prop, val) {
this[prop] = val;
return this;
};
return elem;
},
group: {
renderChild: function(child) {
canvas[child._renderer.type].render.call(child, this.ctx, true, this.clip);
},
render: function(ctx) {
// TODO: Add a check here to only invoke _update if need be.
this._update();
var matrix = this._matrix.elements;
var parent = this.parent;
this._renderer.opacity = this._opacity * (parent && parent._renderer ? parent._renderer.opacity : 1);
var defaultMatrix = isDefaultMatrix(matrix);
var mask = this._mask;
// var clip = this._clip;
if (!this._renderer.context) {
this._renderer.context = {};
}
this._renderer.context.ctx = ctx;
// this._renderer.context.clip = clip;
if (!defaultMatrix) {
ctx.save();
ctx.transform(matrix[0], matrix[3], matrix[1], matrix[4], matrix[2], matrix[5]);
}
if (mask) {
canvas[mask._renderer.type].render.call(mask, ctx, true);
}
if (this.opacity > 0 && this.scale !== 0) {
for (var i = 0; i < this.children.length; i++) {
var child = this.children[i];
canvas[child._renderer.type].render.call(child, ctx);
}
}
if (!defaultMatrix) {
ctx.restore();
}
/**
* Commented two-way functionality of clips / masks with groups and
* polygons. Uncomment when this bug is fixed:
* https://code.google.com/p/chromium/issues/detail?id=370951
*/
// if (clip) {
// ctx.clip();
// }
return this.flagReset();
}
},
path: {
render: function(ctx, forced, parentClipped) {
var matrix, stroke, linewidth, fill, opacity, visible, cap, join, miter,
closed, commands, length, last, next, prev, a, b, c, d, ux, uy, vx, vy,
ar, bl, br, cl, x, y, mask, clip, defaultMatrix, isOffset, dashes;
// TODO: Add a check here to only invoke _update if need be.
this._update();
matrix = this._matrix.elements;
stroke = this._stroke;
linewidth = this._linewidth;
fill = this._fill;
opacity = this._opacity * this.parent._renderer.opacity;
visible = this._visible;
cap = this._cap;
join = this._join;
miter = this._miter;
closed = this._closed;
commands = this._renderer.vertices; // Commands
length = commands.length;
last = length - 1;
defaultMatrix = isDefaultMatrix(matrix);
dashes = this.dashes;
// mask = this._mask;
clip = this._clip;
if (!forced && (!visible || clip)) {
return this;
}
// Transform
if (!defaultMatrix) {
ctx.save();
ctx.transform(matrix[0], matrix[3], matrix[1], matrix[4], matrix[2], matrix[5]);
}
/**
* Commented two-way functionality of clips / masks with groups and
* polygons. Uncomment when this bug is fixed:
* https://code.google.com/p/chromium/issues/detail?id=370951
*/
// if (mask) {
// canvas[mask._renderer.type].render.call(mask, ctx, true);
// }
// Styles
if (fill) {
if (_.isString(fill)) {
ctx.fillStyle = fill;
} else {
canvas[fill._renderer.type].render.call(fill, ctx);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (_.isString(stroke)) {
ctx.strokeStyle = stroke;
} else {
canvas[stroke._renderer.type].render.call(stroke, ctx);
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);
}
ctx.beginPath();
for (var i = 0; i < commands.length; i++) {
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);
canvas.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 = (ar.x + toFixed(a.x));
vy = (ar.y + toFixed(a.y));
} else {
vx = toFixed(ar.x);
vy = toFixed(ar.y);
}
if (b._relative) {
ux = (bl.x + toFixed(b.x));
uy = (bl.y + toFixed(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 = (br.x + toFixed(b.x));
vy = (br.y + toFixed(b.y));
} else {
vx = toFixed(br.x);
vy = toFixed(br.y);
}
if (c._relative) {
ux = (cl.x + toFixed(c.x));
uy = (cl.y + toFixed(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 (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (!clip && !parentClipped) {
if (!canvas.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 (!canvas.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();
}
}
}
if (!defaultMatrix) {
ctx.restore();
}
if (clip && !parentClipped) {
ctx.clip();
}
return this.flagReset();
}
},
text: {
render: function(ctx, forced, parentClipped) {
// TODO: Add a check here to only invoke _update if need be.
this._update();
var matrix = this._matrix.elements;
var stroke = this._stroke;
var linewidth = this._linewidth;
var fill = this._fill;
var opacity = this._opacity * this.parent._renderer.opacity;
var visible = this._visible;
var defaultMatrix = isDefaultMatrix(matrix);
var isOffset = fill._renderer && fill._renderer.offset
&& stroke._renderer && stroke._renderer.offset;
var dashes = this.dashes;
var a, b, c, d, e, sx, sy;
// mask = this._mask;
var clip = this._clip;
if (!forced && (!visible || clip)) {
return this;
}
// Transform
if (!defaultMatrix) {
ctx.save();
ctx.transform(matrix[0], matrix[3], matrix[1], matrix[4], matrix[2], matrix[5]);
}
/**
* Commented two-way functionality of clips / masks with groups and
* polygons. Uncomment when this bug is fixed:
* https://code.google.com/p/chromium/issues/detail?id=370951
*/
// if (mask) {
// canvas[mask._renderer.type].render.call(mask, ctx, true);
// }
if (!isOffset) {
ctx.font = [this._style, this._weight, this._size + 'px/' +
this._leading + 'px', this._family].join(' ');
}
ctx.textAlign = canvas.alignments[this._alignment] || this._alignment;
ctx.textBaseline = this._baseline;
// Styles
if (fill) {
if (_.isString(fill)) {
ctx.fillStyle = fill;
} else {
canvas[fill._renderer.type].render.call(fill, ctx);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (_.isString(stroke)) {
ctx.strokeStyle = stroke;
} else {
canvas[stroke._renderer.type].render.call(stroke, ctx);
ctx.strokeStyle = stroke._renderer.effect;
}
}
if (linewidth) {
ctx.lineWidth = linewidth;
}
if (_.isNumber(opacity)) {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(dashes);
}
if (!clip && !parentClipped) {
if (!canvas.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 = this._size / fill._renderer.scale.y;
b = this._leading / fill._renderer.scale.y;
ctx.font = [this._style, this._weight, toFixed(a) + 'px/',
toFixed(b) + 'px', this._family].join(' ');
c = fill._renderer.offset.x / fill._renderer.scale.x;
d = fill._renderer.offset.y / fill._renderer.scale.y;
ctx.fillText(this.value, toFixed(c), toFixed(d));
ctx.restore();
} else {
ctx.fillText(this.value, 0, 0);
}
}
if (!canvas.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 = this._size / stroke._renderer.scale.y;
b = this._leading / stroke._renderer.scale.y;
ctx.font = [this._style, this._weight, toFixed(a) + 'px/',
toFixed(b) + 'px', this._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(this.value, toFixed(c), toFixed(d));
ctx.restore();
} else {
ctx.strokeText(this.value, 0, 0);
}
}
}
if (!defaultMatrix) {
ctx.restore();
}
// TODO: Test for text
if (clip && !parentClipped) {
ctx.clip();
}
return this.flagReset();
}
},
'linear-gradient': {
render: function(ctx) {
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) {
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) {
this._update();
var image = this.image;
var repeat;
if (!this._renderer.effect || ((this._flagLoaded || this._flagImage || this._flagVideo || this._flagRepeat) && this.loaded)) {
this._renderer.effect = ctx.createPattern(this.image, this._repeat);
}
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();
}
},
renderSvgArcCommand: function(ctx, ax, ay, rx, ry, largeArcFlag, sweepFlag, xAxisRotation, x, y) {
xAxisRotation = xAxisRotation * Math.PI / 180;
// Ensure radii are positive
rx = abs(rx);
ry = abs(ry);
// Compute (x1′, y1′)
var dx2 = (ax - x) / 2.0;
var dy2 = (ay - y) / 2.0;
var x1p = cos(xAxisRotation) * dx2 + sin(xAxisRotation) * dy2;
var y1p = - sin(xAxisRotation) * dx2 + cos(xAxisRotation) * dy2;
// Compute (cx′, cy′)
var rxs = rx * rx;
var rys = ry * ry;
var x1ps = x1p * x1p;
var y1ps = y1p * y1p;
// Ensure radii are large enough
var cr = x1ps / rxs + y1ps / rys;
if (cr > 1) {
// scale up rx,ry equally so cr == 1
var s = sqrt(cr);
rx = s * rx;
ry = s * ry;
rxs = rx * rx;
rys = ry * ry;
}
var dq = (rxs * y1ps + rys * x1ps);
var pq = (rxs * rys - dq) / dq;
var q = sqrt(max(0, pq));
if (largeArcFlag === sweepFlag) q = - q;
var cxp = q * rx * y1p / ry;
var cyp = - q * ry * x1p / rx;
// Step 3: Compute (cx, cy) from (cx′, cy′)
var cx = cos(xAxisRotation) * cxp
- sin(xAxisRotation) * cyp + (ax + x) / 2;
var cy = sin(xAxisRotation) * cxp
+ cos(xAxisRotation) * cyp + (ay + y) / 2;
// Step 4: Compute θ1 and Δθ
var startAngle = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
var delta = svgAngle((x1p - cxp) / rx, (y1p - cyp) / ry,
(- x1p - cxp) / rx, (- y1p - cyp) / ry) % TWO_PI;
var endAngle = startAngle + delta;
var clockwise = sweepFlag === 0;
renderArcEstimate(ctx, cx, cy, rx, ry, startAngle, endAngle,
clockwise, xAxisRotation);
}
};
var Renderer = Two[Two.Types.canvas] = function(params) {
// Smoothing property. Defaults to true
// Set it to false when working with pixel art.
// false can lead to better performance, since it would use a cheaper interpolation algorithm.
// It might not make a big difference on GPU backed canvases.
var smoothing = (params.smoothing !== false);
this.domElement = params.domElement || document.createElement('canvas');
this.ctx = this.domElement.getContext('2d');
this.overdraw = params.overdraw || false;
if (!_.isUndefined(this.ctx.imageSmoothingEnabled)) {
this.ctx.imageSmoothingEnabled = smoothing;
}
// Everything drawn on the canvas needs to be added to the scene.
this.scene = new Two.Group();
this.scene.parent = this;
};
_.extend(Renderer, {
Utils: canvas
});
_.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;
if (this.domElement.style) {
_.extend(this.domElement.style, {
width: width + 'px',
height: height + 'px'
});
}
return this.trigger(Two.Events.resize, width, height, ratio);
},
render: function() {
var isOne = this.ratio === 1;
if (!isOne) {
this.ctx.save();
this.ctx.scale(this.ratio, this.ratio);
}
if (!this.overdraw) {
this.ctx.clearRect(0, 0, this.width, this.height);
}
canvas.group.render.call(this.scene, this.ctx);
if (!isOne) {
this.ctx.restore();
}
return this;
}
});
function renderArcEstimate(ctx, ox, oy, rx, ry, startAngle, endAngle, clockwise, xAxisRotation) {
var epsilon = Two.Utils.Curve.Tolerance.epsilon;
var deltaAngle = endAngle - startAngle;
var samePoints = Math.abs(deltaAngle) < epsilon;
// ensures that deltaAngle is 0 .. 2 PI
deltaAngle = mod(deltaAngle, TWO_PI);
if (deltaAngle < epsilon) {
if (samePoints) {
deltaAngle = 0;
} else {
deltaAngle = TWO_PI;
}
}
if (clockwise === true && ! samePoints) {
if (deltaAngle === TWO_PI) {
deltaAngle = - TWO_PI;
} else {
deltaAngle = deltaAngle - TWO_PI;
}
}
for (var i = 0; i < Two.Resolution; i++) {
var t = i / (Two.Resolution - 1);
var angle = startAngle + t * deltaAngle;
var x = ox + rx * Math.cos(angle);
var y = oy + ry * Math.sin(angle);
if (xAxisRotation !== 0) {
var cos = Math.cos(xAxisRotation);
var sin = Math.sin(xAxisRotation);
var tx = x - ox;
var ty = y - oy;
// Rotate the point about the center of the ellipse.
x = tx * cos - ty * sin + ox;
y = tx * sin + ty * cos + oy;
}
ctx.lineTo(x, y);
}
}
function svgAngle(ux, uy, vx, vy) {
var dot = ux * vx + uy * vy;
var len = sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy);
// floating point precision, slightly over values appear
var ang = acos(max(-1, min(1, dot / len)));
if ((ux * vy - uy * vx) < 0) {
ang = - ang;
}
return ang;
}
function resetTransform(ctx) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
})((typeof global !== 'undefined' ? global : (this || window)).Two);