fugafacere
Version:
A pure-JS implementation of the W3C's Canvas-2D Context API that can run on top of either Expo Graphics or a browser WebGL context.
1,766 lines (1,485 loc) • 76.8 kB
JavaScript
'use strict';
import bezierCubicPoints from 'adaptive-bezier-curve';
import bezierQuadraticPoints from 'adaptive-quadratic-curve';
import parseCssFont from 'css-font-parser';
import DOMException from 'domexception';
import earcut from 'earcut';
import stringFormat from 'string-format';
import tess2 from 'tess2';
import { StrokeExtruder } from './StrokeExtruder';
import { getBuiltinFonts } from './builtinFonts';
import parseColor from './cssColorParser';
import { getEnvironment } from './environment';
import {
ShaderProgram,
patternShaderTxt,
patternShaderRepeatValues,
radialGradShaderTxt,
disjointRadialGradShaderTxt,
linearGradShaderTxt,
flatShaderTxt,
} from './shaders';
import { ImageData as _ImageData } from './utilityObjects';
import Vector from './vector';
const glm = require('gl-matrix');
export const ImageData = _ImageData;
// TODO: rather than setting vertexattribptr on every draw,
// create a separate vbo for coords vs pattern coords vs text coords
// and call once
// TODO: make sure styles don't get reapplied if they're already
// set
// TODO: use same tex coord attrib array for drawImage and fillText
function isValidCanvasImageSource(asset) {
const environment = getEnvironment();
if (asset === undefined) {
return false;
}
if (asset instanceof Expo2DContext) {
return true;
} else if (asset instanceof ImageData) {
return true;
} else {
if (
asset.hasOwnProperty('width') &&
asset.hasOwnProperty('height') &&
(asset.hasOwnProperty('localUri') || asset.hasOwnProperty('data'))
) {
return true;
}
if (environment === 'web' && 'nodeName' in asset) {
if (asset.nodeName.toLowerCase() === 'img' || asset.nodeName.toLowerCase() === 'canvas') {
return true;
}
}
}
return false;
}
export function cssToGlColor(cssStr) {
try {
return parseColor(cssStr);
} catch (e) {
return [];
}
}
function outerTangent(p0, r0, p1, r1) {
const d = Math.sqrt(Math.pow(p1[0] - p0[0], 2) + Math.pow(p1[1] - p0[1], 2));
const gamma = -Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
const beta = Math.asin((r1 - r0) / d);
const alpha = gamma - beta;
const angle = Math.PI / 2 - alpha;
const tanpt1 = [p0[0] + r0 * Math.cos(angle), p0[1] + r0 * Math.sin(angle)];
const tanpt2 = [p1[0] + r1 * Math.cos(angle), p1[1] + r1 * Math.sin(angle)];
//let tanm = (c2[1] - c1[1] + Math.sin(angle)*(c2[2] - c1[2])) / (c2[0] - c1[0] + Math.cos(angle)*(c2[2] - c1[2]));
const tanm = (tanpt2[1] - tanpt1[1]) / (tanpt2[0] - tanpt1[0]);
const tanb = tanpt1[1] - tanm * tanpt1[0];
const centerm = (p1[1] - p0[1]) / (p1[0] - p0[0]);
const centerb = p0[1] - centerm * p0[0];
const o = [0, 0];
if (!isFinite(tanm)) {
o[0] = tanpt1[0];
o[1] = o[0] * centerm + centerb;
} else if (!isFinite(centerm)) {
o[0] = p1[0];
o[1] = o[0] * tanm + tanb;
} else {
o[0] = (centerb - tanb) / (tanm - centerm);
o[1] = o[0] * tanm + tanb;
}
return o;
}
export class CanvasPattern {
constructor(pattern, repeat) {
this.pattern = pattern;
this.repeat = repeat;
}
}
export default class Expo2DContext {
/**************************************************
* Utility methods
**************************************************/
_initDrawingState() {
this.drawingState = {
mvMatrix: glm.mat4.create(),
fillStyle: '#000000',
strokeStyle: '#000000',
lineWidth: 1,
lineCap: 'butt',
lineJoin: 'miter',
miterLimit: 10,
strokeDashes: [],
strokeDashOffset: 0,
// TODO: figure out directionality/font size/other css tweakability
font_css: '10px sans-serif',
font_parsed: null,
font_resources: null,
textAlign: 'start',
textBaseline: 'alphabetic',
globalAlpha: 1.0,
clippingPaths: [],
};
this.drawingStateStack = [];
this._invMvMatrix = null;
this.stencilsEnabled = false;
this.pMatrix = glm.mat4.create();
this.strokeExtruder = new StrokeExtruder();
this._updateStrokeExtruderState();
this.beginPath();
}
_updateStrokeExtruderState() {
Object.assign(this.strokeExtruder, {
thickness: this.drawingState.lineWidth,
cap: this.drawingState.lineCap,
join: this.drawingState.lineJoin,
miterLimit: this.drawingState.miterLimit,
dashList: this.drawingState.strokeDashes,
dashOffset: this.drawingState.strokeDashOffset,
});
}
_getInvMvMatrix() {
if (this._invMvMatrix == null) {
this._invMvMatrix = glm.mat4.create();
glm.mat4.invert(this._invMvMatrix, this.drawingState.mvMatrix);
}
return this._invMvMatrix;
}
_updateMatrixUniforms() {
const gl = this.gl;
this._invMvMatrix = null;
if (this.activeShaderProgram != null) {
gl.uniformMatrix4fv(this.activeShaderProgram.uniforms['uPMatrix'], false, this.pMatrix);
gl.uniformMatrix4fv(
this.activeShaderProgram.uniforms['uMVMatrix'],
false,
this.drawingState.mvMatrix
);
if ('uiMVMatrix' in this.activeShaderProgram.uniforms) {
gl.uniformMatrix4fv(
this.activeShaderProgram.uniforms['uiMVMatrix'],
false,
this._getInvMvMatrix()
);
}
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], false);
}
}
_updateClippingRegion() {
const gl = this.gl;
if (this.drawingState.clippingPaths.length === 0) {
gl.disable(gl.STENCIL_TEST);
this.stencilsEnabled = false;
} else {
if (!this.stencilsEnabled) {
gl.enable(gl.STENCIL_TEST);
this.stencilsEnabled = true;
}
// TODO: can this be done incrementally (eg, across clip() calls)?
gl.colorMask(false, false, false, false);
gl.stencilMask(0xff);
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], true);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
// TODO: this shouldn't have to get called here:
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aVertexPosition'],
2,
gl.FLOAT,
false,
0,
0
);
/// Current procedure:
/// (TODO: clean up this comment)
/// - intersected buffer on bit 0
/// - workspace on bit 1
/// - clear bit 0 to all 1s
/// per clipping path:
/// - build non-0 clipping path on bit 1 with INVERT
/// - draw full-screen rect, stencil test EQUAL 3, set stencil value to 1 on pass and 0 on fail
/// - finally, set test to EQUAL 1 / KEEP
/// if this works, i am a gl stencil witch
for (let i = 0; i < this.drawingState.clippingPaths.length; i++) {
gl.stencilMask(0x2);
gl.clear(gl.STENCIL_BUFFER_BIT); // TODO: Is there any way to be more clever with the algorithm to avoid this clear?
gl.stencilFunc(gl.ALWAYS, 0, 0xff);
gl.stencilOp(gl.INVERT, gl.INVERT, gl.INVERT);
const triangles = this.drawingState.clippingPaths[i];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(triangles), gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, triangles.length / 2);
gl.stencilMask(0x3);
gl.stencilFunc(gl.EQUAL, 0x3, 0x3);
gl.stencilOp(gl.ZERO, gl.KEEP, gl.KEEP);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0,
0,
gl.drawingBufferWidth,
0,
gl.drawingBufferWidth,
gl.drawingBufferHeight,
0,
0,
0,
gl.drawingBufferHeight,
gl.drawingBufferWidth,
gl.drawingBufferHeight,
]),
gl.STATIC_DRAW
);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
// Change draw target back to framebuffer and set up stencilling
gl.colorMask(true, true, true, true);
gl.stencilMask(0x00);
gl.stencilFunc(gl.EQUAL, 1, 1);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], false);
}
}
/**************************************************
* Pixel data methods
**************************************************/
createImageData() {
let sw;
let sh;
/* eslint-disable prefer-rest-params */
if (arguments.length === 1) {
if (!(arguments[0] instanceof ImageData)) {
throw new TypeError('Bad imagedata');
}
sw = arguments[0].width;
sh = arguments[0].height;
} else if (arguments.length === 2) {
sw = arguments[0];
sh = arguments[1];
sw = Math.floor(Math.abs(sw));
sh = Math.floor(Math.abs(sh));
} else {
throw new TypeError();
}
if (!isFinite(sw) || !isFinite(sh)) {
throw new TypeError('Bad dimensions');
}
if (!(this instanceof Expo2DContext)) {
throw new TypeError('Bad object instance');
}
if (sw === 0 || sh === 0) {
throw new DOMException('Bad dimensions', 'IndexSizeError');
}
return new ImageData(sw, sh);
}
getImageData(sx, sy, sw, sh) {
const gl = this.gl;
if (arguments.length !== 4) throw new TypeError();
if (sw < 0) {
sx += sw;
sw = -sw;
}
if (sh < 0) {
sy += sh;
sh = -sh;
}
if (!isFinite(sx) || !isFinite(sy) || !isFinite(sw) || !isFinite(sh)) {
throw new TypeError('Bad geometry');
}
if (this.environment === 'expo' && !this.renderWithOffscreenBuffer) {
console.log(
'WARNING: getImageData() may fail when renderWithOffscreenBuffer param is set to false'
);
}
if (this.environment === 'web' && !gl.getContextAttributes()['preserveDrawingBuffer']) {
console.log(
"WARNING: getImageData() may fail when the underlying GL context's preserveDrawingBuffer attribute is not set to true"
);
}
sx = Math.floor(sx);
sy = Math.floor(sy);
sw = Math.floor(sw);
sh = Math.floor(sh);
if (sw === 0 || sh === 0) {
throw new DOMException('Bad geometry', 'IndexSizeError');
}
// This flush isn't technically necessary because readPixels should cause
// an expo gl flush anyway, but here just in case more operations get added
// to Expo2DContext flush in the future:
this.flush();
const imageDataObj = new ImageData(sw, sh);
const rawTexData = new this._framebuffer_format.typed_array(sw * sh * 4);
const flip_y = this._framebuffer_format.origin === 'internal';
gl.readPixels(
sx,
flip_y ? gl.drawingBufferHeight - sh - sy : sy,
sw,
sh,
gl.RGBA,
this._framebuffer_format.readpixels_type,
rawTexData
);
// Undo premultiplied alpha
// (TODO: is there any way to do this with the GPU??)
for (let y = 0; y < imageDataObj.height; y += 1) {
const src_base = y * imageDataObj.width * 4;
const dst_base = (flip_y ? imageDataObj.height - y - 1 : y) * imageDataObj.width * 4;
for (let i = 0; i < imageDataObj.width * 4; i += 4) {
const src = src_base + i;
const dst = dst_base + i;
imageDataObj.data[dst + 0] = Math.floor(
(rawTexData[src + 0] / rawTexData[src + 3]) * 256.0
);
imageDataObj.data[dst + 1] = Math.floor(
(rawTexData[src + 1] / rawTexData[src + 3]) * 256.0
);
imageDataObj.data[dst + 2] = Math.floor(
(rawTexData[src + 2] / rawTexData[src + 3]) * 256.0
);
imageDataObj.data[dst + 3] = Math.floor(
(rawTexData[src + 3] / this._framebuffer_format.max_alpha) * 256.0
);
}
}
return imageDataObj;
}
putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
const gl = this.gl;
let asset;
let typeError = '';
if (imagedata instanceof Expo2DContext) {
// TODO: in browsers support canvas tags too
asset = this._assetFromContext(imagedata);
} else if (imagedata instanceof ImageData) {
asset = {
width: imagedata.width,
height: imagedata.height,
data: new Uint8Array(imagedata.data.buffer),
};
} else {
typeError = 'Bad imagedata';
}
if (!isFinite(dx) || !isFinite(dy)) {
typeError = 'Bad dx/dy';
}
if (!isFinite(dirtyX)) {
if (arguments.length >= 4) {
typeError = 'Bad dirtyX';
}
dirtyX = 0;
}
if (!isFinite(dirtyY)) {
if (arguments.length >= 5) {
typeError = 'Bad dirtyY';
}
dirtyY = 0;
}
if (!isFinite(dirtyWidth)) {
if (arguments.length >= 6) {
typeError = 'Bad dirtyWidth';
}
dirtyWidth = asset.width;
}
if (!isFinite(dirtyHeight)) {
if (arguments.length >= 7) {
typeError = 'Bad dirtyHeight';
}
dirtyHeight = asset.height;
}
if (typeError !== '') {
throw new TypeError(typeError);
}
if (dirtyWidth < 0) {
dirtyX += dirtyWidth;
dirtyWidth = -dirtyWidth;
}
if (dirtyHeight < 0) {
dirtyY += dirtyHeight;
dirtyHeight = -dirtyHeight;
}
if (dirtyX < 0) {
dirtyWidth += dirtyX;
dirtyX = 0;
}
if (dirtyY < 0) {
dirtyHeight += dirtyY;
dirtyY = 0;
}
if (dirtyX + dirtyWidth > asset.width) {
dirtyWidth = asset.width - dirtyX;
}
if (dirtyY + dirtyHeight > asset.height) {
dirtyHeight = asset.height - dirtyY;
}
dx = Math.floor(dx);
dy = Math.floor(dy);
dirtyX = Math.floor(dirtyX);
dirtyY = Math.floor(dirtyY);
dirtyWidth = Math.floor(dirtyWidth);
dirtyHeight = Math.floor(dirtyHeight);
if (dirtyWidth <= 0 || dirtyHeight <= 0) {
return;
}
const pattern = this.createPattern(asset, 'src-rect');
this._applyStyle(pattern);
if (this.activeShaderProgram == null) {
return;
}
const minScreenX = dx + dirtyX;
const minScreenY = dy + dirtyY;
const maxScreenX = minScreenX + dirtyWidth;
const maxScreenY = minScreenY + dirtyHeight;
const minTexX = dirtyX / asset.width;
const minTexY = dirtyY / asset.height;
const maxTexX = minTexX + dirtyWidth / asset.width;
const maxTexY = minTexY + dirtyHeight / asset.height;
const vertices = [
minScreenX,
minScreenY,
minTexX,
minTexY,
minScreenX,
maxScreenY,
minTexX,
maxTexY,
maxScreenX,
minScreenY,
maxTexX,
minTexY,
maxScreenX,
maxScreenY,
maxTexX,
maxTexY,
];
gl.enableVertexAttribArray(this.activeShaderProgram.attributes['aTexCoord']);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aVertexPosition'],
2,
gl.FLOAT,
false,
4 * 2 * 2,
0
);
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aTexCoord'],
2,
gl.FLOAT,
false,
4 * 2 * 2,
4 * 2
);
if (this.stencilsEnabled) {
gl.disable(gl.STENCIL_TEST);
}
gl.uniform1f(this.activeShaderProgram.uniforms['uGlobalAlpha'], 1.0);
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ZERO, gl.ONE, gl.ZERO);
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], true);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], false);
gl.disableVertexAttribArray(this.activeShaderProgram.attributes['aTexCoord']);
if (this.stencilsEnabled) {
gl.enable(gl.STENCIL_TEST);
}
this._applyCompositingState();
}
/**************************************************
* Image methods
**************************************************/
drawImage() {
const gl = this.gl;
const asset = arguments[0];
if (typeof asset !== 'object' || asset === null || !isValidCanvasImageSource(asset)) {
throw new TypeError('Bad asset');
}
if (asset.width === 0 || asset.height === 0) {
// Zero-sized asset image causes DOMException
throw new DOMException('Bad source rectangle', 'InvalidStateError');
}
let sx = 0;
let sy = 0;
let sw = 1;
let sh = 1;
let dx;
let dy;
let dw;
let dh;
/* eslint-disable prefer-rest-params */
if (arguments.length === 3) {
dx = arguments[1];
dy = arguments[2];
dw = asset.width;
dh = asset.height;
} else if (arguments.length === 5) {
dx = arguments[1];
dy = arguments[2];
dw = arguments[3];
dh = arguments[4];
} else if (arguments.length === 9) {
sx = arguments[1] / asset.width;
sy = arguments[2] / asset.height;
sw = arguments[3] / asset.width;
sh = arguments[4] / asset.height;
dx = arguments[5];
dy = arguments[6];
dw = arguments[7];
dh = arguments[8];
} else {
throw new TypeError();
}
if (
!isFinite(dx) ||
!isFinite(dy) ||
!isFinite(dw) ||
!isFinite(dh) ||
!isFinite(sx) ||
!isFinite(sy) ||
!isFinite(sw) ||
!isFinite(sh)
) {
return;
}
if (sw === 0 || sh === 0) {
// Zero-sized source rect specified by the programmer is A-OK :P
return;
}
// TODO: the shader clipping method for source rectangles that are
// out of bounds relies on BlendFunc being set to SRC_ALPHA/SRC_ONE_MINUS_ALPHA
// if we can't rely on that, we'll have to clip beforehand by messing
// with rectangle dimensions
const dxmin = Math.min(dx, dx + dw);
const dxmax = Math.max(dx, dx + dw);
const dymin = Math.min(dy, dy + dh);
const dymax = Math.max(dy, dy + dh);
const sxmin = Math.min(sx, sx + sw);
const sxmax = Math.max(sx, sx + sw);
const symin = Math.min(sy, sy + sh);
const symax = Math.max(sy, sy + sh);
const vertices = [
dxmin,
dymin,
sxmin,
symin,
dxmin,
dymax,
sxmin,
symax,
dxmax,
dymin,
sxmax,
symin,
dxmax,
dymax,
sxmax,
symax,
];
const pattern = this.createPattern(asset, 'src-rect');
this._applyStyle(pattern);
if (this.activeShaderProgram == null) {
return;
}
gl.enableVertexAttribArray(this.activeShaderProgram.attributes['aTexCoord']);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aVertexPosition'],
2,
gl.FLOAT,
false,
4 * 2 * 2,
0
);
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aTexCoord'],
2,
gl.FLOAT,
false,
4 * 2 * 2,
4 * 2
);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
gl.disableVertexAttribArray(this.activeShaderProgram.attributes['aTexCoord']);
}
/**************************************************
* Text methods
**************************************************/
_getTextAlignmentOffset(text) {
let halign = this.drawingState.textAlign;
if (halign === 'start') {
halign = 'left';
}
if (halign === 'end') {
halign = 'right';
}
const textWidth = this.measureText(text).width;
if (halign === 'right') {
return textWidth;
} else if (halign === 'center') {
return textWidth * 0.5;
} else {
return 0;
}
}
_getTextBaselineOffset(text) {
const valign = this.drawingState.textBaseline;
const font = this.drawingState.font_resources;
if (valign === 'alphabetic') {
return font.common.base;
} else if (valign === 'top') {
return 0;
} else if (valign === 'bottom') {
// TODO
} else if (valign === 'ideographic') {
// TODO: this isn't technically correct,
// but not sure if there is any way to
// actually do it with the BMFont format:
return font.common.base;
} else if (valign === 'hanging') {
// TODO
}
return 0;
}
_prepareText(text, x, y, xscale, geometry) {
// TODO: directionality
const font = this.drawingState.font_resources;
xscale *= this.drawingState.font_parsed['size-scalar'];
const yscale = this.drawingState.font_parsed['size-scalar'];
let xskew = 0;
if (
this.drawingState.font_parsed['font-style'] === 'italic' ||
this.drawingState.font_parsed['font-style'] === 'oblique'
) {
xskew = (font.chars['M'].xadvance / 4) * xscale;
}
const small_caps = this.drawingState.font_parsed['font-variant'] === 'small-caps';
text = text.replace(/\s/g, ' ');
let space_width;
if (font.chars[' ']) {
space_width = font.chars[' '].xadvance * xscale;
} else {
space_width = (font.chars['M'].xadvance / 2) * xscale;
}
let pen_x = x;
const pen_y = y;
for (let i = 0; i < text.length; i++) {
if (text[i] === ' ') {
pen_x += space_width * xscale;
}
let glyph = font.chars[text[i]];
let smallcap_scale = 1.0;
if (small_caps && text[i].toLowerCase() === text[i]) {
glyph = font.chars[text[i].toUpperCase()];
smallcap_scale = 0.75;
}
if (!glyph) {
continue;
// TODO: what to actually do??
}
if (geometry) {
const x1 = pen_x + glyph.xoffset * xscale * smallcap_scale;
let y1 = pen_y + glyph.yoffset * yscale * smallcap_scale;
const x2 = x1 + glyph.width * xscale * smallcap_scale;
let y2 = y1 + glyph.height * yscale * smallcap_scale;
if (small_caps) {
const smallcap_offset = (y2 - y1) / smallcap_scale - (y2 - y1);
y1 += smallcap_offset;
y2 += smallcap_offset;
}
geometry.push(
x1 + xskew,
y1,
glyph.u1,
glyph.v1,
glyph.page,
x2 + xskew,
y1,
glyph.u2,
glyph.v1,
glyph.page,
x1,
y2,
glyph.u1,
glyph.v2,
glyph.page,
x2 + xskew,
y1,
glyph.u2,
glyph.v1,
glyph.page,
x2,
y2,
glyph.u2,
glyph.v2,
glyph.page,
x1,
y2,
glyph.u1,
glyph.v2,
glyph.page
);
}
pen_x += (glyph.xadvance + font.info.spacing[0]) * xscale * smallcap_scale;
// TODO: make sure this is right:
if (i < text.length - 1) {
if (text[i] in font.kernings && text[i + 1] in font.kernings[text[i]]) {
pen_x += font.kernings[text[i]][text[i + 1]] * xscale;
}
}
}
return pen_x - x;
}
measureText(text) {
if (arguments.length !== 1) throw new TypeError();
return { width: this._prepareText(text, 0, 0, 1) };
}
async initializeText() {
if (arguments.length !== 0) throw new TypeError();
const promises = [];
const font_objects = Object.values(this.builtinFonts);
for (let i = 0; i < font_objects.length; i++) {
if (font_objects[i] != null) {
promises.push(font_objects[i].await_assets());
}
}
await Promise.all(promises);
}
_drawText(text, x, y, maxWidth, strokeWidth) {
const gl = this.gl;
const font = this.drawingState.font_resources;
if (font === null) {
throw new ReferenceError('Font system is not initialized (await initializeText())');
}
if (maxWidth !== undefined && !isFinite(maxWidth)) {
return;
}
this._applyStyle(this.drawingState.fillStyle);
if (this.activeShaderProgram == null) {
return;
}
gl.enableVertexAttribArray(this.activeShaderProgram.attributes['aTextPageCoord']);
gl.uniform1i(this.activeShaderProgram.uniforms['uTextEnabled'], 1);
gl.uniform1f(this.activeShaderProgram.uniforms['uTextStrokeWidth'], strokeWidth);
if (this.drawingState.font_parsed['font-weight'] === 'bold') {
gl.uniform1f(
this.activeShaderProgram.uniforms['uTextDistanceFieldThreshold'],
font.info.thresholds.bold
);
} else if (this.drawingState.font_parsed['font-weight'] === 'bolder') {
gl.uniform1f(
this.activeShaderProgram.uniforms['uTextDistanceFieldThreshold'],
font.info.thresholds.bolder
);
} else if (this.drawingState.font_parsed['font-weight'] === 'lighter') {
gl.uniform1f(
this.activeShaderProgram.uniforms['uTextDistanceFieldThreshold'],
font.info.thresholds.lighter
);
} else {
gl.uniform1f(
this.activeShaderProgram.uniforms['uTextDistanceFieldThreshold'],
font.info.thresholds.normal
);
}
const geometry = [];
let xscale = 1;
if (maxWidth !== undefined) {
const textWidth = this.measureText(text).width;
if (textWidth > maxWidth) {
xscale = maxWidth / textWidth;
}
}
x -= this._getTextAlignmentOffset(text) * xscale;
y -= this._getTextBaselineOffset(text);
this._prepareText(text, x, y, xscale, geometry);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D_ARRAY, font.textures);
gl.uniform1i(this.activeShaderProgram.uniforms['uTextPages'], 1);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(geometry), gl.STATIC_DRAW);
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aVertexPosition'],
2,
gl.FLOAT,
false,
4 * 5,
0
);
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aTextPageCoord'],
3,
gl.FLOAT,
false,
4 * 5,
4 * 2
);
gl.drawArrays(gl.TRIANGLES, 0, geometry.length / 5);
gl.disableVertexAttribArray(this.activeShaderProgram.attributes['aTextPageCoord']);
gl.uniform1i(this.activeShaderProgram.uniforms['uTextEnabled'], 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.nullTextPage);
}
fillText(text, x, y, maxWidth) {
if (arguments.length !== 3 && arguments.length !== 4) throw new TypeError();
this._drawText(text, x, y, maxWidth, -1);
}
strokeText(text, x, y, maxWidth) {
if (arguments.length !== 3 && arguments.length !== 4) throw new TypeError();
// TODO: how to actually map lineWidth to distance field thresholds??
// TODO: scale width with mvmatrix? or does texture scaling already take care of that?
let shaderStrokeWidth = this.drawingState.lineWidth / 7.0;
shaderStrokeWidth /= this.drawingState.font_parsed['size-scalar'];
this._drawText(text, x, y, maxWidth, shaderStrokeWidth);
}
/**************************************************
* Rect methods
**************************************************/
clearRect(x, y, w, h) {
if (arguments.length !== 4) throw new TypeError();
const gl = this.gl;
if (!isFinite(x) || !isFinite(y) || !isFinite(w) || !isFinite(h)) {
return;
}
const old_fill_style = this.drawingState.fillStyle;
const old_global_alpha = this.drawingState.globalAlpha;
gl.blendFunc(gl.SRC_ALPHA, gl.ZERO);
this.drawingState.fillStyle = 'rgba(0,0,0,0)';
this.fillRect(x, y, w, h);
this.drawingState.fillStyle = old_fill_style;
this.drawingState.globalAlpha = old_global_alpha;
this._applyCompositingState();
}
fillRect(x, y, w, h) {
if (arguments.length !== 4) throw new TypeError();
if (!isFinite(x) || !isFinite(y) || !isFinite(w) || !isFinite(h)) {
return;
}
const gl = this.gl;
this._applyStyle(this.drawingState.fillStyle);
if (this.activeShaderProgram == null) {
return;
}
const vertices = [x, y, x, y + h, x + w, y, x + w, y + h];
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aVertexPosition'],
2,
gl.FLOAT,
false,
0,
0
);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
strokeRect(x, y, w, h) {
if (arguments.length !== 4) throw new TypeError();
if (!isFinite(x) || !isFinite(y) || !isFinite(w) || !isFinite(h)) {
return;
}
const gl = this.gl;
this._applyStyle(this.drawingState.strokeStyle);
if (this.activeShaderProgram == null) {
return;
}
const topLeft = this._getTransformedPt(x, y);
const bottomRight = this._getTransformedPt(x + w, y + h);
let polyline;
let oldLineCap;
if (w === 0 || h === 0) {
oldLineCap = this.lineCap;
this.lineCap = 'butt';
polyline = [topLeft[0], topLeft[1], bottomRight[0], bottomRight[1]];
} else {
polyline = [
topLeft[0],
topLeft[1],
bottomRight[0],
topLeft[1],
bottomRight[0],
bottomRight[1],
topLeft[0],
bottomRight[1],
];
}
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], true);
this.strokeExtruder.closed = true;
this.strokeExtruder.mvMatrix = this.drawingState.mvMatrix;
this.strokeExtruder.invMvMatrix = this._getInvMvMatrix();
const vertices = this.strokeExtruder.build(polyline);
this._drawStenciled(vertices);
if (w === 0 || h === 0) {
this.lineCap = oldLineCap;
}
}
/**************************************************
* Path methods
**************************************************/
beginPath() {
if (arguments.length !== 0) throw new TypeError();
this.subpaths = [[]];
this.subpathsModified = true;
this.currentSubpath = this.subpaths[0];
this.currentSubpath.closed = false;
}
closePath() {
if (arguments.length !== 0) throw new TypeError();
if (this.currentSubpath.length >= 2) {
this.currentSubpath.closed = true;
const baseIdx = this.currentSubpath.length - 2;
// Note that this is almost moveTo() verbatim, except it doesn't
// apply (or in this case, reapply) the transformation matrix to the
// close point
this.currentSubpath = [];
this.currentSubpath.closed = false;
this.subpathsModified = true;
this.subpaths.push(this.currentSubpath);
this.currentSubpath.push(this.currentSubpath[baseIdx]);
this.currentSubpath.push(this.currentSubpath[baseIdx + 1]);
}
}
_pathTriangles(path) {
if (this.subpathsModified) {
const triangles = [];
const prunedSubpaths = [];
for (let i = 0; i < this.subpaths.length; i++) {
const subpath = this.subpaths[i];
if (subpath.length <= 4) {
continue;
}
prunedSubpaths.push(subpath);
}
// TODO: be smarter about tesselator selection
if (this.fastFillTesselation) {
for (let i = 0; i < prunedSubpaths.length; i++) {
const subpath = prunedSubpaths[i];
const triangleIndices = earcut(subpath, null);
for (let i = 0; i < triangleIndices.length; i++) {
triangles.push(subpath[triangleIndices[i] * 2]);
triangles.push(subpath[triangleIndices[i] * 2 + 1]);
}
}
} else {
const result = tess2.tesselate({
contours: prunedSubpaths,
windingRule: tess2.WINDING_NONZERO,
elementType: tess2.POLYGONS,
polySize: 3,
vertexSize: 2,
});
for (let i = 0; i < result.elements.length; i++) {
const vertexBaseIdx = result.elements[i] * 2;
triangles.push(result.vertices[vertexBaseIdx]);
triangles.push(result.vertices[vertexBaseIdx + 1]);
}
}
this.subpaths.triangles = triangles;
this.subpathsModified = false;
}
return this.subpaths.triangles;
}
_ensureStartPath(x, y) {
if (this.currentSubpath.length === 0) {
const tPt = this._getTransformedPt(x, y);
this.currentSubpath.push(tPt[0]);
this.currentSubpath.push(tPt[1]);
return false;
} else {
return true;
}
}
isPointInPath(x, y) {
if (arguments.length !== 2) throw new TypeError();
if (!isFinite(x) || !isFinite(y)) {
return false;
}
const tPt = [x, y];
// TODO: is this approach more or less efficient than some
// other inclusion test that works on the untesselated polygon?
// investigate....
const triangles = this._pathTriangles(this.subpaths);
for (let j = 0; j < triangles.length; j += 6) {
// Point-in-triangle test adapted from:
// https://koozdra.wordpress.com/2012/06/27/javascript-is-point-in-triangle/
const v0 = [triangles[j + 4] - triangles[j], triangles[j + 5] - triangles[j + 1]];
const v1 = [triangles[j + 2] - triangles[j], triangles[j + 3] - triangles[j + 1]];
const v2 = [tPt[0] - triangles[j], tPt[1] - triangles[j + 1]];
const dot00 = v0[0] * v0[0] + v0[1] * v0[1];
const dot01 = v0[0] * v1[0] + v0[1] * v1[1];
const dot02 = v0[0] * v2[0] + v0[1] * v2[1];
const dot11 = v1[0] * v1[0] + v1[1] * v1[1];
const dot12 = v1[0] * v2[0] + v1[1] * v2[1];
const invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
const u = (dot11 * dot02 - dot01 * dot12) * invDenom;
const v = (dot00 * dot12 - dot01 * dot02) * invDenom;
if (u >= 0 && v >= 0 && u + v <= 1) {
return true;
}
}
return false;
}
clip() {
if (arguments.length !== 0) throw new TypeError();
const newClipPoly = this._pathTriangles(this.subpaths);
this.drawingState.clippingPaths.push(newClipPoly);
this._updateClippingRegion();
}
fill() {
if (arguments.length !== 0) throw new TypeError();
const gl = this.gl;
this._applyStyle(this.drawingState.fillStyle);
if (this.activeShaderProgram == null) {
return;
}
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], true);
const triangles = this._pathTriangles(this.subpaths);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(triangles), gl.STATIC_DRAW);
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aVertexPosition'],
2,
gl.FLOAT,
false,
0,
0
);
gl.drawArrays(gl.TRIANGLES, 0, triangles.length / 2);
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], false);
}
_drawStenciled(vertices) {
const gl = this.gl;
if (!this.stencilsEnabled) {
gl.enable(gl.STENCIL_TEST);
}
gl.stencilMask(0x2); // Use bit 1, as bit 0 stores the clipping bounds
gl.colorMask(false, false, false, false);
gl.clear(gl.STENCIL_BUFFER_BIT); // Clear bit 1 to '0'
gl.stencilFunc(gl.ALWAYS, 0xff, 0xff);
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
this._applyStyle('black');
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], true);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(
this.activeShaderProgram.attributes['aVertexPosition'],
2,
gl.FLOAT,
false,
0,
0
);
gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 2);
gl.stencilMask(0x00);
gl.colorMask(true, true, true, true);
gl.stencilFunc(gl.EQUAL, 3, 0xff);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
this._applyStyle(this.drawingState.strokeStyle);
if (this.activeShaderProgram == null) {
return;
}
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], true);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0,
0,
gl.drawingBufferWidth,
0,
gl.drawingBufferWidth,
gl.drawingBufferHeight,
0,
0,
0,
gl.drawingBufferHeight,
gl.drawingBufferWidth,
gl.drawingBufferHeight,
]),
gl.STATIC_DRAW
);
gl.drawArrays(gl.TRIANGLES, 0, 6);
if (!this.stencilsEnabled) {
gl.disable(gl.STENCIL_TEST);
} else {
// Set things back to normal for clipping system
// TODO: decompose this and the same code at the bottom of _updateClippingRegion()
gl.stencilFunc(gl.EQUAL, 1, 1);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
}
}
stroke() {
if (arguments.length !== 0) throw new TypeError();
const vertices = [];
for (let i = 0; i < this.subpaths.length; i++) {
const subpath = this.subpaths[i];
if (subpath.length === 0) {
continue;
}
this.strokeExtruder.closed = subpath.closed || false;
this.strokeExtruder.mvMatrix = this.drawingState.mvMatrix;
this.strokeExtruder.invMvMatrix = this._getInvMvMatrix();
vertices.push(...this.strokeExtruder.build(subpath));
}
// TODO: test integration with clipping
this._drawStenciled(vertices);
}
moveTo(x, y) {
if (arguments.length !== 2) throw new TypeError();
if (!isFinite(x) || !isFinite(y)) {
return;
}
this.currentSubpath = [];
this.currentSubpath.closed = false;
this.subpathsModified = true;
this.subpaths.push(this.currentSubpath);
const tPt = this._getTransformedPt(x, y);
this.currentSubpath.push(tPt[0]);
this.currentSubpath.push(tPt[1]);
}
lineTo(x, y) {
if (arguments.length !== 2) throw new TypeError();
if (!isFinite(x) || !isFinite(y)) {
y.valueOf(); // Call to make 2d.path.lineTo.nonfinite.details happy
return;
}
if (!this._ensureStartPath(x, y)) {
return;
}
const tPt = this._getTransformedPt(x, y);
if (
tPt[0] === this.currentSubpath[this.currentSubpath.length - 2] &&
tPt[1] === this.currentSubpath[this.currentSubpath.length - 1]
) {
return;
}
this.currentSubpath.push(tPt[0]);
this.currentSubpath.push(tPt[1]);
this.subpathsModified = true;
}
quadraticCurveTo(cpx, cpy, x, y) {
if (arguments.length !== 4) throw new TypeError();
if (!isFinite(cpx) || !isFinite(cpy) || !isFinite(x) || !isFinite(y)) {
return;
}
this._ensureStartPath(cpx, cpy);
const scale = 1; // TODO: ??
const vertsLen = this.currentSubpath.length;
const startPt = [this.currentSubpath[vertsLen - 2], this.currentSubpath[vertsLen - 1]];
const points = bezierQuadraticPoints(
startPt,
this._getTransformedPt(cpx, cpy),
this._getTransformedPt(x, y),
scale
);
for (let i = 0; i < points.length; i++) {
this.currentSubpath.push(points[i][0]);
this.currentSubpath.push(points[i][1]);
}
this.subpathsModified = true;
}
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
if (arguments.length !== 6) throw new TypeError();
if (
!isFinite(cp1x) ||
!isFinite(cp1y) ||
!isFinite(cp2x) ||
!isFinite(cp2y) ||
!isFinite(x) ||
!isFinite(y)
) {
return;
}
this._ensureStartPath(cp1x, cp1y);
// TODO: ensure start path?
const scale = 1; // TODO: ??
const vertsLen = this.currentSubpath.length;
const startPt = [this.currentSubpath[vertsLen - 2], this.currentSubpath[vertsLen - 1]];
const points = bezierCubicPoints(
startPt,
this._getTransformedPt(cp1x, cp1y),
this._getTransformedPt(cp2x, cp2y),
this._getTransformedPt(x, y),
scale
);
for (let i = 0; i < points.length; i++) {
this.currentSubpath.push(points[i][0]);
this.currentSubpath.push(points[i][1]);
}
this.subpathsModified = true;
}
rect(x, y, w, h) {
if (arguments.length !== 4) throw new TypeError();
if (!isFinite(x) || !isFinite(y) || !isFinite(w) || !isFinite(h)) {
return;
}
this.moveTo(x, y);
this.lineTo(x + w, y);
this.lineTo(x + w, y + h);
this.lineTo(x, y + h);
this.closePath();
this.moveTo(x, y);
}
arc(x, y, radius, startAngle, endAngle, counterclockwise) {
if (arguments.length !== 5 && arguments.length !== 6) throw new TypeError();
if (
!isFinite(x) ||
!isFinite(y) ||
!isFinite(radius) ||
!isFinite(startAngle) ||
!isFinite(endAngle)
) {
return;
}
if (radius < 0) {
throw new DOMException('Bad radius', 'IndexSizeError');
}
if (radius === 0) {
this.lineTo(x, y);
return;
}
if (startAngle === endAngle) {
return;
}
counterclockwise = counterclockwise || 0;
const centerPt = [x, y];
if (counterclockwise) {
const temp = startAngle;
startAngle = endAngle;
endAngle = temp;
}
if (startAngle > endAngle) {
endAngle =
(endAngle % (2 * Math.PI)) +
Math.trunc(startAngle / (2 * Math.PI)) * 2 * Math.PI +
2 * Math.PI;
}
if (endAngle > startAngle + 2 * Math.PI) {
endAngle = startAngle + 2 * Math.PI;
}
// Figure out angle increment based on the radius transformed along
// the most stretched axis, assuming anisotropy
const xformedOrigin = this._getTransformedPt(0, 0);
const xformedVectorAxis1 = this._getTransformedPt(radius, 0);
const xformedVectorAxis2 = this._getTransformedPt(0, radius);
const actualRadiusAxis1 = Math.sqrt(
Math.pow(xformedVectorAxis1[0] - xformedOrigin[0], 2) +
Math.pow(xformedVectorAxis1[1] - xformedOrigin[1], 2)
);
const actualRadiusAxis2 = Math.sqrt(
Math.pow(xformedVectorAxis2[0] - xformedOrigin[0], 2) +
Math.pow(xformedVectorAxis2[1] - xformedOrigin[1], 2)
);
const increment = (1 / Math.max(actualRadiusAxis1, actualRadiusAxis2)) * 10.0;
if (increment >= Math.abs(startAngle - endAngle)) {
return;
}
const pathStartIdx = this.currentSubpath.length;
const thetaPt = (theta) => {
const arcPt = this._getTransformedPt(
centerPt[0] + radius * Math.cos(theta),
centerPt[1] + radius * Math.sin(theta)
);
this.currentSubpath.push(arcPt[0]);
this.currentSubpath.push(arcPt[1]);
};
let theta = counterclockwise ? endAngle : startAngle;
while (true) {
thetaPt(theta);
if (!counterclockwise) {
theta += increment;
if (theta >= endAngle) {
theta = endAngle;
break;
}
} else {
theta -= increment;
if (theta <= startAngle) {
theta = startAngle;
break;
}
}
}
thetaPt(theta);
const pathEndIdx = this.currentSubpath.length;
this.currentSubpath._arcs = this.currentSubpath._arcs || [];
this.currentSubpath._arcs.push({
startIdx: pathStartIdx,
endIdx: pathEndIdx,
center: new Vector(centerPt[0], centerPt[1]),
radius,
});
this.subpathsModified = true;
}
arcTo(x1, y1, x2, y2, radius) {
if (arguments.length !== 5) throw new TypeError();
if (!isFinite(x1) || !isFinite(y1) || !isFinite(x2) || !isFinite(y2) || !isFinite(radius)) {
return;
}
if (radius < 0) {
throw new DOMException('Bad radius', 'IndexSizeError');
}
this._ensureStartPath(x1, y1);
this.subpathsModified = true;
const s = new Vector(
...this._getUntransformedPt(
this.currentSubpath[this.currentSubpath.length - 2],
this.currentSubpath[this.currentSubpath.length - 1]
)
);
const t0 = new Vector(x1, y1);
const t1 = new Vector(x2, y2);
// Check for colinearity
if (s.x * (t0.y - t1.y) + t0.x * (t1.y - s.y) + t1.x * (s.y - t0.y) === 0) {
this.lineTo(x1, y1);
return;
}
// For further explanation of the geometry here -
// https://math.stackexchange.com/questions/797828/calculate-center-of-circle-tangent-to-two-lines-in-space
const s_t0 = s.subtract(t0);
const s_t0_hat = s_t0.unit();
const t1_t0 = t1.subtract(t0);
const t1_t0_hat = t1_t0.unit();
// TODO: use Vector class's angleBetween()
const tangent_inner_angle = Math.acos(s_t0.dot(t1_t0) / (s_t0.length() * t1_t0.length()));
// // TODO: should be possible to reduce normalizations here?
const bisector = s_t0_hat.add(t1_t0_hat).divide(2).unit();
const radius_scalar = radius / Math.sin(tangent_inner_angle / 2);
let center_pt = bisector.multiply(radius_scalar);
let start_pt = s_t0_hat.multiply(center_pt.dot(s_t0_hat));
let end_pt = t1_t0_hat.multiply(center_pt.dot(t1_t0_hat));
// Shift center of calculations to center pt
center_pt = center_pt.add(t0);
start_pt = start_pt.add(t0).subtract(center_pt);
end_pt = end_pt.add(t0).subtract(center_pt);
const start_angle = Math.atan2(start_pt.y, start_pt.x);
const end_angle = Math.atan2(end_pt.y, end_pt.x);
// TODO: not sure how to choose cw/ccw here - this might require more thought
const s_t1 = center_pt.subtract(t1);
const t0_t1 = t0.subtract(t1);
const clockwise = s_t1.cross(t0_t1).z <= 0;
this.arc(center_pt.x, center_pt.y, radius, start_angle, end_angle, clockwise);
}
/**************************************************
* Transformation methods
**************************************************/
save() {
if (arguments.length !== 0) throw new TypeError();
this.drawingStateStack.push(this.drawingState);
this.drawingState = Object.assign({}, this.drawingState);
this.drawingState.strokeDashes = this.drawingState.strokeDashes.slice();
this.drawingState.clippingPaths = this.drawingState.clippingPaths.slice();
this.drawingState.mvMatrix = glm.mat4.clone(this.drawingState.mvMatrix);
// TODO: this will make gradients/patterns un-live, is that ok?
this.drawingState.fillStyle = this._cloneStyle(this.drawingState.fillStyle);
this.drawingState.strokeStyle = this._cloneStyle(this.drawingState.strokeStyle);
}
restore() {
if (arguments.length !== 0) throw new TypeError();
if (this.drawingStateStack.length > 0) {
this.drawingState = this.drawingStateStack.pop();
this._updateMatrixUniforms();
this._updateStrokeExtruderState();
this._updateClippingRegion();
}
}
scale(x, y) {
if (arguments.length !== 2) throw new TypeError();
for (let argIdx = 0; argIdx < arguments.length; argIdx++) {
if (!isFinite(arguments[argIdx])) return;
}
glm.mat4.scale(this.drawingState.mvMatrix, this.drawingState.mvMatrix, [x, y, 1.0]);
this._updateMatrixUniforms();
}
rotate(angle) {
if (arguments.length !== 1) throw new TypeError();
for (let argIdx = 0; argIdx < arguments.length; argIdx++) {
if (!isFinite(arguments[argIdx])) return;
}
glm.mat4.rotateZ(this.drawingState.mvMatrix, this.drawingState.mvMatrix, angle);
this._updateMatrixUniforms();
}
translate(x, y) {
if (arguments.length !== 2) throw new TypeError();
for (let argIdx = 0; argIdx < arguments.length; argIdx++) {
if (!isFinite(arguments[argIdx])) return;
}
glm.mat4.translate(this.drawingState.mvMatrix, this.drawingState.mvMatrix, [x, y, 0.0]);
this._updateMatrixUniforms();
}
transform(a, b, c, d, e, f) {
if (arguments.length !== 6) throw new TypeError();
for (let argIdx = 0; argIdx < arguments.length; argIdx++) {
if (!isFinite(arguments[argIdx])) return;
}
glm.mat4.multiply(
this.drawingState.mvMatrix,
this.drawingState.mvMatrix,
glm.mat4.fromValues(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, e, f, 0, 1)
);
this._updateMatrixUniforms();
}
setTransform(a, b, c, d, e, f) {
if (arguments.length !== 6) throw new TypeError();
for (let argIdx = 0; argIdx < arguments.length; argIdx++) {
if (!isFinite(arguments[argIdx])) return;
}
glm.mat4.identity(this.drawingState.mvMatrix);
this.transform(a, b, c, d, e, f);
}
_getTransformedPt(x, y) {
// TODO: creating a new vec3 every time seems potentially inefficient
const tPt = glm.vec3.fromValues(x, y, 0.0);
glm.vec3.transformMat4(tPt, tPt, this.drawingState.mvMatrix);
return [tPt[0], tPt[1]];
}
_getUntransformedPt(x, y) {
// TODO: creating a new vec3 every time seems potentially inefficient
const tPt = glm.vec3.fromValues(x, y, 0.0);
glm.vec3.transformMat4(tPt, tPt, this._getInvMvMatrix());
return [tPt[0], tPt[1]];
}
/**************************************************
* Style methods
**************************************************/
set globalAlpha(val) {
this.drawingState.globalAlpha = val;
}
get globalAlpha() {
return this.drawingState.globalAlpha;
}
set shadowColor(val) {
throw new SyntaxError('Property not supported');
}
get shadowColor() {
throw new SyntaxError('Property not supported');
}
set shadowBlur(val) {
throw new SyntaxError('Property not supported');
}
get shadowBlur() {
throw new SyntaxError('Property not supported');
}
set shadowOffsetX(val) {
throw new SyntaxError('Property not supported');
}
get shadowOffsetX() {
throw new SyntaxError('Property not supported');
}
set shadowOffsetY(val) {
throw new SyntaxError('Property not supported');
}
get shadowOffsetY() {
throw new SyntaxError('Property not supported');
}
set globalCompositeOperation(val) {
throw new SyntaxError('Property not supported');
}
get globalCompositeOperation() {
throw new SyntaxError('Property not supported');
}
// TODO: some day, use an off-screen rendering target to support
// these --
// set globalCompositeOperation(val) {
// let gl = this.gl;
// if (val == 'source-atop') {
// } else if (val == 'source-in') {
// } else if (val == 'source-out') {
// } else if (val == 'source-over') {
// } else if (val == 'destination-atop') {
// } else if (val == 'destination-in') {
// } else if (val == 'des