Skip to content

Instantly share code, notes, and snippets.

@cookiengineer
Created March 14, 2017 15:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cookiengineer/e4e26e2a9467f541849b7daada5bce32 to your computer and use it in GitHub Desktop.
Save cookiengineer/e4e26e2a9467f541849b7daada5bce32 to your computer and use it in GitHub Desktop.
Canvas Renderer with dirty callstack integration
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