Use this WebGL-2d fork for HSL and transparency fixes
Last active
April 2, 2022 11:00
-
-
Save syntagmatic/5023642 to your computer and use it in GitHub Desktop.
WebGL-2d HSL Fix
This file contains 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
<h3>Canvas2D</h3> | |
<canvas id="canvas-default"></canvas> | |
<h3>Webgl-2d</h3> | |
<canvas id="canvas-webgl"></canvas> | |
<script src="webgl-2d.js"></script> | |
<script> | |
var width = 600, | |
height = 200; | |
var canvasDefault = document.getElementById("canvas-default"); | |
canvasDefault.width = width; | |
canvasDefault.height = height; | |
var canvasWebgl = document.getElementById("canvas-webgl"); | |
canvasWebgl.width = width; | |
canvasWebgl.height = height; | |
WebGL2D.enable(canvasWebgl) | |
var ctx = canvasDefault.getContext("2d"); | |
var webgl = canvasWebgl.getContext("webgl-2d"); | |
var j = 10; | |
while (--j > 0) { | |
var i = 20; | |
while (--i > 0) { | |
var color = "hsl(" + (360/i) + "," + (100/j) + "%,50%)"; | |
ctx.fillStyle = color; | |
webgl.fillStyle = color; | |
ctx.fillRect(30*(i-1),20*(j-1),20,15); | |
webgl.fillRect(30*(i-1),20*(j-1),20,15); | |
} | |
} | |
</script> |
This file contains 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
/** | |
* WebGL-2D.js - HTML5 Canvas2D API in a WebGL context | |
* | |
* Created by Corban Brook <corbanbrook@gmail.com> on 2011-03-02. | |
* Amended to by Bobby Richter <secretrobotron@gmail.com> on 2011-03-03 | |
* CubicVR.js by Charles Cliffe <cj@cubicproductions.com> on 2011-03-03 | |
* | |
*/ | |
/* | |
* Copyright (c) 2011 Corban Brook | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining | |
* a copy of this software and associated documentation files (the | |
* "Software"), to deal in the Software without restriction, including | |
* without limitation the rights to use, copy, modify, merge, publish, | |
* distribute, sublicense, and/or sell copies of the Software, and to | |
* permit persons to whom the Software is furnished to do so, subject to | |
* the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be | |
* included in all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
* | |
*/ | |
/** | |
* Usage: | |
* | |
* var cvs = document.getElementById("myCanvas"); | |
* | |
* WebGL2D.enable(cvs); // adds "webgl-2d" to cvs | |
* | |
* cvs.getContext("webgl-2d"); | |
* | |
*/ | |
(function(Math, undefined) { | |
// Vector & Matrix libraries from CubicVR.js | |
var M_PI = 3.1415926535897932384626433832795028841968; | |
var M_TWO_PI = 2.0 * M_PI; | |
var M_HALF_PI = M_PI / 2.0; | |
function isPOT(value) { | |
return value > 0 && ((value - 1) & value) === 0; | |
} | |
var vec3 = { | |
length: function(pt) { | |
return Math.sqrt(pt[0] * pt[0] + pt[1] * pt[1] + pt[2] * pt[2]); | |
}, | |
normalize: function(pt) { | |
var d = Math.sqrt((pt[0] * pt[0]) + (pt[1] * pt[1]) + (pt[2] * pt[2])); | |
if (d === 0) { | |
return [0, 0, 0]; | |
} | |
return [pt[0] / d, pt[1] / d, pt[2] / d]; | |
}, | |
dot: function(v1, v2) { | |
return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]; | |
}, | |
angle: function(v1, v2) { | |
return Math.acos((v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]) / (Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2]) * Math.sqrt(v2[0] * v2[0] + v2[1] * v2[1] + v2[2] * v2[2]))); | |
}, | |
cross: function(vectA, vectB) { | |
return [vectA[1] * vectB[2] - vectB[1] * vectA[2], vectA[2] * vectB[0] - vectB[2] * vectA[0], vectA[0] * vectB[1] - vectB[0] * vectA[1]]; | |
}, | |
multiply: function(vectA, constB) { | |
return [vectA[0] * constB, vectA[1] * constB, vectA[2] * constB]; | |
}, | |
add: function(vectA, vectB) { | |
return [vectA[0] + vectB[0], vectA[1] + vectB[1], vectA[2] + vectB[2]]; | |
}, | |
subtract: function(vectA, vectB) { | |
return [vectA[0] - vectB[0], vectA[1] - vectB[1], vectA[2] - vectB[2]]; | |
}, | |
equal: function(a, b) { | |
var epsilon = 0.0000001; | |
if ((a === undefined) && (b === undefined)) { | |
return true; | |
} | |
if ((a === undefined) || (b === undefined)) { | |
return false; | |
} | |
return (Math.abs(a[0] - b[0]) < epsilon && Math.abs(a[1] - b[1]) < epsilon && Math.abs(a[2] - b[2]) < epsilon); | |
} | |
}; | |
var mat3 = { | |
identity: [1.0, 0.0, 0.0, | |
0.0, 1.0, 0.0, | |
0.0, 0.0, 1.0], | |
multiply: function (m1, m2) { | |
var m10 = m1[0], m11 = m1[1], m12 = m1[2], m13 = m1[3], m14 = m1[4], m15 = m1[5], m16 = m1[6], m17 = m1[7], m18 = m1[8], | |
m20 = m2[0], m21 = m2[1], m22 = m2[2], m23 = m2[3], m24 = m2[4], m25 = m2[5], m26 = m2[6], m27 = m2[7], m28 = m2[8]; | |
m2[0] = m20 * m10 + m23 * m11 + m26 * m12; | |
m2[1] = m21 * m10 + m24 * m11 + m27 * m12; | |
m2[2] = m22 * m10 + m25 * m11 + m28 * m12; | |
m2[3] = m20 * m13 + m23 * m14 + m26 * m15; | |
m2[4] = m21 * m13 + m24 * m14 + m27 * m15; | |
m2[5] = m22 * m13 + m25 * m14 + m28 * m15; | |
m2[6] = m20 * m16 + m23 * m17 + m26 * m18; | |
m2[7] = m21 * m16 + m24 * m17 + m27 * m18; | |
m2[8] = m22 * m16 + m25 * m17 + m28 * m18; | |
}, | |
vec2_multiply: function (m1, m2) { | |
var mOut = []; | |
mOut[0] = m2[0] * m1[0] + m2[3] * m1[1] + m2[6]; | |
mOut[1] = m2[1] * m1[0] + m2[4] * m1[1] + m2[7]; | |
return mOut; | |
}, | |
transpose: function (m) { | |
return [m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]]; | |
} | |
}; //mat3 | |
// Transform library from CubicVR.js | |
function Transform(mat) { | |
return this.clearStack(mat); | |
} | |
var STACK_DEPTH_LIMIT = 16; | |
Transform.prototype.clearStack = function(init_mat) { | |
this.m_stack = []; | |
this.m_cache = []; | |
this.c_stack = 0; | |
this.valid = 0; | |
this.result = null; | |
for (var i = 0; i < STACK_DEPTH_LIMIT; i++) { | |
this.m_stack[i] = this.getIdentity(); | |
} | |
if (init_mat !== undefined) { | |
this.m_stack[0] = init_mat; | |
} else { | |
this.setIdentity(); | |
} | |
}; //clearStack | |
Transform.prototype.setIdentity = function() { | |
this.m_stack[this.c_stack] = this.getIdentity(); | |
if (this.valid === this.c_stack && this.c_stack) { | |
this.valid--; | |
} | |
}; | |
Transform.prototype.getIdentity = function() { | |
return [1.0, 0.0, 0.0, | |
0.0, 1.0, 0.0, | |
0.0, 0.0, 1.0]; | |
}; | |
Transform.prototype.getResult = function() { | |
if (!this.c_stack) { | |
return this.m_stack[0]; | |
} | |
var m = mat3.identity; | |
if (this.valid > this.c_stack-1) { this.valid = this.c_stack-1; } | |
for (var i = this.valid; i < this.c_stack+1; i++) { | |
m = mat3.multiply(this.m_stack[i],m); | |
this.m_cache[i] = m; | |
} | |
this.valid = this.c_stack-1; | |
this.result = this.m_cache[this.c_stack]; | |
return this.result; | |
}; | |
Transform.prototype.pushMatrix = function() { | |
this.c_stack++; | |
this.m_stack[this.c_stack] = this.getIdentity(); | |
}; | |
Transform.prototype.popMatrix = function() { | |
if (this.c_stack === 0) { return; } | |
this.c_stack--; | |
}; | |
var translateMatrix = Transform.prototype.getIdentity(); | |
Transform.prototype.translate = function(x, y) { | |
translateMatrix[6] = x; | |
translateMatrix[7] = y; | |
mat3.multiply(translateMatrix, this.m_stack[this.c_stack]); | |
/* | |
if (this.valid === this.c_stack && this.c_stack) { | |
this.valid--; | |
} | |
*/ | |
}; | |
var scaleMatrix = Transform.prototype.getIdentity(); | |
Transform.prototype.scale = function(x, y) { | |
scaleMatrix[0] = x; | |
scaleMatrix[4] = y; | |
mat3.multiply(scaleMatrix, this.m_stack[this.c_stack]); | |
/* | |
if (this.valid === this.c_stack && this.c_stack) { | |
this.valid--; | |
} | |
*/ | |
}; | |
var rotateMatrix = Transform.prototype.getIdentity(); | |
Transform.prototype.rotate = function(ang) { | |
var sAng, cAng; | |
sAng = Math.sin(-ang); | |
cAng = Math.cos(-ang); | |
rotateMatrix[0] = cAng; | |
rotateMatrix[3] = sAng; | |
rotateMatrix[1] = -sAng; | |
rotateMatrix[4] = cAng; | |
mat3.multiply(rotateMatrix, this.m_stack[this.c_stack]); | |
/* | |
if (this.valid === this.c_stack && this.c_stack) { | |
this.valid--; | |
} | |
*/ | |
}; | |
var WebGL2D = this.WebGL2D = function WebGL2D(canvas, options) { | |
this.canvas = canvas; | |
this.options = options || {}; | |
this.gl = undefined; | |
this.fs = undefined; | |
this.vs = undefined; | |
this.shaderProgram = undefined; | |
this.transform = new Transform(); | |
this.shaderPool = []; | |
this.maxTextureSize = undefined; | |
// Save a reference to the WebGL2D instance on the canvas object | |
canvas.gl2d = this; | |
// Store getContext function for later use | |
canvas.$getContext = canvas.getContext; | |
// Override getContext function with "webgl-2d" enabled version | |
canvas.getContext = (function(gl2d) { | |
return function(context) { | |
if ((gl2d.options.force || context === "webgl-2d") && !(canvas.width === 0 || canvas.height === 0)) { | |
if (gl2d.gl) { return gl2d.gl; } | |
var gl = gl2d.gl = gl2d.canvas.$getContext("experimental-webgl"); | |
gl2d.initShaders(); | |
gl2d.initBuffers(); | |
// Append Canvas2D API features to the WebGL context | |
gl2d.initCanvas2DAPI(); | |
gl.viewport(0, 0, gl2d.canvas.width, gl2d.canvas.height); | |
// Disables writing to dest-alpha | |
// gl.colorMask(1,1,1,0) | |
// Depth options | |
//gl.enable(gl.DEPTH_TEST); | |
//gl.depthFunc(gl.LEQUAL); | |
// Blending options | |
// See http://stackoverflow.com/questions/11521035/blending-with-html-background-in-webgl | |
gl.enable(gl.BLEND); | |
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); | |
gl2d.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); | |
return gl; | |
} else { | |
return gl2d.canvas.$getContext(context); | |
} | |
}; | |
}(this)); | |
this.postInit(); | |
}; | |
// Enables WebGL2D on your canvas | |
WebGL2D.enable = function(canvas, options) { | |
return canvas.gl2d || new WebGL2D(canvas, options); | |
}; | |
// Shader Pool BitMasks, i.e. sMask = (shaderMask.texture+shaderMask.stroke) | |
var shaderMask = { | |
texture: 1, | |
crop: 2, | |
path: 4 | |
}; | |
// Fragment shader source | |
WebGL2D.prototype.getFragmentShaderSource = function getFragmentShaderSource(sMask) { | |
var fsSource = [ | |
"#ifdef GL_ES", | |
"precision highp float;", | |
"#endif", | |
"#define hasTexture " + ((sMask&shaderMask.texture) ? "1" : "0"), | |
"#define hasCrop " + ((sMask&shaderMask.crop) ? "1" : "0"), | |
"varying vec4 vColor;", | |
"#if hasTexture", | |
"varying vec2 vTextureCoord;", | |
"uniform sampler2D uSampler;", | |
"#if hasCrop", | |
"uniform vec4 uCropSource;", | |
"#endif", | |
"#endif", | |
"void main(void) {", | |
"#if hasTexture", | |
"#if hasCrop", | |
"gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.x * uCropSource.z, vTextureCoord.y * uCropSource.w) + uCropSource.xy);", | |
"#else", | |
"gl_FragColor = texture2D(uSampler, vTextureCoord);", | |
"#endif", | |
"#else", | |
"gl_FragColor = vColor;", | |
"#endif", | |
"}" | |
].join("\n"); | |
return fsSource; | |
}; | |
WebGL2D.prototype.getVertexShaderSource = function getVertexShaderSource(stackDepth,sMask) { | |
var w = 2 / this.canvas.width, h = -2 / this.canvas.height; | |
stackDepth = stackDepth || 1; | |
var vsSource = [ | |
"#define hasTexture " + ((sMask&shaderMask.texture) ? "1" : "0"), | |
"attribute vec4 aVertexPosition;", | |
"#if hasTexture", | |
"varying vec2 vTextureCoord;", | |
"#endif", | |
"uniform vec4 uColor;", | |
"uniform mat3 uTransforms[" + stackDepth + "];", | |
"varying vec4 vColor;", | |
"const mat4 pMatrix = mat4(" + w + ",0,0,0, 0," + h + ",0,0, 0,0,1.0,1.0, -1.0,1.0,0,0);", | |
"mat3 crunchStack(void) {", | |
"mat3 result = uTransforms[0];", | |
"for (int i = 1; i < " + stackDepth + "; ++i) {", | |
"result = uTransforms[i] * result;", | |
"}", | |
"return result;", | |
"}", | |
"void main(void) {", | |
"vec3 position = crunchStack() * vec3(aVertexPosition.x, aVertexPosition.y, 1.0);", | |
"gl_Position = pMatrix * vec4(position, 1.0);", | |
"vColor = uColor;", | |
"#if hasTexture", | |
"vTextureCoord = aVertexPosition.zw;", | |
"#endif", | |
"}" | |
].join("\n"); | |
return vsSource; | |
}; | |
// Initialize fragment and vertex shaders | |
WebGL2D.prototype.initShaders = function initShaders(transformStackDepth,sMask) { | |
var gl = this.gl; | |
transformStackDepth = transformStackDepth || 1; | |
sMask = sMask || 0; | |
var storedShader = this.shaderPool[transformStackDepth]; | |
if (!storedShader) { storedShader = this.shaderPool[transformStackDepth] = []; } | |
storedShader = storedShader[sMask]; | |
if (storedShader) { | |
gl.useProgram(storedShader); | |
this.shaderProgram = storedShader; | |
return storedShader; | |
} else { | |
var fs = this.fs = gl.createShader(gl.FRAGMENT_SHADER); | |
gl.shaderSource(this.fs, this.getFragmentShaderSource(sMask)); | |
gl.compileShader(this.fs); | |
if (!gl.getShaderParameter(this.fs, gl.COMPILE_STATUS)) { | |
throw "fragment shader error: "+gl.getShaderInfoLog(this.fs); | |
} | |
var vs = this.vs = gl.createShader(gl.VERTEX_SHADER); | |
gl.shaderSource(this.vs, this.getVertexShaderSource(transformStackDepth,sMask)); | |
gl.compileShader(this.vs); | |
if (!gl.getShaderParameter(this.vs, gl.COMPILE_STATUS)) { | |
throw "vertex shader error: "+gl.getShaderInfoLog(this.vs); | |
} | |
var shaderProgram = this.shaderProgram = gl.createProgram(); | |
shaderProgram.stackDepth = transformStackDepth; | |
gl.attachShader(shaderProgram, fs); | |
gl.attachShader(shaderProgram, vs); | |
gl.linkProgram(shaderProgram); | |
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { | |
throw "Could not initialise shaders."; | |
} | |
gl.useProgram(shaderProgram); | |
shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); | |
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); | |
shaderProgram.uColor = gl.getUniformLocation(shaderProgram, 'uColor'); | |
shaderProgram.uSampler = gl.getUniformLocation(shaderProgram, 'uSampler'); | |
shaderProgram.uCropSource = gl.getUniformLocation(shaderProgram, 'uCropSource'); | |
shaderProgram.uTransforms = []; | |
for (var i=0; i<transformStackDepth; ++i) { | |
shaderProgram.uTransforms[i] = gl.getUniformLocation(shaderProgram, 'uTransforms[' + i + ']'); | |
} //for | |
this.shaderPool[transformStackDepth][sMask] = shaderProgram; | |
return shaderProgram; | |
} //if | |
}; | |
var rectVertexPositionBuffer; | |
var rectVertexColorBuffer; | |
var pathVertexPositionBuffer; | |
var pathVertexColorBuffer; | |
// 2D Vertices and Texture UV coords | |
var rectVerts = new Float32Array([ | |
0,0, 0,0, | |
0,1, 0,1, | |
1,1, 1,1, | |
1,0, 1,0 | |
]); | |
WebGL2D.prototype.initBuffers = function initBuffers() { | |
var gl = this.gl; | |
rectVertexPositionBuffer = gl.createBuffer(); | |
rectVertexColorBuffer = gl.createBuffer(); | |
pathVertexPositionBuffer = gl.createBuffer(); | |
pathVertexColorBuffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, rectVertexPositionBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, rectVerts, gl.STATIC_DRAW); | |
}; | |
// Maintains an array of all WebGL2D instances | |
WebGL2D.instances = []; | |
WebGL2D.prototype.postInit = function() { | |
WebGL2D.instances.push(this); | |
}; | |
// Extends gl context with Canvas2D API | |
WebGL2D.prototype.initCanvas2DAPI = function initCanvas2DAPI() { | |
var gl2d = this, | |
gl = this.gl; | |
// Rendering Canvas for text fonts | |
var textCanvas = document.createElement("canvas"); | |
textCanvas.width = gl2d.canvas.width; | |
textCanvas.height = gl2d.canvas.height; | |
var textCtx = textCanvas.getContext("2d"); | |
var reRGBAColor = /^rgb(a)?\(\s*(-?[\d]+)(%)?\s*,\s*(-?[\d]+)(%)?\s*,\s*(-?[\d]+)(%)?\s*,?\s*(-?[\d\.]+)?\s*\)$/; | |
var reHSLAColor = /^hsl(a)?\(\s*(-?[\d\.]+)\s*,\s*(-?[\d\.]+)%\s*,\s*(-?[\d\.]+)%\s*,?\s*(-?[\d\.]+)?\s*\)$/; | |
var reHex6Color = /^#([0-9A-Fa-f]{6})$/; | |
var reHex3Color = /^#([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])$/; | |
// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c | |
function HSLAToRGBA(h, s, l, a) { | |
var r, g, b, p, q; | |
// Clamp and Normalize values | |
h = (((h % 360) + 360) % 360) / 360; | |
s = s > 100 ? 1 : s / 100; | |
s = s < 0 ? 0 : s; | |
l = l > 100 ? 1 : l / 100; | |
l = l < 0 ? 0 : l; | |
if(s == 0) { | |
r = g = b = l; // achromatic | |
} else { | |
function hue2rgb(p, q, t){ | |
if(t < 0) t += 1; | |
if(t > 1) t -= 1; | |
if(t < 1/6) return p + (q - p) * 6 * t; | |
if(t < 1/2) return q; | |
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; | |
return p; | |
} | |
q = l < 0.5 ? l * (1 + s) : l + s - l * s; | |
p = 2 * l - q; | |
r = hue2rgb(p, q, h + 1/3); | |
g = hue2rgb(p, q, h); | |
b = hue2rgb(p, q, h - 1/3); | |
} | |
return [r, g, b, a]; | |
} | |
// Converts rgb(a) color string to gl color vector | |
function colorStringToVec4(value) { | |
var result = [], match, channel, isPercent, hasAlpha, alphaChannel, sameType; | |
if ((match = reRGBAColor.exec(value))) { | |
hasAlpha = match[1], alphaChannel = parseFloat(match[8]); | |
if ((hasAlpha && isNaN(alphaChannel)) || (!hasAlpha && !isNaN(alphaChannel))) { | |
return false; | |
} | |
sameType = match[3]; | |
for (var i = 2; i < 8; i+=2) { | |
channel = match[i], isPercent = match[i+1]; | |
if (isPercent !== sameType) { | |
return false; | |
} | |
// Clamp and normalize values | |
if (isPercent) { | |
channel = channel > 100 ? 1 : channel / 100; | |
channel = channel < 0 ? 0 : channel; | |
} else { | |
channel = channel > 255 ? 1 : channel / 255; | |
channel = channel < 0 ? 0 : channel; | |
} | |
result.push(channel); | |
} | |
result.push(hasAlpha ? alphaChannel : 1.0); | |
} else if ((match = reHSLAColor.exec(value))) { | |
hasAlpha = match[1], alphaChannel = parseFloat(match[5]); | |
result = HSLAToRGBA(match[2], match[3], match[4], parseFloat(hasAlpha && alphaChannel ? alphaChannel : 1.0)); | |
} else if ((match = reHex6Color.exec(value))) { | |
var colorInt = parseInt(match[1], 16); | |
result = [((colorInt & 0xFF0000) >> 16) / 255, ((colorInt & 0x00FF00) >> 8) / 255, (colorInt & 0x0000FF) / 255, 1.0]; | |
} else if ((match = reHex3Color.exec(value))) { | |
var hexString = "#" + [match[1], match[1], match[2], match[2], match[3], match[3]].join(""); | |
result = colorStringToVec4(hexString); | |
} else if (value.toLowerCase() in colorKeywords) { | |
result = colorStringToVec4(colorKeywords[value.toLowerCase()]); | |
} else if (value.toLowerCase() === "transparent") { | |
result = [0, 0, 0, 0]; | |
} else { | |
// Color keywords not yet implemented, ie "orange", return hot pink | |
return false; | |
} | |
return result; | |
} | |
function colorVecToString(vec4) { | |
return "rgba(" + (vec4[0] * 255) + ", " + (vec4[1] * 255) + ", " + (vec4[2] * 255) + ", " + parseFloat(vec4[3]) + ")"; | |
} | |
var colorKeywords = { | |
aliceblue: "#f0f8ff", | |
antiquewhite: "#faebd7", | |
aqua: "#00ffff", | |
aquamarine: "#7fffd4", | |
azure: "#f0ffff", | |
beige: "#f5f5dc", | |
bisque: "#ffe4c4", | |
black: "#000000", | |
blanchedalmond: "#ffebcd", | |
blue: "#0000ff", | |
blueviolet: "#8a2be2", | |
brown: "#a52a2a", | |
burlywood: "#deb887", | |
cadetblue: "#5f9ea0", | |
chartreuse: "#7fff00", | |
chocolate: "#d2691e", | |
coral: "#ff7f50", | |
cornflowerblue: "#6495ed", | |
cornsilk: "#fff8dc", | |
crimson: "#dc143c", | |
cyan: "#00ffff", | |
darkblue: "#00008b", | |
darkcyan: "#008b8b", | |
darkgoldenrod: "#b8860b", | |
darkgray: "#a9a9a9", | |
darkgreen: "#006400", | |
darkkhaki: "#bdb76b", | |
darkmagenta: "#8b008b", | |
darkolivegreen: "#556b2f", | |
darkorange: "#ff8c00", | |
darkorchid: "#9932cc", | |
darkred: "#8b0000", | |
darksalmon: "#e9967a", | |
darkseagreen: "#8fbc8f", | |
darkslateblue: "#483d8b", | |
darkslategray: "#2f4f4f", | |
darkturquoise: "#00ced1", | |
darkviolet: "#9400d3", | |
deeppink: "#ff1493", | |
deepskyblue: "#00bfff", | |
dimgray: "#696969", | |
dodgerblue: "#1e90ff", | |
firebrick: "#b22222", | |
floralwhite: "#fffaf0", | |
forestgreen: "#228b22", | |
fuchsia: "#ff00ff", | |
gainsboro: "#dcdcdc", | |
ghostwhite: "#f8f8ff", | |
gold: "#ffd700", | |
goldenrod: "#daa520", | |
gray: "#808080", | |
green: "#008000", | |
greenyellow: "#adff2f", | |
grey: "#808080", | |
honeydew: "#f0fff0", | |
hotpink: "#ff69b4", | |
indianred: "#cd5c5c", | |
indigo: "#4b0082", | |
ivory: "#fffff0", | |
khaki: "#f0e68c", | |
lavender: "#e6e6fa", | |
lavenderblush: "#fff0f5", | |
lawngreen: "#7cfc00", | |
lemonchiffon: "#fffacd", | |
lightblue: "#add8e6", | |
lightcoral: "#f08080", | |
lightcyan: "#e0ffff", | |
lightgoldenrodyellow: "#fafad2", | |
lightgrey: "#d3d3d3", | |
lightgreen: "#90ee90", | |
lightpink: "#ffb6c1", | |
lightsalmon: "#ffa07a", | |
lightseagreen: "#20b2aa", | |
lightskyblue: "#87cefa", | |
lightslategray: "#778899", | |
lightsteelblue: "#b0c4de", | |
lightyellow: "#ffffe0", | |
lime: "#00ff00", | |
limegreen: "#32cd32", | |
linen: "#faf0e6", | |
magenta: "#ff00ff", | |
maroon: "#800000", | |
mediumaquamarine: "#66cdaa", | |
mediumblue: "#0000cd", | |
mediumorchid: "#ba55d3", | |
mediumpurple: "#9370d8", | |
mediumseagreen: "#3cb371", | |
mediumslateblue: "#7b68ee", | |
mediumspringgreen: "#00fa9a", | |
mediumturquoise: "#48d1cc", | |
mediumvioletred: "#c71585", | |
midnightblue: "#191970", | |
mintcream: "#f5fffa", | |
mistyrose: "#ffe4e1", | |
moccasin: "#ffe4b5", | |
navajowhite: "#ffdead", | |
navy: "#000080", | |
oldlace: "#fdf5e6", | |
olive: "#808000", | |
olivedrab: "#6b8e23", | |
orange: "#ffa500", | |
orangered: "#ff4500", | |
orchid: "#da70d6", | |
palegoldenrod: "#eee8aa", | |
palegreen: "#98fb98", | |
paleturquoise: "#afeeee", | |
palevioletred: "#d87093", | |
papayawhip: "#ffefd5", | |
peachpuff: "#ffdab9", | |
peru: "#cd853f", | |
pink: "#ffc0cb", | |
plum: "#dda0dd", | |
powderblue: "#b0e0e6", | |
purple: "#800080", | |
red: "#ff0000", | |
rosybrown: "#bc8f8f", | |
royalblue: "#4169e1", | |
saddlebrown: "#8b4513", | |
salmon: "#fa8072", | |
sandybrown: "#f4a460", | |
seagreen: "#2e8b57", | |
seashell: "#fff5ee", | |
sienna: "#a0522d", | |
silver: "#c0c0c0", | |
skyblue: "#87ceeb", | |
slateblue: "#6a5acd", | |
slategray: "#708090", | |
snow: "#fffafa", | |
springgreen: "#00ff7f", | |
steelblue: "#4682b4", | |
tan: "#d2b48c", | |
teal: "#008080", | |
thistle: "#d8bfd8", | |
tomato: "#ff6347", | |
turquoise: "#40e0d0", | |
violet: "#ee82ee", | |
wheat: "#f5deb3", | |
white: "#ffffff", | |
whitesmoke: "#f5f5f5", | |
yellow: "#ffff00", | |
yellowgreen: "#9acd32" | |
}; | |
// Maintain drawing state params during gl.save and gl.restore. see saveDrawState() and restoreDrawState() | |
var drawState = {}, drawStateStack = []; | |
// A fast simple shallow clone | |
function cloneObject(obj) { | |
var target = {}; | |
for (var i in obj) { | |
if (obj.hasOwnProperty(i)) { | |
target[i] = obj[i]; | |
} | |
} | |
return target; | |
} | |
function saveDrawState() { | |
var bakedDrawState = { | |
fillStyle: [drawState.fillStyle[0], drawState.fillStyle[1], drawState.fillStyle[2], drawState.fillStyle[3]], | |
strokeStyle: [drawState.strokeStyle[0], drawState.strokeStyle[1], drawState.strokeStyle[2], drawState.strokeStyle[3]], | |
globalAlpha: drawState.globalAlpha, | |
globalCompositeOperation: drawState.globalCompositeOperation, | |
lineCap: drawState.lineCap, | |
lineJoin: drawState.lineJoin, | |
lineWidth: drawState.lineWidth, | |
miterLimit: drawState.miterLimit, | |
shadowColor: drawState.shadowColor, | |
shadowBlur: drawState.shadowBlur, | |
shadowOffsetX: drawState.shadowOffsetX, | |
shadowOffsetY: drawState.shadowOffsetY, | |
textAlign: drawState.textAlign, | |
font: drawState.font, | |
textBaseline: drawState.textBaseline | |
}; | |
drawStateStack.push(bakedDrawState); | |
} | |
function restoreDrawState() { | |
if (drawStateStack.length) { | |
drawState = drawStateStack.pop(); | |
} | |
} | |
// WebGL requires colors as a vector while Canvas2D sets colors as an rgba string | |
// These getters and setters store the original rgba string as well as convert to a vector | |
drawState.fillStyle = [0, 0, 0, 1]; // default black | |
Object.defineProperty(gl, "fillStyle", { | |
get: function() { return colorVecToString(drawState.fillStyle); }, | |
set: function(value) { | |
drawState.fillStyle = colorStringToVec4(value) || drawState.fillStyle; | |
} | |
}); | |
drawState.strokeStyle = [0, 0, 0, 1]; // default black | |
Object.defineProperty(gl, "strokeStyle", { | |
get: function() { return colorVecToString(drawState.strokeStyle); }, | |
set: function(value) { | |
drawState.strokeStyle = colorStringToVec4(value) || drawStyle.strokeStyle; | |
} | |
}); | |
// WebGL already has a lineWidth() function but Canvas2D requires a lineWidth property | |
// Store the original lineWidth() function for later use | |
gl.$lineWidth = gl.lineWidth; | |
drawState.lineWidth = 1.0; | |
Object.defineProperty(gl, "lineWidth", { | |
get: function() { return drawState.lineWidth; }, | |
set: function(value) { | |
gl.$lineWidth(value); | |
drawState.lineWidth = value; | |
} | |
}); | |
// Currently unsupported attributes and their default values | |
drawState.lineCap = "butt"; | |
Object.defineProperty(gl, "lineCap", { | |
get: function() { return drawState.lineCap; }, | |
set: function(value) { | |
drawState.lineCap = value; | |
} | |
}); | |
drawState.lineJoin = "miter"; | |
Object.defineProperty(gl, "lineJoin", { | |
get: function() { return drawState.lineJoin; }, | |
set: function(value) { | |
drawState.lineJoin = value; | |
} | |
}); | |
drawState.miterLimit = 10; | |
Object.defineProperty(gl, "miterLimit", { | |
get: function() { return drawState.miterLimit; }, | |
set: function(value) { | |
drawState.miterLimit = value; | |
} | |
}); | |
drawState.shadowOffsetX = 0; | |
Object.defineProperty(gl, "shadowOffsetX", { | |
get: function() { return drawState.shadowOffsetX; }, | |
set: function(value) { | |
drawState.shadowOffsetX = value; | |
} | |
}); | |
drawState.shadowOffsetY = 0; | |
Object.defineProperty(gl, "shadowOffsetY", { | |
get: function() { return drawState.shadowOffsetY; }, | |
set: function(value) { | |
drawState.shadowOffsetY = value; | |
} | |
}); | |
drawState.shadowBlur = 0; | |
Object.defineProperty(gl, "shadowBlur", { | |
get: function() { return drawState.shadowBlur; }, | |
set: function(value) { | |
drawState.shadowBlur = value; | |
} | |
}); | |
drawState.shadowColor = "rgba(0, 0, 0, 0.0)"; | |
Object.defineProperty(gl, "shadowColor", { | |
get: function() { return drawState.shadowColor; }, | |
set: function(value) { | |
drawState.shadowColor = value; | |
} | |
}); | |
drawState.font = "10px sans-serif"; | |
Object.defineProperty(gl, "font", { | |
get: function() { return drawState.font; }, | |
set: function(value) { | |
textCtx.font = value; | |
drawState.font = value; | |
} | |
}); | |
drawState.textAlign = "start"; | |
Object.defineProperty(gl, "textAlign", { | |
get: function() { return drawState.textAlign; }, | |
set: function(value) { | |
drawState.textAlign = value; | |
} | |
}); | |
drawState.textBaseline = "alphabetic"; | |
Object.defineProperty(gl, "textBaseline", { | |
get: function() { return drawState.textBaseline; }, | |
set: function(value) { | |
drawState.textBaseline = value; | |
} | |
}); | |
// This attribute will need to control global alpha of objects drawn. | |
drawState.globalAlpha = 1.0; | |
Object.defineProperty(gl, "globalAlpha", { | |
get: function() { return drawState.globalAlpha; }, | |
set: function(value) { | |
drawState.globalAlpha = value; | |
} | |
}); | |
// This attribute will need to set the gl.blendFunc mode | |
drawState.globalCompositeOperation = "source-over"; | |
Object.defineProperty(gl, "globalCompositeOperation", { | |
get: function() { return drawState.globalCompositeOperation; }, | |
set: function(value) { | |
drawState.globalCompositeOperation = value; | |
} | |
}); | |
// Need a solution for drawing text that isnt stupid slow | |
gl.fillText = function fillText(text, x, y) { | |
/* | |
textCtx.clearRect(0, 0, gl2d.canvas.width, gl2d.canvas.height); | |
textCtx.fillStyle = gl.fillStyle; | |
textCtx.fillText(text, x, y); | |
gl.drawImage(textCanvas, 0, 0); | |
*/ | |
}; | |
gl.strokeText = function strokeText() {}; | |
gl.measureText = function measureText() { return 1; }; | |
var tempCanvas = document.createElement('canvas'); | |
var tempCtx = tempCanvas.getContext('2d'); | |
gl.save = function save() { | |
gl2d.transform.pushMatrix(); | |
saveDrawState(); | |
}; | |
gl.restore = function restore() { | |
gl2d.transform.popMatrix(); | |
restoreDrawState(); | |
}; | |
gl.translate = function translate(x, y) { | |
gl2d.transform.translate(x, y); | |
}; | |
gl.rotate = function rotate(a) { | |
gl2d.transform.rotate(a); | |
}; | |
gl.scale = function scale(x, y) { | |
gl2d.transform.scale(x, y); | |
}; | |
gl.createImageData = function createImageData(width, height) { | |
return tempCtx.createImageData(width, height); | |
}; | |
gl.getImageData = function getImageData(x, y, width, height) { | |
var data = tempCtx.createImageData(width, height); | |
var buffer = new Uint8Array(width*height*4); | |
gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer); | |
var w=width*4, h=height; | |
for (var i=0, maxI=h/2; i<maxI; ++i) { | |
for (var j=0, maxJ=w; j<maxJ; ++j) { | |
var index1 = i * w + j; | |
var index2 = (h-i-1) * w + j; | |
data.data[index1] = buffer[index2]; | |
data.data[index2] = buffer[index1]; | |
} //for | |
} //for | |
return data; | |
}; | |
gl.putImageData = function putImageData(imageData, x, y) { | |
gl.drawImage(imageData, x, y); | |
}; | |
gl.transform = function transform(m11, m12, m21, m22, dx, dy) { | |
var m = gl2d.transform.m_stack[gl2d.transform.c_stack]; | |
m[0] *= m11; | |
m[1] *= m21; | |
m[2] *= dx; | |
m[3] *= m12; | |
m[4] *= m22; | |
m[5] *= dy; | |
m[6] = 0; | |
m[7] = 0; | |
}; | |
function sendTransformStack(sp) { | |
var stack = gl2d.transform.m_stack; | |
for (var i = 0, maxI = gl2d.transform.c_stack + 1; i < maxI; ++i) { | |
gl.uniformMatrix3fv(sp.uTransforms[i], false, stack[maxI-1-i]); | |
} //for | |
} | |
gl.setTransform = function setTransform(m11, m12, m21, m22, dx, dy) { | |
gl2d.transform.setIdentity(); | |
gl.transform.apply(this, arguments); | |
}; | |
gl.fillRect = function fillRect(x, y, width, height) { | |
var transform = gl2d.transform; | |
var shaderProgram = gl2d.initShaders(transform.c_stack+2,0); | |
gl.bindBuffer(gl.ARRAY_BUFFER, rectVertexPositionBuffer); | |
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 4, gl.FLOAT, false, 0, 0); | |
transform.pushMatrix(); | |
transform.translate(x, y); | |
transform.scale(width, height); | |
sendTransformStack(shaderProgram); | |
gl.uniform4f(shaderProgram.uColor, drawState.fillStyle[0], drawState.fillStyle[1], drawState.fillStyle[2], drawState.fillStyle[3]); | |
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); | |
transform.popMatrix(); | |
}; | |
gl.strokeRect = function strokeRect(x, y, width, height) { | |
var transform = gl2d.transform; | |
var shaderProgram = gl2d.initShaders(transform.c_stack + 2,0); | |
gl.bindBuffer(gl.ARRAY_BUFFER, rectVertexPositionBuffer); | |
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 4, gl.FLOAT, false, 0, 0); | |
transform.pushMatrix(); | |
transform.translate(x, y); | |
transform.scale(width, height); | |
sendTransformStack(shaderProgram); | |
gl.uniform4f(shaderProgram.uColor, drawState.strokeStyle[0], drawState.strokeStyle[1], drawState.strokeStyle[2], drawState.strokeStyle[3]); | |
gl.drawArrays(gl.LINE_LOOP, 0, 4); | |
transform.popMatrix(); | |
}; | |
gl.clearRect = function clearRect(x, y, width, height) {}; | |
var subPaths = []; | |
function SubPath(x, y) { | |
this.closed = false; | |
this.verts = [x, y, 0, 0]; | |
} | |
// Empty the list of subpaths so that the context once again has zero subpaths | |
gl.beginPath = function beginPath() { | |
subPaths.length = 0; | |
}; | |
// Mark last subpath as closed and create a new subpath with the same starting point as the previous subpath | |
gl.closePath = function closePath() { | |
if (subPaths.length) { | |
// Mark last subpath closed. | |
var prevPath = subPaths[subPaths.length -1], startX = prevPath.verts[0], startY = prevPath.verts[1]; | |
prevPath.closed = true; | |
// Create new subpath using the starting position of previous subpath | |
var newPath = new SubPath(startX, startY); | |
subPaths.push(newPath); | |
} | |
}; | |
// Create a new subpath with the specified point as its first (and only) point | |
gl.moveTo = function moveTo(x, y) { | |
subPaths.push(new SubPath(x, y)); | |
}; | |
gl.lineTo = function lineTo(x, y) { | |
if (subPaths.length) { | |
subPaths[subPaths.length -1].verts.push(x, y, 0, 0); | |
} else { | |
// Create a new subpath if none currently exist | |
gl.moveTo(x, y); | |
} | |
}; | |
gl.quadraticCurveTo = function quadraticCurveTo(cp1x, cp1y, x, y) {}; | |
gl.bezierCurveTo = function bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {}; | |
gl.arcTo = function arcTo() {}; | |
// Adds a closed rect subpath and creates a new subpath | |
gl.rect = function rect(x, y, w, h) { | |
gl.moveTo(x, y); | |
gl.lineTo(x + w, y); | |
gl.lineTo(x + w, y + h); | |
gl.lineTo(x, y + h); | |
gl.closePath(); | |
}; | |
gl.arc = function arc(x, y, radius, startAngle, endAngle, anticlockwise) {}; | |
function fillSubPath(index) { | |
var transform = gl2d.transform; | |
var shaderProgram = gl2d.initShaders(transform.c_stack + 2,0); | |
var subPath = subPaths[index]; | |
var verts = subPath.verts; | |
gl.bindBuffer(gl.ARRAY_BUFFER, pathVertexPositionBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW); | |
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 4, gl.FLOAT, false, 0, 0); | |
transform.pushMatrix(); | |
sendTransformStack(shaderProgram); | |
gl.uniform4f(shaderProgram.uColor, drawState.fillStyle[0], drawState.fillStyle[1], drawState.fillStyle[2], drawState.fillStyle[3]); | |
gl.drawArrays(gl.TRIANGLE_FAN, 0, verts.length/4); | |
transform.popMatrix(); | |
} | |
gl.fill = function fill() { | |
for(var i = 0; i < subPaths.length; i++) { | |
fillSubPath(i); | |
} | |
}; | |
function strokeSubPath(index) { | |
var transform = gl2d.transform; | |
var shaderProgram = gl2d.initShaders(transform.c_stack + 2,0); | |
var subPath = subPaths[index]; | |
var verts = subPath.verts; | |
gl.bindBuffer(gl.ARRAY_BUFFER, pathVertexPositionBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW); | |
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 4, gl.FLOAT, false, 0, 0); | |
transform.pushMatrix(); | |
sendTransformStack(shaderProgram); | |
gl.uniform4f(shaderProgram.uColor, drawState.strokeStyle[0], drawState.strokeStyle[1], drawState.strokeStyle[2], drawState.strokeStyle[3]); | |
if (subPath.closed) { | |
gl.drawArrays(gl.LINE_LOOP, 0, verts.length/4); | |
} else { | |
gl.drawArrays(gl.LINE_STRIP, 0, verts.length/4); | |
} | |
transform.popMatrix(); | |
} | |
gl.stroke = function stroke() { | |
for(var i = 0; i < subPaths.length; i++) { | |
strokeSubPath(i); | |
} | |
}; | |
gl.clip = function clip() {}; | |
gl.isPointInPath = function isPointInPath() {}; | |
gl.drawFocusRing = function drawFocusRing() {}; | |
var imageCache = [], textureCache = []; | |
function Texture(image) { | |
this.obj = gl.createTexture(); | |
this.index = textureCache.push(this); | |
imageCache.push(image); | |
// we may wish to consider tiling large images like this instead of scaling and | |
// adjust appropriately (flip to next texture source and tile offset) when drawing | |
if (image.width > gl2d.maxTextureSize || image.height > gl2d.maxTextureSize) { | |
var canvas = document.createElement("canvas"); | |
canvas.width = (image.width > gl2d.maxTextureSize) ? gl2d.maxTextureSize : image.width; | |
canvas.height = (image.height > gl2d.maxTextureSize) ? gl2d.maxTextureSize : image.height; | |
var ctx = canvas.getContext("2d"); | |
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); | |
image = canvas; | |
} | |
gl.bindTexture(gl.TEXTURE_2D, this.obj); | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, 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_MAG_FILTER, gl.LINEAR); | |
// Enable Mip mapping on power-of-2 textures | |
if (isPOT(image.width) && isPOT(image.height)) { | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); | |
gl.generateMipmap(gl.TEXTURE_2D); | |
} else { | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); | |
} | |
// Unbind texture | |
gl.bindTexture(gl.TEXTURE_2D, null); | |
} | |
gl.drawImage = function drawImage(image, a, b, c, d, e, f, g, h) { | |
var transform = gl2d.transform; | |
transform.pushMatrix(); | |
var sMask = shaderMask.texture; | |
var doCrop = false; | |
//drawImage(image, dx, dy) | |
if (arguments.length === 3) { | |
transform.translate(a, b); | |
transform.scale(image.width, image.height); | |
} | |
//drawImage(image, dx, dy, dw, dh) | |
else if (arguments.length === 5) { | |
transform.translate(a, b); | |
transform.scale(c, d); | |
} | |
//drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) | |
else if (arguments.length === 9) { | |
transform.translate(e, f); | |
transform.scale(g, h); | |
sMask = sMask|shaderMask.crop; | |
doCrop = true; | |
} | |
var shaderProgram = gl2d.initShaders(transform.c_stack, sMask); | |
var texture, cacheIndex = imageCache.indexOf(image); | |
if (cacheIndex !== -1) { | |
texture = textureCache[cacheIndex]; | |
} else { | |
texture = new Texture(image); | |
} | |
if (doCrop) { | |
gl.uniform4f(shaderProgram.uCropSource, a/image.width, b/image.height, c/image.width, d/image.height); | |
} | |
gl.bindBuffer(gl.ARRAY_BUFFER, rectVertexPositionBuffer); | |
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 4, gl.FLOAT, false, 0, 0); | |
gl.bindTexture(gl.TEXTURE_2D, texture.obj); | |
gl.activeTexture(gl.TEXTURE0); | |
gl.uniform1i(shaderProgram.uSampler, 0); | |
sendTransformStack(shaderProgram); | |
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); | |
transform.popMatrix(); | |
}; | |
}; | |
}(Math)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment