Created
March 14, 2017 15:44
-
-
Save cookiengineer/e4e26e2a9467f541849b7daada5bce32 to your computer and use it in GitHub Desktop.
Canvas Renderer with dirty callstack integration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
lychee.define('Renderer').tags({ | |
platform: 'html' | |
}).supports(function(lychee, global) { | |
/* | |
* XXX: typeof CanvasRenderingContext2D is: | |
* > function in Chrome, Firefox, IE10 | |
* > object in Safari, Safari Mobile | |
*/ | |
if ( | |
typeof global.document !== 'undefined' | |
&& typeof global.document.createElement === 'function' | |
&& typeof global.CanvasRenderingContext2D !== 'undefined' | |
) { | |
return true; | |
} | |
return false; | |
}).exports(function(lychee, global, attachments) { | |
const _STATE = { | |
dirty: 0, | |
clean: 1 | |
}; | |
const _PI2 = Math.PI * 2; | |
let _id = 0; | |
let _body = null; | |
/* | |
* FEATURE DETECTION | |
*/ | |
(function(global) { | |
if (typeof global.document !== 'undefined') { | |
if (typeof global.document.body !== 'undefined') { | |
_body = global.document.body; | |
} | |
} | |
})(global); | |
/* | |
* HELPERS | |
*/ | |
const _check_dirty = function() { | |
let al = arguments.length; | |
let cache = this.__stack[this.__sid]; | |
if (cache !== undefined) { | |
if (cache.length !== al) { | |
cache = new Array(al); | |
for (let a = 0; a < al; a++) { | |
cache[a] = arguments[a]; | |
} | |
this.__stack[this.__sid] = cache; | |
this.__state = _STATE.dirty; | |
} else { | |
for (let a = 0; a < al; a++) { | |
if (cache[a] !== arguments[a]) { | |
this.__state = _STATE.dirty; | |
} | |
} | |
} | |
} else { | |
cache = new Array(al); | |
for (let a = 0; a < al; a++) { | |
cache[a] = arguments[a]; | |
} | |
this.__stack[this.__sid] = cache; | |
this.__state = _STATE.dirty; | |
} | |
this.__sid++; | |
}; | |
const _Buffer = function(width, height) { | |
this.width = typeof width === 'number' ? width : 1; | |
this.height = typeof height === 'number' ? height : 1; | |
this.__buffer = global.document.createElement('canvas'); | |
this.__ctx = this.__buffer.getContext('2d'); | |
this.resize(this.width, this.height); | |
}; | |
_Buffer.prototype = { | |
clear: function() { | |
this.__ctx.clearRect(0, 0, this.width, this.height); | |
}, | |
resize: function(width, height) { | |
this.width = width; | |
this.height = height; | |
this.__buffer.width = this.width; | |
this.__buffer.height = this.height; | |
} | |
}; | |
/* | |
* IMPLEMENTATION | |
*/ | |
let Composite = function(data) { | |
let settings = Object.assign({}, data); | |
this.alpha = 1.0; | |
this.background = '#000000'; | |
this.id = 'lychee-Renderer-' + _id++; | |
this.width = null; | |
this.height = null; | |
this.offset = { x: 0, y: 0 }; | |
this.__canvas = global.document.createElement('canvas'); | |
this.__canvas.className = 'lychee-Renderer'; | |
this.__ctx = this.__canvas.getContext('2d'); | |
// Dirty Callstack Analysis | |
this.__stack = []; | |
this.__sid = 0; | |
this.__state = _STATE.clean; | |
if (_body !== null) { | |
_body.appendChild(this.__canvas); | |
} | |
this.setAlpha(settings.alpha); | |
this.setBackground(settings.background); | |
this.setId(settings.id); | |
this.setWidth(settings.width); | |
this.setHeight(settings.height); | |
settings = null; | |
}; | |
Composite.prototype = { | |
destroy: function() { | |
let canvas = this.__canvas; | |
if (canvas.parentNode !== null) { | |
canvas.parentNode.removeChild(canvas); | |
} | |
}, | |
/* | |
* ENTITY API | |
*/ | |
// deserialize: function(blob) {}, | |
serialize: function() { | |
let settings = {}; | |
if (this.alpha !== 1.0) settings.alpha = this.alpha; | |
if (this.background !== '#000000') settings.background = this.background; | |
if (this.id.substr(0, 16) !== 'lychee-Renderer-') settings.id = this.id; | |
if (this.width !== null) settings.width = this.width; | |
if (this.height !== null) settings.height = this.height; | |
return { | |
'constructor': 'lychee.Renderer', | |
'arguments': [ settings ], | |
'blob': null | |
}; | |
}, | |
/* | |
* SETTERS AND GETTERS | |
*/ | |
setAlpha: function(alpha) { | |
alpha = typeof alpha === 'number' ? alpha : null; | |
_check_dirty.call(this, 'setAlpha', alpha); | |
if (alpha !== null) { | |
if (alpha >= 0 && alpha <= 1) { | |
this.alpha = alpha; | |
} | |
} | |
}, | |
setBackground: function(color) { | |
color = /(#[AaBbCcDdEeFf0-9]{6})/g.test(color) ? color : null; | |
_check_dirty.call(this, 'setBackground', color); | |
if (color !== null) { | |
this.background = color; | |
this.__canvas.style.backgroundColor = color; | |
} | |
}, | |
setId: function(id) { | |
id = typeof id === 'string' ? id : null; | |
if (id !== null) { | |
this.id = id; | |
this.__canvas.id = id; | |
} | |
}, | |
setWidth: function(width) { | |
width = typeof width === 'number' ? width : null; | |
if (width !== null) { | |
this.width = width; | |
} else { | |
this.width = global.innerWidth; | |
} | |
this.__canvas.width = this.width; | |
this.__canvas.style.width = this.width + 'px'; | |
this.offset.x = this.__canvas.getBoundingClientRect().left; | |
}, | |
setHeight: function(height) { | |
height = typeof height === 'number' ? height : null; | |
if (height !== null) { | |
this.height = height; | |
} else { | |
this.height = global.innerHeight; | |
} | |
this.__canvas.height = this.height; | |
this.__canvas.style.height = this.height + 'px'; | |
this.offset.y = this.__canvas.getBoundingClientRect().top; | |
}, | |
/* | |
* BUFFER INTEGRATION | |
*/ | |
clear: function(buffer) { | |
buffer = buffer instanceof _Buffer ? buffer : null; | |
if (buffer !== null) { | |
buffer.clear(); | |
} else { | |
let ctx = this.__ctx; | |
let state = this.__state; | |
if (state === _STATE.dirty) { | |
ctx.fillStyle = this.background; | |
ctx.fillRect(0, 0, this.width, this.height); | |
} | |
} | |
}, | |
flush: function() { | |
this.__state = _STATE.clean; | |
this.__sid = 0; | |
}, | |
createBuffer: function(width, height) { | |
return new _Buffer(width, height); | |
}, | |
setBuffer: function(buffer) { | |
buffer = buffer instanceof _Buffer ? buffer : null; | |
if (buffer !== null) { | |
this.__ctx = buffer.__ctx; | |
} else { | |
this.__ctx = this.__canvas.getContext('2d'); | |
} | |
}, | |
/* | |
* DRAWING API | |
*/ | |
drawArc: function(x, y, start, end, radius, color, background, lineWidth) { | |
color = /(#[AaBbCcDdEeFf0-9]{6})/g.test(color) ? color : '#000000'; | |
background = background === true; | |
lineWidth = typeof lineWidth === 'number' ? lineWidth : 1; | |
_check_dirty.call(this, 'drawArc', x, y, start, end, radius, color, background, lineWidth); | |
let ctx = this.__ctx; | |
let state = this.__state; | |
if (state === _STATE.dirty) { | |
ctx.globalAlpha = this.alpha; | |
ctx.beginPath(); | |
ctx.arc( | |
x, | |
y, | |
radius, | |
start * _PI2, | |
end * _PI2 | |
); | |
if (background === false) { | |
ctx.lineWidth = lineWidth; | |
ctx.strokeStyle = color; | |
ctx.stroke(); | |
} else { | |
ctx.fillStyle = color; | |
ctx.fill(); | |
} | |
ctx.closePath(); | |
} | |
}, | |
drawBox: function(x1, y1, x2, y2, color, background, lineWidth) { | |
color = /(#[AaBbCcDdEeFf0-9]{6})/g.test(color) ? color : '#000000'; | |
background = background === true; | |
lineWidth = typeof lineWidth === 'number' ? lineWidth : 1; | |
_check_dirty.call(this, 'drawBox', x1, y1, x2, y2, color, background, lineWidth); | |
let ctx = this.__ctx; | |
let state = this.__state; | |
if (state === _STATE.dirty) { | |
ctx.globalAlpha = this.alpha; | |
if (background === false) { | |
ctx.lineWidth = lineWidth; | |
ctx.strokeStyle = color; | |
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1); | |
} else { | |
ctx.fillStyle = color; | |
ctx.fillRect(x1, y1, x2 - x1, y2 - y1); | |
} | |
} | |
}, | |
drawBuffer: function(x1, y1, buffer) { | |
buffer = buffer instanceof _Buffer ? buffer : null; | |
_check_dirty.call(this, 'drawBuffer', x1, y1, buffer); | |
let state = this.__state; | |
if (state === _STATE.dirty && buffer !== null) { | |
let ctx = this.__ctx; | |
let width = buffer.width; | |
let height = buffer.height; | |
ctx.globalAlpha = this.alpha; | |
ctx.drawImage( | |
buffer.__buffer, | |
0, | |
0, | |
width, | |
height, | |
x1, | |
y1, | |
width, | |
height | |
); | |
} | |
}, | |
drawCircle: function(x, y, radius, color, background, lineWidth) { | |
color = /(#[AaBbCcDdEeFf0-9]{6})/g.test(color) ? color : '#000000'; | |
background = background === true; | |
lineWidth = typeof lineWidth === 'number' ? lineWidth : 1; | |
_check_dirty.call(this, 'drawCircle', x, y, radius, color, background, lineWidth); | |
let ctx = this.__ctx; | |
let state = this.__state; | |
if (state === _STATE.dirty) { | |
ctx.globalAlpha = this.alpha; | |
ctx.beginPath(); | |
ctx.arc( | |
x, | |
y, | |
radius, | |
0, | |
_PI2 | |
); | |
if (background === false) { | |
ctx.lineWidth = lineWidth; | |
ctx.strokeStyle = color; | |
ctx.stroke(); | |
} else { | |
ctx.fillStyle = color; | |
ctx.fill(); | |
} | |
ctx.closePath(); | |
} | |
}, | |
drawLine: function(x1, y1, x2, y2, color, lineWidth) { | |
color = /(#[AaBbCcDdEeFf0-9]{6})/g.test(color) ? color : '#000000'; | |
lineWidth = typeof lineWidth === 'number' ? lineWidth : 1; | |
_check_dirty.call(this, 'drawLine', x1, y1, x2, y2, color, lineWidth); | |
let ctx = this.__ctx; | |
let state = this.__state; | |
if (state === _STATE.dirty) { | |
ctx.globalAlpha = this.alpha; | |
ctx.beginPath(); | |
ctx.moveTo(x1, y1); | |
ctx.lineTo(x2, y2); | |
ctx.lineWidth = lineWidth; | |
ctx.strokeStyle = color; | |
ctx.stroke(); | |
ctx.closePath(); | |
} | |
}, | |
drawTriangle: function(x1, y1, x2, y2, x3, y3, color, background, lineWidth) { | |
color = /(#[AaBbCcDdEeFf0-9]{6})/g.test(color) ? color : '#000000'; | |
background = background === true; | |
lineWidth = typeof lineWidth === 'number' ? lineWidth : 1; | |
_check_dirty.call(this, 'drawTriangle', x1, y1, x2, y2, x3, y3, color, background, lineWidth); | |
let ctx = this.__ctx; | |
let state = this.__state; | |
if (state === _STATE.dirty) { | |
ctx.globalAlpha = this.alpha; | |
ctx.beginPath(); | |
ctx.moveTo(x1, y1); | |
ctx.lineTo(x2, y2); | |
ctx.lineTo(x3, y3); | |
ctx.lineTo(x1, y1); | |
if (background === false) { | |
ctx.lineWidth = lineWidth; | |
ctx.strokeStyle = color; | |
ctx.stroke(); | |
} else { | |
ctx.fillStyle = color; | |
ctx.fill(); | |
} | |
ctx.closePath(); | |
} | |
}, | |
// points, x1, y1, [ ... x(a), y(a) ... ], [ color, background, lineWidth ] | |
drawPolygon: function(points, x1, y1) { | |
let l = arguments.length; | |
if (points > 3) { | |
let optargs = l - (points * 2) - 1; | |
let color, background, lineWidth; | |
if (optargs === 3) { | |
color = arguments[l - 3]; | |
background = arguments[l - 2]; | |
lineWidth = arguments[l - 1]; | |
} else if (optargs === 2) { | |
color = arguments[l - 2]; | |
background = arguments[l - 1]; | |
} else if (optargs === 1) { | |
color = arguments[l - 1]; | |
} | |
color = /(#[AaBbCcDdEeFf0-9]{6})/g.test(color) ? color : '#000000'; | |
background = background === true; | |
lineWidth = typeof lineWidth === 'number' ? lineWidth : 1; | |
_check_dirty.call(this, 'drawPolygon', points, x1, y1, color, background, lineWidth); | |
let ctx = this.__ctx; | |
let state = this.__state; | |
if (state === _STATE.dirty) { | |
ctx.globalAlpha = this.alpha; | |
ctx.beginPath(); | |
ctx.moveTo(x1, y1); | |
for (let p = 1; p < points; p++) { | |
ctx.lineTo( | |
arguments[1 + p * 2], | |
arguments[1 + p * 2 + 1] | |
); | |
} | |
ctx.lineTo(x1, y1); | |
if (background === false) { | |
ctx.lineWidth = lineWidth; | |
ctx.strokeStyle = color; | |
ctx.stroke(); | |
} else { | |
ctx.fillStyle = color; | |
ctx.fill(); | |
} | |
ctx.closePath(); | |
} | |
} | |
}, | |
drawSprite: function(x1, y1, texture, map) { | |
texture = texture instanceof Texture ? texture : null; | |
map = map instanceof Object ? map : null; | |
_check_dirty.call(this, 'drawSprite', x1, y1, texture, map); | |
let state = this.__state; | |
if (state === _STATE.dirty && texture !== null && texture.buffer !== null) { | |
let ctx = this.__ctx; | |
let width = 0; | |
let height = 0; | |
let x = 0; | |
let y = 0; | |
let r = 0; | |
ctx.globalAlpha = this.alpha; | |
if (map === null) { | |
width = texture.width; | |
height = texture.height; | |
ctx.drawImage( | |
texture.buffer, | |
x, | |
y, | |
width, | |
height, | |
x1, | |
y1, | |
width, | |
height | |
); | |
} else { | |
width = map.w; | |
height = map.h; | |
x = map.x; | |
y = map.y; | |
r = map.r || 0; | |
if (r === 0) { | |
ctx.drawImage( | |
texture.buffer, | |
x, | |
y, | |
width, | |
height, | |
x1, | |
y1, | |
width, | |
height | |
); | |
} else { | |
let cos = Math.cos(r * Math.PI / 180); | |
let sin = Math.sin(r * Math.PI / 180); | |
ctx.setTransform( | |
cos, | |
sin, | |
-sin, | |
cos, | |
x1, | |
y1 | |
); | |
ctx.drawImage( | |
texture.buffer, | |
x, | |
y, | |
width, | |
height, | |
-1 / 2 * width, | |
-1 / 2 * height, | |
width, | |
height | |
); | |
ctx.setTransform( | |
1, | |
0, | |
0, | |
1, | |
0, | |
0 | |
); | |
} | |
} | |
} | |
}, | |
drawText: function(x1, y1, text, font, center) { | |
font = font instanceof Font ? font : null; | |
center = center === true; | |
_check_dirty.call(this, 'drawText', x1, y1, text, font, center); | |
let state = this.__state; | |
if (state === _STATE.dirty && font !== null) { | |
if (center === true) { | |
let dim = font.measure(text); | |
x1 -= dim.realwidth / 2; | |
y1 -= (dim.realheight - font.baseline) / 2; | |
} | |
y1 -= font.baseline / 2; | |
let margin = 0; | |
let texture = font.texture; | |
if (texture !== null && texture.buffer !== null) { | |
let ctx = this.__ctx; | |
ctx.globalAlpha = this.alpha; | |
for (let t = 0, l = text.length; t < l; t++) { | |
let chr = font.measure(text[t]); | |
ctx.drawImage( | |
texture.buffer, | |
chr.x, | |
chr.y, | |
chr.width, | |
chr.height, | |
x1 + margin - font.spacing, | |
y1, | |
chr.width, | |
chr.height | |
); | |
margin += chr.realwidth + font.kerning; | |
} | |
} | |
} | |
} | |
}; | |
return Composite; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment