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.
2,616 lines • 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 == 'destination-out') {
// } else if (val == 'destination-over') {
// } else if (val == 'lighter') {
// } else if (val == 'copy') {
// } else if (val == 'xor') {
// } else {
// throw SyntaxError('Bad compositing mode');
// }
// }
set lineWidth(val) {
val = Number(val);
if (isFinite(val) && val > 0) {
this.strokeExtruder.thickness = val;
this.drawingState.lineWidth = val;
}
}
get lineWidth() {
return this.drawingState.lineWidth;
}
set lineCap(val) {
if (this.strokeExtruder.supportedCaps.indexOf(val) >= 0) {
this.strokeExtruder.cap = val;
this.drawingState.lineCap = val;
}
}
get lineCap() {
return this.strokeExtruder.cap;
}
set lineJoin(val) {
if (this.strokeExtruder.supportedJoins.indexOf(val) >= 0) {
this.strokeExtruder.join = val;
this.drawingState.lineJoin = val;
}
}
get lineJoin() {
return this.strokeExtruder.join;
}
set miterLimit(val) {
val = Number(val);
if (isFinite(val) && val > 0) {
this.strokeExtruder.miterLimit = val;
this.drawingState.miterLimit = val;
}
}
get miterLimit() {
return this.strokeExtruder.miterLimit;
}
setLineDash(segments) {
if (arguments.length !== 1) throw new TypeError();
for (let i = 0; i < segments.length; i++) {
if (!isFinite(segments[i]) || segments[i] < 0) {
return;
}
}
if (segments.length % 2 === 0) {
this.drawingState.strokeDashes = segments.slice();
} else {
this.drawingState.strokeDashes = segments.concat(segments);
}
this.strokeExtruder.dashList = this.drawingState.strokeDashes;
}
getLineDash() {
if (arguments.length !== 0) throw new TypeError();
return this.drawingState.strokeDashes.slice();
}
set lineDashOffset(val) {
this.drawingState.strokeDashOffset = val;
this.strokeExtruder.dashOffset = val;
}
get lineDashOffset() {
return this.drawingState.strokeDashOffset;
}
// TODO: only set stroke and fill when type is completely
// valid
_styleSetter(val) {
if (val === undefined || val === null) {
return undefined;
}
if (typeof val === 'string' || val instanceof String) {
if (cssToGlColor(val).length === 0) {
return undefined;
}
}
return val;
}
_styleGetter(val) {
const style = val;
if (typeof style === 'string' || style instanceof String) {
let color = cssToGlColor(style);
const alpha = color[3];
color = color.map((v) => v * 255);
color[3] = alpha;
if (alpha !== 1.0) {
color = color.map((v) => v.toString(10));
return 'rgba(' + color[0] + ', ' + color[1] + ', ' + color[2] + ', ' + color[3] + ')';
} else {
color = color.map((v) => v.toString(16).padStart(2, '0'));
return '#' + color[0] + color[1] + color[2];
}
}
return val;
}
set strokeStyle(val) {
val = this._styleSetter(val);
if (val === undefined) {
return;
}
this.drawingState.strokeStyle = val;
}
get strokeStyle() {
return this._styleGetter(this.drawingState.strokeStyle);
}
set fillStyle(val) {
val = this._styleSetter(val);
if (val === undefined) {
return;
}
this.drawingState.fillStyle = val;
}
get fillStyle() {
return this._styleGetter(this.drawingState.fillStyle);
}
set font(val) {
// TODO: needed to Array.from() wrap the indexof variables in parseCssFont:
this.drawingState.font_css = val;
const parsed_font = parseCssFont(val);
if (!('font-size' in parsed_font)) parsed_font['font-size'] = '10px';
if (!('font-family' in parsed_font)) parsed_font['font-family'] = ['sans-serif'];
if (!('font-weight' in parsed_font)) parsed_font['font-weight'] = 'normal';
if (!('font-style' in parsed_font)) parsed_font['font-style'] = 'normal';
if (!('font-variant' in parsed_font)) parsed_font['font-variant'] = 'normal';
this.drawingState.font_resources = null;
for (let i = 0; i < parsed_font['font-family'].length; i++) {
if (parsed_font['font-family'][i] in this.builtinFonts) {
if (this.builtinFonts[parsed_font['font-family'][i]] != null) {
this.drawingState.font_resources = this.builtinFonts[
parsed_font['font-family'][i]
].initialize_gl_resources(this.gl);
break;
}
}
}
if (this.drawingState.font_resources === null) {
throw new SyntaxError('Could not find supported font family');
}
// TODO: Make sure all these units are right --
// 1em = 12pt = 16px = 100%
const bmfont_size = this.drawingState.font_resources.info.size;
if (parsed_font['font-size'].endsWith('em')) {
const prop_size = parseInt(
parsed_font['font-size'].substring(0, parsed_font['font-size'].length - 2),
10
);
parsed_font['size-scalar'] = (prop_size * (1 / 12)) / bmfont_size;
} else if (parsed_font['font-size'].endsWith('px')) {
const prop_size = parseInt(
parsed_font['font-size'].substring(0, parsed_font['font-size'].length - 2),
10
);
parsed_font['size-scalar'] = (prop_size * 0.75) / bmfont_size;
} else if (parsed_font['font-size'].endsWith('pt')) {
const prop_size = parseInt(
parsed_font['font-size'].substring(0, parsed_font['font-size'].length - 2),
10
);
parsed_font['size-scalar'] = prop_size / bmfont_size;
} else if (parsed_font['font-size'].endsWith('%')) {
const prop_size = parseInt(
parsed_font['font-size'].substring(0, parsed_font['font-size'].length - 1),
10
);
parsed_font['size-scalar'] = (prop_size * (0.01 / 12)) / bmfont_size;
} else {
throw new SyntaxError("Unsupported units for font size '" + parsed_font['font-size'] + "'");
}
// TODO: rem?
// sanitization of other props, etc
this.drawingState.font_parsed = parsed_font;
}
get font() {
return this.drawingState.font_css;
}
set textAlign(val) {
if (['start', 'end', 'left', 'right', 'center'].includes(val)) {
this.drawingState.textAlign = val;
}
}
get textAlign() {
return this.drawingState.textAlign;
}
set textBaseline(val) {
if (['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'].includes(val)) {
this.drawingState.textBaseline = val;
}
}
get textBaseline() {
return this.drawingState.textBaseline;
}
_cloneStyle(val) {
if (typeof val === 'string') {
return val;
} else if (val && typeof val === 'object' && 'gradient' in val) {
return this._cloneGradient(val);
} else if (val && typeof val === 'object' && 'pattern' in val) {
return Object.assign(Object.create(Object.getPrototypeOf(val)), val);
} else {
throw new SyntaxError('Bad color value');
}
}
_applyStyle(val) {
const gl = this.gl;
if (typeof val === 'string') {
this._setShaderProgram(this.flatShaderProgram);
gl.uniform4fv(this.activeShaderProgram.uniforms['uColor'], cssToGlColor(val));
gl.uniform1f(
this.activeShaderProgram.uniforms['uGlobalAlpha'],
this.drawingState.globalAlpha
);
} else if (val && typeof val === 'object' && 'gradient' in val) {
if (val.stops.length > this.maxGradStops) {
throw new RangeError('Too many gradient stops');
}
let p0 = val.p0;
let p1 = val.p1;
let r0 = val.r0;
let r1 = val.r1;
let reverse_stops = false;
const d = Math.sqrt(Math.pow(p1[0] - p0[0], 2) + Math.pow(p1[1] - p0[1], 2));
if (val.gradient === 'linear') {
if (d <= 0.0001) {
// Do nothing for zero-sized gradients
return this._applyStyle('transparent');
}
this._setShaderProgram(this.linearGradShaderProgram);
} else if (val.gradient === 'radial') {
// Make sure circle 1 is always the smaller of the two
if (r0 > r1) {
let temp = r0;
r0 = r1;
r1 = temp;
temp = p0;
p0 = p1;
p1 = temp;
reverse_stops = true;
}
if (r0 === r1 && p0[0] === p1[0] && p0[1] === p1[1]) {
// Perfect overlap; draw nothing
this._setShaderProgram(null);
} else if (r1 > d + r0) {
// One circle circumscribes the other; use normal radial shader
this._setShaderProgram(this.radialGradShaderProgram);
gl.uniform1i(this.activeShaderProgram.uniforms['uCirclesTouching'], 0);
} else if (r1 === d + r0) {
// Total bullshit edgecase
this._setShaderProgram(this.radialGradShaderProgram);
gl.uniform1i(this.activeShaderProgram.uniforms['uCirclesTouching'], 1);
} else {
// Circles are not compact; use disjoint shader
this._setShaderProgram(this.disjointRadialGradShaderProgram);
let pinchPt = [0, 0];
if (r0 !== r1) {
pinchPt = outerTangent(p0, r0, p1, r1);
}
gl.uniform2fv(this.activeShaderProgram.uniforms['uPinchPt'], pinchPt);
gl.uniform1i(this.activeShaderProgram.uniforms['uStopDirection'], reverse_stops);
}
} else {
throw new SyntaxError('Bad color value');
}
if (this.activeShaderProgram != null) {
gl.uniform1f(this.activeShaderProgram.uniforms['r0'], r0);
gl.uniform1f(this.activeShaderProgram.uniforms['r1'], r1);
gl.uniform2fv(this.activeShaderProgram.uniforms['p0'], p0);
gl.uniform2fv(this.activeShaderProgram.uniforms['p1'], p1);
let color_arr = [];
const offset_arr = [];
let stops = val.stops;
if (reverse_stops) {
stops = val.stops.slice().reverse();
}
for (let i = 0; i < stops.length; i++) {
color_arr = color_arr.concat(stops[i][0]);
if (reverse_stops) {
offset_arr.push(1 - stops[i][1]);
} else {
offset_arr.push(stops[i][1]);
}
}
offset_arr.push(-1.0);
gl.uniform4fv(this.activeShaderProgram.uniforms['colors[0]'], new Float32Array(color_arr));
gl.uniform1fv(
this.activeShaderProgram.uniforms['offsets[0]'],
new Float32Array(offset_arr)
);
gl.uniform1f(
this.activeShaderProgram.uniforms['uGlobalAlpha'],
this.drawingState.globalAlpha
);
}
} else if (val && val instanceof CanvasPattern) {
this._setShaderProgram(this.patternShaderProgram);
gl.disableVertexAttribArray(this.activeShaderProgram.attributes['aTexCoord']);
// TODO: cache asset textures
const texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
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.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
val.pattern.width,
val.pattern.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
'data' in val.pattern ? val.pattern.data : val.pattern // accept both assets and raw data arrays
);
gl.uniform2f(
this.activeShaderProgram.uniforms['uTextureSize'],
val.pattern.width,
val.pattern.height
);
gl.uniform1i(this.activeShaderProgram.uniforms['uTexture'], 0);
gl.uniform1i(
this.activeShaderProgram.uniforms['uRepeatMode'],
patternShaderRepeatValues[val.repeat]
);
gl.uniform1f(
this.activeShaderProgram.uniforms['uGlobalAlpha'],
this.drawingState.globalAlpha
);
} else {
throw new SyntaxError('Bad color value');
}
if (this.activeShaderProgram != null) {
gl.enableVertexAttribArray(this.activeShaderProgram.attributes['aVertexPosition']);
gl.disableVertexAttribArray(this.activeShaderProgram.attributes['aTextPageCoord']);
gl.uniform1i(this.activeShaderProgram.uniforms['uTextEnabled'], 0);
gl.uniform1i(this.activeShaderProgram.uniforms['uTextPages'], 1);
}
}
createLinearGradient(x0, y0, x1, y1) {
if (arguments.length !== 4) throw new TypeError();
if (!isFinite(x0) || !isFinite(y0) || !isFinite(x1) || !isFinite(y1)) {
throw new TypeError('One or more nonfinite linear gradient parameters');
}
const gradObj = this._createGradient('linear');
gradObj.p0 = [x0, y0];
gradObj.p1 = [x1, y1];
return gradObj;
}
createRadialGradient(x0, y0, r0, x1, y1, r1) {
if (arguments.length !== 6) throw new TypeError();
if (
!isFinite(x0) ||
!isFinite(y0) ||
!isFinite(r0) ||
!isFinite(x1) ||
!isFinite(y1) ||
!isFinite(r1)
) {
throw new TypeError('One or more nonfinite linear gradient parameters');
}
if (r0 < 0 || r1 < 0) {
throw new DOMException('Bad radius', 'IndexSizeError');
}
const gradObj = this._createGradient('radial');
gradObj.p0 = [x0, y0];
gradObj.r0 = r0;
gradObj.p1 = [x1, y1];
gradObj.r1 = r1;
return gradObj;
}
_createGradient(type) {
const gradObj = {
gradient: type,
stops: [],
addColorStop(offset, color) {
if (arguments.length !== 2) {
throw new TypeError('Need to specify offset and color');
}
const parsedColor = cssToGlColor(color);
if (parsedColor.length === 0) {
throw new DOMException('Bad color value', 'SyntaxError');
}
if (!isFinite(offset)) {
throw new TypeError('Non-finite gradient stop');
}
if (offset < 0 || offset > 1) {
throw new DOMException('Bad stop offset', 'IndexSizeError');
}
// Insert the stop in the right pre-sorted position
let i;
for (i = 0; i < this.stops.length; i++) {
if (this.stops[i][1] === offset) {
// Only two stops can exist at a given offset:
// - the first one (approaching from the "left")
// - the second one (approaching from the "right")
// If we have a stop collision, check if the second
// stop has been added yet and just overwrite that
// one. Otherwise, insert the new from-the-right stop
// after the from-the-left stop
if (i < this.stops.length - 1 && this.stops[i + 1][1] === offset) {
this.stops[i + 1][0] = parsedColor;
i = -1;
} else {
i++;
}
break;
} else if (this.stops[i][1] >= offset) {
break;
}
}
if (i > -1) {
this.stops.splice(i, 0, [parsedColor, offset]);
}
},
};
return gradObj;
}
_cloneGradient(val) {
const newGrad = this._createGradient('');
// Deep copy all the gradient properties, including various
// type-specific coordinates and the stop list, without
// overwriting the instance-specific object methods created
// by _createGradient
Object.assign(newGrad, JSON.parse(JSON.stringify(val)));
return newGrad;
}
_assetFromContext(context) {
const assetWidth = context.gl.drawingBufferWidth;
const assetHeight = context.gl.drawingBufferHeight;
const assetImage = context.getImageData(0, 0, assetWidth, assetHeight);
return {
width: assetImage.width,
height: assetImage.height,
data: new Uint8Array(assetImage.data.buffer),
};
}
createPattern(asset, repeat) {
if (arguments.length !== 2) throw new TypeError();
// TODO: make sure this doesn't pick up asset changes later on
if (repeat !== undefined && (!repeat || repeat === '')) {
repeat = 'repeat';
} else if (!(repeat in patternShaderRepeatValues)) {
throw new DOMException('Bad repeat value', 'SyntaxError');
}
if (asset instanceof Expo2DContext) {
asset = this._assetFromContext(asset);
} else if (!isValidCanvasImageSource(asset)) {
throw new TypeError('Bad asset');
}
return new CanvasPattern(asset, repeat);
}
_setShaderProgram(shaderProgram) {
const gl = this.gl;
if (this.activeShaderProgram !== shaderProgram) {
if (shaderProgram !== null) {
shaderProgram.bind();
} else {
gl.useProgram(null);
gl.bindVertexArray(null);
}
this.activeShaderProgram = shaderProgram;
this._updateMatrixUniforms();
}
}
_applyCompositingState() {
// TODO: actually set things up based on the compositing operation,
// later
const gl = this.gl;
gl.enable(gl.BLEND);
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
}
_drawOffscreenBuffer() {
const gl = this.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
if (this.stencilsEnabled) {
gl.disable(gl.STENCIL_TEST);
}
this._setShaderProgram(this.patternShaderProgram);
gl.enableVertexAttribArray(this.activeShaderProgram.attributes['aTexCoord']);
gl.uniform2f(
this.activeShaderProgram.uniforms['uTextureSize'],
gl.drawingBufferWidth,
gl.drawingBufferHeight
);
gl.uniform1i(this.activeShaderProgram.uniforms['uTexture'], 0);
gl.uniform1i(this.activeShaderProgram.uniforms['uRepeatMode'], 4);
gl.uniform1f(this.activeShaderProgram.uniforms['uGlobalAlpha'], 1);
gl.disableVertexAttribArray(this.activeShaderProgram.attributes['aTextPageCoord']);
gl.uniform1i(this.activeShaderProgram.uniforms['uTextEnabled'], 0);
gl.uniform1i(this.activeShaderProgram.uniforms['uTextPages'], 1); // TODO: causing trips in web-land
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], true);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.framebufferTexture);
const vertices = [
0,
0,
0,
1,
0,
gl.drawingBufferHeight,
0,
0,
gl.drawingBufferWidth,
0,
1,
1,
gl.drawingBufferWidth,
gl.drawingBufferHeight,
1,
0,
];
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']);
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
gl.uniform1i(this.activeShaderProgram.uniforms['uSkipMVTransform'], false);
if (this.stencilsEnabled) {
gl.enable(gl.STENCIL_TEST);
}
}
_initOffscreenBuffer() {
// TODO: this is to work around gl.readPixels not working on the ios default
// framebuffer - remove once that's fixed
const gl = this.gl;
const buffer_format = {
origin: 'texture',
internal_format: null,
type: null,
max_alpha: null,
};
this.framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
this.framebuffer.width = gl.drawingBufferWidth;
this.framebuffer.height = gl.drawingBufferHeight;
this.framebufferTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.framebufferTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
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);
// ******* IMPORTANT **********
// The framebuffer (whether built-in or to-texture) _has_ to be greater
// than 8B/channel precision in order for putImageData(getImageData(...))
// to work.
//
// We're using HALF_FLOAT/16F for the moment because of iOS limitations:
// https://github.com/pex-gl/pex-glu/issues/3
//
// TODO: maybe instead of choosing texture formats based on environement,
// have an ordered list of good-to-bad ones and try them in a loop,
// falling back as necessary? and log appropriately
if (this.environment === 'web') {
const ext = gl.getExtension('EXT_color_buffer_float');
if (!ext) {
console.log('WARNING: Could not get float framebuffer, getImageData() may be lossy');
buffer_format.internal_format = gl.RGBA;
buffer_format.type = gl.UNSIGNED_BYTE;
buffer_format.typed_array = Uint8Array;
buffer_format.max_alpha = 256.0;
} else {
buffer_format.internal_format = gl.RGBA32F;
buffer_format.type = gl.FLOAT;
buffer_format.readpixels_type = gl.FLOAT;
buffer_format.typed_array = Float32Array;
buffer_format.max_alpha = 1.0;
}
} else {
buffer_format.internal_format = gl.RGBA16F;
buffer_format.type = gl.HALF_FLOAT;
buffer_format.readpixels_type = gl.FLOAT;
buffer_format.typed_array = Float32Array;
buffer_format.max_alpha = 1.0;
}
gl.texImage2D(
gl.TEXTURE_2D,
0,
buffer_format.internal_format,
this.framebuffer.width,
this.framebuffer.height,
0,
gl.RGBA,
buffer_format.type,
null
);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
this.framebufferTexture,
0
);
gl.bindTexture(gl.TEXTURE_2D, null);
const renderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
gl.renderbufferStorage(
gl.RENDERBUFFER,
gl.DEPTH24_STENCIL8,
this.framebuffer.width,
this.framebuffer.height
);
gl.framebufferRenderbuffer(
gl.FRAMEBUFFER,
gl.DEPTH_STENCIL_ATTACHMENT,
gl.RENDERBUFFER,
renderbuffer
);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
return buffer_format;
}
/**************************************************
* Main
**************************************************/
get width() {
return this.gl.drawingBufferWidth;
}
set width(val) {
console.log('WARNING: setting context width/height at runtime is not supported');
}
get height() {
return this.gl.drawingBufferHeight;
}
set height(val) {
console.log('WARNING: setting context width/height at runtime is not supported');
}
constructor(gl, options) {
// Paramters
// TODO: how do we make these parameters more parameterizable
// (that is, settable at creation fixed afterwards) ?
options = options || {};
this.maxGradStops = options.maxGradStops || 128;
this.renderWithOffscreenBuffer = options.renderWithOffscreenBuffer || false;
this.fastFillTesselation = options.fastFillTesselation || false;
this.environment = getEnvironment();
this.builtinFonts = getBuiltinFonts();
// Initialization
this.gl = gl;
this.activeShaderProgram = null;
this.vertexBuffer = gl.createBuffer();
// TODO: Put these into a generator function in shaders.js and
// put all shader params (right now, just gradient stops) as function
// arguments
this.flatShaderProgram = new ShaderProgram(gl, flatShaderTxt['vert'], flatShaderTxt['frag']);
this.linearGradShaderProgram = new ShaderProgram(
gl,
linearGradShaderTxt['vert'],
stringFormat(linearGradShaderTxt['frag'], {
maxGradStops: this.maxGradStops,
})
);
this.radialGradShaderProgram = new ShaderProgram(
gl,
radialGradShaderTxt['vert'],
stringFormat(radialGradShaderTxt['frag'], {
maxGradStops: this.maxGradStops,
})
);
this.disjointRadialGradShaderProgram = new ShaderProgram(
gl,
disjointRadialGradShaderTxt['vert'],
stringFormat(disjointRadialGradShaderTxt['frag'], {
maxGradStops: this.maxGradStops,
})
);
this.patternShaderProgram = new ShaderProgram(
gl,
patternShaderTxt['vert'],
patternShaderTxt['frag']
);
this._initDrawingState();
if (this.renderWithOffscreenBuffer) {
this._framebuffer_format = this._initOffscreenBuffer();
// top and bottom are swapped while drawOffscreenBuffer() is in use
glm.mat4.ortho(this.pMatrix, 0, gl.drawingBufferWidth, 0, gl.drawingBufferHeight, -1, 1);
} else {
this._framebuffer_format = {
origin: 'internal',
internal_format: gl.RGBA,
typed_array: Uint8Array,
type: gl.UNSIGNED_BYTE,
readpixels_type: gl.UNSIGNED_BYTE,
max_alpha: 256.0,
};
glm.mat4.ortho(this.pMatrix, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, -1, 1);
}
this._setShaderProgram(this.flatShaderProgram);
this._applyCompositingState();
this.nullTextPage = gl.createTexture();
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.nullTextPage);
gl.texStorage3D(
gl.TEXTURE_2D_ARRAY,
1,
gl.RGBA8,
1, //w
1, //h
1
);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texSubImage3D(
gl.TEXTURE_2D_ARRAY,
0,
0,
0,
0,
1, //w
1, //h
1,
gl.RGBA,
gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 0, 0])
);
gl.clearColor(0, 0, 0, 0.0);
gl.clearStencil(1);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
glm.mat4.identity(this.drawingState.mvMatrix);
this._updateMatrixUniforms();
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT | this.gl.STENCIL_BUFFER_BIT);
if (!(gl.getParameter(gl.STENCIL_BITS) >= 2)) {
console.log(
'WARNING: Was given ' +
gl.getParameter(gl.STENCIL_BITS) +
' stencil bits - strokes and clipping will be broken'
);
}
}
flush() {
if (this.renderWithOffscreenBuffer) {
this._drawOffscreenBuffer();
}
if (this.environment === 'expo') {
this.gl.endFrameEXP();
}
}
}