diff --git a/package.json b/package.json index cf916508e6..e34e3facd0 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "author": "", "husky": { "hooks": { - "pre-commit": "lint-staged" + } }, "msw": { diff --git a/preview/global/sketch.js b/preview/global/sketch.js index 00719764a7..4789f83f36 100644 --- a/preview/global/sketch.js +++ b/preview/global/sketch.js @@ -1,14 +1,55 @@ -console.log(p5); +const vertSrc = `#version 300 es + precision mediump float; + uniform mat4 uModelViewMatrix; + uniform mat4 uProjectionMatrix; + in vec3 aPosition; + in vec2 aOffset; + + void main(){ + vec4 positionVec4 = vec4(aPosition.xyz, 1.0); + positionVec4.xy += aOffset; + gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + } +`; + +const fragSrc = `#version 300 es + precision mediump float; + out vec4 outColor; + void main(){ + outColor = vec4(0.0, 1.0, 1.0, 1.0); + } +`; + +let myShader; function setup(){ - createCanvas(200, 200); + createCanvas(100, 100, WEBGL); + + // Create and use the custom shader. + myShader = createShader(vertSrc, fragSrc); + + describe('A wobbly, cyan circle on a gray background.'); } -async function draw(){ - background(0, 50, 50); - circle(100, 100, 50); +function draw(){ + // Set the styles + background(125); + noStroke(); + shader(myShader); + + // Draw the circle. + beginShape(); + for (let i = 0; i < 30; i++){ + const x = 40 * cos(i/30 * TWO_PI); + const y = 40 * sin(i/30 * TWO_PI); + + // Apply some noise to the coordinates. + const xOff = 10 * noise(x + millis()/1000) - 5; + const yOff = 10 * noise(y + millis()/1000) - 5; - fill('white'); - textSize(30); - text('hello', 10, 30); + // Apply these noise values to the following vertex. + vertexProperty('aOffset', [xOff, yOff]); + vertex(x, y); + } + endShape(CLOSE); } diff --git a/preview/index.html b/preview/index.html index ac5bedefcc..702811727d 100644 --- a/preview/index.html +++ b/preview/index.html @@ -20,25 +20,25 @@ import p5 from '../src/app.js'; const sketch = function (p) { - let g, f; - p.setup = function () { - p.createCanvas(200, 200); - g = p.createGraphics(200, 200); - f = p.createGraphics(200, 200, p.WEBGL); + p.createCanvas(100, 100, p.WEBGL); }; p.draw = function () { - p.background(0, 50, 50); - p.circle(100, 100, 50); - - p.fill('white'); - p.textSize(30); - p.text('hello', 10, 30); - - // f.fill('red'); - f.sphere(); - p.image(f, 0, 0); + p.background(200); + p.strokeCap(p.SQUARE); + p.strokeJoin(p.MITER); + p.translate(-p.width/2, -p.height/2); + p.noStroke(); + p.beginShape(); + p.bezierOrder(2); + p.fill('red'); + p.vertex(10, 10); + p.fill('lime'); + p.bezierVertex(40, 25); + p.fill('blue'); + p.bezierVertex(10, 40); + p.endShape(); }; }; diff --git a/src/app.js b/src/app.js index ad4b74c83c..9de84f07f2 100644 --- a/src/app.js +++ b/src/app.js @@ -49,11 +49,6 @@ io(p5); import math from './math'; math(p5); -// typography -import './typography/attributes'; -import './typography/loading_displaying'; -import './typography/p5.Font'; - // utilities import utilities from './utilities'; utilities(p5); @@ -62,6 +57,10 @@ utilities(p5); import webgl from './webgl'; webgl(p5); +// typography +import type from './type' +type(p5); + import './core/init'; export default p5; diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index ab4b3f13e6..213821bf14 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -466,6 +466,10 @@ class Color { } get _array() { + return this.array(); + } + + array() { return [...this.color.coords, this.color.alpha]; } diff --git a/src/color/setting.js b/src/color/setting.js index 3c7a68c003..8103aae975 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -1269,7 +1269,7 @@ function setting(p5, fn){ * */ fn.noFill = function() { - this._renderer.states.doFill = false; + this._renderer.noFill(); return this; }; @@ -1325,7 +1325,7 @@ function setting(p5, fn){ * */ fn.noStroke = function() { - this._renderer.states.doStroke = false; + this._renderer.states.strokeColor = null; return this; }; diff --git a/src/core/constants.js b/src/core/constants.js index 5ffefa0b46..8142772026 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -768,6 +768,18 @@ export const QUAD_STRIP = 'quad_strip'; * @final */ export const TESS = 'tess'; +/** + * @typedef {0x0007} EMPTY_PATH + * @property {EMPTY_PATH} EMPTY_PATH + * @final + */ +export const EMPTY_PATH = 0x0007; +/** + * @typedef {0x0008} PATH + * @property {PATH} PATH + * @final + */ +export const PATH = 0x0008; /** * @typedef {'close'} CLOSE * @property {CLOSE} CLOSE @@ -1330,4 +1342,32 @@ export const HALF_FLOAT = 'half-float'; * @property {RGBA} RGBA * @final */ -export const RGBA = 'rgba'; \ No newline at end of file +export const RGBA = 'rgba'; + +/** + * The `splineEnds` mode where splines curve through + * their first and last points. + * @typedef {unique symbol} INCLUDE + * @property {INCLUDE} INCLUDE + * @final + */ +export const INCLUDE = Symbol('include'); + +/** + * The `splineEnds` mode where the first and last points in a spline + * affect the direction of the curve, but are not rendered. + * @typedef {unique symbol} EXCLUDE + * @property {EXCLUDE} EXCLUDE + * @final + */ +export const EXCLUDE = Symbol('exclude'); + +/** + * The `splineEnds` mode where the spline loops back to its first point. + * Only used internally. + * @typedef {unique symbol} JOIN + * @property {JOIN} JOIN + * @final + * @private + */ +export const JOIN = Symbol('join'); diff --git a/src/core/main.js b/src/core/main.js index 06d6251afb..b7b935dbb0 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -59,9 +59,9 @@ class p5 { this._initializeInstanceVariables(); this._events = { // keep track of user-events for unregistering later - mousemove: null, - mousedown: null, - mouseup: null, + pointerdown: null, + pointerup: null, + pointermove: null, dragend: null, dragover: null, click: null, @@ -72,16 +72,11 @@ class p5 { keyup: null, keypress: null, wheel: null, - touchstart: null, - touchmove: null, - touchend: null, resize: null, blur: null }; this._millisStart = -1; this._recording = false; - this._touchstart = false; - this._touchend = false; // States used in the custom random generators this._lcg_random_state = null; // NOTE: move to random.js @@ -233,6 +228,14 @@ class p5 { // unhide any hidden canvases that were created const canvases = document.getElementsByTagName('canvas'); + // Apply touchAction = 'none' to canvases if pointer events exist + if (Object.keys(this._events).some(event => event.startsWith('pointer'))) { + for (const k of canvases) { + k.style.touchAction = 'none'; + } + } + + for (const k of canvases) { if (k.dataset.hidden === 'true') { k.style.visibility = ''; @@ -411,9 +414,6 @@ class p5 { this._styles = []; - this._bezierDetail = 20; - this._curveDetail = 20; - this._colorMode = constants.RGB; this._colorMaxes = { rgb: [255, 255, 255, 255], diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 09eebec1bd..61d6ea0c2f 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -4,8 +4,11 @@ * @for p5 */ +import { Color } from '../color/p5.Color'; import * as constants from '../core/constants'; import { Image } from '../image/p5.Image'; +import { Vector } from '../math/p5.Vector'; +import { Shape } from '../shape/custom_shapes'; class Renderer { constructor(pInst, w, h, isMainCanvas) { @@ -25,22 +28,34 @@ class Renderer { // Renderer state machine this.states = { - doStroke: true, + strokeColor: new Color([0, 0, 0]), strokeSet: false, - doFill: true, + fillColor: new Color([255, 255, 255]), fillSet: false, tint: null, imageMode: constants.CORNER, rectMode: constants.CORNER, ellipseMode: constants.CENTER, - textFont: 'sans-serif', + strokeWeight: 1, + + textFont: { family: 'sans-serif' }, textLeading: 15, leadingSet: false, textSize: 12, textAlign: constants.LEFT, textBaseline: constants.BASELINE, - textStyle: constants.NORMAL, - textWrap: constants.WORD + bezierOrder: 3, + splineEnds: constants.INCLUDE, + + textWrap: constants.WORD, + + // added v2.0 + fontStyle: constants.NORMAL, // v1: textStyle + fontStretch: constants.NORMAL, + fontWeight: constants.NORMAL, + lineHeight: constants.NORMAL, + fontVariant: constants.NORMAL, + direction: 'inherit' }; this._pushPopStack = []; // NOTE: can use the length of the push pop stack instead @@ -49,6 +64,15 @@ class Renderer { this._clipping = false; this._clipInvert = false; this._curveTightness = 0; + + this._currentShape = undefined; // Lazily generate current shape + } + + get currentShape() { + if (!this._currentShape) { + this._currentShape = new Shape(this.getCommonVertexProperties()); + } + return this._currentShape; } remove() { @@ -91,6 +115,80 @@ class Renderer { pop() { this._pushPopDepth--; Object.assign(this.states, this._pushPopStack.pop()); + this.updateShapeVertexProperties(); + this.updateShapeProperties(); + } + + bezierOrder(order) { + if (order === undefined) { + return this.states.bezierOrder; + } else { + this.states.bezierOrder = order; + this.updateShapeProperties(); + } + } + + bezierVertex(x, y, z = 0, u = 0, v = 0) { + const position = new Vector(x, y, z); + const textureCoordinates = this.getSupportedIndividualVertexProperties().textureCoordinates + ? new Vector(u, v) + : undefined; + this.currentShape.bezierVertex(position, textureCoordinates); + } + + splineEnds(mode) { + if (mode === undefined) { + return this.states.splineEnds; + } else { + this.states.splineEnds = mode; + } + this.updateShapeProperties(); + } + + splineVertex(x, y, z = 0, u = 0, v = 0) { + const position = new Vector(x, y, z); + const textureCoordinates = this.getSupportedIndividualVertexProperties().textureCoordinates + ? new Vector(u, v) + : undefined; + this.currentShape.splineVertex(position, textureCoordinates); + } + + curveDetail(d) { + if (d === undefined) { + return this.states.curveDetail; + } else { + this.states.curveDetail = d; + } + } + + beginShape(...args) { + this.currentShape.reset(); + this.currentShape.beginShape(...args); + } + + endShape(...args) { + this.currentShape.endShape(...args); + this.drawShape(this.currentShape); + } + + beginContour(shapeKind) { + this.currentShape.beginContour(shapeKind); + } + + endContour(mode) { + this.currentShape.endContour(mode); + } + + drawShape(shape, count) { + throw new Error('Unimplemented') + } + + vertex(x, y, z = 0, u = 0, v = 0) { + const position = new Vector(x, y, z); + const textureCoordinates = this.getSupportedIndividualVertexProperties().textureCoordinates + ? new Vector(u, v) + : undefined; + this.currentShape.vertex(position, textureCoordinates); } beginClip(options = {}) { @@ -153,14 +251,54 @@ class Renderer { } - fill() { + fill(...args) { this.states.fillSet = true; - this.states.doFill = true; + this.states.fillColor = this._pInst.color(...args); + this.updateShapeVertexProperties(); + } + + noFill() { + this.states.fillColor = null; + } + + strokeWeight(w) { + if (w === undefined) { + return this.states.strokeWeight; + } else { + this.states.strokeWeight = w; + } } - stroke() { + stroke(...args) { this.states.strokeSet = true; - this.states.doStroke = true; + this.states.strokeColor = this._pInst.color(...args); + this.updateShapeVertexProperties(); + } + + noStroke() { + this.states.strokeColor = null; + } + + getCommonVertexProperties() { + return {} + } + + getSupportedIndividualVertexProperties() { + return { + textureCoordinates: false, + } + } + + updateShapeProperties() { + this.currentShape.bezierOrder(this.states.bezierOrder); + this.currentShape.splineEnds(this.states.splineEnds); + } + + updateShapeVertexProperties() { + const props = this.getCommonVertexProperties(); + for (const key in props) { + this.currentShape[key](props[key]); + } } textSize(s) { @@ -194,13 +332,13 @@ class Renderer { s === constants.BOLD || s === constants.BOLDITALIC ) { - this.states.textStyle = s; + this.states.fontStyle = s; } return this._applyTextProperties(); } - return this.states.textStyle; + return this.states.fontStyle; } textAscent () { @@ -254,7 +392,7 @@ class Renderer { // fix for #5785 (top of bounding box) let finalMinHeight = y; - if (!(this.states.doFill || this.states.doStroke)) { + if (!(this.states.fillColor || this.states.strokeColor)) { return; } @@ -477,8 +615,8 @@ class Renderer { /** * Helper function to check font type (system or otf) */ - _isOpenType(f = this.states.textFont) { - return typeof f === 'object' && f.font && f.font.supported; + _isOpenType({ font: f } = this.states.textFont) { + return typeof f === 'object' && f.data; } _updateTextMetrics() { diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index b6707d21a5..d914c52439 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -5,8 +5,12 @@ import { Graphics } from './p5.Graphics'; import { Image } from '../image/p5.Image'; import { Element } from '../dom/p5.Element'; import { MediaElement } from '../dom/p5.MediaElement'; + import FilterRenderer2D from '../image/filterRenderer2D'; +import { PrimitiveToPath2DConverter } from '../shape/custom_shapes'; + + const styleEmpty = 'rgba(0,0,0,0)'; // const alphaThreshold = 0.00125; // minimum visible @@ -72,6 +76,8 @@ class Renderer2D extends Renderer { } // Set and return p5.Element this.wrappedElt = new Element(this.elt, this._pInst); + + this.clipPath = null; } remove(){ @@ -205,7 +211,7 @@ class Renderer2D extends Renderer { fill(...args) { super.fill(...args); - const color = this._pInst.color(...args); + const color = this.states.fillColor; this._setFill(color.toString()); //accessible Outputs @@ -216,7 +222,7 @@ class Renderer2D extends Renderer { stroke(...args) { super.stroke(...args); - const color = this._pInst.color(...args); + const color = this.states.strokeColor; this._setStroke(color.toString()); //accessible Outputs @@ -256,6 +262,21 @@ class Renderer2D extends Renderer { } } + drawShape(shape) { + const visitor = new PrimitiveToPath2DConverter({ strokeWeight: this.states.strokeWeight }); + shape.accept(visitor); + if (this._clipping) { + this.clipPath.addPath(visitor.path); + } else { + if (this.states.fillColor) { + this.drawingContext.fill(visitor.path); + } + if (this.states.strokeColor) { + this.drawingContext.stroke(visitor.path); + } + } + } + beginClip(options = {}) { super.beginClip(options); @@ -274,36 +295,37 @@ class Renderer2D extends Renderer { this.blendMode(constants.BLEND); this._cachedBlendMode = tempBlendMode; + // Since everything must be in one path, create a new single Path2D to chain all shapes onto. // Start a new path. Everything from here on out should become part of this // one path so that we can clip to the whole thing. - this.drawingContext.beginPath(); + this.clipPath = new Path2D(); if (this._clipInvert) { // Slight hack: draw a big rectangle over everything with reverse winding // order. This is hopefully large enough to cover most things. - this.drawingContext.moveTo( + this.clipPath.moveTo( -2 * this.width, -2 * this.height ); - this.drawingContext.lineTo( + this.clipPath.lineTo( -2 * this.width, 2 * this.height ); - this.drawingContext.lineTo( + this.clipPath.lineTo( 2 * this.width, 2 * this.height ); - this.drawingContext.lineTo( + this.clipPath.lineTo( 2 * this.width, -2 * this.height ); - this.drawingContext.closePath(); + this.clipPath.closePath(); } } endClip() { - this._doFillStrokeClose(); - this.drawingContext.clip(); + this.drawingContext.clip(this.clipPath); + this.clipPath = null; super.endClip(); @@ -658,7 +680,7 @@ class Renderer2D extends Renderer { * start <= stop < start + TWO_PI */ arc(x, y, w, h, start, stop, mode) { - const ctx = this.drawingContext; + const ctx = this.clipPa || this.drawingContext; const rx = w / 2.0; const ry = h / 2.0; const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. @@ -676,7 +698,7 @@ class Renderer2D extends Renderer { } // Fill curves - if (this.states.doFill) { + if (this.states.fillColor) { if (!this._clipping) ctx.beginPath(); curves.forEach((curve, index) => { if (index === 0) { @@ -696,7 +718,7 @@ class Renderer2D extends Renderer { } // Stroke curves - if (this.states.doStroke) { + if (this.states.strokeColor) { if (!this._clipping) ctx.beginPath(); curves.forEach((curve, index) => { if (index === 0) { @@ -720,9 +742,9 @@ class Renderer2D extends Renderer { } ellipse(args) { - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; + const ctx = this.clipPath || this.drawingContext; + const doFill = !!this.states.fillColor, + doStroke = this.states.strokeColor; const x = parseFloat(args[0]), y = parseFloat(args[1]), w = parseFloat(args[2]), @@ -753,8 +775,8 @@ class Renderer2D extends Renderer { } line(x1, y1, x2, y2) { - const ctx = this.drawingContext; - if (!this.states.doStroke) { + const ctx = this.clipPath || this.drawingContext; + if (!this.states.strokeColor) { return this; } else if (this._getStroke() === styleEmpty) { return this; @@ -767,8 +789,8 @@ class Renderer2D extends Renderer { } point(x, y) { - const ctx = this.drawingContext; - if (!this.states.doStroke) { + const ctx = this.clipPath || this.drawingContext; + if (!this.states.strokeColor) { return this; } else if (this._getStroke() === styleEmpty) { return this; @@ -788,9 +810,9 @@ class Renderer2D extends Renderer { } quad(x1, y1, x2, y2, x3, y3, x4, y4) { - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; + const ctx = this.clipPath || this.drawingContext; + const doFill = !!this.states.fillColor, + doStroke = this.states.strokeColor; if (doFill && !doStroke) { if (this._getFill() === styleEmpty) { return this; @@ -824,9 +846,9 @@ class Renderer2D extends Renderer { let tr = args[5]; let br = args[6]; let bl = args[7]; - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; + const ctx = this.clipPath || this.drawingContext; + const doFill = !!this.states.fillColor, + doStroke = this.states.strokeColor; if (doFill && !doStroke) { if (this._getFill() === styleEmpty) { return this; @@ -895,10 +917,10 @@ class Renderer2D extends Renderer { ctx.arcTo(x, y, x + w, y, tl); ctx.closePath(); } - if (!this._clipping && this.states.doFill) { + if (!this._clipping && this.states.fillColor) { ctx.fill(); } - if (!this._clipping && this.states.doStroke) { + if (!this._clipping && this.states.strokeColor) { ctx.stroke(); } return this; @@ -906,9 +928,9 @@ class Renderer2D extends Renderer { triangle(args) { - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; + const ctx = this.clipPath || this.drawingContext; + const doFill = !!this.states.fillColor, + doStroke = this.states.strokeColor; const x1 = args[0], y1 = args[1]; const x2 = args[2], @@ -937,270 +959,6 @@ class Renderer2D extends Renderer { } } - endShape( - mode, - vertices, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind - ) { - if (vertices.length === 0) { - return this; - } - if (!this.states.doStroke && !this.states.doFill) { - return this; - } - const closeShape = mode === constants.CLOSE; - let v; - if (closeShape && !isContour) { - vertices.push(vertices[0]); - } - let i, j; - const numVerts = vertices.length; - if (isCurve && shapeKind === null) { - if (numVerts > 3) { - const b = [], - s = 1 - this._curveTightness; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(vertices[1][0], vertices[1][1]); - for (i = 1; i + 2 < numVerts; i++) { - v = vertices[i]; - b[0] = [v[0], v[1]]; - b[1] = [ - v[0] + (s * vertices[i + 1][0] - s * vertices[i - 1][0]) / 6, - v[1] + (s * vertices[i + 1][1] - s * vertices[i - 1][1]) / 6 - ]; - b[2] = [ - vertices[i + 1][0] + - (s * vertices[i][0] - s * vertices[i + 2][0]) / 6, - vertices[i + 1][1] + - (s * vertices[i][1] - s * vertices[i + 2][1]) / 6 - ]; - b[3] = [vertices[i + 1][0], vertices[i + 1][1]]; - this.drawingContext.bezierCurveTo( - b[1][0], - b[1][1], - b[2][0], - b[2][1], - b[3][0], - b[3][1] - ); - } - if (closeShape) { - this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); - } - this._doFillStrokeClose(closeShape); - } - } else if ( - isBezier && - shapeKind === null - ) { - if (!this._clipping) this.drawingContext.beginPath(); - for (i = 0; i < numVerts; i++) { - if (vertices[i].isVert) { - if (vertices[i].moveTo) { - this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); - } else { - this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); - } - } else { - this.drawingContext.bezierCurveTo( - vertices[i][0], - vertices[i][1], - vertices[i][2], - vertices[i][3], - vertices[i][4], - vertices[i][5] - ); - } - } - this._doFillStrokeClose(closeShape); - } else if ( - isQuadratic && - shapeKind === null - ) { - if (!this._clipping) this.drawingContext.beginPath(); - for (i = 0; i < numVerts; i++) { - if (vertices[i].isVert) { - if (vertices[i].moveTo) { - this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); - } else { - this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); - } - } else { - this.drawingContext.quadraticCurveTo( - vertices[i][0], - vertices[i][1], - vertices[i][2], - vertices[i][3] - ); - } - } - this._doFillStrokeClose(closeShape); - } else { - if (shapeKind === constants.POINTS) { - for (i = 0; i < numVerts; i++) { - v = vertices[i]; - if (this.states.doStroke) { - this._pInst.stroke(v[6]); - } - this._pInst.point(v[0], v[1]); - } - } else if (shapeKind === constants.LINES) { - for (i = 0; i + 1 < numVerts; i += 2) { - v = vertices[i]; - if (this.states.doStroke) { - this._pInst.stroke(vertices[i + 1][6]); - } - this._pInst.line(v[0], v[1], vertices[i + 1][0], vertices[i + 1][1]); - } - } else if (shapeKind === constants.TRIANGLES) { - for (i = 0; i + 2 < numVerts; i += 3) { - v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(v[0], v[1]); - this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); - this.drawingContext.lineTo(vertices[i + 2][0], vertices[i + 2][1]); - this.drawingContext.closePath(); - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 2][5]); - this.drawingContext.fill(); - } - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 2][6]); - this.drawingContext.stroke(); - } - } - } else if (shapeKind === constants.TRIANGLE_STRIP) { - for (i = 0; i + 1 < numVerts; i++) { - v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(vertices[i + 1][0], vertices[i + 1][1]); - this.drawingContext.lineTo(v[0], v[1]); - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 1][6]); - } - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 1][5]); - } - if (i + 2 < numVerts) { - this.drawingContext.lineTo(vertices[i + 2][0], vertices[i + 2][1]); - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 2][6]); - } - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 2][5]); - } - } - this._doFillStrokeClose(closeShape); - } - } else if (shapeKind === constants.TRIANGLE_FAN) { - if (numVerts > 2) { - // For performance reasons, try to batch as many of the - // fill and stroke calls as possible. - if (!this._clipping) this.drawingContext.beginPath(); - for (i = 2; i < numVerts; i++) { - v = vertices[i]; - this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); - this.drawingContext.lineTo(vertices[i - 1][0], vertices[i - 1][1]); - this.drawingContext.lineTo(v[0], v[1]); - this.drawingContext.lineTo(vertices[0][0], vertices[0][1]); - // If the next colour is going to be different, stroke / fill now - if (i < numVerts - 1) { - if ( - (this.states.doFill && v[5] !== vertices[i + 1][5]) || - (this.states.doStroke && v[6] !== vertices[i + 1][6]) - ) { - if (!this._clipping && this.states.doFill) { - this._pInst.fill(v[5]); - this.drawingContext.fill(); - this._pInst.fill(vertices[i + 1][5]); - } - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(v[6]); - this.drawingContext.stroke(); - this._pInst.stroke(vertices[i + 1][6]); - } - this.drawingContext.closePath(); - if (!this._clipping) this.drawingContext.beginPath(); // Begin the next one - } - } - } - this._doFillStrokeClose(closeShape); - } - } else if (shapeKind === constants.QUADS) { - for (i = 0; i + 3 < numVerts; i += 4) { - v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(v[0], v[1]); - for (j = 1; j < 4; j++) { - this.drawingContext.lineTo(vertices[i + j][0], vertices[i + j][1]); - } - this.drawingContext.lineTo(v[0], v[1]); - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 3][5]); - } - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 3][6]); - } - this._doFillStrokeClose(closeShape); - } - } else if (shapeKind === constants.QUAD_STRIP) { - if (numVerts > 3) { - for (i = 0; i + 1 < numVerts; i += 2) { - v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - if (i + 3 < numVerts) { - this.drawingContext.moveTo( - vertices[i + 2][0], vertices[i + 2][1]); - this.drawingContext.lineTo(v[0], v[1]); - this.drawingContext.lineTo( - vertices[i + 1][0], vertices[i + 1][1]); - this.drawingContext.lineTo( - vertices[i + 3][0], vertices[i + 3][1]); - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 3][5]); - } - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 3][6]); - } - } else { - this.drawingContext.moveTo(v[0], v[1]); - this.drawingContext.lineTo( - vertices[i + 1][0], vertices[i + 1][1]); - } - this._doFillStrokeClose(closeShape); - } - } - } else { - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); - for (i = 1; i < numVerts; i++) { - v = vertices[i]; - if (v.isVert) { - if (v.moveTo) { - if (closeShape) this.drawingContext.closePath(); - this.drawingContext.moveTo(v[0], v[1]); - } else { - this.drawingContext.lineTo(v[0], v[1]); - } - } - } - this._doFillStrokeClose(closeShape); - } - } - isCurve = false; - isBezier = false; - isQuadratic = false; - isContour = false; - if (closeShape) { - vertices.pop(); - } - - return this; - } ////////////////////////////////////////////// // SHAPE | Attributes ////////////////////////////////////////////// @@ -1228,6 +986,7 @@ class Renderer2D extends Renderer { } strokeWeight(w) { + super.strokeWeight(w); if (typeof w === 'undefined' || w === 0) { // hack because lineWidth 0 doesn't work this.drawingContext.lineWidth = 0.0001; @@ -1278,30 +1037,14 @@ class Renderer2D extends Renderer { curve(x1, y1, x2, y2, x3, y3, x4, y4) { this._pInst.beginShape(); - this._pInst.curveVertex(x1, y1); - this._pInst.curveVertex(x2, y2); - this._pInst.curveVertex(x3, y3); - this._pInst.curveVertex(x4, y4); + this._pInst.splineVertex(x1, y1); + this._pInst.splineVertex(x2, y2); + this._pInst.splineVertex(x3, y3); + this._pInst.splineVertex(x4, y4); this._pInst.endShape(); return this; } - ////////////////////////////////////////////// - // SHAPE | Vertex - ////////////////////////////////////////////// - - _doFillStrokeClose(closeShape) { - if (closeShape) { - this.drawingContext.closePath(); - } - if (!this._clipping && this.states.doFill) { - this.drawingContext.fill(); - } - if (!this._clipping && this.states.doStroke) { - this.drawingContext.stroke(); - } - } - ////////////////////////////////////////////// // TRANSFORM ////////////////////////////////////////////// @@ -1356,11 +1099,11 @@ class Renderer2D extends Renderer { // a system/browser font // no stroke unless specified by user - if (this.states.doStroke && this.states.strokeSet) { + if (this.states.strokeColor && this.states.strokeSet) { this.drawingContext.strokeText(line, x, y); } - if (!this._clipping && this.states.doFill) { + if (!this._clipping && this.states.fillColor) { // if fill hasn't been set by user, use default text fill if (!this.states.fillSet) { this._setFill(constants._DEFAULT_TEXT_FILL); @@ -1409,7 +1152,7 @@ class Renderer2D extends Renderer { return p; } - _applyTextProperties() { + /*_applyTextProperties() { let font; const p = this._pInst; @@ -1439,7 +1182,7 @@ class Renderer2D extends Renderer { } return p; - } + }*/ ////////////////////////////////////////////// // STRUCTURE diff --git a/src/dom/dom.js b/src/dom/dom.js index dfea7a84f2..76265cffcb 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -213,7 +213,7 @@ function dom(p5, fn){ let container = document; if (typeof p === 'string') { container = document.querySelector(p) || document; - } else if (p instanceof p5.Element) { + } else if (p instanceof Element) { container = p.elt; } else if (p instanceof HTMLElement) { container = p; @@ -256,6 +256,64 @@ function dom(p5, fn){ } }; + /** + * Creates a new p5.Element object. + * + * The first parameter, `tag`, is a string an HTML tag such as `'h5'`. + * + * The second parameter, `content`, is optional. It's a string that sets the + * HTML content to insert into the new element. New elements have no content + * by default. + * + * @method createElement + * @param {String} tag tag for the new element. + * @param {String} [content] HTML content to insert into the element. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create an h5 element with nothing in it.
+ * createElement('h5');
+ *
+ * describe('A gray square.');
+ * }
+ *
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Create an h5 element with the content "p5*js".
+ * let h5 = createElement('h5', 'p5*js');
+ *
+ * // Set the element's style and position.
+ * h5.style('color', 'deeppink');
+ * h5.position(30, 15);
+ *
+ * describe('The text "p5*js" written in pink in the middle of a gray square.');
+ * }
+ *
+ *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Create an h5 element with nothing in it.
- * createElement('h5');
- *
- * describe('A gray square.');
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Create an h5 element with the content "p5*js".
- * let h5 = createElement('h5', 'p5*js');
- *
- * // Set the element's style and position.
- * h5.style('color', 'deeppink');
- * h5.position(30, 15);
- *
- * describe('The text "p5*js" written in pink in the middle of a gray square.');
- * }
- *
- *
+ * // On a touchscreen device, touch the canvas using one or more fingers
+ * // at the same time.
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * describe(
+ * 'A gray square. White circles appear where the user touches the square.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Draw a circle at each touch point.
+ * for (let touch of touches) {
+ * circle(touch.x, touch.y, 40);
+ * }
+ * }
+ *
+ *
+ * // On a touchscreen device, touch the canvas using one or more fingers
+ * // at the same time.
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * describe(
+ * 'A gray square. Labels appear where the user touches the square, displaying the coordinates.'
+ * );
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Draw a label above each touch point.
+ * for (let touch of touches) {
+ * text(`${touch.x}, ${touch.y}`, touch.x, touch.y - 40);
+ * }
+ * }
+ *
+ *
- * // On a touchscreen device, touch the canvas using one or more fingers
- * // at the same time.
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * describe(
- * 'A gray square. White circles appear where the user touches the square.'
- * );
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Draw a circle at each touch point.
- * for (let touch of touches) {
- * circle(touch.x, touch.y, 40);
- * }
- * }
- *
- *
- * // On a touchscreen device, touch the canvas using one or more fingers
- * // at the same time.
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * describe(
- * 'A gray square. Labels appear where the user touches the square, displaying the coordinates.'
- * );
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Draw a label above each touch point.
- * for (let touch of touches) {
- * text(`${touch.x}, ${touch.y}`, touch.x, touch.y - 40);
- * }
- * }
- *
- *
- * // On a touchscreen device, touch the canvas using one or more fingers
- * // at the same time.
- *
- * let value = 0;
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * describe(
- * 'A gray square with a black square at its center. The inner square switches color between black and white each time the user touches the screen.'
- * );
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Style the square.
- * fill(value);
- *
- * // Draw the square.
- * square(25, 25, 50);
- * }
- *
- * // Toggle colors with each touch.
- * function touchStarted() {
- * if (value === 0) {
- * value = 255;
- * } else {
- * value = 0;
- * }
- * }
- *
- *
- * // On a touchscreen device, touch the canvas using one or more fingers
- * // at the same time.
- *
- * let bgColor = 50;
- * let fillColor = 255;
- * let borderWidth = 0.5;
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * describe(
- * 'A gray square with the number 0 at the top-center. The number tracks the number of places the user is touching the screen. Circles appear at each touch point and change style in response to events.'
- * );
- * }
- *
- * function draw() {
- * background(bgColor);
- *
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * fill(0);
- * noStroke();
- *
- * // Display the number of touch points.
- * text(touches.length, 50, 20);
- *
- * // Style the touch points.
- * fill(fillColor);
- * stroke(0);
- * strokeWeight(borderWidth);
- *
- * // Display the touch points as circles.
- * for (let touch of touches) {
- * circle(touch.x, touch.y, 40);
- * }
- * }
- *
- * // Set the background color to a random grayscale value.
- * function touchStarted() {
- * bgColor = random(80, 255);
- * }
- *
- * // Set the fill color to a random grayscale value.
- * function touchEnded() {
- * fillColor = random(0, 255);
- * }
- *
- * // Set the stroke weight.
- * function touchMoved() {
- * // Increment the border width.
- * borderWidth += 0.1;
- *
- * // Reset the border width once it's too thick.
- * if (borderWidth > 20) {
- * borderWidth = 0.5;
- * }
- * }
- *
- *
- * // On a touchscreen device, touch the canvas using one or more fingers
- * // at the same time.
- *
- * let value = 0;
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * describe(
- * 'A gray square with a black square at its center. The inner square becomes lighter when the user touches the screen and moves.'
- * );
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Style the square.
- * fill(value);
- *
- * // Draw the square.
- * square(25, 25, 50);
- * }
- *
- * function touchMoved() {
- * // Update the grayscale value.
- * value += 5;
- *
- * // Reset the grayscale value.
- * if (value > 255) {
- * value = 0;
- * }
- * }
- *
- *
- * // On a touchscreen device, touch the canvas using one or more fingers
- * // at the same time.
- *
- * let bgColor = 50;
- * let fillColor = 255;
- * let borderWidth = 0.5;
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * describe(
- * 'A gray square with the number 0 at the top-center. The number tracks the number of places the user is touching the screen. Circles appear at each touch point and change style in response to events.'
- * );
- * }
- *
- * function draw() {
- * background(bgColor);
- *
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * fill(0);
- * noStroke();
- *
- * // Display the number of touch points.
- * text(touches.length, 50, 20);
- *
- * // Style the touch points.
- * fill(fillColor);
- * stroke(0);
- * strokeWeight(borderWidth);
- *
- * // Display the touch points as circles.
- * for (let touch of touches) {
- * circle(touch.x, touch.y, 40);
- * }
- * }
- *
- * // Set the background color to a random grayscale value.
- * function touchStarted() {
- * bgColor = random(80, 255);
- * }
- *
- * // Set the fill color to a random grayscale value.
- * function touchEnded() {
- * fillColor = random(0, 255);
- * }
- *
- * // Set the stroke weight.
- * function touchMoved() {
- * // Increment the border width.
- * borderWidth += 0.1;
- *
- * // Reset the border width once it's too thick.
- * if (borderWidth > 20) {
- * borderWidth = 0.5;
- * }
- * }
- *
- *
- * // On a touchscreen device, touch the canvas using one or more fingers
- * // at the same time.
- *
- * let value = 0;
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * describe(
- * 'A gray square with a black square at its center. The inner square switches color between black and white each time the user stops touching the screen.'
- * );
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Style the square.
- * fill(value);
- *
- * // Draw the square.
- * square(25, 25, 50);
- * }
- *
- * // Toggle colors when a touch ends.
- * function touchEnded() {
- * if (value === 0) {
- * value = 255;
- * } else {
- * value = 0;
- * }
- * }
- *
- *
- * // On a touchscreen device, touch the canvas using one or more fingers
- * // at the same time.
- *
- * let bgColor = 50;
- * let fillColor = 255;
- * let borderWidth = 0.5;
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * describe(
- * 'A gray square with the number 0 at the top-center. The number tracks the number of places the user is touching the screen. Circles appear at each touch point and change style in response to events.'
- * );
- * }
- *
- * function draw() {
- * background(bgColor);
- *
- * // Style the text.
- * textAlign(CENTER);
- * textSize(16);
- * fill(0);
- * noStroke();
- *
- * // Display the number of touch points.
- * text(touches.length, 50, 20);
- *
- * // Style the touch points.
- * fill(fillColor);
- * stroke(0);
- * strokeWeight(borderWidth);
- *
- * // Display the touch points as circles.
- * for (let touch of touches) {
- * circle(touch.x, touch.y, 40);
- * }
- * }
- *
- * // Set the background color to a random grayscale value.
- * function touchStarted() {
- * bgColor = random(80, 255);
- * }
- *
- * // Set the fill color to a random grayscale value.
- * function touchEnded() {
- * fillColor = random(0, 255);
- * }
- *
- * // Set the stroke weight.
- * function touchMoved() {
- * // Increment the border width.
- * borderWidth += 0.1;
- *
- * // Reset the border width once it's too thick.
- * if (borderWidth > 20) {
- * borderWidth = 0.5;
- * }
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Draw a black spline curve.
- * noFill();
- * strokeWeight(1);
- * stroke(0);
- * curve(5, 26, 73, 24, 73, 61, 15, 65);
- *
- * // Draw red spline curves from the anchor points to the control points.
- * stroke(255, 0, 0);
- * curve(5, 26, 5, 26, 73, 24, 73, 61);
- * curve(73, 24, 73, 61, 15, 65, 15, 65);
- *
- * // Draw the anchor points in black.
- * strokeWeight(5);
- * stroke(0);
- * point(73, 24);
- * point(73, 61);
- *
- * // Draw the control points in red.
- * stroke(255, 0, 0);
- * point(5, 26);
- * point(15, 65);
- *
- * describe(
- * 'A gray square with a curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.'
- * );
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * background(200);
- *
- * // Set the curveDetail() to 3.
- * curveDetail(3);
- *
- * // Draw a black spline curve.
- * noFill();
- * strokeWeight(1);
- * stroke(0);
- * curve(-45, -24, 0, 23, -26, 0, 23, 11, 0, -35, 15, 0);
- *
- * // Draw red spline curves from the anchor points to the control points.
- * stroke(255, 0, 0);
- * curve(-45, -24, 0, -45, -24, 0, 23, -26, 0, 23, 11, 0);
- * curve(23, -26, 0, 23, 11, 0, -35, 15, 0, -35, 15, 0);
- *
- * // Draw the anchor points in black.
- * strokeWeight(5);
- * stroke(0);
- * point(23, -26);
- * point(23, 11);
- *
- * // Draw the control points in red.
- * stroke(255, 0, 0);
- * point(-45, -24);
- * point(-35, 15);
- *
- * describe(
- * 'A gray square with a jagged curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.'
- * );
- * }
- *
- *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Style the shape.
+ * strokeWeight(3);
+ *
+ * // Start drawing the shape.
+ * // Only draw the vertices.
+ * beginShape(POINTS);
+ *
+ * // Add the vertices.
+ * vertex(30, 20);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ * vertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ *
+ * describe('Four black dots that form a square are drawn on a gray background.');
+ * }
+ *
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(30, 20);
+ * vertex(85, 20);
+ * vertex(85, 75);
+ * vertex(30, 75);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ *
+ * describe('A white square on a gray background.');
+ * }
+ *
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(-20, -30, 0);
+ * vertex(35, -30, 0);
+ * vertex(35, 25, 0);
+ * vertex(-20, 25, 0);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ *
+ * describe('A white square on a gray background.');
+ * }
+ *
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white square spins around slowly on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(-20, -30, 0);
+ * vertex(35, -30, 0);
+ * vertex(35, 25, 0);
+ * vertex(-20, 25, 0);
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ * }
+ *
+ *
+ * let img;
+ *
+ * // Load an image to apply as a texture.
+ * function preload() {
+ * img = loadImage('assets/laDefense.jpg');
+ * }
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A photograph of a ceiling rotates slowly against a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Rotate around the y-axis.
+ * rotateY(frameCount * 0.01);
+ *
+ * // Style the shape.
+ * noStroke();
+ *
+ * // Apply the texture.
+ * texture(img);
+ * textureMode(NORMAL);
+ *
+ * // Start drawing the shape
+ * beginShape();
+ *
+ * // Add vertices.
+ * vertex(-20, -30, 0, 0, 0);
+ * vertex(35, -30, 0, 1, 0);
+ * vertex(35, 25, 0, 1, 1);
+ * vertex(-20, 25, 0, 0, 1);
+ *
+ * // Stop drawing the shape.
+ * endShape();
+ * }
+ *
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Exterior vertices, clockwise winding.
+ * vertex(10, 10);
+ * vertex(90, 10);
+ * vertex(90, 90);
+ * vertex(10, 90);
+ *
+ * // Interior vertices, counter-clockwise winding.
+ * beginContour();
+ * vertex(30, 30);
+ * vertex(30, 70);
+ * vertex(70, 70);
+ * vertex(70, 30);
+ * endContour();
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ *
+ * describe('A white square with a square hole in its center drawn on a gray background.');
+ * }
+ *
+ *
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white square with a square hole in its center drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Exterior vertices, clockwise winding.
+ * vertex(-40, -40);
+ * vertex(40, -40);
+ * vertex(40, 40);
+ * vertex(-40, 40);
+ *
+ * // Interior vertices, counter-clockwise winding.
+ * beginContour();
+ * vertex(-20, -20);
+ * vertex(-20, 20);
+ * vertex(20, 20);
+ * vertex(20, -20);
+ * endContour();
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ * }
+ *
+ *
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Exterior vertices, clockwise winding.
+ * vertex(10, 10);
+ * vertex(90, 10);
+ * vertex(90, 90);
+ * vertex(10, 90);
+ *
+ * // Interior vertices, counter-clockwise winding.
+ * beginContour();
+ * vertex(30, 30);
+ * vertex(30, 70);
+ * vertex(70, 70);
+ * vertex(70, 30);
+ * endContour();
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ *
+ * describe('A white square with a square hole in its center drawn on a gray background.');
+ * }
+ *
+ *
+ * // Click and drag the mouse to view the scene from different angles.
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * describe('A white square with a square hole in its center drawn on a gray background.');
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Enable orbiting with the mouse.
+ * orbitControl();
+ *
+ * // Start drawing the shape.
+ * beginShape();
+ *
+ * // Exterior vertices, clockwise winding.
+ * vertex(-40, -40);
+ * vertex(40, -40);
+ * vertex(40, 40);
+ * vertex(-40, 40);
+ *
+ * // Interior vertices, counter-clockwise winding.
+ * beginContour();
+ * vertex(-20, -20);
+ * vertex(-20, 20);
+ * vertex(20, 20);
+ * vertex(20, -20);
+ * endContour();
+ *
+ * // Stop drawing the shape.
+ * endShape(CLOSE);
+ * }
+ *
+ *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Start drawing the shape.
- * beginShape();
- *
- * // Exterior vertices, clockwise winding.
- * vertex(10, 10);
- * vertex(90, 10);
- * vertex(90, 90);
- * vertex(10, 90);
- *
- * // Interior vertices, counter-clockwise winding.
- * beginContour();
- * vertex(30, 30);
- * vertex(30, 70);
- * vertex(70, 70);
- * vertex(70, 30);
- * endContour();
- *
- * // Stop drawing the shape.
- * endShape(CLOSE);
- *
- * describe('A white square with a square hole in its center drawn on a gray background.');
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * describe('A white square with a square hole in its center drawn on a gray background.');
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Start drawing the shape.
- * beginShape();
- *
- * // Exterior vertices, clockwise winding.
- * vertex(-40, -40);
- * vertex(40, -40);
- * vertex(40, 40);
- * vertex(-40, 40);
- *
- * // Interior vertices, counter-clockwise winding.
- * beginContour();
- * vertex(-20, -20);
- * vertex(-20, 20);
- * vertex(20, 20);
- * vertex(20, -20);
- * endContour();
- *
- * // Stop drawing the shape.
- * endShape(CLOSE);
- * }
- *
- *
* beginShape();
*
* // Add the first control point and draw a segment to it.
- * curveVertex(84, 91);
- * curveVertex(84, 91);
+ * splineVertex(84, 91);
+ * splineVertex(84, 91);
*
* // Add the anchor points to draw between.
- * curveVertex(68, 19);
- * curveVertex(21, 17);
+ * splineVertex(68, 19);
+ * splineVertex(21, 17);
*
* // Add the second control point.
- * curveVertex(32, 91);
+ * splineVertex(32, 91);
*
* // Uncomment the next line to draw the segment to the second control point.
- * // curveVertex(32, 91);
+ * // splineVertex(32, 91);
*
* endShape();
*
*
* The first two parameters, `x` and `y`, set the vertex’s location. For
- * example, calling `curveVertex(10, 10)` adds a point to the curve at
+ * example, calling `splineVertex(10, 10)` adds a point to the curve at
* `(10, 10)`.
*
* Spline curves can also be drawn in 3D using WebGL mode. The 3D version of
- * `curveVertex()` has three arguments because each point has x-, y-, and
+ * `splineVertex()` has three arguments because each point has x-, y-, and
* z-coordinates. By default, the vertex’s z-coordinate is set to 0.
*
- * Note: `curveVertex()` won’t work when an argument is passed to
+ * Note: `splineVertex()` won’t work when an argument is passed to
* beginShape().
*
* @method curveVertex
@@ -919,14 +768,14 @@ function vertex(p5, fn){
* beginShape();
*
* // Add the first control point.
- * curveVertex(32, 91);
+ * splineVertex(32, 91);
*
* // Add the anchor points.
- * curveVertex(21, 17);
- * curveVertex(68, 19);
+ * splineVertex(21, 17);
+ * splineVertex(68, 19);
*
* // Add the second control point.
- * curveVertex(84, 91);
+ * splineVertex(84, 91);
*
* // Stop drawing the shape.
* endShape();
@@ -966,15 +815,15 @@ function vertex(p5, fn){
* beginShape();
*
* // Add the first control point and draw a segment to it.
- * curveVertex(32, 91);
- * curveVertex(32, 91);
+ * splineVertex(32, 91);
+ * splineVertex(32, 91);
*
* // Add the anchor points.
- * curveVertex(21, 17);
- * curveVertex(68, 19);
+ * splineVertex(21, 17);
+ * splineVertex(68, 19);
*
* // Add the second control point.
- * curveVertex(84, 91);
+ * splineVertex(84, 91);
*
* // Stop drawing the shape.
* endShape();
@@ -1014,16 +863,16 @@ function vertex(p5, fn){
* beginShape();
*
* // Add the first control point and draw a segment to it.
- * curveVertex(32, 91);
- * curveVertex(32, 91);
+ * splineVertex(32, 91);
+ * splineVertex(32, 91);
*
* // Add the anchor points.
- * curveVertex(21, 17);
- * curveVertex(68, 19);
+ * splineVertex(21, 17);
+ * splineVertex(68, 19);
*
* // Add the second control point and draw a segment to it.
- * curveVertex(84, 91);
- * curveVertex(84, 91);
+ * splineVertex(84, 91);
+ * splineVertex(84, 91);
*
* // Stop drawing the shape.
* endShape();
@@ -1077,16 +926,16 @@ function vertex(p5, fn){
* beginShape();
*
* // Add the first control point and draw a segment to it.
- * curveVertex(x1, y1);
- * curveVertex(x1, y1);
+ * splineVertex(x1, y1);
+ * splineVertex(x1, y1);
*
* // Add the anchor points.
- * curveVertex(21, 17);
- * curveVertex(68, 19);
+ * splineVertex(21, 17);
+ * splineVertex(68, 19);
*
* // Add the second control point and draw a segment to it.
- * curveVertex(84, 91);
- * curveVertex(84, 91);
+ * splineVertex(84, 91);
+ * splineVertex(84, 91);
*
* // Stop drawing the shape.
* endShape();
@@ -1138,16 +987,16 @@ function vertex(p5, fn){
* beginShape();
*
* // Add the first control point and draw a segment to it.
- * curveVertex(32, 91);
- * curveVertex(32, 91);
+ * splineVertex(32, 91);
+ * splineVertex(32, 91);
*
* // Add the anchor points.
- * curveVertex(21, 17);
- * curveVertex(68, 19);
+ * splineVertex(21, 17);
+ * splineVertex(68, 19);
*
* // Add the second control point.
- * curveVertex(84, 91);
- * curveVertex(84, 91);
+ * splineVertex(84, 91);
+ * splineVertex(84, 91);
*
* // Stop drawing the shape.
* endShape();
@@ -1187,12 +1036,12 @@ function vertex(p5, fn){
* fill('ghostwhite');
*
* beginShape();
- * curveVertex(-28, 41, 0);
- * curveVertex(-28, 41, 0);
- * curveVertex(-29, -33, 0);
- * curveVertex(18, -31, 0);
- * curveVertex(34, 41, 0);
- * curveVertex(34, 41, 0);
+ * splineVertex(-28, 41, 0);
+ * splineVertex(-28, 41, 0);
+ * splineVertex(-29, -33, 0);
+ * splineVertex(18, -31, 0);
+ * splineVertex(34, 41, 0);
+ * splineVertex(34, 41, 0);
* endShape();
*
* // Draw the second ghost.
@@ -1200,12 +1049,12 @@ function vertex(p5, fn){
* stroke('ghostwhite');
*
* beginShape();
- * curveVertex(-28, 41, -20);
- * curveVertex(-28, 41, -20);
- * curveVertex(-29, -33, -20);
- * curveVertex(18, -31, -20);
- * curveVertex(34, 41, -20);
- * curveVertex(34, 41, -20);
+ * splineVertex(-28, 41, -20);
+ * splineVertex(-28, 41, -20);
+ * splineVertex(-29, -33, -20);
+ * splineVertex(18, -31, -20);
+ * splineVertex(34, 41, -20);
+ * splineVertex(34, 41, -20);
* endShape();
* }
*
@@ -1213,132 +1062,7 @@ function vertex(p5, fn){
*/
fn.curveVertex = function(...args) {
p5._validateParameters('curveVertex', args);
- if (this._renderer.isP3D) {
- this._renderer.curveVertex(...args);
- } else {
- isCurve = true;
- this.vertex(args[0], args[1]);
- }
- return this;
- };
-
- /**
- * Stops creating a hole within a flat shape.
- *
- * The beginContour() and `endContour()`
- * functions allow for creating negative space within custom shapes that are
- * flat. beginContour() begins adding vertices
- * to a negative space and `endContour()` stops adding them.
- * beginContour() and `endContour()` must be
- * called between beginShape() and
- * endShape().
- *
- * Transformations such as translate(),
- * rotate(), and scale()
- * don't work between beginContour() and
- * `endContour()`. It's also not possible to use other shapes, such as
- * ellipse() or rect(),
- * between beginContour() and `endContour()`.
- *
- * Note: The vertices that define a negative space must "wind" in the opposite
- * direction from the outer shape. First, draw vertices for the outer shape
- * clockwise order. Then, draw vertices for the negative space in
- * counter-clockwise order.
- *
- * @method endContour
- * @chainable
- *
- * @example
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Start drawing the shape.
- * beginShape();
- *
- * // Exterior vertices, clockwise winding.
- * vertex(10, 10);
- * vertex(90, 10);
- * vertex(90, 90);
- * vertex(10, 90);
- *
- * // Interior vertices, counter-clockwise winding.
- * beginContour();
- * vertex(30, 30);
- * vertex(30, 70);
- * vertex(70, 70);
- * vertex(70, 30);
- * endContour();
- *
- * // Stop drawing the shape.
- * endShape(CLOSE);
- *
- * describe('A white square with a square hole in its center drawn on a gray background.');
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * describe('A white square with a square hole in its center drawn on a gray background.');
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Start drawing the shape.
- * beginShape();
- *
- * // Exterior vertices, clockwise winding.
- * vertex(-40, -40);
- * vertex(40, -40);
- * vertex(40, 40);
- * vertex(-40, 40);
- *
- * // Interior vertices, counter-clockwise winding.
- * beginContour();
- * vertex(-20, -20);
- * vertex(-20, 20);
- * vertex(20, 20);
- * vertex(20, -20);
- * endContour();
- *
- * // Stop drawing the shape.
- * endShape(CLOSE);
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the shape.
- * strokeWeight(3);
- *
- * // Start drawing the shape.
- * // Only draw the vertices.
- * beginShape(POINTS);
- *
- * // Add the vertices.
- * vertex(30, 20);
- * vertex(85, 20);
- * vertex(85, 75);
- * vertex(30, 75);
- *
- * // Stop drawing the shape.
- * endShape();
- *
- * describe('Four black dots that form a square are drawn on a gray background.');
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Start drawing the shape.
- * beginShape();
- *
- * // Add vertices.
- * vertex(30, 20);
- * vertex(85, 20);
- * vertex(85, 75);
- * vertex(30, 75);
- *
- * // Stop drawing the shape.
- * endShape(CLOSE);
- *
- * describe('A white square on a gray background.');
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * background(200);
- *
- * // Start drawing the shape.
- * beginShape();
- *
- * // Add vertices.
- * vertex(-20, -30, 0);
- * vertex(35, -30, 0);
- * vertex(35, 25, 0);
- * vertex(-20, 25, 0);
- *
- * // Stop drawing the shape.
- * endShape(CLOSE);
- *
- * describe('A white square on a gray background.');
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * describe('A white square spins around slowly on a gray background.');
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Rotate around the y-axis.
- * rotateY(frameCount * 0.01);
- *
- * // Start drawing the shape.
- * beginShape();
- *
- * // Add vertices.
- * vertex(-20, -30, 0);
- * vertex(35, -30, 0);
- * vertex(35, 25, 0);
- * vertex(-20, 25, 0);
- *
- * // Stop drawing the shape.
- * endShape(CLOSE);
- * }
- *
- *
- * let img;
- *
- * // Load an image to apply as a texture.
- * function preload() {
- * img = loadImage('assets/laDefense.jpg');
- * }
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * describe('A photograph of a ceiling rotates slowly against a gray background.');
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Rotate around the y-axis.
- * rotateY(frameCount * 0.01);
- *
- * // Style the shape.
- * noStroke();
- *
- * // Apply the texture.
- * texture(img);
- * textureMode(NORMAL);
- *
- * // Start drawing the shape
- * beginShape();
- *
- * // Add vertices.
- * vertex(-20, -30, 0, 0, 0);
- * vertex(35, -30, 0, 1, 0);
- * vertex(35, 25, 0, 1, 1);
- * vertex(-20, 25, 0, 0, 1);
- *
- * // Stop drawing the shape.
- * endShape();
- * }
- *
- *
@@ -2283,40 +1715,40 @@ function vertex(p5, fn){
* precision mediump float;
* uniform mat4 uModelViewMatrix;
* uniform mat4 uProjectionMatrix;
- *
+ *
* in vec3 aPosition;
* in vec2 aOffset;
- *
+ *
* void main(){
* vec4 positionVec4 = vec4(aPosition.xyz, 1.0);
- * positionVec4.xy += aOffset;
+ * positionVec4.xy += aOffset;
* gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
* }
* `;
- *
+ *
* const fragSrc = `#version 300 es
* precision mediump float;
- * out vec4 outColor;
+ * out vec4 outColor;
* void main(){
- * outColor = vec4(0.0, 1.0, 1.0, 1.0);
+ * outColor = vec4(0.0, 1.0, 1.0, 1.0);
* }
* `;
- *
+ *
* function setup(){
* createCanvas(100, 100, WEBGL);
*
* // Create and use the custom shader.
* const myShader = createShader(vertSrc, fragSrc);
* shader(myShader);
- *
+ *
* describe('A wobbly, cyan circle on a gray background.');
* }
- *
+ *
* function draw(){
* // Set the styles
* background(125);
* noStroke();
- *
+ *
* // Draw the circle.
* beginShape();
* for (let i = 0; i < 30; i++){
@@ -2326,7 +1758,7 @@ function vertex(p5, fn){
* // Apply some noise to the coordinates.
* const xOff = 10 * noise(x + millis()/1000) - 5;
* const yOff = 10 * noise(y + millis()/1000) - 5;
- *
+ *
* // Apply these noise values to the following vertex.
* vertexProperty('aOffset', [xOff, yOff]);
* vertex(x, y);
@@ -2335,26 +1767,26 @@ function vertex(p5, fn){
* }
*
*
* let myShader;
* const cols = 10;
* const rows = 10;
* const cellSize = 6;
- *
+ *
* const vertSrc = `#version 300 es
* precision mediump float;
* uniform mat4 uProjectionMatrix;
* uniform mat4 uModelViewMatrix;
- *
+ *
* in vec3 aPosition;
* in vec3 aNormal;
* in vec3 aVertexColor;
* in float aDistance;
- *
+ *
* out vec3 vVertexColor;
- *
+ *
* void main(){
* vec4 positionVec4 = vec4(aPosition, 1.0);
* positionVec4.xyz += aDistance * aNormal * 2.0;;
@@ -2362,49 +1794,49 @@ function vertex(p5, fn){
* gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
* }
* `;
- *
+ *
* const fragSrc = `#version 300 es
* precision mediump float;
- *
+ *
* in vec3 vVertexColor;
* out vec4 outColor;
- *
+ *
* void main(){
* outColor = vec4(vVertexColor, 1.0);
* }
* `;
- *
+ *
* function setup(){
* createCanvas(100, 100, WEBGL);
- *
+ *
* // Create and apply the custom shader.
* myShader = createShader(vertSrc, fragSrc);
* shader(myShader);
* noStroke();
* describe('A blue grid, which moves away from the mouse position, on a gray background.');
* }
- *
+ *
* function draw(){
* background(200);
- *
+ *
* // Draw the grid in the middle of the screen.
* translate(-cols*cellSize/2, -rows*cellSize/2);
* beginShape(QUADS);
* for (let i = 0; i < cols; i++) {
* for (let j = 0; j < rows; j++) {
- *
+ *
* // Calculate the cell position.
* let x = i * cellSize;
* let y = j * cellSize;
- *
+ *
* fill(j/rows*255, j/cols*255, 255);
- *
+ *
* // Calculate the distance from the corner of each cell to the mouse.
- * let distance = dist(x1,y1, mouseX, mouseY);
- *
+ * let distance = dist(x, y, mouseX, mouseY);
+ *
* // Send the distance to the shader.
* vertexProperty('aDistance', min(distance, 100));
- *
+ *
* vertex(x, y);
* vertex(x + cellSize, y);
* vertex(x + cellSize, y + cellSize);
diff --git a/src/type/index.js b/src/type/index.js
new file mode 100644
index 0000000000..576ae5e792
--- /dev/null
+++ b/src/type/index.js
@@ -0,0 +1,9 @@
+
+import text2d from './text2d.js';
+import p5font from './p5.Font.js';
+
+export default function(p5){
+ p5.registerAddon(text2d);
+ p5.registerAddon(p5font);
+}
+
diff --git a/src/type/lib/Typr.U.js b/src/type/lib/Typr.U.js
new file mode 100644
index 0000000000..93c3c0c320
--- /dev/null
+++ b/src/type/lib/Typr.U.js
@@ -0,0 +1,1290 @@
+
+
+Typr["U"] = {
+ "shape": function (font, str, ltr) {
+
+ var getGlyphPosition = function (font, gls, i1, ltr) {
+ var g1 = gls[i1], g2 = gls[i1 + 1], kern = font["kern"];
+ if (kern) {
+ var ind1 = kern.glyph1.indexOf(g1);
+ if (ind1 != -1) {
+ var ind2 = kern.rval[ind1].glyph2.indexOf(g2);
+ if (ind2 != -1) return [0, 0, kern.rval[ind1].vals[ind2], 0];
+ }
+ }
+ //console.log("no kern");
+ return [0, 0, 0, 0];
+ }
+
+
+ var gls = [];
+ for (var i = 0; i < str.length; i++) {
+ var cc = str.codePointAt(i); if (cc > 0xffff) i++;
+ gls.push(Typr["U"]["codeToGlyph"](font, cc));
+ }
+ var shape = [];
+ var x = 0, y = 0;
+
+ for (var i = 0; i < gls.length; i++) {
+ var padj = getGlyphPosition(font, gls, i, ltr);
+ var gid = gls[i];
+ var ax = font["hmtx"].aWidth[gid] + padj[2];
+ shape.push({ "g": gid, "cl": i, "dx": 0, "dy": 0, "ax": ax, "ay": 0 });
+ x += ax;
+ }
+ return shape;
+ },
+
+ "shapeToPath": function (font, shape, clr) {
+ var tpath = { cmds: [], crds: [] };
+ var x = 0, y = 0;
+
+ for (var i = 0; i < shape.length; i++) {
+ var it = shape[i]
+ var path = Typr["U"]["glyphToPath"](font, it["g"]), crds = path["crds"];
+ for (var j = 0; j < crds.length; j += 2) {
+ tpath.crds.push(crds[j] + x + it["dx"]);
+ tpath.crds.push(crds[j + 1] + y + it["dy"]);
+ }
+ if (clr) tpath.cmds.push(clr);
+ for (var j = 0; j < path["cmds"].length; j++) tpath.cmds.push(path["cmds"][j]);
+ var clen = tpath.cmds.length;
+ if (clr) if (clen != 0 && tpath.cmds[clen - 1] != "X") tpath.cmds.push("X"); // SVG fonts might contain "X". Then, nothing would stroke non-SVG glyphs.
+
+ x += it["ax"]; y += it["ay"];
+ }
+ return { "cmds": tpath.cmds, "crds": tpath.crds };
+ },
+
+ "codeToGlyph": function () {
+
+ // find the greatest index with a value <=v
+ function arrSearch(arr, k, v) {
+ var l = 0, r = ~~(arr.length / k);
+ while (l + 1 != r) { var mid = l + ((r - l) >>> 1); if (arr[mid * k] <= v) l = mid; else r = mid; }
+
+ //var mi = 0; for(var i=0; i= tab.map.length) gid = 0;
+ else gid = tab.map[code];
+ }
+ /*else if(fmt==2) {
+ var data=font["_data"], off = cmap.off+tab.off+6, bin=Typr["B"];
+ var shKey = bin.readUshort(data,off + 2*(code>>>8));
+ var shInd = off + 256*2 + shKey*8;
+
+ var firstCode = bin.readUshort(data,shInd);
+ var entryCount= bin.readUshort(data,shInd+2);
+ var idDelta = bin.readShort (data,shInd+4);
+ var idRangeOffset = bin.readUshort(data,shInd+6);
+
+ if(firstCode<=code && code<=firstCode+entryCount) {
+ // not completely correct
+ gid = bin.readUshort(data, shInd+6+idRangeOffset + (code&255)*2);
+ }
+ else gid=0;
+ //if(code>256) console.log(code,(code>>>8),shKey,firstCode,entryCount,idDelta,idRangeOffset);
+
+ //throw "e";
+ //console.log(tab, bin.readUshort(data,off));
+ //throw "e";
+ }*/
+ else if (fmt == 4) {
+ var ec = tab.endCount; gid = 0;
+ if (code <= ec[ec.length - 1]) {
+ // smallest index with code <= value
+ var sind = arrSearch(ec, 1, code);
+ if (ec[sind] < code) sind++;
+
+ if (code >= tab.startCount[sind]) {
+ var gli = 0;
+ if (tab.idRangeOffset[sind] != 0) gli = tab.glyphIdArray[(code - tab.startCount[sind]) + (tab.idRangeOffset[sind] >> 1) - (tab.idRangeOffset.length - sind)];
+ else gli = code + tab.idDelta[sind];
+ gid = (gli & 0xFFFF);
+ }
+ }
+ }
+ else if (fmt == 6) {
+ var off = code - tab.firstCode, arr = tab.glyphIdArray;
+ if (off < 0 || off >= arr.length) gid = 0;
+ else gid = arr[off];
+ }
+ else if (fmt == 12) {
+ var grp = tab.groups; gid = 0; //console.log(grp); throw "e";
+
+ if (code <= grp[grp.length - 2]) {
+ var i = arrSearch(grp, 3, code);
+ if (grp[i] <= code && code <= grp[i + 1]) { gid = grp[i + 2] + (code - grp[i]); }
+ }
+ }
+ else throw "unknown cmap table format " + tab.format;
+
+ //*
+ var SVG = font["SVG "], loca = font["loca"];
+ // if the font claims to have a Glyph for a character, but the glyph is empty, and the character is not "white", it is a lie!
+ if (gid != 0 && font["CFF "] == null && (SVG == null || SVG.entries[gid] == null) && loca && loca[gid] == loca[gid + 1] // loca not present in CFF or SVG fonts
+ && whm[code] == null) gid = 0;
+ //*/
+
+ return gid;
+ }
+ return ctg;
+ }(),
+
+ "glyphToPath": function (font, gid, noColor) {
+ var path = { cmds: [], crds: [] };
+
+
+ var SVG = font["SVG "], CFF = font["CFF "], COLR = font["COLR"], CBLC = font["CBLC"], CBDT = font["CBDT"], sbix = font["sbix"], upng = window["UPNG"];
+ var U = Typr["U"];
+
+ var strike = null;
+ if (CBLC && upng) for (var i = 0; i < CBLC.length; i++) if (CBLC[i][0] <= gid && gid <= CBLC[i][1]) strike = CBLC[i];
+
+ if (strike || (sbix && sbix[gid])) {
+ if (strike && strike[2] != 17) throw "not a PNG";
+
+ if (font["__tmp"] == null) font["__tmp"] = {};
+ var cmd = font["__tmp"]["g" + gid];
+ if (cmd == null) {
+ var bmp, len;
+ if (sbix) { bmp = sbix[gid]; len = bmp.length; }
+ else {
+ var boff = strike[3][gid - strike[0]] + 5; // smallGlyphMetrics
+ len = (CBDT[boff + 1] << 16) | (CBDT[boff + 2] << 8) | CBDT[boff + 3]; boff += 4;
+ bmp = new Uint8Array(CBDT.buffer, CBDT.byteOffset + boff, len);
+ }
+ var str = ""; for (var i = 0; i < len; i++) str += String.fromCharCode(bmp[i]);
+ cmd = font["__tmp"]["g" + gid] = "data:image/png;base64," + btoa(str);
+ }
+
+ path.cmds.push(cmd);
+ var upe = font["head"]["unitsPerEm"] * 1.15;
+ var gw = Math.round(upe), gh = Math.round(upe), dy = Math.round(-gh * 0.15);
+ path.crds.push(0, gh + dy, gw, gh + dy, gw, dy, 0, dy); //*/
+ }
+ else if (SVG && SVG.entries[gid]) {
+ var p = SVG.entries[gid];
+ if (p != null) {
+ if (typeof p == "number") {
+ var svg = SVG.svgs[p];
+ if (typeof svg == "string") {
+ var prsr = new DOMParser();
+ var doc = prsr["parseFromString"](svg, "image/svg+xml");
+ svg = SVG.svgs[p] = doc.getElementsByTagName("svg")[0];
+ }
+ p = U["SVG"].toPath(svg, gid); SVG.entries[gid] = p;
+ }
+ path = p;
+ }
+ }
+ else if (noColor != true && COLR && COLR[0]["g" + gid] && COLR[0]["g" + gid][1] > 1) {
+
+ function toHex(n) { var o = n.toString(16); return (o.length == 1 ? "0" : "") + o; }
+
+ var CPAL = font["CPAL"], gl = COLR[0]["g" + gid];
+ for (var i = 0; i < gl[1]; i++) {
+ var lid = gl[0] + i;
+ var cgl = COLR[1][2 * lid], pid = COLR[1][2 * lid + 1] * 4;
+ var pth = Typr["U"]["glyphToPath"](font, cgl, cgl == gid);
+
+ var col = "#" + toHex(CPAL[pid + 2]) + toHex(CPAL[pid + 1]) + toHex(CPAL[pid + 0]);
+ path.cmds.push(col);
+
+ path.cmds = path.cmds.concat(pth["cmds"]);
+ path.crds = path.crds.concat(pth["crds"]);
+ //console.log(gid, cgl,pid,col);
+
+ path.cmds.push("X");
+ }
+ }
+ else if (CFF) {
+ var pdct = CFF["Private"];
+ var state = { x: 0, y: 0, stack: [], nStems: 0, haveWidth: false, width: pdct ? pdct["defaultWidthX"] : 0, open: false };
+ if (CFF["ROS"]) {
+ var gi = 0;
+ while (CFF["FDSelect"][gi + 2] <= gid) gi += 2;
+ pdct = CFF["FDArray"][CFF["FDSelect"][gi + 1]]["Private"];
+ }
+ U["_drawCFF"](CFF["CharStrings"][gid], state, CFF, pdct, path);
+ }
+ else if (font["glyf"]) { U["_drawGlyf"](gid, font, path); }
+ return { "cmds": path.cmds, "crds": path.crds };
+ },
+
+ "_drawGlyf": function (gid, font, path) {
+ var gl = font["glyf"][gid];
+ if (gl == null) gl = font["glyf"][gid] = Typr["T"].glyf._parseGlyf(font, gid);
+ if (gl != null) {
+ if (gl.noc > -1) Typr["U"]["_simpleGlyph"](gl, path);
+ else Typr["U"]["_compoGlyph"](gl, font, path);
+ }
+ },
+ "_simpleGlyph": function (gl, p) {
+ var P = Typr["U"]["P"];
+ for (var c = 0; c < gl.noc; c++) {
+ var i0 = (c == 0) ? 0 : (gl.endPts[c - 1] + 1);
+ var il = gl.endPts[c];
+
+ for (var i = i0; i <= il; i++) {
+ var pr = (i == i0) ? il : (i - 1);
+ var nx = (i == il) ? i0 : (i + 1);
+ var onCurve = gl.flags[i] & 1;
+ var prOnCurve = gl.flags[pr] & 1;
+ var nxOnCurve = gl.flags[nx] & 1;
+
+ var x = gl.xs[i], y = gl.ys[i];
+
+ if (i == i0) {
+ if (onCurve) {
+ if (prOnCurve) P.MoveTo(p, gl.xs[pr], gl.ys[pr]);
+ else { P.MoveTo(p, x, y); continue; /* will do CurveTo at il */ }
+ }
+ else {
+ if (prOnCurve) P.MoveTo(p, gl.xs[pr], gl.ys[pr]);
+ else P.MoveTo(p, Math.floor((gl.xs[pr] + x) * 0.5), Math.floor((gl.ys[pr] + y) * 0.5));
+ }
+ }
+ if (onCurve) {
+ if (prOnCurve) P.LineTo(p, x, y);
+ }
+ else {
+ if (nxOnCurve) P.qCurveTo(p, x, y, gl.xs[nx], gl.ys[nx]);
+ else P.qCurveTo(p, x, y, Math.floor((x + gl.xs[nx]) * 0.5), Math.floor((y + gl.ys[nx]) * 0.5));
+ }
+ }
+ P.ClosePath(p);
+ }
+ },
+ "_compoGlyph": function (gl, font, p) {
+ for (var j = 0; j < gl.parts.length; j++) {
+ var path = { cmds: [], crds: [] };
+ var prt = gl.parts[j];
+ Typr["U"]["_drawGlyf"](prt.glyphIndex, font, path);
+
+ var m = prt.m;
+ for (var i = 0; i < path.crds.length; i += 2) {
+ var x = path.crds[i], y = path.crds[i + 1];
+ p.crds.push(x * m.a + y * m.c + m.tx); // not sure, probably right
+ p.crds.push(x * m.b + y * m.d + m.ty);
+ }
+ for (var i = 0; i < path.cmds.length; i++) p.cmds.push(path.cmds[i]);
+ }
+ },
+
+ "pathToSVG": function (path, prec) {
+ var cmds = path["cmds"], crds = path["crds"];
+ if (prec == null) prec = 5;
+ function num(v) { return parseFloat(v.toFixed(prec)); }
+ function merge(o) {
+ var no = [], lstF = false, lstC = "";
+ for (var i = 0; i < o.length; i++) {
+ var it = o[i], isF = (typeof it) == "number";
+ if (!isF) { if (it == lstC && it.length == 1 && it != "m") continue; lstC = it; } // move should not be merged (it actually means lineTo)
+ if (lstF && isF && it >= 0) no.push(" ");
+ no.push(it); lstF = isF;
+ }
+ return no.join("");
+ }
+
+
+ var out = [], co = 0, lmap = { "M": 2, "L": 2, "Q": 4, "C": 6 };
+ var x = 0, y = 0, // perfect coords
+ //dx=0, dy=0, // relative perfect coords
+ //rx=0, ry=0, // relative rounded coords
+ ex = 0, ey = 0, // error between perfect and output coords
+ mx = 0, my = 0; // perfect coords of the last "Move"
+
+ for (var i = 0; i < cmds.length; i++) {
+ var cmd = cmds[i], cc = (lmap[cmd] ? lmap[cmd] : 0);
+
+ var o0 = [], dx, dy, rx, ry; // o1=[], cx, cy, ax,ay;
+ if (cmd == "L") {
+ dx = crds[co] - x; dy = crds[co + 1] - y;
+ rx = num(dx + ex); ry = num(dy + ey);
+ // if this "lineTo" leads to the starting point, and "Z" follows, do not output anything.
+ if (cmds[i + 1] == "Z" && crds[co] == mx && crds[co + 1] == my) { rx = dx; ry = dy; }
+ else if (rx == 0 && ry == 0) { }
+ else if (rx == 0) o0.push("v", ry);
+ else if (ry == 0) o0.push("h", rx);
+ else { o0.push("l", rx, ry); }
+ }
+ else {
+ o0.push(cmd.toLowerCase());
+ for (var j = 0; j < cc; j += 2) {
+ dx = crds[co + j] - x; dy = crds[co + j + 1] - y;
+ rx = num(dx + ex); ry = num(dy + ey);
+ o0.push(rx, ry);
+ }
+ }
+ if (cc != 0) { ex += dx - rx; ey += dy - ry; }
+
+ /*
+ if(cmd=="L") {
+ cx=crds[co]; cy=crds[co+1];
+ ax = num(cx); ay=num(cy);
+ // if this "lineTo" leads to the starting point, and "Z" follows, do not output anything.
+ if(cmds[i+1]=="Z" && crds[co]==mx && crds[co+1]==my) { ax=cx; ay=cy; }
+ else if(ax==num(x) && ay==num(y)) {}
+ else if(ax==num(x)) o1.push("V",ay);
+ else if(ay==num(y)) o1.push("H",ax);
+ else { o1.push("L",ax,ay); }
+ }
+ else {
+ o1.push(cmd);
+ for(var j=0; j> 1, nh = h >> 1;
+ var nbuf = (hlp && hlp.length == nw * nh * 4) ? hlp : new Uint8Array(nw * nh * 4);
+ var sb32 = new Uint32Array(buff.buffer), nb32 = new Uint32Array(nbuf.buffer);
+ for (var y = 0; y < nh; y++)
+ for (var x = 0; x < nw; x++) {
+ var ti = (y * nw + x), si = ((y << 1) * w + (x << 1));
+ //nbuf[ti ] = buff[si ]; nbuf[ti+1] = buff[si+1]; nbuf[ti+2] = buff[si+2]; nbuf[ti+3] = buff[si+3];
+ //*
+ var c0 = sb32[si], c1 = sb32[si + 1], c2 = sb32[si + w], c3 = sb32[si + w + 1];
+
+ var a0 = (c0 >>> 24), a1 = (c1 >>> 24), a2 = (c2 >>> 24), a3 = (c3 >>> 24), a = (a0 + a1 + a2 + a3);
+
+ if (a == 1020) {
+ var r = (((c0 >>> 0) & 255) + ((c1 >>> 0) & 255) + ((c2 >>> 0) & 255) + ((c3 >>> 0) & 255) + 2) >>> 2;
+ var g = (((c0 >>> 8) & 255) + ((c1 >>> 8) & 255) + ((c2 >>> 8) & 255) + ((c3 >>> 8) & 255) + 2) >>> 2;
+ var b = (((c0 >>> 16) & 255) + ((c1 >>> 16) & 255) + ((c2 >>> 16) & 255) + ((c3 >>> 16) & 255) + 2) >>> 2;
+ nb32[ti] = (255 << 24) | (b << 16) | (g << 8) | r;
+ }
+ else if (a == 0) nb32[ti] = 0;
+ else {
+ var r = ((c0 >>> 0) & 255) * a0 + ((c1 >>> 0) & 255) * a1 + ((c2 >>> 0) & 255) * a2 + ((c3 >>> 0) & 255) * a3;
+ var g = ((c0 >>> 8) & 255) * a0 + ((c1 >>> 8) & 255) * a1 + ((c2 >>> 8) & 255) * a2 + ((c3 >>> 8) & 255) * a3;
+ var b = ((c0 >>> 16) & 255) * a0 + ((c1 >>> 16) & 255) * a1 + ((c2 >>> 16) & 255) * a2 + ((c3 >>> 16) & 255) * a3;
+
+ var ia = 1 / a; r = ~~(r * ia + 0.5); g = ~~(g * ia + 0.5); b = ~~(b * ia + 0.5);
+ nb32[ti] = (((a + 2) >>> 2) << 24) | (b << 16) | (g << 8) | r;
+ }
+ }
+ return { buff: nbuf, w: nw, h: nh };
+ }
+
+ return ptc;
+ }(),
+
+ "P": {
+ MoveTo: function (p, x, y) { p.cmds.push("M"); p.crds.push(x, y); },
+ LineTo: function (p, x, y) { p.cmds.push("L"); p.crds.push(x, y); },
+ CurveTo: function (p, a, b, c, d, e, f) { p.cmds.push("C"); p.crds.push(a, b, c, d, e, f); },
+ qCurveTo: function (p, a, b, c, d) { p.cmds.push("Q"); p.crds.push(a, b, c, d); },
+ ClosePath: function (p) { p.cmds.push("Z"); }
+ },
+
+ "_drawCFF": function (cmds, state, font, pdct, p) {
+ var stack = state.stack;
+ var nStems = state.nStems, haveWidth = state.haveWidth, width = state.width, open = state.open;
+ var i = 0;
+ var x = state.x, y = state.y, c1x = 0, c1y = 0, c2x = 0, c2y = 0, c3x = 0, c3y = 0, c4x = 0, c4y = 0, jpx = 0, jpy = 0;
+ var CFF = Typr["T"].CFF, P = Typr["U"]["P"];
+
+ var nominalWidthX = pdct["nominalWidthX"];
+ var o = { val: 0, size: 0 };
+ //console.log(cmds);
+ while (i < cmds.length) {
+ CFF.getCharString(cmds, i, o);
+ var v = o.val;
+ i += o.size;
+
+ if (false) { }
+ else if (v == "o1" || v == "o18") // hstem || hstemhm
+ {
+ var hasWidthArg;
+
+ // The number of stem operators on the stack is always even.
+ // If the value is uneven, that means a width is specified.
+ hasWidthArg = stack.length % 2 !== 0;
+ if (hasWidthArg && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ }
+
+ nStems += stack.length >> 1;
+ stack.length = 0;
+ haveWidth = true;
+ }
+ else if (v == "o3" || v == "o23") // vstem || vstemhm
+ {
+ var hasWidthArg;
+
+ // The number of stem operators on the stack is always even.
+ // If the value is uneven, that means a width is specified.
+ hasWidthArg = stack.length % 2 !== 0;
+ if (hasWidthArg && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ }
+
+ nStems += stack.length >> 1;
+ stack.length = 0;
+ haveWidth = true;
+ }
+ else if (v == "o4") {
+ if (stack.length > 1 && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ haveWidth = true;
+ }
+ if (open) P.ClosePath(p);
+
+ y += stack.pop();
+ P.MoveTo(p, x, y); open = true;
+ }
+ else if (v == "o5") {
+ while (stack.length > 0) {
+ x += stack.shift();
+ y += stack.shift();
+ P.LineTo(p, x, y);
+ }
+ }
+ else if (v == "o6" || v == "o7") // hlineto || vlineto
+ {
+ var count = stack.length;
+ var isX = (v == "o6");
+
+ for (var j = 0; j < count; j++) {
+ var sval = stack.shift();
+
+ if (isX) x += sval; else y += sval;
+ isX = !isX;
+ P.LineTo(p, x, y);
+ }
+ }
+ else if (v == "o8" || v == "o24") // rrcurveto || rcurveline
+ {
+ var count = stack.length;
+ var index = 0;
+ while (index + 6 <= count) {
+ c1x = x + stack.shift();
+ c1y = y + stack.shift();
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x + stack.shift();
+ y = c2y + stack.shift();
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ index += 6;
+ }
+ if (v == "o24") {
+ x += stack.shift();
+ y += stack.shift();
+ P.LineTo(p, x, y);
+ }
+ }
+ else if (v == "o11") break;
+ else if (v == "o1234" || v == "o1235" || v == "o1236" || v == "o1237")//if((v+"").slice(0,3)=="o12")
+ {
+ if (v == "o1234") {
+ c1x = x + stack.shift(); // dx1
+ c1y = y; // dy1
+ c2x = c1x + stack.shift(); // dx2
+ c2y = c1y + stack.shift(); // dy2
+ jpx = c2x + stack.shift(); // dx3
+ jpy = c2y; // dy3
+ c3x = jpx + stack.shift(); // dx4
+ c3y = c2y; // dy4
+ c4x = c3x + stack.shift(); // dx5
+ c4y = y; // dy5
+ x = c4x + stack.shift(); // dx6
+ P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
+ P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
+
+ }
+ if (v == "o1235") {
+ c1x = x + stack.shift(); // dx1
+ c1y = y + stack.shift(); // dy1
+ c2x = c1x + stack.shift(); // dx2
+ c2y = c1y + stack.shift(); // dy2
+ jpx = c2x + stack.shift(); // dx3
+ jpy = c2y + stack.shift(); // dy3
+ c3x = jpx + stack.shift(); // dx4
+ c3y = jpy + stack.shift(); // dy4
+ c4x = c3x + stack.shift(); // dx5
+ c4y = c3y + stack.shift(); // dy5
+ x = c4x + stack.shift(); // dx6
+ y = c4y + stack.shift(); // dy6
+ stack.shift(); // flex depth
+ P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
+ P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
+ }
+ if (v == "o1236") {
+ c1x = x + stack.shift(); // dx1
+ c1y = y + stack.shift(); // dy1
+ c2x = c1x + stack.shift(); // dx2
+ c2y = c1y + stack.shift(); // dy2
+ jpx = c2x + stack.shift(); // dx3
+ jpy = c2y; // dy3
+ c3x = jpx + stack.shift(); // dx4
+ c3y = c2y; // dy4
+ c4x = c3x + stack.shift(); // dx5
+ c4y = c3y + stack.shift(); // dy5
+ x = c4x + stack.shift(); // dx6
+ P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
+ P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
+ }
+ if (v == "o1237") {
+ c1x = x + stack.shift(); // dx1
+ c1y = y + stack.shift(); // dy1
+ c2x = c1x + stack.shift(); // dx2
+ c2y = c1y + stack.shift(); // dy2
+ jpx = c2x + stack.shift(); // dx3
+ jpy = c2y + stack.shift(); // dy3
+ c3x = jpx + stack.shift(); // dx4
+ c3y = jpy + stack.shift(); // dy4
+ c4x = c3x + stack.shift(); // dx5
+ c4y = c3y + stack.shift(); // dy5
+ if (Math.abs(c4x - x) > Math.abs(c4y - y)) {
+ x = c4x + stack.shift();
+ } else {
+ y = c4y + stack.shift();
+ }
+ P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
+ P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
+ }
+ }
+ else if (v == "o14") {
+ if (stack.length > 0 && stack.length != 4 && !haveWidth) {
+ width = stack.shift() + font["nominalWidthX"];
+ haveWidth = true;
+ }
+ if (stack.length == 4) // seac = standard encoding accented character
+ {
+
+ var asb = 0;
+ var adx = stack.shift();
+ var ady = stack.shift();
+ var bchar = stack.shift();
+ var achar = stack.shift();
+
+
+ var bind = CFF.glyphBySE(font, bchar);
+ var aind = CFF.glyphBySE(font, achar);
+
+ //console.log(bchar, bind);
+ //console.log(achar, aind);
+ //state.x=x; state.y=y; state.nStems=nStems; state.haveWidth=haveWidth; state.width=width; state.open=open;
+
+ Typr["U"]["_drawCFF"](font["CharStrings"][bind], state, font, pdct, p);
+ state.x = adx; state.y = ady;
+ Typr["U"]["_drawCFF"](font["CharStrings"][aind], state, font, pdct, p);
+
+ //x=state.x; y=state.y; nStems=state.nStems; haveWidth=state.haveWidth; width=state.width; open=state.open;
+ }
+ if (open) { P.ClosePath(p); open = false; }
+ }
+ else if (v == "o19" || v == "o20") {
+ var hasWidthArg;
+
+ // The number of stem operators on the stack is always even.
+ // If the value is uneven, that means a width is specified.
+ hasWidthArg = stack.length % 2 !== 0;
+ if (hasWidthArg && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ }
+
+ nStems += stack.length >> 1;
+ stack.length = 0;
+ haveWidth = true;
+
+ i += (nStems + 7) >> 3;
+ }
+
+ else if (v == "o21") {
+ if (stack.length > 2 && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ haveWidth = true;
+ }
+
+ y += stack.pop();
+ x += stack.pop();
+
+ if (open) P.ClosePath(p);
+ P.MoveTo(p, x, y); open = true;
+ }
+ else if (v == "o22") {
+ if (stack.length > 1 && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ haveWidth = true;
+ }
+
+ x += stack.pop();
+
+ if (open) P.ClosePath(p);
+ P.MoveTo(p, x, y); open = true;
+ }
+ else if (v == "o25") {
+ while (stack.length > 6) {
+ x += stack.shift();
+ y += stack.shift();
+ P.LineTo(p, x, y);
+ }
+
+ c1x = x + stack.shift();
+ c1y = y + stack.shift();
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x + stack.shift();
+ y = c2y + stack.shift();
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ }
+ else if (v == "o26") {
+ if (stack.length % 2) {
+ x += stack.shift();
+ }
+
+ while (stack.length > 0) {
+ c1x = x;
+ c1y = y + stack.shift();
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x;
+ y = c2y + stack.shift();
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ }
+
+ }
+ else if (v == "o27") {
+ if (stack.length % 2) {
+ y += stack.shift();
+ }
+
+ while (stack.length > 0) {
+ c1x = x + stack.shift();
+ c1y = y;
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x + stack.shift();
+ y = c2y;
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ }
+ }
+ else if (v == "o10" || v == "o29") // callsubr || callgsubr
+ {
+ var obj = (v == "o10" ? pdct : font);
+ if (stack.length == 0) { console.log("error: empty stack"); }
+ else {
+ var ind = stack.pop();
+ var subr = obj["Subrs"][ind + obj["Bias"]];
+ state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open;
+ Typr["U"]["_drawCFF"](subr, state, font, pdct, p);
+ x = state.x; y = state.y; nStems = state.nStems; haveWidth = state.haveWidth; width = state.width; open = state.open;
+ }
+ }
+ else if (v == "o30" || v == "o31") // vhcurveto || hvcurveto
+ {
+ var count, count1 = stack.length;
+ var index = 0;
+ var alternate = v == "o31";
+
+ count = count1 & ~2;
+ index += count1 - count;
+
+ while (index < count) {
+ if (alternate) {
+ c1x = x + stack.shift();
+ c1y = y;
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ y = c2y + stack.shift();
+ if (count - index == 5) { x = c2x + stack.shift(); index++; }
+ else x = c2x;
+ alternate = false;
+ }
+ else {
+ c1x = x;
+ c1y = y + stack.shift();
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x + stack.shift();
+ if (count - index == 5) { y = c2y + stack.shift(); index++; }
+ else y = c2y;
+ alternate = true;
+ }
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ index += 4;
+ }
+ }
+
+ else if ((v + "").charAt(0) == "o") { console.log("Unknown operation: " + v, cmds); throw v; }
+ else stack.push(v);
+ }
+ //console.log(cmds);
+ state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open;
+ },
+
+
+ "SVG": function () {
+ var M = {
+ getScale: function (m) { return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2])); },
+ translate: function (m, x, y) { M.concat(m, [1, 0, 0, 1, x, y]); },
+ rotate: function (m, a) { M.concat(m, [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a), 0, 0]); },
+ scale: function (m, x, y) { M.concat(m, [x, 0, 0, y, 0, 0]); },
+ concat: function (m, w) {
+ var a = m[0], b = m[1], c = m[2], d = m[3], tx = m[4], ty = m[5];
+ m[0] = (a * w[0]) + (b * w[2]); m[1] = (a * w[1]) + (b * w[3]);
+ m[2] = (c * w[0]) + (d * w[2]); m[3] = (c * w[1]) + (d * w[3]);
+ m[4] = (tx * w[0]) + (ty * w[2]) + w[4]; m[5] = (tx * w[1]) + (ty * w[3]) + w[5];
+ },
+ invert: function (m) {
+ var a = m[0], b = m[1], c = m[2], d = m[3], tx = m[4], ty = m[5], adbc = a * d - b * c;
+ m[0] = d / adbc; m[1] = -b / adbc; m[2] = -c / adbc; m[3] = a / adbc;
+ m[4] = (c * ty - d * tx) / adbc; m[5] = (b * tx - a * ty) / adbc;
+ },
+ multPoint: function (m, p) { var x = p[0], y = p[1]; return [x * m[0] + y * m[2] + m[4], x * m[1] + y * m[3] + m[5]]; },
+ multArray: function (m, a) { for (var i = 0; i < a.length; i += 2) { var x = a[i], y = a[i + 1]; a[i] = x * m[0] + y * m[2] + m[4]; a[i + 1] = x * m[1] + y * m[3] + m[5]; } }
+ }
+
+ function _bracketSplit(str, lbr, rbr) {
+ var out = [], pos = 0, ci = 0, lvl = 0;
+ while (true) { //throw "e";
+ var li = str.indexOf(lbr, ci);
+ var ri = str.indexOf(rbr, ci);
+ if (li == -1 && ri == -1) break;
+ if (ri == -1 || (li != -1 && li < ri)) {
+ if (lvl == 0) { out.push(str.slice(pos, li).trim()); pos = li + 1; }
+ lvl++; ci = li + 1;
+ }
+ else if (li == -1 || (ri != -1 && ri < li)) {
+ lvl--;
+ if (lvl == 0) { out.push(str.slice(pos, ri).trim()); pos = ri + 1; }
+ ci = ri + 1;
+ }
+ }
+ return out;
+ }
+ //"cssMap":
+ function cssMap(str) {
+ var pts = _bracketSplit(str, "{", "}");
+ var css = {};
+ for (var i = 0; i < pts.length; i += 2) {
+ var cn = pts[i].split(",");
+ for (var j = 0; j < cn.length; j++) {
+ var cnj = cn[j].trim(); if (css[cnj] == null) css[cnj] = "";
+ css[cnj] += pts[i + 1];
+ }
+ }
+ return css;
+ }
+ //"readTrnf"
+ function readTrnf(trna) {
+ var pts = _bracketSplit(trna, "(", ")");
+ var m = [1, 0, 0, 1, 0, 0];
+ for (var i = 0; i < pts.length; i += 2) { var om = m; m = _readTrnsAttr(pts[i], pts[i + 1]); M.concat(m, om); }
+ return m;
+ }
+
+ function _readTrnsAttr(fnc, vls) {
+ //console.log(vls);
+ //vls = vls.replace(/\-/g, " -").trim();
+ var m = [1, 0, 0, 1, 0, 0], gotSep = true;
+ for (var i = 0; i < vls.length; i++) { // matrix(.99915 0 0 .99915.418.552) matrix(1 0 0-.9474-22.535 271.03)
+ var ch = vls.charAt(i);
+ if (ch == "," || ch == " ") gotSep = true;
+ else if (ch == ".") {
+ if (!gotSep) { vls = vls.slice(0, i) + "," + vls.slice(i); i++; } gotSep = false;
+ }
+ else if (ch == "-" && i > 0 && vls[i - 1] != "e") { vls = vls.slice(0, i) + " " + vls.slice(i); i++; gotSep = true; }
+ }
+
+ vls = vls.split(/\s*[\s,]\s*/).map(parseFloat);
+ if (false) { }
+ else if (fnc == "translate") { if (vls.length == 1) M.translate(m, vls[0], 0); else M.translate(m, vls[0], vls[1]); }
+ else if (fnc == "scale") { if (vls.length == 1) M.scale(m, vls[0], vls[0]); else M.scale(m, vls[0], vls[1]); }
+ else if (fnc == "rotate") { var tx = 0, ty = 0; if (vls.length != 1) { tx = vls[1]; ty = vls[2]; } M.translate(m, -tx, -ty); M.rotate(m, -Math.PI * vls[0] / 180); M.translate(m, tx, ty); }
+ else if (fnc == "matrix") m = vls;
+ else console.log("unknown transform: ", fnc);
+ return m;
+ }
+
+ function toPath(svg, gid) {
+ var pth = { cmds: [], crds: [] };
+
+ var vb = svg.getAttribute("viewBox");
+ if (vb) vb = vb.trim().split(" ").map(parseFloat); else vb = [0, 0, 1000, 1000];
+
+ var nod = svg;
+ if (gid != null) { var nd = svg.getElementById("glyph" + gid); if (nd) nod = nd; }
+
+ _toPath(nod.children, pth, null, svg);
+ for (var i = 0; i < pth.crds.length; i += 2) {
+ var x = pth.crds[i], y = pth.crds[i + 1];
+ x -= vb[0];
+ y -= vb[1];
+ y = -y;
+ pth.crds[i] = x;
+ pth.crds[i + 1] = y;
+ }
+ return pth;
+ }
+
+ var cmap = {
+ "aliceblue": "#f0f8ff", "antiquewhite": "#faebd7", "aqua": "#00ffff", "aquamarine": "#7fffd4", "azure": "#f0ffff", "beige": "#f5f5dc", "bisque": "#ffe4c4",
+ "black": "#000000", "blanchedalmond": "#ffebcd", "blue": "#0000ff", "blueviolet": "#8a2be2", "brown": "#a52a2a", "burlywood": "#deb887", "cadetblue": "#5f9ea0",
+ "chartreuse": "#7fff00", "chocolate": "#d2691e", "coral": "#ff7f50", "cornflowerblue": "#6495ed", "cornsilk": "#fff8dc", "crimson": "#dc143c", "cyan": "#00ffff",
+ "darkblue": "#00008b", "darkcyan": "#008b8b", "darkgoldenrod": "#b8860b", "darkgray": "#a9a9a9", "darkgreen": "#006400", "darkgrey": "#a9a9a9", "darkkhaki": "#bdb76b",
+ "darkmagenta": "#8b008b", "darkolivegreen": "#556b2f", "darkorange": "#ff8c00", "darkorchid": "#9932cc", "darkred": "#8b0000", "darksalmon": "#e9967a", "darkseagreen": "#8fbc8f",
+ "darkslateblue": "#483d8b", "darkslategray": "#2f4f4f", "darkslategrey": "#2f4f4f", "darkturquoise": "#00ced1", "darkviolet": "#9400d3", "deeppink": "#ff1493",
+ "deepskyblue": "#00bfff", "dimgray": "#696969", "dimgrey": "#696969", "dodgerblue": "#1e90ff", "firebrick": "#b22222", "floralwhite": "#fffaf0", "forestgreen": "#228b22",
+ "fuchsia": "#ff00ff", "gainsboro": "#dcdcdc", "ghostwhite": "#f8f8ff", "gold": "#ffd700", "goldenrod": "#daa520", "gray": "#808080", "green": "#008000", "greenyellow": "#adff2f",
+ "grey": "#808080", "honeydew": "#f0fff0", "hotpink": "#ff69b4", "indianred": "#cd5c5c", "indigo": "#4b0082", "ivory": "#fffff0", "khaki": "#f0e68c", "lavender": "#e6e6fa",
+ "lavenderblush": "#fff0f5", "lawngreen": "#7cfc00", "lemonchiffon": "#fffacd", "lightblue": "#add8e6", "lightcoral": "#f08080", "lightcyan": "#e0ffff",
+ "lightgoldenrodyellow": "#fafad2", "lightgray": "#d3d3d3", "lightgreen": "#90ee90", "lightgrey": "#d3d3d3", "lightpink": "#ffb6c1", "lightsalmon": "#ffa07a",
+ "lightseagreen": "#20b2aa", "lightskyblue": "#87cefa", "lightslategray": "#778899", "lightslategrey": "#778899", "lightsteelblue": "#b0c4de", "lightyellow": "#ffffe0",
+ "lime": "#00ff00", "limegreen": "#32cd32", "linen": "#faf0e6", "magenta": "#ff00ff", "maroon": "#800000", "mediumaquamarine": "#66cdaa", "mediumblue": "#0000cd",
+ "mediumorchid": "#ba55d3", "mediumpurple": "#9370db", "mediumseagreen": "#3cb371", "mediumslateblue": "#7b68ee", "mediumspringgreen": "#00fa9a",
+ "mediumturquoise": "#48d1cc", "mediumvioletred": "#c71585", "midnightblue": "#191970", "mintcream": "#f5fffa", "mistyrose": "#ffe4e1", "moccasin": "#ffe4b5",
+ "navajowhite": "#ffdead", "navy": "#000080", "oldlace": "#fdf5e6", "olive": "#808000", "olivedrab": "#6b8e23", "orange": "#ffa500", "orangered": "#ff4500",
+ "orchid": "#da70d6", "palegoldenrod": "#eee8aa", "palegreen": "#98fb98", "paleturquoise": "#afeeee", "palevioletred": "#db7093", "papayawhip": "#ffefd5",
+ "peachpuff": "#ffdab9", "peru": "#cd853f", "pink": "#ffc0cb", "plum": "#dda0dd", "powderblue": "#b0e0e6", "purple": "#800080", "rebeccapurple": "#663399", "red": "#ff0000",
+ "rosybrown": "#bc8f8f", "royalblue": "#4169e1", "saddlebrown": "#8b4513", "salmon": "#fa8072", "sandybrown": "#f4a460", "seagreen": "#2e8b57", "seashell": "#fff5ee",
+ "sienna": "#a0522d", "silver": "#c0c0c0", "skyblue": "#87ceeb", "slateblue": "#6a5acd", "slategray": "#708090", "slategrey": "#708090", "snow": "#fffafa", "springgreen": "#00ff7f",
+ "steelblue": "#4682b4", "tan": "#d2b48c", "teal": "#008080", "thistle": "#d8bfd8", "tomato": "#ff6347", "turquoise": "#40e0d0", "violet": "#ee82ee", "wheat": "#f5deb3",
+ "white": "#ffffff", "whitesmoke": "#f5f5f5", "yellow": "#ffff00", "yellowgreen": "#9acd32"
+ };
+
+ function _toPath(nds, pth, fill, root) {
+ for (var ni = 0; ni < nds.length; ni++) {
+ var nd = nds[ni], tn = nd.tagName;
+ var cfl = nd.getAttribute("fill"); if (cfl == null) cfl = fill;
+ if (cfl && cfl.startsWith("url")) {
+ var gid = cfl.slice(5, -1);
+ var grd = root.getElementById(gid), s0 = grd.children[0];
+ if (s0.getAttribute("stop-opacity") != null) continue;
+ cfl = s0.getAttribute("stop-color");
+ }
+ if (cmap[cfl]) cfl = cmap[cfl];
+ if (tn == "g" || tn == "use") {
+ var tp = { crds: [], cmds: [] };
+ if (tn == "g") _toPath(nd.children, tp, cfl, root);
+ else {
+ var lnk = nd.getAttribute("xlink:href").slice(1);
+ var pel = root.getElementById(lnk);
+ _toPath([pel], tp, cfl, root);
+ }
+ var m = [1, 0, 0, 1, 0, 0];
+ var x = nd.getAttribute("x"), y = nd.getAttribute("y"); x = x ? parseFloat(x) : 0; y = y ? parseFloat(y) : 0;
+ M.concat(m, [1, 0, 0, 1, x, y]);
+
+ var trf = nd.getAttribute("transform"); if (trf) M.concat(m, readTrnf(trf));
+
+ M.multArray(m, tp.crds);
+ pth.crds = pth.crds.concat(tp.crds);
+ pth.cmds = pth.cmds.concat(tp.cmds);
+ }
+ else if (tn == "path" || tn == "circle" || tn == "ellipse") {
+ pth.cmds.push(cfl ? cfl : "#000000");
+ var d;
+ if (tn == "path") d = nd.getAttribute("d"); //console.log(d);
+ if (tn == "circle" || tn == "ellipse") {
+ var vls = [0, 0, 0, 0], nms = ["cx", "cy", "rx", "ry", "r"];
+ for (var i = 0; i < 5; i++) { var V = nd.getAttribute(nms[i]); if (V) { V = parseFloat(V); if (i < 4) vls[i] = V; else vls[2] = vls[3] = V; } }
+ var cx = vls[0], cy = vls[1], rx = vls[2], ry = vls[3];
+ d = ["M", cx - rx, cy, "a", rx, ry, 0, 1, 0, rx * 2, 0, "a", rx, ry, 0, 1, 0, -rx * 2, 0].join(" ");
+ }
+ svgToPath(d, pth); pth.cmds.push("X");
+ }
+ else if (tn == "image") {
+ var w = parseFloat(nd.getAttribute("width")), h = parseFloat(nd.getAttribute("height"));
+ pth.cmds.push(nd.getAttribute("xlink:href"));
+ pth.crds.push(0, 0, w, 0, w, h, 0, h);
+ }
+ else if (tn == "defs") { }
+ else console.log(tn);
+ }
+ }
+
+ function _tokens(d) {
+ var ts = [], off = 0, rn = false, cn = "", pc = "", lc = "", nc = 0; // reading number, current number, prev char, lastCommand, number count (after last command
+ while (off < d.length) {
+ var cc = d.charCodeAt(off), ch = d.charAt(off); off++;
+ var isNum = (48 <= cc && cc <= 57) || ch == "." || ch == "-" || ch == "+" || ch == "e" || ch == "E";
+
+ if (rn) {
+ if (((ch == "+" || ch == "-") && pc != "e") || (ch == "." && cn.indexOf(".") != -1) || (isNum && (lc == "a" || lc == "A") && ((nc % 7) == 3 || (nc % 7) == 4))) { ts.push(parseFloat(cn)); nc++; cn = ch; }
+ else if (isNum) cn += ch;
+ else { ts.push(parseFloat(cn)); nc++; if (ch != "," && ch != " ") { ts.push(ch); lc = ch; nc = 0; } rn = false; }
+ }
+ else {
+ if (isNum) { cn = ch; rn = true; }
+ else if (ch != "," && ch != " ") { ts.push(ch); lc = ch; nc = 0; }
+ }
+ pc = ch;
+ }
+ if (rn) ts.push(parseFloat(cn));
+ return ts;
+ }
+
+ function _reps(ts, off, ps) {
+ var i = off;
+ while (i < ts.length) { if ((typeof ts[i]) == "string") break; i += ps; }
+ return (i - off) / ps;
+ }
+
+ function svgToPath(d, pth) {
+ var ts = _tokens(d);
+ var i = 0, x = 0, y = 0, ox = 0, oy = 0, oldo = pth.crds.length;
+ var pc = { "M": 2, "L": 2, "H": 1, "V": 1, "T": 2, "S": 4, "A": 7, "Q": 4, "C": 6 };
+ var cmds = pth.cmds, crds = pth.crds;
+
+ while (i < ts.length) {
+ var cmd = ts[i]; i++;
+ var cmu = cmd.toUpperCase();
+
+ if (cmu == "Z") { cmds.push("Z"); x = ox; y = oy; }
+ else {
+ var ps = pc[cmu], reps = _reps(ts, i, ps);
+
+ for (var j = 0; j < reps; j++) {
+ // If a moveto is followed by multiple pairs of coordinates, the subsequent pairs are treated as implicit lineto commands.
+ if (j == 1 && cmu == "M") { cmd = (cmd == cmu) ? "L" : "l"; cmu = "L"; }
+
+ var xi = 0, yi = 0; if (cmd != cmu) { xi = x; yi = y; }
+
+ if (false) { }
+ else if (cmu == "M") { x = xi + ts[i++]; y = yi + ts[i++]; cmds.push("M"); crds.push(x, y); ox = x; oy = y; }
+ else if (cmu == "L") { x = xi + ts[i++]; y = yi + ts[i++]; cmds.push("L"); crds.push(x, y); }
+ else if (cmu == "H") { x = xi + ts[i++]; cmds.push("L"); crds.push(x, y); }
+ else if (cmu == "V") { y = yi + ts[i++]; cmds.push("L"); crds.push(x, y); }
+ else if (cmu == "Q") {
+ var x1 = xi + ts[i++], y1 = yi + ts[i++], x2 = xi + ts[i++], y2 = yi + ts[i++];
+ cmds.push("Q"); crds.push(x1, y1, x2, y2); x = x2; y = y2;
+ }
+ else if (cmu == "T") {
+ var co = Math.max(crds.length - (cmds[cmds.length - 1] == "Q" ? 4 : 2), oldo);
+ var x1 = x + x - crds[co], y1 = y + y - crds[co + 1];
+ var x2 = xi + ts[i++], y2 = yi + ts[i++];
+ cmds.push("Q"); crds.push(x1, y1, x2, y2); x = x2; y = y2;
+ }
+ else if (cmu == "C") {
+ var x1 = xi + ts[i++], y1 = yi + ts[i++], x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++];
+ cmds.push("C"); crds.push(x1, y1, x2, y2, x3, y3); x = x3; y = y3;
+ }
+ else if (cmu == "S") {
+ var co = Math.max(crds.length - (cmds[cmds.length - 1] == "C" ? 4 : 2), oldo);
+ var x1 = x + x - crds[co], y1 = y + y - crds[co + 1];
+ var x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++];
+ cmds.push("C"); crds.push(x1, y1, x2, y2, x3, y3); x = x3; y = y3;
+ }
+ else if (cmu == "A") { // convert SVG Arc to four cubic bézier segments "C"
+ var x1 = x, y1 = y;
+ var rx = ts[i++], ry = ts[i++];
+ var phi = ts[i++] * (Math.PI / 180), fA = ts[i++], fS = ts[i++];
+ var x2 = xi + ts[i++], y2 = yi + ts[i++];
+ if (x2 == x && y2 == y && rx == 0 && ry == 0) continue;
+
+ var hdx = (x1 - x2) / 2, hdy = (y1 - y2) / 2;
+ var cosP = Math.cos(phi), sinP = Math.sin(phi);
+ var x1A = cosP * hdx + sinP * hdy;
+ var y1A = -sinP * hdx + cosP * hdy;
+
+ var rxS = rx * rx, ryS = ry * ry;
+ var x1AS = x1A * x1A, y1AS = y1A * y1A;
+ var frc = (rxS * ryS - rxS * y1AS - ryS * x1AS) / (rxS * y1AS + ryS * x1AS);
+ var coef = (fA != fS ? 1 : -1) * Math.sqrt(Math.max(frc, 0));
+ var cxA = coef * (rx * y1A) / ry;
+ var cyA = -coef * (ry * x1A) / rx;
+
+ var cx = cosP * cxA - sinP * cyA + (x1 + x2) / 2;
+ var cy = sinP * cxA + cosP * cyA + (y1 + y2) / 2;
+
+ var angl = function (ux, uy, vx, vy) {
+ var lU = Math.sqrt(ux * ux + uy * uy), lV = Math.sqrt(vx * vx + vy * vy);
+ var num = (ux * vx + uy * vy) / (lU * lV); //console.log(num, Math.acos(num));
+ return (ux * vy - uy * vx >= 0 ? 1 : -1) * Math.acos(Math.max(-1, Math.min(1, num)));
+ }
+
+ var vX = (x1A - cxA) / rx, vY = (y1A - cyA) / ry;
+ var theta1 = angl(1, 0, vX, vY);
+ var dtheta = angl(vX, vY, (-x1A - cxA) / rx, (-y1A - cyA) / ry);
+ dtheta = dtheta % (2 * Math.PI);
+
+ var arc = function (gst, x, y, r, a0, a1, neg) {
+ var rotate = function (m, a) {
+ var si = Math.sin(a), co = Math.cos(a);
+ var a = m[0], b = m[1], c = m[2], d = m[3];
+ m[0] = (a * co) + (b * si); m[1] = (-a * si) + (b * co);
+ m[2] = (c * co) + (d * si); m[3] = (-c * si) + (d * co);
+ }
+ var multArr = function (m, a) {
+ for (var j = 0; j < a.length; j += 2) {
+ var x = a[j], y = a[j + 1];
+ a[j] = m[0] * x + m[2] * y + m[4];
+ a[j + 1] = m[1] * x + m[3] * y + m[5];
+ }
+ }
+ var concatA = function (a, b) { for (var j = 0; j < b.length; j++) a.push(b[j]); }
+ var concatP = function (p, r) { concatA(p.cmds, r.cmds); concatA(p.crds, r.crds); }
+ // circle from a0 counter-clock-wise to a1
+ if (neg) while (a1 > a0) a1 -= 2 * Math.PI;
+ else while (a1 < a0) a1 += 2 * Math.PI;
+ var th = (a1 - a0) / 4;
+
+ var x0 = Math.cos(th / 2), y0 = -Math.sin(th / 2);
+ var x1 = (4 - x0) / 3, y1 = y0 == 0 ? y0 : (1 - x0) * (3 - x0) / (3 * y0);
+ var x2 = x1, y2 = -y1;
+ var x3 = x0, y3 = -y0;
+
+ var ps = [x1, y1, x2, y2, x3, y3];
+
+ var pth = { cmds: ["C", "C", "C", "C"], crds: ps.slice(0) };
+ var rot = [1, 0, 0, 1, 0, 0]; rotate(rot, -th);
+ for (var j = 0; j < 3; j++) { multArr(rot, ps); concatA(pth.crds, ps); }
+
+ rotate(rot, -a0 + th / 2); rot[0] *= r; rot[1] *= r; rot[2] *= r; rot[3] *= r; rot[4] = x; rot[5] = y;
+ multArr(rot, pth.crds);
+ multArr(gst.ctm, pth.crds);
+ concatP(gst.pth, pth);
+ }
+
+ var gst = { pth: pth, ctm: [rx * cosP, rx * sinP, -ry * sinP, ry * cosP, cx, cy] };
+ arc(gst, 0, 0, 1, theta1, theta1 + dtheta, fS == 0);
+ x = x2; y = y2;
+ }
+ else console.log("Unknown SVG command " + cmd);
+ }
+ }
+ }
+ };
+ return { "cssMap": cssMap, "readTrnf": readTrnf, svgToPath: svgToPath, toPath: toPath };
+ }(),
+
+
+
+
+ "initHB": function (hurl, resp) {
+ var codeLength = function (code) {
+ var len = 0;
+ if ((code & (0xffffffff - (1 << 7) + 1)) == 0) { len = 1; }
+ else if ((code & (0xffffffff - (1 << 11) + 1)) == 0) { len = 2; }
+ else if ((code & (0xffffffff - (1 << 16) + 1)) == 0) { len = 3; }
+ else if ((code & (0xffffffff - (1 << 21) + 1)) == 0) { len = 4; }
+ return len;
+ }
+
+ fetch(hurl)
+ .then(function (x) { return x["arrayBuffer"](); })
+ .then(function (ab) { return WebAssembly["instantiate"](ab); })
+ .then(function (res) {
+ console.log("HB ready");
+ var exp = res["instance"]["exports"], mem = exp["memory"];
+ //mem["grow"](30); // each page is 64kb in size
+ var heapu8, u32, i32;
+ var __lastFnt, blob, blobPtr, face, font;
+
+ Typr["U"]["shapeHB"] = (function () {
+
+ var toJson = function (ptr) {
+ var length = exp["hb_buffer_get_length"](ptr);
+ var result = [];
+ var iPtr32 = exp["hb_buffer_get_glyph_infos"](ptr, 0) >>> 2;
+ var pPtr32 = exp["hb_buffer_get_glyph_positions"](ptr, 0) >>> 2;
+ for (var i = 0; i < length; ++i) {
+ var a = iPtr32 + i * 5, b = pPtr32 + i * 5;
+ result.push({
+ "g": u32[a + 0],
+ "cl": u32[a + 2],
+ "ax": i32[b + 0],
+ "ay": i32[b + 1],
+ "dx": i32[b + 2],
+ "dy": i32[b + 3]
+ });
+ }
+ //console.log(result);
+ return result;
+ }
+ var te;
+
+ return function (fnt, str, ltr) {
+ var fdata = fnt["_data"], fn = fnt["name"]["postScriptName"];
+
+ //var olen = mem.buffer.byteLength, nlen = 2*fdata.length+str.length*16 + 4e6;
+ //if(olen>>16)+4); //console.log("growing",nlen);
+
+ heapu8 = new Uint8Array(mem.buffer);
+ u32 = new Uint32Array(mem.buffer);
+ i32 = new Int32Array(mem.buffer);
+
+ if (__lastFnt != fn) {
+ if (blob != null) {
+ exp["hb_blob_destroy"](blob);
+ exp["free"](blobPtr);
+ exp["hb_face_destroy"](face);
+ exp["hb_font_destroy"](font);
+ }
+ blobPtr = exp["malloc"](fdata.byteLength); heapu8.set(fdata, blobPtr);
+ blob = exp["hb_blob_create"](blobPtr, fdata.byteLength, 2, 0, 0);
+ face = exp["hb_face_create"](blob, 0);
+ font = exp["hb_font_create"](face)
+ __lastFnt = fn;
+ }
+ if (window["TextEncoder"] == null) { alert("Your browser is too old. Please, update it."); return; }
+ if (te == null) te = new window["TextEncoder"]("utf8");
+
+ var buffer = exp["hb_buffer_create"]();
+ var bytes = te["encode"](str);
+ var len = bytes.length, strp = exp["malloc"](len); heapu8.set(bytes, strp);
+ exp["hb_buffer_add_utf8"](buffer, strp, len, 0, len);
+ exp["free"](strp);
+
+ exp["hb_buffer_set_direction"](buffer, ltr ? 4 : 5);
+ exp["hb_buffer_guess_segment_properties"](buffer);
+ exp["hb_shape"](font, buffer, 0, 0);
+ var json = toJson(buffer)//buffer["json"]();
+ exp["hb_buffer_destroy"](buffer);
+
+ var arr = json.slice(0); if (!ltr) arr.reverse();
+ var ci = 0, bi = 0; // character index, binary index
+ for (var i = 1; i < arr.length; i++) {
+ var gl = arr[i], cl = gl["cl"];
+ while (true) {
+ var cpt = str.codePointAt(ci), cln = codeLength(cpt);
+ if (bi + cln <= cl) { bi += cln; ci += cpt <= 0xffff ? 1 : 2; }
+ else break;
+ }
+ //while(bi+codeLength(str.charCodeAt(ci)) <=cl) { bi+=codeLength(str.charCodeAt(ci)); ci++; }
+ gl["cl"] = ci;
+ }
+ return json;
+ }
+ }());
+ resp();
+ });
+ }
+}
+
diff --git a/src/type/lib/Typr.js b/src/type/lib/Typr.js
new file mode 100644
index 0000000000..8af0273ca8
--- /dev/null
+++ b/src/type/lib/Typr.js
@@ -0,0 +1,2828 @@
+
+
+var Typr = {};
+
+Typr["parse"] = function(buff)
+{
+ var readFont = function(data, idx, offset,tmap) {
+ var bin = Typr["B"];
+
+ var T = Typr["T"];
+ var prsr = {
+ "cmap":T.cmap,
+ "head":T.head,
+ "hhea":T.hhea,
+ "maxp":T.maxp,
+ "hmtx":T.hmtx,
+ "name":T.name,
+ "OS/2":T.OS2,
+ "post":T.post,
+
+ "loca":T.loca,
+ "kern":T.kern,
+ "glyf":T.glyf,
+
+ "CFF ":T.CFF,
+ /*
+ "GPOS",
+ "GSUB",
+ "GDEF",*/
+ "CBLC":T.CBLC,
+ "CBDT":T.CBDT,
+
+ "SVG ":T.SVG,
+ "COLR":T.colr,
+ "CPAL":T.cpal,
+ "sbix":T.sbix
+ //"VORG",
+ };
+ var obj = {"_data":data, "_index":idx, "_offset":offset};
+
+ for(var t in prsr) {
+ var tab = Typr["findTable"](data, t, offset);
+ if(tab) {
+ var off=tab[0], tobj = tmap[off];
+ if(tobj==null) tobj = prsr[t].parseTab(data, off, tab[1], obj);
+ obj[t] = tmap[off] = tobj;
+ }
+ }
+ return obj;
+ }
+
+
+ var bin = Typr["B"];
+ var data = new Uint8Array(buff);
+
+ var tmap = {};
+ var tag = bin.readASCII(data, 0, 4);
+ if(tag=="ttcf") {
+ var offset = 4;
+ var majV = bin.readUshort(data, offset); offset+=2;
+ var minV = bin.readUshort(data, offset); offset+=2;
+ var numF = bin.readUint (data, offset); offset+=4;
+ var fnts = [];
+ for(var i=0; i=buff.length) throw "error";
+ var a = Typr["B"].t.uint8;
+ a[0] = buff[p+3];
+ a[1] = buff[p+2];
+ a[2] = buff[p+1];
+ a[3] = buff[p];
+ return Typr["B"].t.int32[0];
+ },
+
+ readInt8 : function(buff, p)
+ {
+ //if(p>=buff.length) throw "error";
+ var a = Typr["B"].t.uint8;
+ a[0] = buff[p];
+ return Typr["B"].t.int8[0];
+ },
+ readShort : function(buff, p)
+ {
+ //if(p>=buff.length) throw "error";
+ var a = Typr["B"].t.uint16;
+ a[0] = (buff[p]<<8) | buff[p+1];
+ return Typr["B"].t.int16[0];
+ },
+ readUshort : function(buff, p)
+ {
+ //if(p>=buff.length) throw "error";
+ return (buff[p]<<8) | buff[p+1];
+ },
+ writeUshort : function(buff, p, n)
+ {
+ buff[p] = (n>>8)&255; buff[p+1] = n&255;
+ },
+ readUshorts : function(buff, p, len)
+ {
+ var arr = [];
+ for(var i=0; i=buff.length) throw "error";
+ var a = Typr["B"].t.uint8;
+ a[3] = buff[p]; a[2] = buff[p+1]; a[1] = buff[p+2]; a[0] = buff[p+3];
+ return Typr["B"].t.uint32[0];
+ },
+ writeUint: function(buff, p, n)
+ {
+ buff[p] = (n>>24)&255; buff[p+1] = (n>>16)&255; buff[p+2] = (n>>8)&255; buff[p+3] = (n>>0)&255;
+ },
+ readUint64 : function(buff, p)
+ {
+ //if(p>=buff.length) throw "error";
+ return (Typr["B"].readUint(buff, p)*(0xffffffff+1)) + Typr["B"].readUint(buff, p+4);
+ },
+ readASCII : function(buff, p, l) // l : length in Characters (not Bytes)
+ {
+ //if(p>=buff.length) throw "error";
+ var s = "";
+ for(var i = 0; i < l; i++) s += String.fromCharCode(buff[p+i]);
+ return s;
+ },
+ writeASCII : function(buff, p, s) // l : length in Characters (not Bytes)
+ {
+ for(var i = 0; i < s.length; i++)
+ buff[p+i] = s.charCodeAt(i);
+ },
+ readUnicode : function(buff, p, l)
+ {
+ //if(p>=buff.length) throw "error";
+ var s = "";
+ for(var i = 0; i < l; i++)
+ {
+ var c = (buff[p++]<<8) | buff[p++];
+ s += String.fromCharCode(c);
+ }
+ return s;
+ },
+ _tdec : window["TextDecoder"] ? new window["TextDecoder"]() : null,
+ readUTF8 : function(buff, p, l) {
+ var tdec = Typr["B"]._tdec;
+ if(tdec && p==0 && l==buff.length) return tdec["decode"](buff);
+ return Typr["B"].readASCII(buff,p,l);
+ },
+ readBytes : function(buff, p, l)
+ {
+ //if(p>=buff.length) throw "error";
+ var arr = [];
+ for(var i=0; i=buff.length) throw "error";
+ var s = [];
+ for(var i = 0; i < l; i++)
+ s.push(String.fromCharCode(buff[p+i]));
+ return s;
+ },
+ t : function() {
+ var ab = new ArrayBuffer(8);
+ return {
+ buff : ab,
+ int8 : new Int8Array (ab),
+ uint8 : new Uint8Array (ab),
+ int16 : new Int16Array (ab),
+ uint16 : new Uint16Array(ab),
+ int32 : new Int32Array (ab),
+ uint32 : new Uint32Array(ab)
+ }
+ }()
+};
+
+
+
+
+
+
+ Typr["T"].CFF = {
+ parseTab : function(data, offset, length)
+ {
+ var bin = Typr["B"];
+ var CFF = Typr["T"].CFF;
+
+ data = new Uint8Array(data.buffer, offset, length);
+ offset = 0;
+
+ // Header
+ var major = data[offset]; offset++;
+ var minor = data[offset]; offset++;
+ var hdrSize = data[offset]; offset++;
+ var offsize = data[offset]; offset++;
+ //console.log(major, minor, hdrSize, offsize);
+
+ // Name INDEX
+ var ninds = [];
+ offset = CFF.readIndex(data, offset, ninds);
+ var names = [];
+
+ for(var i=0; i 255 ) return -1;
+ return Typr["T"].CFF.glyphByUnicode(cff, Typr["T"].CFF.tableSE[charcode]);
+ },
+
+ /*readEncoding : function(data, offset, num)
+ {
+ var bin = Typr["B"];
+
+ var array = ['.notdef'];
+ var format = data[offset]; offset++;
+ //console.log("Encoding");
+ //console.log(format);
+
+ if(format==0)
+ {
+ var nCodes = data[offset]; offset++;
+ for(var i=0; i>4, nib1 = b&0xf;
+ if(nib0 != 0xf) nibs.push(nib0); if(nib1!=0xf) nibs.push(nib1);
+ if(nib1==0xf) break;
+ }
+ var s = "";
+ var chars = [0,1,2,3,4,5,6,7,8,9,".","e","e-","reserved","-","endOfNumber"];
+ for(var i=0; i>>1;
+ obj.searchRange = rU(data, offset); offset+=2;
+ obj.entrySelector = rU(data, offset); offset+=2;
+ obj.rangeShift = rU(data, offset); offset+=2;
+ obj.endCount = rUs(data, offset, segCount); offset += segCount*2;
+ offset+=2;
+ obj.startCount = rUs(data, offset, segCount); offset += segCount*2;
+ obj.idDelta = [];
+ for(var i=0; i>>1); //offset += segCount*2;
+ return obj;
+ },
+
+ parse6 : function(data, offset, obj)
+ {
+ var bin = Typr["B"];
+ var offset0 = offset;
+ offset+=2;
+ var length = bin.readUshort(data, offset); offset+=2;
+ var language = bin.readUshort(data, offset); offset+=2;
+ obj.firstCode = bin.readUshort(data, offset); offset+=2;
+ var entryCount = bin.readUshort(data, offset); offset+=2;
+ obj.glyphIdArray = [];
+ for(var i=0; i=gl.xMax || gl.yMin>=gl.yMax) return null;
+
+ if(gl.noc>0)
+ {
+ gl.endPts = [];
+ for(var i=0; i>>8;
+ /* I have seen format 128 once, that's why I do */ format &= 0xf;
+ if(format==0) offset = kern.readFormat0(data, offset, map);
+ //else throw "unknown kern table format: "+format;
+ }
+ return map;
+ },
+
+ parseV1 : function(data, offset, length, font)
+ {
+ var bin = Typr["B"], kern=Typr["T"].kern;
+
+ var version = bin.readFixed(data, offset); // 0x00010000
+ var nTables = bin.readUint (data, offset+4); offset+=8;
+
+ var map = {glyph1: [], rval:[]};
+ for(var i=0; i 0xffff) i++;
+ gls.push(Typr["U"]["codeToGlyph"](font, cc));
+ }
+ var shape = [];
+ var x = 0, y = 0;
+
+ for (var i = 0; i < gls.length; i++) {
+ var padj = getGlyphPosition(font, gls, i, ltr);
+ var gid = gls[i];
+ var ax = font["hmtx"].aWidth[gid] + padj[2];
+ shape.push({ "g": gid, "cl": i, "dx": 0, "dy": 0, "ax": ax, "ay": 0 });
+ x += ax;
+ }
+ return shape;
+ },
+
+ "shapeToPath": function (font, shape, clr) {
+ var tpath = { cmds: [], crds: [] };
+ var x = 0, y = 0;
+
+ for (var i = 0; i < shape.length; i++) {
+ var it = shape[i]
+ var path = Typr["U"]["glyphToPath"](font, it["g"]), crds = path["crds"];
+ for (var j = 0; j < crds.length; j += 2) {
+ tpath.crds.push(crds[j] + x + it["dx"]);
+ tpath.crds.push(crds[j + 1] + y + it["dy"]);
+ }
+ if (clr) tpath.cmds.push(clr);
+ for (var j = 0; j < path["cmds"].length; j++) tpath.cmds.push(path["cmds"][j]);
+ var clen = tpath.cmds.length;
+ if (clr) if (clen != 0 && tpath.cmds[clen - 1] != "X") tpath.cmds.push("X"); // SVG fonts might contain "X". Then, nothing would stroke non-SVG glyphs.
+
+ x += it["ax"]; y += it["ay"];
+ }
+ return { "cmds": tpath.cmds, "crds": tpath.crds };
+ },
+
+ "codeToGlyph": function () {
+
+ // find the greatest index with a value <=v
+ function arrSearch(arr, k, v) {
+ var l = 0, r = ~~(arr.length / k);
+ while (l + 1 != r) { var mid = l + ((r - l) >>> 1); if (arr[mid * k] <= v) l = mid; else r = mid; }
+
+ //var mi = 0; for(var i=0; i= tab.map.length) gid = 0;
+ else gid = tab.map[code];
+ }
+ /*else if(fmt==2) {
+ var data=font["_data"], off = cmap.off+tab.off+6, bin=Typr["B"];
+ var shKey = bin.readUshort(data,off + 2*(code>>>8));
+ var shInd = off + 256*2 + shKey*8;
+
+ var firstCode = bin.readUshort(data,shInd);
+ var entryCount= bin.readUshort(data,shInd+2);
+ var idDelta = bin.readShort (data,shInd+4);
+ var idRangeOffset = bin.readUshort(data,shInd+6);
+
+ if(firstCode<=code && code<=firstCode+entryCount) {
+ // not completely correct
+ gid = bin.readUshort(data, shInd+6+idRangeOffset + (code&255)*2);
+ }
+ else gid=0;
+ //if(code>256) console.log(code,(code>>>8),shKey,firstCode,entryCount,idDelta,idRangeOffset);
+
+ //throw "e";
+ //console.log(tab, bin.readUshort(data,off));
+ //throw "e";
+ }*/
+ else if (fmt == 4) {
+ var ec = tab.endCount; gid = 0;
+ if (code <= ec[ec.length - 1]) {
+ // smallest index with code <= value
+ var sind = arrSearch(ec, 1, code);
+ if (ec[sind] < code) sind++;
+
+ if (code >= tab.startCount[sind]) {
+ var gli = 0;
+ if (tab.idRangeOffset[sind] != 0) gli = tab.glyphIdArray[(code - tab.startCount[sind]) + (tab.idRangeOffset[sind] >> 1) - (tab.idRangeOffset.length - sind)];
+ else gli = code + tab.idDelta[sind];
+ gid = (gli & 0xFFFF);
+ }
+ }
+ }
+ else if (fmt == 6) {
+ var off = code - tab.firstCode, arr = tab.glyphIdArray;
+ if (off < 0 || off >= arr.length) gid = 0;
+ else gid = arr[off];
+ }
+ else if (fmt == 12) {
+ var grp = tab.groups; gid = 0; //console.log(grp); throw "e";
+
+ if (code <= grp[grp.length - 2]) {
+ var i = arrSearch(grp, 3, code);
+ if (grp[i] <= code && code <= grp[i + 1]) { gid = grp[i + 2] + (code - grp[i]); }
+ }
+ }
+ else throw "unknown cmap table format " + tab.format;
+
+ //*
+ var SVG = font["SVG "], loca = font["loca"];
+ // if the font claims to have a Glyph for a character, but the glyph is empty, and the character is not "white", it is a lie!
+ if (gid != 0 && font["CFF "] == null && (SVG == null || SVG.entries[gid] == null) && loca && loca[gid] == loca[gid + 1] // loca not present in CFF or SVG fonts
+ && whm[code] == null) gid = 0;
+ //*/
+
+ return gid;
+ }
+ return ctg;
+ }(),
+
+ "glyphToPath": function (font, gid, noColor) {
+ var path = { cmds: [], crds: [] };
+
+
+ var SVG = font["SVG "], CFF = font["CFF "], COLR = font["COLR"], CBLC = font["CBLC"], CBDT = font["CBDT"], sbix = font["sbix"], upng = window["UPNG"];
+ var U = Typr["U"];
+
+ var strike = null;
+ if (CBLC && upng) for (var i = 0; i < CBLC.length; i++) if (CBLC[i][0] <= gid && gid <= CBLC[i][1]) strike = CBLC[i];
+
+ if (strike || (sbix && sbix[gid])) {
+ if (strike && strike[2] != 17) throw "not a PNG";
+
+ if (font["__tmp"] == null) font["__tmp"] = {};
+ var cmd = font["__tmp"]["g" + gid];
+ if (cmd == null) {
+ var bmp, len;
+ if (sbix) { bmp = sbix[gid]; len = bmp.length; }
+ else {
+ var boff = strike[3][gid - strike[0]] + 5; // smallGlyphMetrics
+ len = (CBDT[boff + 1] << 16) | (CBDT[boff + 2] << 8) | CBDT[boff + 3]; boff += 4;
+ bmp = new Uint8Array(CBDT.buffer, CBDT.byteOffset + boff, len);
+ }
+ var str = ""; for (var i = 0; i < len; i++) str += String.fromCharCode(bmp[i]);
+ cmd = font["__tmp"]["g" + gid] = "data:image/png;base64," + btoa(str);
+ }
+
+ path.cmds.push(cmd);
+ var upe = font["head"]["unitsPerEm"] * 1.15;
+ var gw = Math.round(upe), gh = Math.round(upe), dy = Math.round(-gh * 0.15);
+ path.crds.push(0, gh + dy, gw, gh + dy, gw, dy, 0, dy); //*/
+ }
+ else if (SVG && SVG.entries[gid]) {
+ var p = SVG.entries[gid];
+ if (p != null) {
+ if (typeof p == "number") {
+ var svg = SVG.svgs[p];
+ if (typeof svg == "string") {
+ var prsr = new DOMParser();
+ var doc = prsr["parseFromString"](svg, "image/svg+xml");
+ svg = SVG.svgs[p] = doc.getElementsByTagName("svg")[0];
+ }
+ p = U["SVG"].toPath(svg, gid); SVG.entries[gid] = p;
+ }
+ path = p;
+ }
+ }
+ else if (noColor != true && COLR && COLR[0]["g" + gid] && COLR[0]["g" + gid][1] > 1) {
+
+ function toHex(n) { var o = n.toString(16); return (o.length == 1 ? "0" : "") + o; }
+
+ var CPAL = font["CPAL"], gl = COLR[0]["g" + gid];
+ for (var i = 0; i < gl[1]; i++) {
+ var lid = gl[0] + i;
+ var cgl = COLR[1][2 * lid], pid = COLR[1][2 * lid + 1] * 4;
+ var pth = Typr["U"]["glyphToPath"](font, cgl, cgl == gid);
+
+ var col = "#" + toHex(CPAL[pid + 2]) + toHex(CPAL[pid + 1]) + toHex(CPAL[pid + 0]);
+ path.cmds.push(col);
+
+ path.cmds = path.cmds.concat(pth["cmds"]);
+ path.crds = path.crds.concat(pth["crds"]);
+ //console.log(gid, cgl,pid,col);
+
+ path.cmds.push("X");
+ }
+ }
+ else if (CFF) {
+ var pdct = CFF["Private"];
+ var state = { x: 0, y: 0, stack: [], nStems: 0, haveWidth: false, width: pdct ? pdct["defaultWidthX"] : 0, open: false };
+ if (CFF["ROS"]) {
+ var gi = 0;
+ while (CFF["FDSelect"][gi + 2] <= gid) gi += 2;
+ pdct = CFF["FDArray"][CFF["FDSelect"][gi + 1]]["Private"];
+ }
+ U["_drawCFF"](CFF["CharStrings"][gid], state, CFF, pdct, path);
+ }
+ else if (font["glyf"]) { U["_drawGlyf"](gid, font, path); }
+ return { "cmds": path.cmds, "crds": path.crds };
+ },
+
+ "_drawGlyf": function (gid, font, path) {
+ var gl = font["glyf"][gid];
+ if (gl == null) gl = font["glyf"][gid] = Typr["T"].glyf._parseGlyf(font, gid);
+ if (gl != null) {
+ if (gl.noc > -1) Typr["U"]["_simpleGlyph"](gl, path);
+ else Typr["U"]["_compoGlyph"](gl, font, path);
+ }
+ },
+ "_simpleGlyph": function (gl, p) {
+ var P = Typr["U"]["P"];
+ for (var c = 0; c < gl.noc; c++) {
+ var i0 = (c == 0) ? 0 : (gl.endPts[c - 1] + 1);
+ var il = gl.endPts[c];
+
+ for (var i = i0; i <= il; i++) {
+ var pr = (i == i0) ? il : (i - 1);
+ var nx = (i == il) ? i0 : (i + 1);
+ var onCurve = gl.flags[i] & 1;
+ var prOnCurve = gl.flags[pr] & 1;
+ var nxOnCurve = gl.flags[nx] & 1;
+
+ var x = gl.xs[i], y = gl.ys[i];
+
+ if (i == i0) {
+ if (onCurve) {
+ if (prOnCurve) P.MoveTo(p, gl.xs[pr], gl.ys[pr]);
+ else { P.MoveTo(p, x, y); continue; /* will do CurveTo at il */ }
+ }
+ else {
+ if (prOnCurve) P.MoveTo(p, gl.xs[pr], gl.ys[pr]);
+ else P.MoveTo(p, Math.floor((gl.xs[pr] + x) * 0.5), Math.floor((gl.ys[pr] + y) * 0.5));
+ }
+ }
+ if (onCurve) {
+ if (prOnCurve) P.LineTo(p, x, y);
+ }
+ else {
+ if (nxOnCurve) P.qCurveTo(p, x, y, gl.xs[nx], gl.ys[nx]);
+ else P.qCurveTo(p, x, y, Math.floor((x + gl.xs[nx]) * 0.5), Math.floor((y + gl.ys[nx]) * 0.5));
+ }
+ }
+ P.ClosePath(p);
+ }
+ },
+ "_compoGlyph": function (gl, font, p) {
+ for (var j = 0; j < gl.parts.length; j++) {
+ var path = { cmds: [], crds: [] };
+ var prt = gl.parts[j];
+ Typr["U"]["_drawGlyf"](prt.glyphIndex, font, path);
+
+ var m = prt.m;
+ for (var i = 0; i < path.crds.length; i += 2) {
+ var x = path.crds[i], y = path.crds[i + 1];
+ p.crds.push(x * m.a + y * m.c + m.tx); // not sure, probably right
+ p.crds.push(x * m.b + y * m.d + m.ty);
+ }
+ for (var i = 0; i < path.cmds.length; i++) p.cmds.push(path.cmds[i]);
+ }
+ },
+
+ "pathToSVG": function (path, prec) {
+ var cmds = path["cmds"], crds = path["crds"];
+ if (prec == null) prec = 5;
+ function num(v) { return parseFloat(v.toFixed(prec)); }
+ function merge(o) {
+ var no = [], lstF = false, lstC = "";
+ for (var i = 0; i < o.length; i++) {
+ var it = o[i], isF = (typeof it) == "number";
+ if (!isF) { if (it == lstC && it.length == 1 && it != "m") continue; lstC = it; } // move should not be merged (it actually means lineTo)
+ if (lstF && isF && it >= 0) no.push(" ");
+ no.push(it); lstF = isF;
+ }
+ return no.join("");
+ }
+
+
+ var out = [], co = 0, lmap = { "M": 2, "L": 2, "Q": 4, "C": 6 };
+ var x = 0, y = 0, // perfect coords
+ //dx=0, dy=0, // relative perfect coords
+ //rx=0, ry=0, // relative rounded coords
+ ex = 0, ey = 0, // error between perfect and output coords
+ mx = 0, my = 0; // perfect coords of the last "Move"
+
+ for (var i = 0; i < cmds.length; i++) {
+ var cmd = cmds[i], cc = (lmap[cmd] ? lmap[cmd] : 0);
+
+ var o0 = [], dx, dy, rx, ry; // o1=[], cx, cy, ax,ay;
+ if (cmd == "L") {
+ dx = crds[co] - x; dy = crds[co + 1] - y;
+ rx = num(dx + ex); ry = num(dy + ey);
+ // if this "lineTo" leads to the starting point, and "Z" follows, do not output anything.
+ if (cmds[i + 1] == "Z" && crds[co] == mx && crds[co + 1] == my) { rx = dx; ry = dy; }
+ else if (rx == 0 && ry == 0) { }
+ else if (rx == 0) o0.push("v", ry);
+ else if (ry == 0) o0.push("h", rx);
+ else { o0.push("l", rx, ry); }
+ }
+ else {
+ o0.push(cmd.toLowerCase());
+ for (var j = 0; j < cc; j += 2) {
+ dx = crds[co + j] - x; dy = crds[co + j + 1] - y;
+ rx = num(dx + ex); ry = num(dy + ey);
+ o0.push(rx, ry);
+ }
+ }
+ if (cc != 0) { ex += dx - rx; ey += dy - ry; }
+
+ /*
+ if(cmd=="L") {
+ cx=crds[co]; cy=crds[co+1];
+ ax = num(cx); ay=num(cy);
+ // if this "lineTo" leads to the starting point, and "Z" follows, do not output anything.
+ if(cmds[i+1]=="Z" && crds[co]==mx && crds[co+1]==my) { ax=cx; ay=cy; }
+ else if(ax==num(x) && ay==num(y)) {}
+ else if(ax==num(x)) o1.push("V",ay);
+ else if(ay==num(y)) o1.push("H",ax);
+ else { o1.push("L",ax,ay); }
+ }
+ else {
+ o1.push(cmd);
+ for(var j=0; j> 1, nh = h >> 1;
+ var nbuf = (hlp && hlp.length == nw * nh * 4) ? hlp : new Uint8Array(nw * nh * 4);
+ var sb32 = new Uint32Array(buff.buffer), nb32 = new Uint32Array(nbuf.buffer);
+ for (var y = 0; y < nh; y++)
+ for (var x = 0; x < nw; x++) {
+ var ti = (y * nw + x), si = ((y << 1) * w + (x << 1));
+ //nbuf[ti ] = buff[si ]; nbuf[ti+1] = buff[si+1]; nbuf[ti+2] = buff[si+2]; nbuf[ti+3] = buff[si+3];
+ //*
+ var c0 = sb32[si], c1 = sb32[si + 1], c2 = sb32[si + w], c3 = sb32[si + w + 1];
+
+ var a0 = (c0 >>> 24), a1 = (c1 >>> 24), a2 = (c2 >>> 24), a3 = (c3 >>> 24), a = (a0 + a1 + a2 + a3);
+
+ if (a == 1020) {
+ var r = (((c0 >>> 0) & 255) + ((c1 >>> 0) & 255) + ((c2 >>> 0) & 255) + ((c3 >>> 0) & 255) + 2) >>> 2;
+ var g = (((c0 >>> 8) & 255) + ((c1 >>> 8) & 255) + ((c2 >>> 8) & 255) + ((c3 >>> 8) & 255) + 2) >>> 2;
+ var b = (((c0 >>> 16) & 255) + ((c1 >>> 16) & 255) + ((c2 >>> 16) & 255) + ((c3 >>> 16) & 255) + 2) >>> 2;
+ nb32[ti] = (255 << 24) | (b << 16) | (g << 8) | r;
+ }
+ else if (a == 0) nb32[ti] = 0;
+ else {
+ var r = ((c0 >>> 0) & 255) * a0 + ((c1 >>> 0) & 255) * a1 + ((c2 >>> 0) & 255) * a2 + ((c3 >>> 0) & 255) * a3;
+ var g = ((c0 >>> 8) & 255) * a0 + ((c1 >>> 8) & 255) * a1 + ((c2 >>> 8) & 255) * a2 + ((c3 >>> 8) & 255) * a3;
+ var b = ((c0 >>> 16) & 255) * a0 + ((c1 >>> 16) & 255) * a1 + ((c2 >>> 16) & 255) * a2 + ((c3 >>> 16) & 255) * a3;
+
+ var ia = 1 / a; r = ~~(r * ia + 0.5); g = ~~(g * ia + 0.5); b = ~~(b * ia + 0.5);
+ nb32[ti] = (((a + 2) >>> 2) << 24) | (b << 16) | (g << 8) | r;
+ }
+ }
+ return { buff: nbuf, w: nw, h: nh };
+ }
+
+ return ptc;
+ }(),
+
+ "P": {
+ MoveTo: function (p, x, y) { p.cmds.push("M"); p.crds.push(x, y); },
+ LineTo: function (p, x, y) { p.cmds.push("L"); p.crds.push(x, y); },
+ CurveTo: function (p, a, b, c, d, e, f) { p.cmds.push("C"); p.crds.push(a, b, c, d, e, f); },
+ qCurveTo: function (p, a, b, c, d) { p.cmds.push("Q"); p.crds.push(a, b, c, d); },
+ ClosePath: function (p) { p.cmds.push("Z"); }
+ },
+
+ "_drawCFF": function (cmds, state, font, pdct, p) {
+ var stack = state.stack;
+ var nStems = state.nStems, haveWidth = state.haveWidth, width = state.width, open = state.open;
+ var i = 0;
+ var x = state.x, y = state.y, c1x = 0, c1y = 0, c2x = 0, c2y = 0, c3x = 0, c3y = 0, c4x = 0, c4y = 0, jpx = 0, jpy = 0;
+ var CFF = Typr["T"].CFF, P = Typr["U"]["P"];
+
+ var nominalWidthX = pdct["nominalWidthX"];
+ var o = { val: 0, size: 0 };
+ //console.log(cmds);
+ while (i < cmds.length) {
+ CFF.getCharString(cmds, i, o);
+ var v = o.val;
+ i += o.size;
+
+ if (false) { }
+ else if (v == "o1" || v == "o18") // hstem || hstemhm
+ {
+ var hasWidthArg;
+
+ // The number of stem operators on the stack is always even.
+ // If the value is uneven, that means a width is specified.
+ hasWidthArg = stack.length % 2 !== 0;
+ if (hasWidthArg && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ }
+
+ nStems += stack.length >> 1;
+ stack.length = 0;
+ haveWidth = true;
+ }
+ else if (v == "o3" || v == "o23") // vstem || vstemhm
+ {
+ var hasWidthArg;
+
+ // The number of stem operators on the stack is always even.
+ // If the value is uneven, that means a width is specified.
+ hasWidthArg = stack.length % 2 !== 0;
+ if (hasWidthArg && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ }
+
+ nStems += stack.length >> 1;
+ stack.length = 0;
+ haveWidth = true;
+ }
+ else if (v == "o4") {
+ if (stack.length > 1 && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ haveWidth = true;
+ }
+ if (open) P.ClosePath(p);
+
+ y += stack.pop();
+ P.MoveTo(p, x, y); open = true;
+ }
+ else if (v == "o5") {
+ while (stack.length > 0) {
+ x += stack.shift();
+ y += stack.shift();
+ P.LineTo(p, x, y);
+ }
+ }
+ else if (v == "o6" || v == "o7") // hlineto || vlineto
+ {
+ var count = stack.length;
+ var isX = (v == "o6");
+
+ for (var j = 0; j < count; j++) {
+ var sval = stack.shift();
+
+ if (isX) x += sval; else y += sval;
+ isX = !isX;
+ P.LineTo(p, x, y);
+ }
+ }
+ else if (v == "o8" || v == "o24") // rrcurveto || rcurveline
+ {
+ var count = stack.length;
+ var index = 0;
+ while (index + 6 <= count) {
+ c1x = x + stack.shift();
+ c1y = y + stack.shift();
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x + stack.shift();
+ y = c2y + stack.shift();
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ index += 6;
+ }
+ if (v == "o24") {
+ x += stack.shift();
+ y += stack.shift();
+ P.LineTo(p, x, y);
+ }
+ }
+ else if (v == "o11") break;
+ else if (v == "o1234" || v == "o1235" || v == "o1236" || v == "o1237")//if((v+"").slice(0,3)=="o12")
+ {
+ if (v == "o1234") {
+ c1x = x + stack.shift(); // dx1
+ c1y = y; // dy1
+ c2x = c1x + stack.shift(); // dx2
+ c2y = c1y + stack.shift(); // dy2
+ jpx = c2x + stack.shift(); // dx3
+ jpy = c2y; // dy3
+ c3x = jpx + stack.shift(); // dx4
+ c3y = c2y; // dy4
+ c4x = c3x + stack.shift(); // dx5
+ c4y = y; // dy5
+ x = c4x + stack.shift(); // dx6
+ P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
+ P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
+
+ }
+ if (v == "o1235") {
+ c1x = x + stack.shift(); // dx1
+ c1y = y + stack.shift(); // dy1
+ c2x = c1x + stack.shift(); // dx2
+ c2y = c1y + stack.shift(); // dy2
+ jpx = c2x + stack.shift(); // dx3
+ jpy = c2y + stack.shift(); // dy3
+ c3x = jpx + stack.shift(); // dx4
+ c3y = jpy + stack.shift(); // dy4
+ c4x = c3x + stack.shift(); // dx5
+ c4y = c3y + stack.shift(); // dy5
+ x = c4x + stack.shift(); // dx6
+ y = c4y + stack.shift(); // dy6
+ stack.shift(); // flex depth
+ P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
+ P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
+ }
+ if (v == "o1236") {
+ c1x = x + stack.shift(); // dx1
+ c1y = y + stack.shift(); // dy1
+ c2x = c1x + stack.shift(); // dx2
+ c2y = c1y + stack.shift(); // dy2
+ jpx = c2x + stack.shift(); // dx3
+ jpy = c2y; // dy3
+ c3x = jpx + stack.shift(); // dx4
+ c3y = c2y; // dy4
+ c4x = c3x + stack.shift(); // dx5
+ c4y = c3y + stack.shift(); // dy5
+ x = c4x + stack.shift(); // dx6
+ P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
+ P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
+ }
+ if (v == "o1237") {
+ c1x = x + stack.shift(); // dx1
+ c1y = y + stack.shift(); // dy1
+ c2x = c1x + stack.shift(); // dx2
+ c2y = c1y + stack.shift(); // dy2
+ jpx = c2x + stack.shift(); // dx3
+ jpy = c2y + stack.shift(); // dy3
+ c3x = jpx + stack.shift(); // dx4
+ c3y = jpy + stack.shift(); // dy4
+ c4x = c3x + stack.shift(); // dx5
+ c4y = c3y + stack.shift(); // dy5
+ if (Math.abs(c4x - x) > Math.abs(c4y - y)) {
+ x = c4x + stack.shift();
+ } else {
+ y = c4y + stack.shift();
+ }
+ P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
+ P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
+ }
+ }
+ else if (v == "o14") {
+ if (stack.length > 0 && stack.length != 4 && !haveWidth) {
+ width = stack.shift() + font["nominalWidthX"];
+ haveWidth = true;
+ }
+ if (stack.length == 4) // seac = standard encoding accented character
+ {
+
+ var asb = 0;
+ var adx = stack.shift();
+ var ady = stack.shift();
+ var bchar = stack.shift();
+ var achar = stack.shift();
+
+
+ var bind = CFF.glyphBySE(font, bchar);
+ var aind = CFF.glyphBySE(font, achar);
+
+ //console.log(bchar, bind);
+ //console.log(achar, aind);
+ //state.x=x; state.y=y; state.nStems=nStems; state.haveWidth=haveWidth; state.width=width; state.open=open;
+
+ Typr["U"]["_drawCFF"](font["CharStrings"][bind], state, font, pdct, p);
+ state.x = adx; state.y = ady;
+ Typr["U"]["_drawCFF"](font["CharStrings"][aind], state, font, pdct, p);
+
+ //x=state.x; y=state.y; nStems=state.nStems; haveWidth=state.haveWidth; width=state.width; open=state.open;
+ }
+ if (open) { P.ClosePath(p); open = false; }
+ }
+ else if (v == "o19" || v == "o20") {
+ var hasWidthArg;
+
+ // The number of stem operators on the stack is always even.
+ // If the value is uneven, that means a width is specified.
+ hasWidthArg = stack.length % 2 !== 0;
+ if (hasWidthArg && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ }
+
+ nStems += stack.length >> 1;
+ stack.length = 0;
+ haveWidth = true;
+
+ i += (nStems + 7) >> 3;
+ }
+
+ else if (v == "o21") {
+ if (stack.length > 2 && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ haveWidth = true;
+ }
+
+ y += stack.pop();
+ x += stack.pop();
+
+ if (open) P.ClosePath(p);
+ P.MoveTo(p, x, y); open = true;
+ }
+ else if (v == "o22") {
+ if (stack.length > 1 && !haveWidth) {
+ width = stack.shift() + nominalWidthX;
+ haveWidth = true;
+ }
+
+ x += stack.pop();
+
+ if (open) P.ClosePath(p);
+ P.MoveTo(p, x, y); open = true;
+ }
+ else if (v == "o25") {
+ while (stack.length > 6) {
+ x += stack.shift();
+ y += stack.shift();
+ P.LineTo(p, x, y);
+ }
+
+ c1x = x + stack.shift();
+ c1y = y + stack.shift();
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x + stack.shift();
+ y = c2y + stack.shift();
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ }
+ else if (v == "o26") {
+ if (stack.length % 2) {
+ x += stack.shift();
+ }
+
+ while (stack.length > 0) {
+ c1x = x;
+ c1y = y + stack.shift();
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x;
+ y = c2y + stack.shift();
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ }
+
+ }
+ else if (v == "o27") {
+ if (stack.length % 2) {
+ y += stack.shift();
+ }
+
+ while (stack.length > 0) {
+ c1x = x + stack.shift();
+ c1y = y;
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x + stack.shift();
+ y = c2y;
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ }
+ }
+ else if (v == "o10" || v == "o29") // callsubr || callgsubr
+ {
+ var obj = (v == "o10" ? pdct : font);
+ if (stack.length == 0) { console.log("error: empty stack"); }
+ else {
+ var ind = stack.pop();
+ var subr = obj["Subrs"][ind + obj["Bias"]];
+ state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open;
+ Typr["U"]["_drawCFF"](subr, state, font, pdct, p);
+ x = state.x; y = state.y; nStems = state.nStems; haveWidth = state.haveWidth; width = state.width; open = state.open;
+ }
+ }
+ else if (v == "o30" || v == "o31") // vhcurveto || hvcurveto
+ {
+ var count, count1 = stack.length;
+ var index = 0;
+ var alternate = v == "o31";
+
+ count = count1 & ~2;
+ index += count1 - count;
+
+ while (index < count) {
+ if (alternate) {
+ c1x = x + stack.shift();
+ c1y = y;
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ y = c2y + stack.shift();
+ if (count - index == 5) { x = c2x + stack.shift(); index++; }
+ else x = c2x;
+ alternate = false;
+ }
+ else {
+ c1x = x;
+ c1y = y + stack.shift();
+ c2x = c1x + stack.shift();
+ c2y = c1y + stack.shift();
+ x = c2x + stack.shift();
+ if (count - index == 5) { y = c2y + stack.shift(); index++; }
+ else y = c2y;
+ alternate = true;
+ }
+ P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
+ index += 4;
+ }
+ }
+
+ else if ((v + "").charAt(0) == "o") { console.log("Unknown operation: " + v, cmds); throw v; }
+ else stack.push(v);
+ }
+ //console.log(cmds);
+ state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open;
+ },
+
+
+ "SVG": function () {
+ var M = {
+ getScale: function (m) { return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2])); },
+ translate: function (m, x, y) { M.concat(m, [1, 0, 0, 1, x, y]); },
+ rotate: function (m, a) { M.concat(m, [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a), 0, 0]); },
+ scale: function (m, x, y) { M.concat(m, [x, 0, 0, y, 0, 0]); },
+ concat: function (m, w) {
+ var a = m[0], b = m[1], c = m[2], d = m[3], tx = m[4], ty = m[5];
+ m[0] = (a * w[0]) + (b * w[2]); m[1] = (a * w[1]) + (b * w[3]);
+ m[2] = (c * w[0]) + (d * w[2]); m[3] = (c * w[1]) + (d * w[3]);
+ m[4] = (tx * w[0]) + (ty * w[2]) + w[4]; m[5] = (tx * w[1]) + (ty * w[3]) + w[5];
+ },
+ invert: function (m) {
+ var a = m[0], b = m[1], c = m[2], d = m[3], tx = m[4], ty = m[5], adbc = a * d - b * c;
+ m[0] = d / adbc; m[1] = -b / adbc; m[2] = -c / adbc; m[3] = a / adbc;
+ m[4] = (c * ty - d * tx) / adbc; m[5] = (b * tx - a * ty) / adbc;
+ },
+ multPoint: function (m, p) { var x = p[0], y = p[1]; return [x * m[0] + y * m[2] + m[4], x * m[1] + y * m[3] + m[5]]; },
+ multArray: function (m, a) { for (var i = 0; i < a.length; i += 2) { var x = a[i], y = a[i + 1]; a[i] = x * m[0] + y * m[2] + m[4]; a[i + 1] = x * m[1] + y * m[3] + m[5]; } }
+ }
+
+ function _bracketSplit(str, lbr, rbr) {
+ var out = [], pos = 0, ci = 0, lvl = 0;
+ while (true) { //throw "e";
+ var li = str.indexOf(lbr, ci);
+ var ri = str.indexOf(rbr, ci);
+ if (li == -1 && ri == -1) break;
+ if (ri == -1 || (li != -1 && li < ri)) {
+ if (lvl == 0) { out.push(str.slice(pos, li).trim()); pos = li + 1; }
+ lvl++; ci = li + 1;
+ }
+ else if (li == -1 || (ri != -1 && ri < li)) {
+ lvl--;
+ if (lvl == 0) { out.push(str.slice(pos, ri).trim()); pos = ri + 1; }
+ ci = ri + 1;
+ }
+ }
+ return out;
+ }
+ //"cssMap":
+ function cssMap(str) {
+ var pts = _bracketSplit(str, "{", "}");
+ var css = {};
+ for (var i = 0; i < pts.length; i += 2) {
+ var cn = pts[i].split(",");
+ for (var j = 0; j < cn.length; j++) {
+ var cnj = cn[j].trim(); if (css[cnj] == null) css[cnj] = "";
+ css[cnj] += pts[i + 1];
+ }
+ }
+ return css;
+ }
+ //"readTrnf"
+ function readTrnf(trna) {
+ var pts = _bracketSplit(trna, "(", ")");
+ var m = [1, 0, 0, 1, 0, 0];
+ for (var i = 0; i < pts.length; i += 2) { var om = m; m = _readTrnsAttr(pts[i], pts[i + 1]); M.concat(m, om); }
+ return m;
+ }
+
+ function _readTrnsAttr(fnc, vls) {
+ //console.log(vls);
+ //vls = vls.replace(/\-/g, " -").trim();
+ var m = [1, 0, 0, 1, 0, 0], gotSep = true;
+ for (var i = 0; i < vls.length; i++) { // matrix(.99915 0 0 .99915.418.552) matrix(1 0 0-.9474-22.535 271.03)
+ var ch = vls.charAt(i);
+ if (ch == "," || ch == " ") gotSep = true;
+ else if (ch == ".") {
+ if (!gotSep) { vls = vls.slice(0, i) + "," + vls.slice(i); i++; } gotSep = false;
+ }
+ else if (ch == "-" && i > 0 && vls[i - 1] != "e") { vls = vls.slice(0, i) + " " + vls.slice(i); i++; gotSep = true; }
+ }
+
+ vls = vls.split(/\s*[\s,]\s*/).map(parseFloat);
+ if (false) { }
+ else if (fnc == "translate") { if (vls.length == 1) M.translate(m, vls[0], 0); else M.translate(m, vls[0], vls[1]); }
+ else if (fnc == "scale") { if (vls.length == 1) M.scale(m, vls[0], vls[0]); else M.scale(m, vls[0], vls[1]); }
+ else if (fnc == "rotate") { var tx = 0, ty = 0; if (vls.length != 1) { tx = vls[1]; ty = vls[2]; } M.translate(m, -tx, -ty); M.rotate(m, -Math.PI * vls[0] / 180); M.translate(m, tx, ty); }
+ else if (fnc == "matrix") m = vls;
+ else console.log("unknown transform: ", fnc);
+ return m;
+ }
+
+ function toPath(svg, gid) {
+ var pth = { cmds: [], crds: [] };
+
+ var vb = svg.getAttribute("viewBox");
+ if (vb) vb = vb.trim().split(" ").map(parseFloat); else vb = [0, 0, 1000, 1000];
+
+ var nod = svg;
+ if (gid != null) { var nd = svg.getElementById("glyph" + gid); if (nd) nod = nd; }
+
+ _toPath(nod.children, pth, null, svg);
+ for (var i = 0; i < pth.crds.length; i += 2) {
+ var x = pth.crds[i], y = pth.crds[i + 1];
+ x -= vb[0];
+ y -= vb[1];
+ y = -y;
+ pth.crds[i] = x;
+ pth.crds[i + 1] = y;
+ }
+ return pth;
+ }
+
+ var cmap = {
+ "aliceblue": "#f0f8ff", "antiquewhite": "#faebd7", "aqua": "#00ffff", "aquamarine": "#7fffd4", "azure": "#f0ffff", "beige": "#f5f5dc", "bisque": "#ffe4c4",
+ "black": "#000000", "blanchedalmond": "#ffebcd", "blue": "#0000ff", "blueviolet": "#8a2be2", "brown": "#a52a2a", "burlywood": "#deb887", "cadetblue": "#5f9ea0",
+ "chartreuse": "#7fff00", "chocolate": "#d2691e", "coral": "#ff7f50", "cornflowerblue": "#6495ed", "cornsilk": "#fff8dc", "crimson": "#dc143c", "cyan": "#00ffff",
+ "darkblue": "#00008b", "darkcyan": "#008b8b", "darkgoldenrod": "#b8860b", "darkgray": "#a9a9a9", "darkgreen": "#006400", "darkgrey": "#a9a9a9", "darkkhaki": "#bdb76b",
+ "darkmagenta": "#8b008b", "darkolivegreen": "#556b2f", "darkorange": "#ff8c00", "darkorchid": "#9932cc", "darkred": "#8b0000", "darksalmon": "#e9967a", "darkseagreen": "#8fbc8f",
+ "darkslateblue": "#483d8b", "darkslategray": "#2f4f4f", "darkslategrey": "#2f4f4f", "darkturquoise": "#00ced1", "darkviolet": "#9400d3", "deeppink": "#ff1493",
+ "deepskyblue": "#00bfff", "dimgray": "#696969", "dimgrey": "#696969", "dodgerblue": "#1e90ff", "firebrick": "#b22222", "floralwhite": "#fffaf0", "forestgreen": "#228b22",
+ "fuchsia": "#ff00ff", "gainsboro": "#dcdcdc", "ghostwhite": "#f8f8ff", "gold": "#ffd700", "goldenrod": "#daa520", "gray": "#808080", "green": "#008000", "greenyellow": "#adff2f",
+ "grey": "#808080", "honeydew": "#f0fff0", "hotpink": "#ff69b4", "indianred": "#cd5c5c", "indigo": "#4b0082", "ivory": "#fffff0", "khaki": "#f0e68c", "lavender": "#e6e6fa",
+ "lavenderblush": "#fff0f5", "lawngreen": "#7cfc00", "lemonchiffon": "#fffacd", "lightblue": "#add8e6", "lightcoral": "#f08080", "lightcyan": "#e0ffff",
+ "lightgoldenrodyellow": "#fafad2", "lightgray": "#d3d3d3", "lightgreen": "#90ee90", "lightgrey": "#d3d3d3", "lightpink": "#ffb6c1", "lightsalmon": "#ffa07a",
+ "lightseagreen": "#20b2aa", "lightskyblue": "#87cefa", "lightslategray": "#778899", "lightslategrey": "#778899", "lightsteelblue": "#b0c4de", "lightyellow": "#ffffe0",
+ "lime": "#00ff00", "limegreen": "#32cd32", "linen": "#faf0e6", "magenta": "#ff00ff", "maroon": "#800000", "mediumaquamarine": "#66cdaa", "mediumblue": "#0000cd",
+ "mediumorchid": "#ba55d3", "mediumpurple": "#9370db", "mediumseagreen": "#3cb371", "mediumslateblue": "#7b68ee", "mediumspringgreen": "#00fa9a",
+ "mediumturquoise": "#48d1cc", "mediumvioletred": "#c71585", "midnightblue": "#191970", "mintcream": "#f5fffa", "mistyrose": "#ffe4e1", "moccasin": "#ffe4b5",
+ "navajowhite": "#ffdead", "navy": "#000080", "oldlace": "#fdf5e6", "olive": "#808000", "olivedrab": "#6b8e23", "orange": "#ffa500", "orangered": "#ff4500",
+ "orchid": "#da70d6", "palegoldenrod": "#eee8aa", "palegreen": "#98fb98", "paleturquoise": "#afeeee", "palevioletred": "#db7093", "papayawhip": "#ffefd5",
+ "peachpuff": "#ffdab9", "peru": "#cd853f", "pink": "#ffc0cb", "plum": "#dda0dd", "powderblue": "#b0e0e6", "purple": "#800080", "rebeccapurple": "#663399", "red": "#ff0000",
+ "rosybrown": "#bc8f8f", "royalblue": "#4169e1", "saddlebrown": "#8b4513", "salmon": "#fa8072", "sandybrown": "#f4a460", "seagreen": "#2e8b57", "seashell": "#fff5ee",
+ "sienna": "#a0522d", "silver": "#c0c0c0", "skyblue": "#87ceeb", "slateblue": "#6a5acd", "slategray": "#708090", "slategrey": "#708090", "snow": "#fffafa", "springgreen": "#00ff7f",
+ "steelblue": "#4682b4", "tan": "#d2b48c", "teal": "#008080", "thistle": "#d8bfd8", "tomato": "#ff6347", "turquoise": "#40e0d0", "violet": "#ee82ee", "wheat": "#f5deb3",
+ "white": "#ffffff", "whitesmoke": "#f5f5f5", "yellow": "#ffff00", "yellowgreen": "#9acd32"
+ };
+
+ function _toPath(nds, pth, fill, root) {
+ for (var ni = 0; ni < nds.length; ni++) {
+ var nd = nds[ni], tn = nd.tagName;
+ var cfl = nd.getAttribute("fill"); if (cfl == null) cfl = fill;
+ if (cfl && cfl.startsWith("url")) {
+ var gid = cfl.slice(5, -1);
+ var grd = root.getElementById(gid), s0 = grd.children[0];
+ if (s0.getAttribute("stop-opacity") != null) continue;
+ cfl = s0.getAttribute("stop-color");
+ }
+ if (cmap[cfl]) cfl = cmap[cfl];
+ if (tn == "g" || tn == "use") {
+ var tp = { crds: [], cmds: [] };
+ if (tn == "g") _toPath(nd.children, tp, cfl, root);
+ else {
+ var lnk = nd.getAttribute("xlink:href").slice(1);
+ var pel = root.getElementById(lnk);
+ _toPath([pel], tp, cfl, root);
+ }
+ var m = [1, 0, 0, 1, 0, 0];
+ var x = nd.getAttribute("x"), y = nd.getAttribute("y"); x = x ? parseFloat(x) : 0; y = y ? parseFloat(y) : 0;
+ M.concat(m, [1, 0, 0, 1, x, y]);
+
+ var trf = nd.getAttribute("transform"); if (trf) M.concat(m, readTrnf(trf));
+
+ M.multArray(m, tp.crds);
+ pth.crds = pth.crds.concat(tp.crds);
+ pth.cmds = pth.cmds.concat(tp.cmds);
+ }
+ else if (tn == "path" || tn == "circle" || tn == "ellipse") {
+ pth.cmds.push(cfl ? cfl : "#000000");
+ var d;
+ if (tn == "path") d = nd.getAttribute("d"); //console.log(d);
+ if (tn == "circle" || tn == "ellipse") {
+ var vls = [0, 0, 0, 0], nms = ["cx", "cy", "rx", "ry", "r"];
+ for (var i = 0; i < 5; i++) { var V = nd.getAttribute(nms[i]); if (V) { V = parseFloat(V); if (i < 4) vls[i] = V; else vls[2] = vls[3] = V; } }
+ var cx = vls[0], cy = vls[1], rx = vls[2], ry = vls[3];
+ d = ["M", cx - rx, cy, "a", rx, ry, 0, 1, 0, rx * 2, 0, "a", rx, ry, 0, 1, 0, -rx * 2, 0].join(" ");
+ }
+ svgToPath(d, pth); pth.cmds.push("X");
+ }
+ else if (tn == "image") {
+ var w = parseFloat(nd.getAttribute("width")), h = parseFloat(nd.getAttribute("height"));
+ pth.cmds.push(nd.getAttribute("xlink:href"));
+ pth.crds.push(0, 0, w, 0, w, h, 0, h);
+ }
+ else if (tn == "defs") { }
+ else console.log(tn);
+ }
+ }
+
+ function _tokens(d) {
+ var ts = [], off = 0, rn = false, cn = "", pc = "", lc = "", nc = 0; // reading number, current number, prev char, lastCommand, number count (after last command
+ while (off < d.length) {
+ var cc = d.charCodeAt(off), ch = d.charAt(off); off++;
+ var isNum = (48 <= cc && cc <= 57) || ch == "." || ch == "-" || ch == "+" || ch == "e" || ch == "E";
+
+ if (rn) {
+ if (((ch == "+" || ch == "-") && pc != "e") || (ch == "." && cn.indexOf(".") != -1) || (isNum && (lc == "a" || lc == "A") && ((nc % 7) == 3 || (nc % 7) == 4))) { ts.push(parseFloat(cn)); nc++; cn = ch; }
+ else if (isNum) cn += ch;
+ else { ts.push(parseFloat(cn)); nc++; if (ch != "," && ch != " ") { ts.push(ch); lc = ch; nc = 0; } rn = false; }
+ }
+ else {
+ if (isNum) { cn = ch; rn = true; }
+ else if (ch != "," && ch != " ") { ts.push(ch); lc = ch; nc = 0; }
+ }
+ pc = ch;
+ }
+ if (rn) ts.push(parseFloat(cn));
+ return ts;
+ }
+
+ function _reps(ts, off, ps) {
+ var i = off;
+ while (i < ts.length) { if ((typeof ts[i]) == "string") break; i += ps; }
+ return (i - off) / ps;
+ }
+
+ function svgToPath(d, pth) {
+ var ts = _tokens(d);
+ var i = 0, x = 0, y = 0, ox = 0, oy = 0, oldo = pth.crds.length;
+ var pc = { "M": 2, "L": 2, "H": 1, "V": 1, "T": 2, "S": 4, "A": 7, "Q": 4, "C": 6 };
+ var cmds = pth.cmds, crds = pth.crds;
+
+ while (i < ts.length) {
+ var cmd = ts[i]; i++;
+ var cmu = cmd.toUpperCase();
+
+ if (cmu == "Z") { cmds.push("Z"); x = ox; y = oy; }
+ else {
+ var ps = pc[cmu], reps = _reps(ts, i, ps);
+
+ for (var j = 0; j < reps; j++) {
+ // If a moveto is followed by multiple pairs of coordinates, the subsequent pairs are treated as implicit lineto commands.
+ if (j == 1 && cmu == "M") { cmd = (cmd == cmu) ? "L" : "l"; cmu = "L"; }
+
+ var xi = 0, yi = 0; if (cmd != cmu) { xi = x; yi = y; }
+
+ if (false) { }
+ else if (cmu == "M") { x = xi + ts[i++]; y = yi + ts[i++]; cmds.push("M"); crds.push(x, y); ox = x; oy = y; }
+ else if (cmu == "L") { x = xi + ts[i++]; y = yi + ts[i++]; cmds.push("L"); crds.push(x, y); }
+ else if (cmu == "H") { x = xi + ts[i++]; cmds.push("L"); crds.push(x, y); }
+ else if (cmu == "V") { y = yi + ts[i++]; cmds.push("L"); crds.push(x, y); }
+ else if (cmu == "Q") {
+ var x1 = xi + ts[i++], y1 = yi + ts[i++], x2 = xi + ts[i++], y2 = yi + ts[i++];
+ cmds.push("Q"); crds.push(x1, y1, x2, y2); x = x2; y = y2;
+ }
+ else if (cmu == "T") {
+ var co = Math.max(crds.length - (cmds[cmds.length - 1] == "Q" ? 4 : 2), oldo);
+ var x1 = x + x - crds[co], y1 = y + y - crds[co + 1];
+ var x2 = xi + ts[i++], y2 = yi + ts[i++];
+ cmds.push("Q"); crds.push(x1, y1, x2, y2); x = x2; y = y2;
+ }
+ else if (cmu == "C") {
+ var x1 = xi + ts[i++], y1 = yi + ts[i++], x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++];
+ cmds.push("C"); crds.push(x1, y1, x2, y2, x3, y3); x = x3; y = y3;
+ }
+ else if (cmu == "S") {
+ var co = Math.max(crds.length - (cmds[cmds.length - 1] == "C" ? 4 : 2), oldo);
+ var x1 = x + x - crds[co], y1 = y + y - crds[co + 1];
+ var x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++];
+ cmds.push("C"); crds.push(x1, y1, x2, y2, x3, y3); x = x3; y = y3;
+ }
+ else if (cmu == "A") { // convert SVG Arc to four cubic bézier segments "C"
+ var x1 = x, y1 = y;
+ var rx = ts[i++], ry = ts[i++];
+ var phi = ts[i++] * (Math.PI / 180), fA = ts[i++], fS = ts[i++];
+ var x2 = xi + ts[i++], y2 = yi + ts[i++];
+ if (x2 == x && y2 == y && rx == 0 && ry == 0) continue;
+
+ var hdx = (x1 - x2) / 2, hdy = (y1 - y2) / 2;
+ var cosP = Math.cos(phi), sinP = Math.sin(phi);
+ var x1A = cosP * hdx + sinP * hdy;
+ var y1A = -sinP * hdx + cosP * hdy;
+
+ var rxS = rx * rx, ryS = ry * ry;
+ var x1AS = x1A * x1A, y1AS = y1A * y1A;
+ var frc = (rxS * ryS - rxS * y1AS - ryS * x1AS) / (rxS * y1AS + ryS * x1AS);
+ var coef = (fA != fS ? 1 : -1) * Math.sqrt(Math.max(frc, 0));
+ var cxA = coef * (rx * y1A) / ry;
+ var cyA = -coef * (ry * x1A) / rx;
+
+ var cx = cosP * cxA - sinP * cyA + (x1 + x2) / 2;
+ var cy = sinP * cxA + cosP * cyA + (y1 + y2) / 2;
+
+ var angl = function (ux, uy, vx, vy) {
+ var lU = Math.sqrt(ux * ux + uy * uy), lV = Math.sqrt(vx * vx + vy * vy);
+ var num = (ux * vx + uy * vy) / (lU * lV); //console.log(num, Math.acos(num));
+ return (ux * vy - uy * vx >= 0 ? 1 : -1) * Math.acos(Math.max(-1, Math.min(1, num)));
+ }
+
+ var vX = (x1A - cxA) / rx, vY = (y1A - cyA) / ry;
+ var theta1 = angl(1, 0, vX, vY);
+ var dtheta = angl(vX, vY, (-x1A - cxA) / rx, (-y1A - cyA) / ry);
+ dtheta = dtheta % (2 * Math.PI);
+
+ var arc = function (gst, x, y, r, a0, a1, neg) {
+ var rotate = function (m, a) {
+ var si = Math.sin(a), co = Math.cos(a);
+ var a = m[0], b = m[1], c = m[2], d = m[3];
+ m[0] = (a * co) + (b * si); m[1] = (-a * si) + (b * co);
+ m[2] = (c * co) + (d * si); m[3] = (-c * si) + (d * co);
+ }
+ var multArr = function (m, a) {
+ for (var j = 0; j < a.length; j += 2) {
+ var x = a[j], y = a[j + 1];
+ a[j] = m[0] * x + m[2] * y + m[4];
+ a[j + 1] = m[1] * x + m[3] * y + m[5];
+ }
+ }
+ var concatA = function (a, b) { for (var j = 0; j < b.length; j++) a.push(b[j]); }
+ var concatP = function (p, r) { concatA(p.cmds, r.cmds); concatA(p.crds, r.crds); }
+ // circle from a0 counter-clock-wise to a1
+ if (neg) while (a1 > a0) a1 -= 2 * Math.PI;
+ else while (a1 < a0) a1 += 2 * Math.PI;
+ var th = (a1 - a0) / 4;
+
+ var x0 = Math.cos(th / 2), y0 = -Math.sin(th / 2);
+ var x1 = (4 - x0) / 3, y1 = y0 == 0 ? y0 : (1 - x0) * (3 - x0) / (3 * y0);
+ var x2 = x1, y2 = -y1;
+ var x3 = x0, y3 = -y0;
+
+ var ps = [x1, y1, x2, y2, x3, y3];
+
+ var pth = { cmds: ["C", "C", "C", "C"], crds: ps.slice(0) };
+ var rot = [1, 0, 0, 1, 0, 0]; rotate(rot, -th);
+ for (var j = 0; j < 3; j++) { multArr(rot, ps); concatA(pth.crds, ps); }
+
+ rotate(rot, -a0 + th / 2); rot[0] *= r; rot[1] *= r; rot[2] *= r; rot[3] *= r; rot[4] = x; rot[5] = y;
+ multArr(rot, pth.crds);
+ multArr(gst.ctm, pth.crds);
+ concatP(gst.pth, pth);
+ }
+
+ var gst = { pth: pth, ctm: [rx * cosP, rx * sinP, -ry * sinP, ry * cosP, cx, cy] };
+ arc(gst, 0, 0, 1, theta1, theta1 + dtheta, fS == 0);
+ x = x2; y = y2;
+ }
+ else console.log("Unknown SVG command " + cmd);
+ }
+ }
+ }
+ };
+ return { "cssMap": cssMap, "readTrnf": readTrnf, svgToPath: svgToPath, toPath: toPath };
+ }(),
+
+
+
+
+ "initHB": function (hurl, resp) {
+ var codeLength = function (code) {
+ var len = 0;
+ if ((code & (0xffffffff - (1 << 7) + 1)) == 0) { len = 1; }
+ else if ((code & (0xffffffff - (1 << 11) + 1)) == 0) { len = 2; }
+ else if ((code & (0xffffffff - (1 << 16) + 1)) == 0) { len = 3; }
+ else if ((code & (0xffffffff - (1 << 21) + 1)) == 0) { len = 4; }
+ return len;
+ }
+
+ fetch(hurl)
+ .then(function (x) { return x["arrayBuffer"](); })
+ .then(function (ab) { return WebAssembly["instantiate"](ab); })
+ .then(function (res) {
+ console.log("HB ready");
+ var exp = res["instance"]["exports"], mem = exp["memory"];
+ //mem["grow"](30); // each page is 64kb in size
+ var heapu8, u32, i32;
+ var __lastFnt, blob, blobPtr, face, font;
+
+ Typr["U"]["shapeHB"] = (function () {
+
+ var toJson = function (ptr) {
+ var length = exp["hb_buffer_get_length"](ptr);
+ var result = [];
+ var iPtr32 = exp["hb_buffer_get_glyph_infos"](ptr, 0) >>> 2;
+ var pPtr32 = exp["hb_buffer_get_glyph_positions"](ptr, 0) >>> 2;
+ for (var i = 0; i < length; ++i) {
+ var a = iPtr32 + i * 5, b = pPtr32 + i * 5;
+ result.push({
+ "g": u32[a + 0],
+ "cl": u32[a + 2],
+ "ax": i32[b + 0],
+ "ay": i32[b + 1],
+ "dx": i32[b + 2],
+ "dy": i32[b + 3]
+ });
+ }
+ //console.log(result);
+ return result;
+ }
+ var te;
+
+ return function (fnt, str, ltr) {
+ var fdata = fnt["_data"], fn = fnt["name"]["postScriptName"];
+
+ //var olen = mem.buffer.byteLength, nlen = 2*fdata.length+str.length*16 + 4e6;
+ //if(olen>>16)+4); //console.log("growing",nlen);
+
+ heapu8 = new Uint8Array(mem.buffer);
+ u32 = new Uint32Array(mem.buffer);
+ i32 = new Int32Array(mem.buffer);
+
+ if (__lastFnt != fn) {
+ if (blob != null) {
+ exp["hb_blob_destroy"](blob);
+ exp["free"](blobPtr);
+ exp["hb_face_destroy"](face);
+ exp["hb_font_destroy"](font);
+ }
+ blobPtr = exp["malloc"](fdata.byteLength); heapu8.set(fdata, blobPtr);
+ blob = exp["hb_blob_create"](blobPtr, fdata.byteLength, 2, 0, 0);
+ face = exp["hb_face_create"](blob, 0);
+ font = exp["hb_font_create"](face)
+ __lastFnt = fn;
+ }
+ if (window["TextEncoder"] == null) { alert("Your browser is too old. Please, update it."); return; }
+ if (te == null) te = new window["TextEncoder"]("utf8");
+
+ var buffer = exp["hb_buffer_create"]();
+ var bytes = te["encode"](str);
+ var len = bytes.length, strp = exp["malloc"](len); heapu8.set(bytes, strp);
+ exp["hb_buffer_add_utf8"](buffer, strp, len, 0, len);
+ exp["free"](strp);
+
+ exp["hb_buffer_set_direction"](buffer, ltr ? 4 : 5);
+ exp["hb_buffer_guess_segment_properties"](buffer);
+ exp["hb_shape"](font, buffer, 0, 0);
+ var json = toJson(buffer)//buffer["json"]();
+ exp["hb_buffer_destroy"](buffer);
+
+ var arr = json.slice(0); if (!ltr) arr.reverse();
+ var ci = 0, bi = 0; // character index, binary index
+ for (var i = 1; i < arr.length; i++) {
+ var gl = arr[i], cl = gl["cl"];
+ while (true) {
+ var cpt = str.codePointAt(ci), cln = codeLength(cpt);
+ if (bi + cln <= cl) { bi += cln; ci += cpt <= 0xffff ? 1 : 2; }
+ else break;
+ }
+ //while(bi+codeLength(str.charCodeAt(ci)) <=cl) { bi+=codeLength(str.charCodeAt(ci)); ci++; }
+ gl["cl"] = ci;
+ }
+ return json;
+ }
+ }());
+ resp();
+ });
+ }
+}
+
+export default Typr;
+
+
diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js
new file mode 100644
index 0000000000..b039cd84f2
--- /dev/null
+++ b/src/type/p5.Font.js
@@ -0,0 +1,1097 @@
+/**
+ * API:
+ * loadFont("https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap")
+ * loadFont("{ font-family: "Bricolage Grotesque", serif; font-optical-sizing: auto; font-weight: font-style: normal; font-variation-settings: "wdth" 100; });
+ * loadFont({
+ * fontFamily: '"Bricolage Grotesque", serif';
+ * fontOpticalSizing: 'auto';
+ * fontWeight: '';
+ * fontStyle: 'normal';
+ * fontVariationSettings: '"wdth" 100';
+ * });
+ * loadFont("https://fonts.gstatic.com/s/bricolagegrotesque/v1/pxiAZBhjZQIdd8jGnEotWQ.woff2");
+ * loadFont("./path/to/localFont.ttf");
+ * loadFont("system-font-name");
+ *
+ *
+ * NEXT:
+ * extract axes from font file
+ *
+ * TEST:
+ * const font = new FontFace("Inter", "url(./fonts/inter-latin-variable-full-font.woff2)", {
+ style: "oblique 0deg 10deg",
+ weight: "100 900",
+ display: 'fallback'
+ });
+*/
+
+/**
+ * This module defines the p5.Font class and P5 methods for
+ * loading fonts from files and urls, and extracting points from their paths.
+ */
+import Typr from './lib/Typr.js';
+
+function font(p5, fn) {
+
+ const pathArgCounts = { M: 2, L: 2, C: 6, Q: 4 };
+ const validFontTypes = ['ttf', 'otf', 'woff', 'woff2'];
+ const validFontTypesRe = new RegExp(`\\.(${validFontTypes.join('|')})`, 'i');
+ const extractFontNameRe = new RegExp(`([^/]+)(\\.(?:${validFontTypes.join('|')}))`, 'i');
+ const invalidFontError = 'Sorry, only TTF, OTF, WOFF and WOFF2 files are supported.';
+ const fontFaceVariations = ['weight', 'stretch', 'style'];
+
+ p5.Font = class Font {
+
+ constructor(p, fontFace, name, path, data) {
+ if (!(fontFace instanceof FontFace)) {
+ throw Error('FontFace is required');
+ }
+ this._pInst = p;
+ this.name = name;
+ this.path = path;
+ this.data = data;
+ this.face = fontFace;
+ }
+
+ verticalAlign(size) {
+ const { sCapHeight } = this.data?.['OS/2'] || {};
+ const { unitsPerEm = 1000 } = this.data?.head || {};
+ const { ascender = 0, descender = 0 } = this.data?.hhea || {};
+ const current = ascender / 2;
+ const target = (sCapHeight || (ascender + descender)) / 2;
+ const offset = target - current;
+ return offset * size / unitsPerEm;
+ }
+
+ variations() {
+ let vars = {};
+ if (this.data) {
+ let axes = this.face?.axes;
+ if (axes) {
+ axes.forEach(ax => {
+ vars[ax.tag] = ax.value;
+ });
+ }
+ }
+ fontFaceVariations.forEach(v => {
+ let val = this.face[v];
+ if (val !== 'normal') {
+ vars[v] = vars[v] || val;
+ }
+ });
+ return vars;
+ }
+
+ metadata() {
+ let meta = this.data?.name || {};
+ for (let p in this.face) {
+ if (!/^load/.test(p)) {
+ meta[p] = meta[p] || this.face[p];
+ }
+ }
+ return meta;
+ }
+
+ fontBounds(...args) { // alias for p5.fontBounds
+ if (!this._pInst) throw Error('p5 required for fontBounds()');
+ return this._pInst.fontBounds(...args);
+ }
+
+ textBounds(...args) { // alias for p5.textBounds
+ if (!this._pInst) throw Error('p5 required for textBounds()'); // TODO:
+ return this._pInst.textBounds(...args);
+ }
+
+ textToPaths(str, x, y, width, height, options) {
+
+ ({ width, height, options } = this._parseArgs(width, height, options));
+
+ if (!this.data) {
+ throw Error('No font data available for "' + this.name
+ + '"\nTry downloading a local copy of the font file');
+ }
+
+ // lineate and get glyphs/paths for each line
+ let lines = this._lineateAndPathify(str, x, y, width, height, options);
+
+ // flatten into a single array containing all the glyphs
+ let glyphs = lines.map(o => o.glyphs).flat();
+
+ // flatten into a single array with all the path commands
+ return glyphs.map(g => g.path.commands).flat();
+ }
+
+ textToPoints(str, x, y, width, height, options) {
+ ({ width, height, options } = this._parseArgs(width, height, options));
+
+ // lineate and get the glyphs for each line
+ let glyphs = this.textToPaths(str, x, y, width, height, options);
+
+ // convert glyphs to points array with {sampleFactor, simplifyThreshold}
+ return pathToPoints(glyphs, options);
+ }
+
+ static async list(log = false) { // tmp
+ if (log) {
+ console.log('There are', document.fonts.size, 'font-faces\n');
+ let loaded = 0;
+ for (let fontFace of document.fonts.values()) {
+ console.log('FontFace: {');
+ for (let property in fontFace) {
+ console.log(' ' + property + ': ' + fontFace[property]);
+ }
+ console.log('}\n');
+ if (fontFace.status === 'loaded') {
+ loaded++;
+ }
+ }
+ console.log(loaded + ' loaded');
+ }
+ return await Array.from(document.fonts);
+ }
+
+ /////////////////////////////// HELPERS ////////////////////////////////
+
+ /*
+ Returns an array of line objects, each containing { text, x, y, glyphs: [ {g, path} ] }
+ */
+ _lineateAndPathify(str, x, y, width, height, options = {}) {
+
+ let renderer = options?.graphics?._renderer || this._pInst._renderer;
+
+ // save the baseline
+ let setBaseline = renderer.drawingContext.textBaseline;
+
+ // lineate and compute bounds for the text
+ let { lines, bounds } = renderer._computeBounds
+ (fn._FONT_BOUNDS, str, x, y, width, height,
+ { ignoreRectMode: true, ...options });
+
+ // compute positions for each of the lines
+ lines = this._position(renderer, lines, bounds, width, height);
+
+ // convert lines to paths
+ let uPE = this.data?.head?.unitsPerEm || 1000;
+ let scale = renderer.states.textSize / uPE;
+ let pathsForLine = lines.map(l => this._lineToGlyphs(l, scale));
+
+ // restore the baseline
+ renderer.drawingContext.textBaseline = setBaseline;
+
+ return pathsForLine;
+ }
+
+ _textToPathPoints(str, x, y, width, height, options) {
+
+ ({ width, height, options } = this._parseArgs(width, height, options));
+
+ // lineate and get the points for each line
+ let cmds = this.textToPaths(str, x, y, width, height, options);
+
+ // divide line-segments with intermediate points
+ const subdivide = (pts, pt1, pt2, md) => {
+ if (fn.dist(pt1.x, pt1.y, pt2.x, pt2.y) > md) {
+ let middle = { x: (pt1.x + pt2.x) / 2, y: (pt1.y + pt2.y) / 2 };
+ pts.push(middle);
+ subdivide(pts, pt1, middle, md);
+ subdivide(pts, middle, pt2, md);
+ }
+ }
+
+ // a point for each path-command plus line subdivisions
+ let pts = [];
+ let { textSize } = this._pInst._renderer.states;
+ let maxDist = (textSize / this.data.head.unitsPerEm) * 500;
+
+ for (let i = 0; i < cmds.length; i++) {
+ let { type, data: d } = cmds[i];
+ if (type !== 'Z') {
+ let pt = { x: d[d.length - 2], y: d[d.length - 1] }
+ if (type === 'L' && pts.length && !options?.nodivide > 0) {
+ subdivide(pts, pts[pts.length - 1], pt, maxDist);
+ }
+ pts.push(pt);
+ }
+ }
+
+ return pts;
+ }
+
+ _parseArgs(width, height, options = {}) {
+
+ if (typeof width === 'object') {
+ options = width;
+ width = height = undefined;
+ }
+ else if (typeof height === 'object') {
+ options = height;
+ height = undefined;
+ }
+ return { width, height, options };
+ }
+
+ _position(renderer, lines, bounds, width, height) {
+
+ let { textAlign, textLeading } = renderer.states;
+ let metrics = this._measureTextDefault(renderer, 'X');
+ let ascent = metrics.fontBoundingBoxAscent;
+
+ let coordify = (text, i) => {
+ let x = bounds.x;
+ let y = bounds.y + (i * textLeading) + ascent;
+ let lineWidth = renderer._fontWidthSingle(text);
+ if (textAlign === fn.CENTER) {
+ x += (bounds.w - lineWidth) / 2;
+ }
+ else if (textAlign === fn.RIGHT) {
+ x += (bounds.w - lineWidth);
+ }
+ if (typeof width !== 'undefined') {
+ switch (renderer.states.rectMode) {
+ case fn.CENTER:
+ x -= width / 2;
+ y -= height / 2;
+ break;
+ case fn.RADIUS:
+ x -= width;
+ y -= height;
+ break;
+ }
+ }
+ return { text, x, y };
+ }
+
+ return lines.map(coordify);
+ }
+
+ _lineToGlyphs(line, scale = 1) {
+
+ if (!this.data) {
+ throw Error('No font data available for "' + this.name
+ + '"\nTry downloading a local copy of the font file');
+ }
+ let glyphShapes = Typr.U.shape(this.data, line.text);
+ line.glyphShapes = glyphShapes;
+ line.glyphs = this._shapeToPaths(glyphShapes, line, scale);
+
+ return line;
+ }
+
+ _positionGlyphs(text) {
+ const glyphShapes = Typr.U.shape(this.data, text);
+ const positionedGlyphs = [];
+ let x = 0;
+ for (const glyph of glyphShapes) {
+ positionedGlyphs.push({ x, index: glyph.g, shape: glyph });
+ x += glyph.ax;
+ }
+ return positionedGlyphs;
+ }
+
+ _singleShapeToPath(shape, { scale = 1, x = 0, y = 0, lineX = 0, lineY = 0 } = {}) {
+ let font = this.data;
+ let crdIdx = 0;
+ let { g, ax, ay, dx, dy } = shape;
+ let { crds, cmds } = Typr.U.glyphToPath(font, g);
+
+ // can get simple points for each glyph here, but we don't need them ?
+ let glyph = { /*g: line.text[i], points: [],*/ path: { commands: [] } };
+
+ for (let j = 0; j < cmds.length; j++) {
+ let type = cmds[j], command = [type];
+ if (type in pathArgCounts) {
+ let argCount = pathArgCounts[type];
+ for (let k = 0; k < argCount; k += 2) {
+ let gx = crds[k + crdIdx] + x + dx;
+ let gy = crds[k + crdIdx + 1] + y + dy;
+ let fx = lineX + gx * scale;
+ let fy = lineY + gy * -scale;
+ command.push(fx);
+ command.push(fy);
+ /*if (k === argCount - 2) {
+ glyph.points.push({ x: fx, y: fy });
+ }*/
+ }
+ crdIdx += argCount;
+ }
+ glyph.path.commands.push(command);
+ }
+
+ return { glyph, ax, ay };
+ }
+
+ _shapeToPaths(glyphs, line, scale = 1) {
+ let x = 0, y = 0, paths = [];
+
+ if (glyphs.length !== line.text.length) {
+ throw Error('Invalid shape data');
+ }
+
+ // iterate over the glyphs, converting each to a glyph object
+ // with a path property containing an array of commands
+ for (let i = 0; i < glyphs.length; i++) {
+ const { glyph, ax, ay } = this._singleShapeToPath(glyphs[i], {
+ scale,
+ x,
+ y,
+ lineX: line.x,
+ lineY: line.y,
+ });
+
+ paths.push(glyph);
+ x += ax; y += ay;
+ }
+
+ return paths;
+ }
+
+ _measureTextDefault(renderer, str) {
+ let { textAlign, textBaseline } = renderer.states;
+ let ctx = renderer.drawingContext;
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'alphabetic';
+ let metrics = ctx.measureText(str);
+ ctx.textAlign = textAlign;
+ ctx.textBaseline = textBaseline;
+ return metrics;
+ }
+
+ drawPaths(ctx, commands, opts) { // for debugging
+ ctx.strokeStyle = opts?.stroke || ctx.strokeStyle;
+ ctx.fillStyle = opts?.fill || ctx.fillStyle;
+ ctx.beginPath();
+ commands.forEach(([type, ...data]) => {
+ if (type === 'M') {
+ ctx.moveTo(...data);
+ } else if (type === 'L') {
+ ctx.lineTo(...data);
+ } else if (type === 'C') {
+ ctx.bezierCurveTo(...data);
+ } else if (type === 'Q') {
+ ctx.quadraticCurveTo(...data);
+ } else if (type === 'Z') {
+ ctx.closePath();
+ }
+ });
+ if (opts?.fill) ctx.fill();
+ if (opts?.stroke) ctx.stroke();
+ }
+
+ _pathsToCommands(paths, scale) {
+ let commands = [];
+ for (let i = 0; i < paths.length; i++) {
+ let pathData = paths[i];
+ let { x, y, path } = pathData;
+ let { crds, cmds } = path;
+
+ // iterate over the path, storing each non-control point
+ for (let c = 0, j = 0; j < cmds.length; j++) {
+ let cmd = cmds[j], obj = { type: cmd, data: [] };
+ if (cmd == "M" || cmd == "L") {
+ obj.data.push(x + crds[c] * scale, y + crds[c + 1] * -scale);
+ c += 2;
+ }
+ else if (cmd == "C") {
+ for (let i = 0; i < 6; i += 2) {
+ obj.data.push(x + crds[c + i] * scale, y + crds[c + i + 1] * -scale);
+ }
+ c += 6;
+ }
+ else if (cmd == "Q") {
+ for (let i = 0; i < 4; i += 2) {
+ obj.data.push(x + crds[c + i] * scale, y + crds[c + i + 1] * -scale);
+ }
+ c += 4;
+ }
+ commands.push(obj);
+ }
+ }
+
+ return commands;
+ }
+ }// end p5.Font
+
+ function parseCreateArgs(...args/*path, name, onSuccess, onError*/) {
+
+ // parse the path
+ let path = args.shift();
+ if (typeof path !== 'string' || path.length === 0) {
+ p5._friendlyError(invalidFontError, 'p5.loadFont'); // ?
+ }
+
+ // parse the name
+ let name;
+ if (typeof args[0] === 'string') {
+ name = args.shift();
+ }
+
+ // get the callbacks/descriptors if any
+ let success, error, descriptors;
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+ if (typeof arg === 'function') {
+ if (!success) {
+ success = arg;
+ } else {
+ error = arg;
+ }
+ }
+ else if (typeof arg === 'object') {
+ descriptors = arg;
+ }
+ }
+
+ return { path, name, success, error, descriptors };
+ }
+
+ /**
+ * Load a font and returns a p5.Font instance. The font can be specified by its path or a url.
+ * Optional arguments include the font name, descriptors for the FontFace object,
+ * and callbacks for success and error.
+ * @param {...any} args - path, name, onSuccess, onError, descriptors
+ * @returns a Promise that resolves with a p5.Font instance
+ */
+ p5.prototype.loadFont = async function (...args/*path, name, onSuccess, onError, descriptors*/) {
+
+ let { path, name, success, error, descriptors } = parseCreateArgs(...args);
+
+ let pfont;
+ try {
+ // load the raw font bytes
+ let result = await fn.loadBytes(path);
+ if (!result) {
+ throw Error('Failed to load font data');
+ }
+
+ // parse the font data
+ let fonts = Typr.parse(result);
+
+ if (fonts.length !== 1 || fonts[0].cmap === undefined) {
+ throw Error(23);
+ }
+
+ // make sure we have a valid name
+ name = name || extractFontName(fonts[0], path);
+
+ // create a FontFace object and pass it to the p5.Font constructor
+ pfont = await create(this, name, path, descriptors, fonts[0]);
+
+ } catch (err) {
+ // failed to parse the font, load it as a simple FontFace
+ console.warn('Failed to parse font data:', err);
+ try {
+ // create a FontFace object and pass it to p5.Font
+ console.log(`Retrying '${name}' without font-data: '${path}'`);
+ pfont = await create(this, name, path, descriptors);
+ }
+ catch (err) {
+ if (error) {
+ error(err);
+ }
+ throw err;
+ }
+ }
+ if (success) {
+ success(pfont);
+ }
+
+ return pfont;
+ }
+
+ async function create(pInst, name, path, descriptors, rawFont) {
+
+ let face = createFontFace(name, path, descriptors, rawFont);
+
+ // load if we need to
+ if (face.status !== 'loaded') {
+ await face.load();
+ }
+
+ // add it to the document
+ document.fonts.add(face);
+
+ // return a p5.Font instance
+ return new p5.Font(pInst, face, name, path, rawFont);
+ }
+
+ function createFontFace(name, path, descriptors, rawFont) {
+ let fontArg = rawFont?._data;
+ if (!fontArg) {
+ if (!validFontTypesRe.test(path)) {
+ throw Error(invalidFontError);
+ }
+ if (!path.startsWith('url(')) {
+ path = 'url(' + path + ')';
+ }
+ fontArg = path;
+ }
+
+ // create/return the FontFace object
+ let face = new FontFace(name, fontArg, descriptors);
+ if (face.status === 'error') {
+ throw Error('Failed to create FontFace for "' + name + '"');
+ }
+ return face;
+ }
+
+ function extractFontName(font, path) {
+ let meta = font?.name;
+
+ // use the metadata if we have it
+ if (meta) {
+ if (meta.fullName) {
+ return meta.fullName;
+ }
+ if (meta.familyName) {
+ return meta.familyName;
+ }
+ }
+
+ // if not, extract the name from the path
+ let matches = extractFontNameRe.exec(path);
+ if (matches && matches.length >= 3) {
+ return matches[1];
+ }
+
+ // give up and return the full path
+ return path;
+ };
+
+ function pathToPoints(cmds, options) {
+
+ const parseOpts = (options, defaults) => {
+ if (typeof options !== 'object') {
+ options = defaults;
+ } else {
+ for (const key in defaults) {
+ if (typeof options[key] === 'undefined') {
+ options[key] = defaults[key];
+ }
+ }
+ }
+ return options;
+ }
+
+ const at = (v, i) => {
+ const s = v.length;
+ return v[i < 0 ? i % s + s : i % s];
+ }
+
+ const simplify = (pts, angle) => {
+ angle = angle || 0;
+ let num = 0;
+ for (let i = pts.length - 1; pts.length > 3 && i >= 0; --i) {
+ if (collinear(at(pts, i - 1), at(pts, i), at(pts, i + 1), angle)) {
+ pts.splice(i % pts.length, 1); // Remove middle point
+ num++;
+ }
+ }
+ return num;
+ }
+
+ const findDotsAtSegment = (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) => {
+ const t1 = 1 - t;
+ const t13 = Math.pow(t1, 3);
+ const t12 = Math.pow(t1, 2);
+ const t2 = t * t;
+ const t3 = t2 * t;
+ const x = t13 * p1x + t12 * 3 * t * c1x + t1 * 3 * t * t * c2x + t3 * p2x;
+ const y = t13 * p1y + t12 * 3 * t * c1y + t1 * 3 * t * t * c2y + t3 * p2y;
+ const mx = p1x + 2 * t * (c1x - p1x) + t2 * (c2x - 2 * c1x + p1x);
+ const my = p1y + 2 * t * (c1y - p1y) + t2 * (c2y - 2 * c1y + p1y);
+ const nx = c1x + 2 * t * (c2x - c1x) + t2 * (p2x - 2 * c2x + c1x);
+ const ny = c1y + 2 * t * (c2y - c1y) + t2 * (p2y - 2 * c2y + c1y);
+ const ax = t1 * p1x + t * c1x;
+ const ay = t1 * p1y + t * c1y;
+ const cx = t1 * c2x + t * p2x;
+ const cy = t1 * c2y + t * p2y;
+ let alpha = 90 - Math.atan2(mx - nx, my - ny) * 180 / Math.PI;
+ if (mx > nx || my < ny) {
+ alpha += 180;
+ }
+ return {
+ x, y, m: { x: mx, y: my }, n: { x: nx, y: ny },
+ start: { x: ax, y: ay }, end: { x: cx, y: cy }, alpha
+ };
+ }
+
+ const getPointAtSegmentLength = (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length) => {
+ return length == null ? bezlen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) :
+ findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y,
+ getTatLen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length));
+ }
+
+ const pointAtLength = (path, length, isTotal) => {
+ path = path2curve(path);
+ let x, y, p, l, point;
+ let sp = '', len = 0, subpaths = {}
+ for (let i = 0, ii = path.length; i < ii; i++) {
+ p = path[i];
+ if (p[0] === 'M') {
+ x = +p[1];
+ y = +p[2];
+ } else {
+ l = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
+ if (len + l > length) {
+ if (!isTotal) {
+ point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
+ return { x: point.x, y: point.y, alpha: point.alpha };
+ }
+ }
+ len += l;
+ x = +p[5];
+ y = +p[6];
+ }
+ sp += p.shift() + p;
+ }
+ subpaths.end = sp;
+
+ point = isTotal ? len : findDotsAtSegment
+ (x, y, p[0], p[1], p[2], p[3], p[4], p[5], 1);
+
+ if (point.alpha) {
+ point = { x: point.x, y: point.y, alpha: point.alpha };
+ }
+
+ return point;
+ }
+
+ const pathToAbsolute = (pathArray) => {
+ let res = [], x = 0, y = 0, mx = 0, my = 0, start = 0;
+ if (!pathArray) {
+ // console.warn("Unexpected state: undefined pathArray"); // shouldn't happen
+ return res;
+ }
+ if (pathArray[0][0] === 'M') {
+ x = +pathArray[0][1];
+ y = +pathArray[0][2];
+ mx = x;
+ my = y;
+ start++;
+ res[0] = ['M', x, y];
+ }
+
+ let dots, crz =
+ pathArray.length === 3 &&
+ pathArray[0][0] === 'M' &&
+ pathArray[1][0].toUpperCase() === 'R' &&
+ pathArray[2][0].toUpperCase() === 'Z';
+
+ for (let r, pa, i = start, ii = pathArray.length; i < ii; i++) {
+ res.push((r = []));
+ pa = pathArray[i];
+ if (pa[0] !== pa[0].toUpperCase()) {
+ r[0] = pa[0].toUpperCase();
+ switch (r[0]) {
+ case 'A':
+ r[1] = pa[1];
+ r[2] = pa[2];
+ r[3] = pa[3];
+ r[4] = pa[4];
+ r[5] = pa[5];
+ r[6] = +(pa[6] + x);
+ r[7] = +(pa[7] + y);
+ break;
+ case 'V':
+ r[1] = +pa[1] + y;
+ break;
+ case 'H':
+ r[1] = +pa[1] + x;
+ break;
+ case 'R':
+ dots = [x, y].concat(pa.slice(1));
+ for (let j = 2, jj = dots.length; j < jj; j++) {
+ dots[j] = +dots[j] + x;
+ dots[++j] = +dots[j] + y;
+ }
+ res.pop();
+ res = res.concat(catmullRom2bezier(dots, crz));
+ break;
+ case 'M':
+ mx = +pa[1] + x;
+ my = +pa[2] + y;
+ break;
+ default:
+ for (let j = 1, jj = pa.length; j < jj; j++) {
+ r[j] = +pa[j] + (j % 2 ? x : y);
+ }
+ }
+ } else if (pa[0] === 'R') {
+ dots = [x, y].concat(pa.slice(1));
+ res.pop();
+ res = res.concat(catmullRom2bezier(dots, crz));
+ r = ['R'].concat(pa.slice(-2));
+ } else {
+ for (let k = 0, kk = pa.length; k < kk; k++) {
+ r[k] = pa[k];
+ }
+ }
+ switch (r[0]) {
+ case 'Z':
+ x = mx;
+ y = my;
+ break;
+ case 'H':
+ x = r[1];
+ break;
+ case 'V':
+ y = r[1];
+ break;
+ case 'M':
+ mx = r[r.length - 2];
+ my = r[r.length - 1];
+ break;
+ default:
+ x = r[r.length - 2];
+ y = r[r.length - 1];
+ }
+ }
+ return res;
+ }
+
+ const path2curve = (path, path2) => {
+ const p = pathToAbsolute(path), p2 = path2 && pathToAbsolute(path2);
+ const attrs = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null };
+ const attrs2 = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null };
+ const pcoms1 = []; // path commands of original path p
+ const pcoms2 = []; // path commands of original path p2
+ let ii;
+ const processPath = (path, d, pcom) => {
+ let nx, ny, tq = { T: 1, Q: 1 };
+ if (!path) {
+ return ['C', d.x, d.y, d.x, d.y, d.x, d.y];
+ }
+ if (!(path[0] in tq)) {
+ d.qx = d.qy = null;
+ }
+ switch (path[0]) {
+ case 'M':
+ d.X = path[1];
+ d.Y = path[2];
+ break;
+ case 'A':
+ path = ['C'].concat(a2c.apply(0, [d.x, d.y].concat(path.slice(1))));
+ break;
+ case 'S':
+ if (pcom === 'C' || pcom === 'S') {
+ nx = d.x * 2 - d.bx;
+ ny = d.y * 2 - d.by;
+ } else {
+ nx = d.x;
+ ny = d.y;
+ }
+ path = ['C', nx, ny].concat(path.slice(1));
+ break;
+ case 'T':
+ if (pcom === 'Q' || pcom === 'T') {
+ d.qx = d.x * 2 - d.qx;
+ d.qy = d.y * 2 - d.qy;
+ } else {
+ d.qx = d.x;
+ d.qy = d.y;
+ }
+ path = ['C'].concat(q2c(d.x, d.y, d.qx, d.qy, path[1], path[2]));
+ break;
+ case 'Q':
+ d.qx = path[1];
+ d.qy = path[2];
+ path = ['C'].concat(
+ q2c(d.x, d.y, path[1], path[2], path[3], path[4])
+ );
+ break;
+ case 'L':
+ path = ['C'].concat(l2c(d.x, d.y, path[1], path[2]));
+ break;
+ case 'H':
+ path = ['C'].concat(l2c(d.x, d.y, path[1], d.y));
+ break;
+ case 'V':
+ path = ['C'].concat(l2c(d.x, d.y, d.x, path[1]));
+ break;
+ case 'Z':
+ path = ['C'].concat(l2c(d.x, d.y, d.X, d.Y));
+ break;
+ }
+ return path;
+ },
+ fixArc = (pp, i) => {
+ if (pp[i].length > 7) {
+ pp[i].shift();
+ const pi = pp[i];
+ while (pi.length) {
+ pcoms1[i] = 'A';
+ if (p2) {
+ pcoms2[i] = 'A';
+ }
+ pp.splice(i++, 0, ['C'].concat(pi.splice(0, 6)));
+ }
+ pp.splice(i, 1);
+ ii = Math.max(p.length, (p2 && p2.length) || 0);
+ }
+ },
+ fixM = (path1, path2, a1, a2, i) => {
+ if (path1 && path2 && path1[i][0] === 'M' && path2[i][0] !== 'M') {
+ path2.splice(i, 0, ['M', a2.x, a2.y]);
+ a1.bx = 0;
+ a1.by = 0;
+ a1.x = path1[i][1];
+ a1.y = path1[i][2];
+ ii = Math.max(p.length, (p2 && p2.length) || 0);
+ }
+ };
+
+ let pfirst = ''; // temporary holder for original path command
+ let pcom = ''; // holder for previous path command of original path
+
+ ii = Math.max(p.length, (p2 && p2.length) || 0);
+ for (let i = 0; i < ii; i++) {
+ if (p[i]) {
+ pfirst = p[i][0];
+ } // save current path command
+ if (pfirst !== 'C') {
+ pcoms1[i] = pfirst; // Save current path command
+ if (i) {
+ pcom = pcoms1[i - 1];
+ } // Get previous path command pcom
+ }
+ p[i] = processPath(p[i], attrs, pcom);
+ if (pcoms1[i] !== 'A' && pfirst === 'C') {
+ pcoms1[i] = 'C';
+ }
+ fixArc(p, i); // fixArc adds also the right amount of A:s to pcoms1
+ if (p2) {
+ // the same procedures is done to p2
+ if (p2[i]) {
+ pfirst = p2[i][0];
+ }
+ if (pfirst !== 'C') {
+ pcoms2[i] = pfirst;
+ if (i) {
+ pcom = pcoms2[i - 1];
+ }
+ }
+ p2[i] = processPath(p2[i], attrs2, pcom);
+ if (pcoms2[i] !== 'A' && pfirst === 'C') {
+ pcoms2[i] = 'C';
+ }
+ fixArc(p2, i);
+ }
+ fixM(p, p2, attrs, attrs2, i);
+ fixM(p2, p, attrs2, attrs, i);
+ const seg = p[i], seg2 = p2 && p2[i], seglen = seg.length, seg2len = p2 && seg2.length;
+ attrs.x = seg[seglen - 2];
+ attrs.y = seg[seglen - 1];
+ attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x;
+ attrs.by = parseFloat(seg[seglen - 3]) || attrs.y;
+ attrs2.bx = p2 && (parseFloat(seg2[seg2len - 4]) || attrs2.x);
+ attrs2.by = p2 && (parseFloat(seg2[seg2len - 3]) || attrs2.y);
+ attrs2.x = p2 && seg2[seg2len - 2];
+ attrs2.y = p2 && seg2[seg2len - 1];
+ }
+
+ return p2 ? [p, p2] : p;
+ }
+
+ const a2c = (x1, y1, rx, ry, angle, lac, sweep_flag, x2, y2, recursive) => {
+ // see: http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
+ const PI = Math.PI, _120 = PI * 120 / 180;
+ let f1, f2, cx, cy, xy;
+ const rad = PI / 180 * (+angle || 0);
+ let res = [];
+ const rotate = (x, y, rad) => {
+ const X = x * Math.cos(rad) - y * Math.sin(rad),
+ Y = x * Math.sin(rad) + y * Math.cos(rad);
+ return { x: X, y: Y };
+ };
+
+ if (!recursive) {
+ xy = rotate(x1, y1, -rad);
+ x1 = xy.x;
+ y1 = xy.y;
+ xy = rotate(x2, y2, -rad);
+ x2 = xy.x;
+ y2 = xy.y;
+ const x = (x1 - x2) / 2;
+ const y = (y1 - y2) / 2;
+ let h = x * x / (rx * rx) + y * y / (ry * ry);
+ if (h > 1) {
+ h = Math.sqrt(h);
+ rx = h * rx;
+ ry = h * ry;
+ }
+ const rx2 = rx * rx, ry2 = ry * ry;
+ const k = (lac === sweep_flag ? -1 : 1) * Math.sqrt(Math.abs(
+ (rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)));
+
+ cx = k * rx * y / ry + (x1 + x2) / 2;
+ cy = k * -ry * x / rx + (y1 + y2) / 2;
+ f1 = Math.asin(((y1 - cy) / ry).toFixed(9));
+ f2 = Math.asin(((y2 - cy) / ry).toFixed(9));
+
+ f1 = x1 < cx ? PI - f1 : f1;
+ f2 = x2 < cx ? PI - f2 : f2;
+
+ if (f1 < 0) {
+ f1 = PI * 2 + f1;
+ }
+ if (f2 < 0) {
+ f2 = PI * 2 + f2;
+ }
+
+ if (sweep_flag && f1 > f2) {
+ f1 = f1 - PI * 2;
+ }
+ if (!sweep_flag && f2 > f1) {
+ f2 = f2 - PI * 2;
+ }
+ } else {
+ f1 = recursive[0];
+ f2 = recursive[1];
+ cx = recursive[2];
+ cy = recursive[3];
+ }
+ let df = f2 - f1;
+ if (Math.abs(df) > _120) {
+ const f2old = f2, x2old = x2, y2old = y2;
+ f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
+ x2 = cx + rx * Math.cos(f2);
+ y2 = cy + ry * Math.sin(f2);
+ res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag,
+ x2old, y2old, [f2, f2old, cx, cy]);
+ }
+ df = f2 - f1;
+ const c1 = Math.cos(f1),
+ s1 = Math.sin(f1),
+ c2 = Math.cos(f2),
+ s2 = Math.sin(f2),
+ t = Math.tan(df / 4),
+ hx = 4 / 3 * rx * t,
+ hy = 4 / 3 * ry * t,
+ m1 = [x1, y1],
+ m2 = [x1 + hx * s1, y1 - hy * c1],
+ m3 = [x2 + hx * s2, y2 - hy * c2],
+ m4 = [x2, y2];
+ m2[0] = 2 * m1[0] - m2[0];
+ m2[1] = 2 * m1[1] - m2[1];
+ if (recursive) {
+ return [m2, m3, m4].concat(res);
+ } else {
+ res = [m2, m3, m4].concat(res).join().split(',');
+ const newres = [];
+ for (let i = 0, ii = res.length; i < ii; i++) {
+ newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y
+ : rotate(res[i], res[i + 1], rad).x;
+ }
+ return newres;
+ }
+ }
+
+ // http://schepers.cc/getting-to-the-point
+ function catmullRom2bezier(crp, z) {
+ const d = [];
+ for (let i = 0, iLen = crp.length; iLen - 2 * !z > i; i += 2) {
+ const p = [
+ { x: +crp[i - 2], y: +crp[i - 1] },
+ { x: +crp[i], y: +crp[i + 1] },
+ { x: +crp[i + 2], y: +crp[i + 3] },
+ { x: +crp[i + 4], y: +crp[i + 5] }
+ ];
+ if (z) {
+ if (!i) {
+ p[0] = { x: +crp[iLen - 2], y: +crp[iLen - 1] };
+ } else if (iLen - 4 === i) {
+ p[3] = { x: +crp[0], y: +crp[1] };
+ } else if (iLen - 2 === i) {
+ p[2] = { x: +crp[0], y: +crp[1] };
+ p[3] = { x: +crp[2], y: +crp[3] };
+ }
+ } else {
+ if (iLen - 4 === i) {
+ p[3] = p[2];
+ } else if (!i) {
+ p[0] = { x: +crp[i], y: +crp[i + 1] };
+ }
+ }
+ d.push(['C',
+ (-p[0].x + 6 * p[1].x + p[2].x) / 6,
+ (-p[0].y + 6 * p[1].y + p[2].y) / 6,
+ (p[1].x + 6 * p[2].x - p[3].x) / 6,
+ (p[1].y + 6 * p[2].y - p[3].y) / 6,
+ p[2].x, p[2].y
+ ]);
+ }
+ return d;
+ }
+
+ function l2c(x1, y1, x2, y2) {
+ return [x1, y1, x2, y2, x2, y2];
+ }
+
+ function q2c(x1, y1, ax, ay, x2, y2) {
+ const _13 = 1 / 3, _23 = 2 / 3;
+ return [_13 * x1 + _23 * ax, _13 * y1 + _23 * ay,
+ _13 * x2 + _23 * ax, _13 * y2 + _23 * ay, x2, y2];
+ }
+
+ const bezlen = (x1, y1, x2, y2, x3, y3, x4, y4, z) => {
+ z = z ?? 1;
+ z = z > 1 ? 1 : z < 0 ? 0 : z;
+ const z2 = z / 2, n = 12;
+ let sum = 0;
+ const Tvalues = [-0.1252, 0.1252, -0.3678, 0.3678, -0.5873, 0.5873, -0.7699, 0.7699, -0.9041, 0.9041, -0.9816, 0.9816];
+ const Cvalues = [0.2491, 0.2491, 0.2335, 0.2335, 0.2032, 0.2032, 0.1601, 0.1601, 0.1069, 0.1069, 0.0472, 0.0472];
+ for (let i = 0; i < n; i++) {
+ const ct = z2 * Tvalues[i] + z2, xbase = base3(ct, x1, x2, x3, x4),
+ ybase = base3(ct, y1, y2, y3, y4), comb = xbase * xbase + ybase * ybase;
+ sum += Cvalues[i] * Math.sqrt(comb);
+ }
+ return z2 * sum;
+ }
+
+ const getTatLen = (x1, y1, x2, y2, x3, y3, x4, y4, ll) => {
+ if (ll < 0 || bezlen(x1, y1, x2, y2, x3, y3, x4, y4) < ll) {
+ return;
+ }
+ const t = 1, e = 0.01;
+ let step = t / 2, t2 = t - step;
+ let l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
+ while (Math.abs(l - ll) > e) {
+ step /= 2;
+ t2 += (l < ll ? 1 : -1) * step;
+ l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
+ }
+ return t2;
+ }
+
+ const base3 = (t, p1, p2, p3, p4) => {
+ const t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
+ t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
+ return t * t2 - 3 * p1 + 3 * p2;
+ }
+
+ let opts = parseOpts(options, {
+ sampleFactor: 0.05,
+ simplifyThreshold: 0
+ });
+
+ let points = [];
+ let len = pointAtLength(cmds, 0, 1);
+ let t = len / (len * opts.sampleFactor);
+
+ for (let i = 0; i < len; i += t) {
+ points.push(pointAtLength(cmds, i));
+ }
+
+ if (opts.simplifyThreshold) {
+ simplify(points, opts.simplifyThreshold);
+ }
+
+ return points;
+ }
+};
+
+export default font;
+
+if (typeof p5 !== 'undefined') {
+ font(p5, p5.prototype);
+}
diff --git a/src/type/text2d.js b/src/type/text2d.js
new file mode 100644
index 0000000000..446bf496b4
--- /dev/null
+++ b/src/type/text2d.js
@@ -0,0 +1,1260 @@
+import { Renderer } from '../core/p5.Renderer';
+
+/*
+ * TODO:
+ * - more with variable fonts, do slider example
+ * - better font-loading? (google fonts, font-face declarations, multiple fonts with Promise.all())
+ * - test textToPoints with google/variable fonts?
+ * - add test for line-height property in textFont() and textProperty()
+ * - how does this integrate with textLeading?
+ * - spurious warning in oneoff.html (local)
+
+ * ON HOLD:
+ * - get axes and values for parsed fonts
+ * - change renderer.state to use getters for textAlign, textBaseline, etc. ??
+ * DONE:
+ * - textToPoints/Paths should accept offscreen `graphics` passed in as `options.graphics` [x]
+ * - textToPaths: test rendering in p5 [x]
+ * - support direct setting of context2d.font with string [x]
+ * - textToPoints/Path: add re-sampling support with current options [x]
+ * - add fontAscent/Descent and textWeight functions [x]
+ * - textToPaths should split into glyphs and paths [x]
+ * - add textFont(string) that forces context2d.font to be set (if including size part) [x]
+ * - textToPoints: test rectMode for all alignments [x]
+ * - test textToPoints with single line, and overlapping text [x]
+ * ENHANCEMENTS:
+ * - cache parsed fonts
+ * - support idographic and hanging baselines
+ * - support start and end text-alignments
+ * - add 'justify' alignment
+ */
+
+import { Graphics } from '../core/p5.Graphics';
+
+/**
+ * @module Type
+ * @submodule text2d
+ * @for p5
+ * @requires core
+ */
+function text2d(p5, fn) {
+
+ // additional constants
+ fn.IDEOGRAPHIC = 'ideographic';
+ fn.RIGHT_TO_LEFT = 'rtl';
+ fn.LEFT_TO_RIGHT = 'ltr';
+ fn._CTX_MIDDLE = 'middle';
+ fn._TEXT_BOUNDS = '_textBoundsSingle';
+ fn._FONT_BOUNDS = '_fontBoundsSingle';
+ fn.HANGING = 'hanging';
+ fn.START = 'start';
+ fn.END = 'end';
+
+ const LeadingScale = 1.275;
+ const DefaultFill = '#000000';
+ const LinebreakRe = /\r?\n/g;
+ const CommaDelimRe = /,\s+/;
+ const QuotedRe = /^".*"$/;
+ const TabsRe = /\t/g;
+
+ const FontVariationSettings = 'fontVariationSettings';
+ const VariableAxes = ['wght', 'wdth', 'ital', 'slnt', 'opsz'];
+ const VariableAxesRe = new RegExp(`(?:${VariableAxes.join('|')})`);
+
+ const textFunctions = [
+ 'text',
+ 'textAlign',
+ 'textAscent',
+ 'textDescent',
+ 'textLeading',
+ 'textMode',
+ 'textFont',
+ 'textSize',
+ 'textStyle',
+ 'textWidth',
+ 'textWrap',
+
+ 'textBounds',
+ 'textToPoints',
+ 'textDirection',
+ 'textProperty',
+ 'textProperties',
+ 'fontBounds',
+ 'fontWidth',
+ 'fontAscent',
+ 'fontDescent',
+ 'textWeight'
+ ];
+
+ // attach each text func to p5, delegating to the renderer
+ textFunctions.forEach(func => {
+ fn[func] = function (...args) {
+ if (!(func in Renderer.prototype)) {
+ throw Error(`Renderer2D.prototype.${func} is not defined.`);
+ }
+ return this._renderer[func](...args);
+ };
+ // TODO: is this necessary?
+ p5.Graphics.prototype[func] = function (...args) {
+ return this._renderer[func](...args);
+ };
+ });
+
+ const RendererTextProps = {
+ textAlign: { default: fn.LEFT, type: 'Context2d' },
+ textBaseline: { default: fn.BASELINE, type: 'Context2d' },
+ textFont: { default: { family: 'sans-serif' } },
+ textLeading: { default: 15 },
+ textSize: { default: 12 },
+ textWrap: { default: fn.WORD },
+
+ // added v2.0
+ fontStretch: { default: fn.NORMAL, isShorthand: true }, // font-stretch: { default: normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded }
+ fontWeight: { default: fn.NORMAL, isShorthand: true }, // font-stretch: { default: normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded }
+ lineHeight: { default: fn.NORMAL, isShorthand: true }, // line-height: { default: normal | number | length | percentage }
+ fontVariant: { default: fn.NORMAL, isShorthand: true }, // font-variant: { default: normal | small-caps }
+ fontStyle: { default: fn.NORMAL, isShorthand: true }, // font-style: { default: normal | italic | oblique } [was 'textStyle' in v1]
+
+ direction: { default: 'inherit' }, // direction: { default: inherit | ltr | rtl }
+ };
+
+ // note: font must be first here otherwise it may reset other properties
+ const ContextTextProps = ['font', 'direction', 'fontKerning', 'fontStretch', 'fontVariantCaps', 'letterSpacing', 'textAlign', 'textBaseline', 'textRendering', 'wordSpacing'];
+
+ // shorthand font properties that can be set with context2d.font
+ const ShorthandFontProps = Object.keys(RendererTextProps).filter(p => RendererTextProps[p].isShorthand);
+
+ // allowable values for font-stretch property for context2d.font
+ const FontStretchKeys = ["ultra-condensed", "extra-condensed", "condensed", "semi-condensed", "normal", "semi-expanded", "expanded", "extra-expanded", "ultra-expanded"];
+
+ let contextQueue, cachedDiv; // lazy
+
+ ////////////////////////////// start API ///////////////////////////////
+
+ Renderer.prototype.text = function (str, x, y, width, height) {
+
+ let setBaseline = this.textDrawingContext().textBaseline; // store baseline
+
+ // adjust {x,y,w,h} properties based on rectMode
+ ({ x, y, width, height } = this._handleRectMode(x, y, width, height));
+
+ // parse the lines according to width, height & linebreaks
+ let lines = this._processLines(str, width, height);
+
+ // add the adjusted positions [x,y] to each line
+ lines = this._positionLines(x, y, width, height, lines);
+
+ // render each line at the adjusted position
+ lines.forEach(line => this._renderText(line.text, line.x, line.y));
+
+ this.textDrawingContext().textBaseline = setBaseline; // restore baseline
+ };
+
+ /**
+ * Computes the precise (tight) bounding box for a block of text
+ * @param {string} str - the text to measure
+ * @param {number} x - the x-coordinate of the text
+ * @param {number} y - the y-coordinate of the text
+ * @param {number} width - the max width of the text block
+ * @param {number} height - the max height of the text block
+ * @returns - a bounding box object for the text block: {x,y,w,h}
+ */
+ Renderer.prototype.textBounds = function (str, x, y, width, height) {
+ //console.log('TEXT BOUNDS: ', str, x, y, width, height);
+ // delegate to _textBoundsSingle measure function
+ return this._computeBounds(fn._TEXT_BOUNDS, str, x, y, width, height).bounds;
+ };
+
+ /**
+ * Computes a generic (non-tight) bounding box for a block of text
+ * @param {string} str - the text to measure
+ * @param {number} x - the x-coordinate of the text
+ * @param {number} y - the y-coordinate of the text
+ * @param {number} width - the max width of the text block
+ * @param {number} height - the max height of the text block
+ * @returns - a bounding box object for the text block: {x,y,w,h}
+ */
+ Renderer.prototype.fontBounds = function (str, x, y, width, height) {
+ // delegate to _fontBoundsSingle measure function
+ return this._computeBounds(fn._FONT_BOUNDS, str, x, y, width, height).bounds;
+ };
+
+ /**
+ * Get the width of a text string in pixels (tight bounds)
+ * @param {string} theText
+ * @returns - the width of the text in pixels
+ */
+ Renderer.prototype.textWidth = function (theText) {
+ let lines = this._processLines(theText, null, null);
+ // return the max width of the lines (using tight bounds)
+ let widths = lines.map(l => this._textWidthSingle(l));
+ return Math.max(...widths);
+ };
+
+ /**
+ * Get the width of a text string in pixels (loose bounds)
+ * @param {string} theText
+ * @returns - the width of the text in pixels
+ */
+ Renderer.prototype.fontWidth = function (theText) {
+ // return the max width of the lines (using loose bounds)
+ let lines = this._processLines(theText, null, null);
+ let widths = lines.map(l => this._fontWidthSingle(l));
+ return Math.max(...widths);
+ };
+
+ /**
+ *
+ * @param {*} txt - optional text to measure, if provided will be
+ * used to compute the ascent, otherwise the font's ascent will be used
+ * @returns - the ascent of the text
+ */
+ Renderer.prototype.textAscent = function (txt = '') {
+ if (!txt.length) return this.fontAscent();
+ return this.textDrawingContext().measureText(txt)[prop];
+ };
+
+ /**
+ * @returns - returns the ascent for the current font
+ */
+ Renderer.prototype.fontAscent = function () {
+ return this.textDrawingContext().measureText('_').fontBoundingBoxAscent;
+ };
+
+ /**
+ * @param {*} txt - optional text to measure, if provided will
+ * be used to compute the descent, otherwise the font's descent will be used
+ * @returns - the descent of the text
+ */
+ Renderer.prototype.textDescent = function (txt = '') {
+ if (!txt.length) return this.fontDescent();
+ return this.textDrawingContext().measureText(txt)[prop];
+ };
+
+ /**
+ * @returns - returns the descent for the current font
+ */
+ Renderer.prototype.fontDescent = function () {
+ return this.textDrawingContext().measureText('_').fontBoundingBoxDescent;
+ };
+
+ // setters/getters for text properties //////////////////////////
+
+ Renderer.prototype.textAlign = function (h, v) {
+
+ // the setter
+ if (typeof h !== 'undefined') {
+ this.states.textAlign = h;
+ if (typeof v !== 'undefined') {
+ if (v === fn.CENTER) {
+ v = fn._CTX_MIDDLE;
+ }
+ this.states.textBaseline = v;
+ }
+ return this._applyTextProperties();
+ }
+ // the getter
+ return {
+ horizontal: this.states.textAlign,
+ vertical: this.states.textBaseline
+ };
+ };
+
+ /**
+ * Set the font and [size] and [options] for rendering text
+ * @param {p5.Font | string} font - the font to use for rendering text
+ * @param {number} size - the size of the text, can be a number or a css-style string
+ * @param {object} options - additional options for rendering text, see FontProps
+ */
+ Renderer.prototype.textFont = function (font, size, options) {
+
+ if (arguments.length === 0) {
+ return this.states.textFont;
+ }
+
+ let family = font;
+
+ // do we have a custon loaded font ?
+ if (font instanceof p5.Font) {
+ family = font.face.family;
+ }
+ else if (font.data instanceof Uint8Array) {
+ family = font.name.fontFamily;
+ if (font.name?.fontSubfamily) {
+ family += '-' + font.name.fontSubfamily;
+ }
+ }
+
+ if (typeof family !== 'string') {
+ throw Error('null font passed to textFont', font);
+ }
+
+ // handle two-arg case: textFont(font, options)
+ if (arguments.length === 2 && typeof size === 'object') {
+ options = size;
+ size = undefined;
+ }
+
+ // check for font-string with size in first arg
+ if (typeof size === 'undefined' && /[.0-9]+(%|em|p[xt])/.test(family)) {
+ ({ family, size } = this._directSetFontString(family));
+ }
+
+ // update font properties in this.states
+ this.states.textFont = { font, family, size };
+
+ // convert/update the size in this.states
+ if (typeof size !== 'undefined') {
+ this._setTextSize(size);
+ }
+
+ // apply any options to this.states
+ if (typeof options === 'object') {
+ this.textProperties(options);
+ }
+
+ this._applyTextProperties();
+ //console.log('ctx.font="' + this.textDrawingContext().font + '"');
+ return this._pInst;
+ }
+
+ Renderer.prototype._directSetFontString = function (font, debug = 0) {
+ if (debug) console.log('_directSetFontString"' + font + '"');
+ let defaults = ShorthandFontProps.reduce((props, p) => {
+ props[p] = RendererTextProps[p].default;
+ return props;
+ }, {});
+ let el = this._cachedDiv(defaults);
+ el.style.font = font;
+ let style = getComputedStyle(el);
+ ShorthandFontProps.forEach(prop => {
+ this.states[prop] = style[prop];
+ if (debug) console.log(' this.states.' + prop + '="' + style[prop] + '"');
+ });
+ if (debug) console.log(' this.states.textFont="' + style.fontFamily + '"');
+ if (debug) console.log(' this.states.textSize="' + style.fontSize + '"');
+ return { family: style.fontFamily, size: style.fontSize };
+ }
+
+ Renderer.prototype.textLeading = function (leading) {
+ // the setter
+ if (typeof leading === 'number') {
+ this.states.leadingSet = true;
+ this.states.textLeading = leading;
+ return this._applyTextProperties();
+ }
+ // the getter
+ return this.states.textLeading;
+ }
+
+ Renderer.prototype.textWeight = function (weight) {
+ // the setter
+ if (typeof weight === 'number') {
+ this.states.fontWeight = weight;
+ return this._applyTextProperties();
+ }
+ // the getter
+ return this.states.fontWeight;
+ }
+
+ /**
+ * @param {*} size - the size of the text, can be a number or a css-style string
+ */
+ Renderer.prototype.textSize = function (size) {
+
+ // the setter
+ if (typeof size !== 'undefined') {
+ this._setTextSize(size);
+ return this._applyTextProperties();
+ }
+ // the getter
+ return this.states.textSize;
+ }
+
+ Renderer.prototype.textStyle = function (style) {
+
+ // the setter
+ if (typeof style !== 'undefined') {
+ this.states.fontStyle = style;
+ return this._applyTextProperties();
+ }
+ // the getter
+ return this.states.fontStyle;
+ }
+
+ Renderer.prototype.textWrap = function (wrapStyle) {
+
+ if (wrapStyle === fn.WORD || wrapStyle === fn.CHAR) {
+ this.states.textWrap = wrapStyle;
+ // no need to apply text properties here as not a context property
+ return this._pInst;
+ }
+ return this.states.textWrap;
+ };
+
+ Renderer.prototype.textDirection = function (direction) {
+
+ if (typeof direction !== 'undefined') {
+ this.states.direction = direction;
+ return this._applyTextProperties();
+ }
+ return this.states.direction;
+ };
+
+ /**
+ * Sets/gets a single text property for the renderer (eg. fontStyle, fontStretch, etc.)
+ * The property to be set can be a mapped or unmapped property on `this.states` or a property
+ * on `this.textDrawingContext()` or on `this.canvas.style`
+ * The property to get can exist in `this.states` or `this.textDrawingContext()` or `this.canvas.style`
+ */
+ Renderer.prototype.textProperty = function (prop, value, opts) {
+
+ let modified = false, debug = opts?.debug || false;
+
+ // getter: return option from this.states or this.textDrawingContext()
+ if (typeof value === 'undefined') {
+ let props = this.textProperties();
+ if (prop in props) return props[prop];
+ throw Error('Unknown text option "' + prop + '"'); // FES?
+ }
+
+ // set the option in this.states if it exists
+ if (prop in this.states && this.states[prop] !== value) {
+ this.states[prop] = value;
+ modified = true;
+ if (debug) {
+ console.log('this.states.' + prop + '="' + options[prop] + '"');
+ }
+ }
+ // does it exist in CanvasRenderingContext2D ?
+ else if (prop in this.textDrawingContext()) {
+ this._setContextProperty(prop, value, debug);
+ modified = true;
+ }
+ // does it exist in the canvas.style ?
+ else if (prop in this.canvas.style) {
+ this._setCanvasStyleProperty(prop, value, debug);
+ modified = true;
+ }
+ else {
+ console.warn('Ignoring unknown text option: "' + prop + '"\n'); // FES?
+ }
+
+ return modified ? this._applyTextProperties() : this._pInst;
+ };
+
+ /**
+ * Batch set/get text properties for the renderer.
+ * The properties can be either on `states` or `drawingContext`
+ */
+ Renderer.prototype.textProperties = function (properties) {
+
+ // setter
+ if (typeof properties !== 'undefined') {
+ Object.keys(properties).forEach(opt => {
+ this.textProperty(opt, properties[opt]);
+ });
+ return this._pInst;
+ }
+
+ // getter: get props from this.textDrawingContext()
+ properties = ContextTextProps.reduce((props, p) => {
+ props[p] = this.textDrawingContext()[p];
+ return props;
+ }, {});
+
+ // add renderer.states props
+ Object.keys(RendererTextProps).forEach(p => {
+ properties[p] = this.states[p];
+ if (RendererTextProps[p]?.type === 'Context2d') {
+ properties[p] = this.textDrawingContext()[p];
+ }
+ });
+
+ return properties;
+ };
+
+ Renderer.prototype.textMode = function () { /* no-op for processing api */ };
+
+ /////////////////////////////// end API ////////////////////////////////
+
+
+ /*
+ Compute the bounds for a block of text based on the specified
+ measure function, either _textBoundsSingle or _fontBoundsSingle
+ */
+ Renderer.prototype._computeBounds = function (type, str, x, y, width, height, opts) {
+
+ let setBaseline = this.textDrawingContext().textBaseline;
+ let { textLeading, textAlign } = this.states;
+
+ // adjust width, height based on current rectMode
+ ({ width, height } = this._rectModeAdjust(x, y, width, height));
+
+ // parse the lines according to the width & linebreaks
+ let lines = this._processLines(str, width, height);
+
+ // get the adjusted positions [x,y] for each line
+ let boxes = lines.map((line, i) => this[type].bind(this)
+ (line, x, y + i * textLeading));
+
+ // adjust the bounding boxes based on horiz. text alignment
+ if (lines.length > 1) {
+ boxes.forEach(bb => bb.x += this._xAlignOffset(textAlign, width));
+ }
+
+ // adjust the bounding boxes based on vert. text alignment
+ if (typeof height !== 'undefined') {
+ this._yAlignOffset(boxes, height);
+ }
+
+ // get the bounds for the text block
+ let bounds = boxes[0];
+ if (lines.length > 1) {
+
+ // get the bounds for the multi-line text block
+ bounds = this._aggregateBounds(boxes);
+
+ // align the multi-line bounds
+ if (!opts?.ignoreRectMode) {
+ this._rectModeAlign(bounds, width || 0, height || 0);
+ }
+ }
+
+ if (0 && opts?.ignoreRectMode) boxes.forEach((b, i) => { // draw bounds for debugging
+ let ss = this.textDrawingContext().strokeStyle;
+ this.textDrawingContext().strokeStyle = 'green';
+ this.textDrawingContext().strokeRect(bounds.x, bounds.y, bounds.w, bounds.h);
+ this.textDrawingContext().strokeStyle = ss;
+ });
+
+ this.textDrawingContext().textBaseline = setBaseline; // restore baseline
+
+ return { bounds, lines };
+ };
+
+ /*
+ Adjust width, height of bounds based on current rectMode
+ */
+ Renderer.prototype._rectModeAdjust = function (x, y, width, height) {
+
+ if (typeof width !== 'undefined') {
+ switch (this.states.rectMode) {
+ case fn.CENTER:
+ break;
+ case fn.CORNERS:
+ width -= x;
+ height -= y;
+ break;
+ case fn.RADIUS:
+ width *= 2;
+ height *= 2;
+ break;
+ }
+ }
+ return { x, y, width, height };
+ }
+
+ /*
+ Attempts to set a property directly on the canvas.style object
+ */
+ Renderer.prototype._setCanvasStyleProperty = function (opt, val, debug) {
+
+ let value = val.toString(); // ensure its a string
+
+ if (debug) console.log('canvas.style.' + opt + '="' + value + '"');
+
+ // handle variable fonts options
+ if (opt === FontVariationSettings) {
+ this._handleFontVariationSettings(value);
+ }
+
+ // lets try to set it on the canvas style
+ this.canvas.style[opt] = value;
+
+ // check if the value was set successfully
+ if (this.canvas.style[opt] !== value) {
+
+ // fails on precision for floating points, also quotes and spaces
+
+ if (0) console.warn(`Unable to set '${opt}' property` // FES?
+ + ' on canvas.style. It may not be supported. Expected "'
+ + value + '" but got: "' + this.canvas.style[opt] + "'");
+ }
+ };
+
+ /*
+ Parses the fontVariationSettings string and sets the font properties, only font-weight
+ working consistently across browsers at present
+ */
+ Renderer.prototype._handleFontVariationSettings = function (value, debug = false) {
+ // check if the value is a string or an object
+ if (typeof value === 'object') {
+ value = Object.keys(value).map(k => k + ' ' + value[k]).join(', ');
+ }
+ let values = value.split(CommaDelimRe);
+ values.forEach(v => {
+ v = v.replace(/["']/g, ''); // remove quotes
+ let matches = VariableAxesRe.exec(v);
+ //console.log('matches: ', matches);
+ if (matches && matches.length) {
+ let axis = matches[0];
+ // get the value to 3 digits of precision with no trailing zeros
+ let val = parseFloat(parseFloat(v.replace(axis, '').trim()).toFixed(3));
+ switch (axis) {
+ case 'wght':
+ if (debug) console.log('setting font-weight=' + val);
+ // manually set the font-weight via the font string
+ this.textWeight(val);
+ return val;
+ case 'wdth':
+ if (0) { // attempt to map font-stretch to allowed keywords
+ const FontStretchMap = {
+ "ultra-condensed": 50,
+ "extra-condensed": 62.5,
+ "condensed": 75,
+ "semi-condensed": 87.5,
+ "normal": 100,
+ "semi-expanded": 112.5,
+ "expanded": 125,
+ "extra-expanded": 150,
+ "ultra-expanded": 200,
+ };
+ let values = Object.values(FontStretchMap);
+ const indexArr = values.map(function (k) { return Math.abs(k - val) })
+ const min = Math.min.apply(Math, indexArr)
+ let idx = indexArr.indexOf(min);
+ let stretch = Object.keys(FontStretchMap)[idx];
+ this.states.fontStretch = stretch;
+ }
+ break;
+ case 'ital':
+ if (debug) console.log('setting font-style=' + (val ? 'italic' : 'normal'));
+ break;
+ case 'slnt':
+ if (debug) console.log('setting font-style=' + (val ? 'oblique' : 'normal'));
+ break;
+ case 'opsz':
+ if (debug) console.log('setting font-optical-size=' + val);
+ break;
+ }
+ }
+ });
+ };
+
+
+
+
+ /*
+ For properties not directly managed by the renderer in this.states
+ we check if it has a mapping to a property in this.states
+ Otherwise, add the property to the context-queue for later application
+ */
+ Renderer.prototype._setContextProperty = function (prop, val, debug = false) {
+
+ // check if the value is actually different, else short-circuit
+ if (this.textDrawingContext()[prop] === val) {
+ return this._pInst;
+ }
+
+ // otherwise, we will set the property directly on the `this.textDrawingContext()`
+ // by adding [property, value] to context-queue for later application
+ (contextQueue ??= []).push([prop, val]);
+
+ if (debug) console.log('queued context2d.' + prop + '="' + val + '"');
+ };
+
+ /*
+ Adjust parameters (x,y,w,h) based on current rectMode
+ */
+ Renderer.prototype._handleRectMode = function (x, y, width, height) {
+
+ let rectMode = this.states.rectMode;
+
+ if (typeof width !== 'undefined') {
+ switch (rectMode) {
+ case fn.RADIUS:
+ width *= 2;
+ x -= width / 2;
+ if (typeof height !== 'undefined') {
+ height *= 2;
+ y -= height / 2;
+ }
+ break;
+ case fn.CENTER:
+ x -= width / 2;
+ if (typeof height !== 'undefined') {
+ y -= height / 2;
+ }
+ break;
+ case fn.CORNERS:
+ width -= x;
+ if (typeof height !== 'undefined') {
+ height -= y;
+ }
+ break;
+ }
+ }
+ return { x, y, width, height };
+ };
+
+ /*
+ Get the computed font-size in pixels for a given size string
+ @param {string} size - the font-size string to compute
+ @returns {number} - the computed font-size in pixels
+ */
+ Renderer.prototype._fontSizePx = function (theSize, { family } = this.states.textFont) {
+
+ const isNumString = (num) => !isNaN(num) && num.trim() !== '';
+
+ // check for a number in a string, eg '12'
+ if (isNumString(theSize)) {
+ return parseFloat(theSize);
+ }
+ let ele = this._cachedDiv({ fontSize: theSize });
+ ele.style.fontSize = theSize;
+ ele.style.fontFamily = family;
+ let fontSizeStr = getComputedStyle(ele).fontSize;
+ let fontSize = parseFloat(fontSizeStr);
+ if (typeof fontSize !== 'number') {
+ throw Error('textSize: invalid font-size');
+ }
+ return fontSize;
+ };
+
+ Renderer.prototype._cachedDiv = function (props) {
+ if (typeof cachedDiv === 'undefined') {
+ let ele = document.createElement('div');
+ ele.ariaHidden = 'true';
+ ele.style.display = 'none';
+ Object.entries(props).forEach(([prop, val]) => {
+ ele.style[prop] = val;
+ });
+ this.canvas.appendChild(ele);
+ cachedDiv = ele;
+ }
+ return cachedDiv;
+ }
+
+
+ /*
+ Aggregate the bounding boxes of multiple lines of text
+ @param {array} bboxes - the bounding boxes to aggregate
+ @returns {object} - the aggregated bounding box
+ */
+ Renderer.prototype._aggregateBounds = function (bboxes) {
+ // loop over the bounding boxes to get the min/max x/y values
+ let minX = Math.min(...bboxes.map(b => b.x));
+ let minY = Math.min(...bboxes.map(b => b.y));
+ let maxY = Math.max(...bboxes.map(b => b.y + b.h));
+ let maxX = Math.max(...bboxes.map(b => b.x + b.w));
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
+ };
+
+ // Renderer.prototype._aggregateBounds = function (tx, ty, bboxes) {
+ // let x = Math.min(...bboxes.map(b => b.x));
+ // let y = Math.min(...bboxes.map(b => b.y));
+ // // the width is the max of the x-offset + the box width
+ // let w = Math.max(...bboxes.map(b => (b.x - tx) + b.w));
+ // let h = bboxes[bboxes.length - 1].y - bboxes[0].y + bboxes[bboxes.length - 1].h;
+
+
+ // return { x, y, w, h };
+ // };
+
+ /*
+ Process the text string to handle line-breaks and text wrapping
+ @param {string} str - the text to process
+ @param {number} width - the width to wrap the text to
+ @returns {array} - the processed lines of text
+ */
+ Renderer.prototype._processLines = function (str, width, height) {
+
+ if (typeof width !== 'undefined') { // only for text with bounds
+ if (this.textDrawingContext().textBaseline === fn.BASELINE) {
+ this.textDrawingContext().textBaseline = fn.TOP;
+ }
+ }
+
+ let lines = this._splitOnBreaks(str.toString());
+ let hasLineBreaks = lines.length > 1;
+ let hasWidth = typeof width !== 'undefined';
+ let exceedsWidth = hasWidth && lines.some(l => this._textWidthSingle(l) > width);
+ let { textLeading: leading, textWrap } = this.states;
+
+ //if (!hasLineBreaks && !exceedsWidth) return lines; // a single-line
+ if (hasLineBreaks || exceedsWidth) {
+ if (hasWidth) lines = this._lineate(textWrap, lines, width);
+ }
+
+ // handle height truncation
+ if (hasWidth && typeof height !== 'undefined') {
+
+ if (typeof leading === 'undefined') {
+ throw Error('leading is required if height is specified');
+ }
+
+ // truncate lines that exceed the height
+ for (let i = 0; i < lines.length; i++) {
+ let lh = leading * (i + 1);
+ if (lh > height) {
+ //console.log('TRUNCATING: ', i, '-', lines.length, '"' + lines.slice(i) + '"');
+ lines = lines.slice(0, i);
+ break;
+ }
+ }
+ }
+
+ return lines;
+ };
+
+ /*
+ Get the x-offset for text given the width and textAlign property
+ */
+ Renderer.prototype._xAlignOffset = function (textAlign, width) {
+ switch (textAlign) {
+ case fn.LEFT:
+ return 0;
+ case fn.CENTER:
+ return width / 2;
+ case fn.RIGHT:
+ return width;
+ case fn.START:
+ return 0;
+ case fn.END:
+ throw new Error('textBounds: END not yet supported for textAlign');
+ default:
+ return 0;
+ }
+ }
+
+ /*
+ Align the bounding box based on the current rectMode setting
+ */
+ Renderer.prototype._rectModeAlign = function (bb, width, height) {
+ if (typeof width !== 'undefined') {
+
+ switch (this.states.rectMode) {
+ case fn.CENTER:
+ bb.x -= (width - bb.w) / 2;
+ bb.y -= (height - bb.h) / 2;
+ break;
+ case fn.CORNERS:
+ bb.w += bb.x;
+ bb.h += bb.y;
+ break;
+ case fn.RADIUS:
+ bb.x -= (width - bb.w) / 2;
+ bb.y -= (height - bb.h) / 2;
+ bb.w /= 2;
+ bb.h /= 2;
+ break;
+ }
+ return bb;
+ }
+ }
+
+ Renderer.prototype._rectModeAlignRevert = function (bb, width, height) {
+ if (typeof width !== 'undefined') {
+
+ switch (this.states.rectMode) {
+ case fn.CENTER:
+ bb.x += (width - bb.w) / 2;
+ bb.y += (height - bb.h) / 2;
+ break;
+ case fn.CORNERS:
+ bb.w -= bb.x;
+ bb.h -= bb.y;
+ break;
+ case fn.RADIUS:
+ bb.x += (width - bb.w) / 2;
+ bb.y += (height - bb.h) / 2;
+ bb.w *= 2;
+ bb.h *= 2;
+ break;
+ }
+ return bb;
+ }
+ }
+
+ /*
+ Get the (tight) width of a single line of text
+ */
+ Renderer.prototype._textWidthSingle = function (s) {
+ let metrics = this.textDrawingContext().measureText(s);
+ let abl = metrics.actualBoundingBoxLeft;
+ let abr = metrics.actualBoundingBoxRight;
+ return abr + abl;
+ };
+
+ /*
+ Get the (loose) width of a single line of text as specified by the font
+ */
+ Renderer.prototype._fontWidthSingle = function (s) {
+ return this.textDrawingContext().measureText(s).width;
+ };
+
+ /*
+ Get the (tight) bounds of a single line of text based on its actual bounding box
+ */
+ Renderer.prototype._textBoundsSingle = function (s, x = 0, y = 0) {
+
+ let metrics = this.textDrawingContext().measureText(s);
+ let asc = metrics.actualBoundingBoxAscent;
+ let desc = metrics.actualBoundingBoxDescent;
+ let abl = metrics.actualBoundingBoxLeft;
+ let abr = metrics.actualBoundingBoxRight;
+ return { x: x - abl, y: y - asc, w: abr + abl, h: asc + desc };
+ };
+
+ /*
+ Get the (loose) bounds of a single line of text based on its font's bounding box
+ */
+ Renderer.prototype._fontBoundsSingle = function (s, x = 0, y = 0) {
+
+ let metrics = this.textDrawingContext().measureText(s);
+ let asc = metrics.fontBoundingBoxAscent;
+ let desc = metrics.fontBoundingBoxDescent;
+ x -= this._xAlignOffset(this.states.textAlign, metrics.width);
+ return { x, y: y - asc, w: metrics.width, h: asc + desc };;
+ };
+
+ /*
+ Set the textSize property in `this.states` if it has changed
+ @param {number | string} theSize - the font-size to set
+ @returns {boolean} - true if the size was changed, false otherwise
+ */
+ Renderer.prototype._setTextSize = function (theSize) {
+
+ if (typeof theSize === 'string') {
+ // parse the size string via computed style, eg '2em'
+ theSize = this._fontSizePx(theSize);
+ }
+
+ // should be a number now
+ if (typeof theSize === 'number') {
+
+ // set it in `this.states` if its been changed
+ if (this.states.textSize !== theSize) {
+ this.states.textSize = theSize;
+
+ // handle leading here, if not set otherwise
+ if (!this.states.leadingSet) {
+ this.states.textLeading = this.states.textSize * LeadingScale;
+ }
+ return true; // size was changed
+ }
+ }
+ else {
+ console.warn('textSize: invalid size: ' + theSize);
+ }
+
+ return false;
+ };
+
+ /*
+ Split the lines of text based on the width and the textWrap property
+ @param {array} lines - the lines of text to split
+ @param {number} maxWidth - the maximum width of the lines
+ @param {object} opts - additional options for splitting the lines
+ @returns {array} - the split lines of text
+ */
+ Renderer.prototype._lineate = function (textWrap, lines, maxWidth = Infinity, opts = {}) {
+
+ let splitter = opts.splitChar ?? (textWrap === fn.WORD ? ' ' : '');
+ let line, testLine, testWidth, words, newLines = [];
+
+ for (let lidx = 0; lidx < lines.length; lidx++) {
+ line = '';
+ words = lines[lidx].split(splitter);
+ for (let widx = 0; widx < words.length; widx++) {
+ testLine = `${line + words[widx]}` + splitter;
+ testWidth = this._textWidthSingle(testLine);
+ if (line.length > 0 && testWidth > maxWidth) {
+ newLines.push(line.trim());
+ line = `${words[widx]}` + splitter;
+ } else {
+ line = testLine;
+ }
+ }
+ newLines.push(line.trim());
+ }
+ return newLines;
+ };
+
+ /*
+ Split the text into lines based on line-breaks and tabs
+ */
+ Renderer.prototype._splitOnBreaks = function (s) {
+ if (!s || s.length === 0) return [''];
+ return s.replace(TabsRe, ' ').split(LinebreakRe);
+ };
+
+ /*
+ Parse the font-family string to handle complex names, fallbacks, etc.
+ */
+ Renderer.prototype._parseFontFamily = function (familyStr) {
+
+ let parts = familyStr.split(CommaDelimRe);
+ let family = parts.map(part => {
+ part = part.trim();
+ if (part.indexOf(' ') > -1 && !QuotedRe.test(part)) {
+ part = `"${part}"`; // quote font names with spaces
+ }
+ return part;
+ }).join(', ');
+
+ return family;
+ };
+
+ Renderer.prototype._applyFontString = function () {
+ /*
+ Create the font-string according to the CSS font-string specification:
+ If font is specified as a shorthand for several font-related properties, then:
+ - it must include values for: and
+ - it may optionally include values for:
+ [, , , , ]
+ Format:
+ - font-style, font-variant and font-weight must precede font-size
+ - font-variant may only specify the values defined in CSS 2.1, that is 'normal' and 'small-caps'.
+ - font-stretch may only be a single keyword value.
+ - line-height must immediately follow font-size, preceded by "/", eg 16px/3.
+ - font-family must be the last value specified.
+ */
+ let { textFont, textSize, lineHeight, fontStyle, fontWeight, fontVariant } = this.states;
+ let family = this._parseFontFamily(textFont.family);
+ let style = fontStyle !== fn.NORMAL ? `${fontStyle} ` : '';
+ let weight = fontWeight !== fn.NORMAL ? `${fontWeight} ` : '';
+ let variant = fontVariant !== fn.NORMAL ? `${fontVariant} ` : '';
+ let fsize = `${textSize}px` + (lineHeight !== fn.NORMAL ? `/${lineHeight} ` : ' ');
+ let fontString = `${style}${variant}${weight}${fsize}${family}`.trim();
+ //console.log('fontString="' + fontString + '"');
+
+ // set the font string on the context
+ this.textDrawingContext().font = fontString;
+
+ // verify that it was set successfully
+ if (this.textDrawingContext().font !== fontString) {
+ let expected = fontString;
+ let actual = this.textDrawingContext().font;
+ if (expected !== actual) {
+ //console.warn(`Unable to set font property on context2d. It may not be supported.`);
+ //console.log('Expected "' + expected + '" but got: "' + actual + '"'); // TMP
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /*
+ Apply the text properties in `this.states` to the `this.textDrawingContext()`
+ Then apply any properties in the context-queue
+ */
+ Renderer.prototype._applyTextProperties = function (debug = false) {
+
+ this._applyFontString();
+
+ // set these after the font so they're not overridden
+ this.textDrawingContext().direction = this.states.direction;
+ this.textDrawingContext().textAlign = this.states.textAlign;
+ this.textDrawingContext().textBaseline = this.states.textBaseline;
+
+ // set manually as (still) not fully supported as part of font-string
+ let stretch = this.states.fontStretch;
+ if (FontStretchKeys.includes(stretch) && this.textDrawingContext().fontStretch !== stretch) {
+ this.textDrawingContext().fontStretch = stretch;
+ }
+
+ // apply each property in queue after the font so they're not overridden
+ while (contextQueue?.length) {
+
+ let [prop, val] = contextQueue.shift();
+ if (debug) console.log('apply context property "' + prop + '" = "' + val + '"');
+ this.textDrawingContext()[prop] = val;
+
+ // check if the value was set successfully
+ if (this.textDrawingContext()[prop] !== val) {
+ console.warn(`Unable to set '${prop}' property on context2d. It may not be supported.`); // FES?
+ console.log('Expected "' + val + '" but got: "' + this.textDrawingContext()[prop] + '"');
+ }
+ }
+
+ return this._pInst;
+ };
+
+ if (p5.Renderer2D) {
+ p5.Renderer2D.prototype.textDrawingContext = function() {
+ return this.drawingContext;
+ };
+ p5.Renderer2D.prototype._renderText = function (text, x, y, maxY, minY) {
+ let states = this.states;
+
+ if (y < minY || y >= maxY) {
+ return; // don't render lines beyond minY/maxY
+ }
+
+ this.push();
+
+ // no stroke unless specified by user
+ if (states.strokeColor && states.strokeSet) {
+ this.textDrawingContext().strokeText(text, x, y);
+ }
+
+ if (!this._clipping && states.fillColor) {
+
+ // if fill hasn't been set by user, use default text fill
+ if (!states.fillSet) {
+ this._setFill(DefaultFill);
+ }
+
+ //console.log(`fillText(${x},${y},'${text}') font='${this.textDrawingContext().font}'`);
+ this.textDrawingContext().fillText(text, x, y);
+ }
+
+ this.pop();
+ };
+
+ /*
+ Position the lines of text based on their textAlign/textBaseline properties
+ */
+ p5.Renderer2D.prototype._positionLines = function (x, y, width, height, lines) {
+
+ let { textLeading, textAlign } = this.states;
+ let adjustedX, lineData = new Array(lines.length);
+ let adjustedW = typeof width === 'undefined' ? 0 : width;
+ let adjustedH = typeof height === 'undefined' ? 0 : height;
+
+ for (let i = 0; i < lines.length; i++) {
+ switch (textAlign) {
+ case fn.START:
+ throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT
+ case fn.LEFT:
+ adjustedX = x;
+ break;
+ case fn.CENTER:
+ adjustedX = x + adjustedW / 2;
+ break;
+ case fn.RIGHT:
+ adjustedX = x + adjustedW;
+ break;
+ case fn.END: // TODO: add fn.END:
+ throw new Error('textBounds: END not yet supported for textAlign');
+ }
+ lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading };
+ }
+
+ return this._yAlignOffset(lineData, adjustedH);
+ };
+
+ /*
+ Get the y-offset for text given the height, leading, line-count and textBaseline property
+ */
+ p5.Renderer2D.prototype._yAlignOffset = function (dataArr, height) {
+
+ if (typeof height === 'undefined') {
+ throw Error('_yAlignOffset: height is required');
+ }
+
+ let { textLeading, textBaseline } = this.states;
+ let yOff = 0, numLines = dataArr.length;
+ let ydiff = height - (textLeading * (numLines - 1));
+ switch (textBaseline) { // drawingContext ?
+ case fn.TOP:
+ break; // ??
+ case fn.BASELINE:
+ break;
+ case fn._CTX_MIDDLE:
+ yOff = ydiff / 2;
+ break;
+ case fn.BOTTOM:
+ yOff = ydiff;
+ break;
+ case fn.IDEOGRAPHIC:
+ console.warn('textBounds: IDEOGRAPHIC not yet supported for textBaseline'); // FES?
+ break;
+ case fn.HANGING:
+ console.warn('textBounds: HANGING not yet supported for textBaseline'); // FES?
+ break;
+ }
+ dataArr.forEach(ele => ele.y += yOff);
+ return dataArr;
+ }
+ }
+ if (p5.RendererGL) {
+ p5.RendererGL.prototype.textDrawingContext = function() {
+ if (!this._textDrawingContext) {
+ this._textCanvas = document.createElement('canvas');
+ this._textCanvas.width = 1;
+ this._textCanvas.height = 1;
+ this._textDrawingContext = this._textCanvas.getContext('2d');
+ }
+ return this._textDrawingContext;
+ };
+
+ p5.RendererGL.prototype._positionLines = function (x, y, width, height, lines) {
+
+ let { textLeading, textAlign } = this.states;
+ const widths = lines.map((line) => this._fontWidthSingle(line));
+ let adjustedX, lineData = new Array(lines.length);
+ let adjustedW = typeof width === 'undefined' ? Math.max(0, ...widths) : width;
+ let adjustedH = typeof height === 'undefined' ? 0 : height;
+
+ for (let i = 0; i < lines.length; i++) {
+ switch (textAlign) {
+ case fn.START:
+ throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT
+ case fn.LEFT:
+ adjustedX = x;
+ break;
+ case fn._CTX_MIDDLE:
+ adjustedX = x + (adjustedW - widths[i]) / 2;
+ break;
+ case fn.RIGHT:
+ adjustedX = x + adjustedW - widths[i];
+ break;
+ case fn.END: // TODO: add fn.END:
+ throw new Error('textBounds: END not yet supported for textAlign');
+ }
+ lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading };
+ }
+
+ return this._yAlignOffset(lineData, adjustedH);
+ };
+
+ p5.RendererGL.prototype._yAlignOffset = function (dataArr, height) {
+
+ if (typeof height === 'undefined') {
+ throw Error('_yAlignOffset: height is required');
+ }
+
+ let { textLeading, textBaseline, textSize, textFont } = this.states;
+ let yOff = 0, numLines = dataArr.length;
+ let totalHeight = textSize * numLines + ((textLeading - textSize) * (numLines - 1));
+ switch (textBaseline) { // drawingContext ?
+ case fn.TOP:
+ yOff = textSize;
+ break;
+ case fn.BASELINE:
+ break;
+ case fn._CTX_MIDDLE:
+ yOff = -totalHeight / 2 + textSize;
+ break;
+ case fn.BOTTOM:
+ yOff = -(totalHeight - textSize);
+ break;
+ default:
+ console.warn(`${textBaseline} is not supported in WebGL mode.`); // FES?
+ break;
+ }
+ yOff += this.states.textFont.font?.verticalAlign(textSize) || 0;
+ dataArr.forEach(ele => ele.y += yOff);
+ return dataArr;
+ }
+ }
+}
+
+export default text2d;
+
+if (typeof p5 !== 'undefined') {
+ text2d(p5, p5.prototype);
+}
diff --git a/src/typography/attributes.js b/src/typography/attributes.js
deleted file mode 100644
index 886a682dc7..0000000000
--- a/src/typography/attributes.js
+++ /dev/null
@@ -1,547 +0,0 @@
-/**
- * @module Typography
- * @submodule Attributes
- * @for p5
- * @requires core
- * @requires constants
- */
-
-import p5 from '../core/main';
-
-/**
- * Sets the way text is aligned when text() is called.
- *
- * By default, calling `text('hi', 10, 20)` places the bottom-left corner of
- * the text's bounding box at (10, 20).
- *
- * The first parameter, `horizAlign`, changes the way
- * text() interprets x-coordinates. By default, the
- * x-coordinate sets the left edge of the bounding box. `textAlign()` accepts
- * the following values for `horizAlign`: `LEFT`, `CENTER`, or `RIGHT`.
- *
- * The second parameter, `vertAlign`, is optional. It changes the way
- * text() interprets y-coordinates. By default, the
- * y-coordinate sets the bottom edge of the bounding box. `textAlign()`
- * accepts the following values for `vertAlign`: `TOP`, `BOTTOM`, `CENTER`,
- * or `BASELINE`.
- *
- * @method textAlign
- * @param {(LEFT|CENTER|RIGHT)} horizAlign horizontal alignment, either LEFT,
- * CENTER, or RIGHT.
- * @param {(TOP|BOTTOM|BASELINE|CENTER)} [vertAlign] vertical alignment, either TOP,
- * BOTTOM, CENTER, or BASELINE.
- * @chainable
- * @example
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Draw a vertical line.
- * strokeWeight(0.5);
- * line(50, 0, 50, 100);
- *
- * // Top line.
- * textSize(16);
- * textAlign(RIGHT);
- * text('ABCD', 50, 30);
- *
- * // Middle line.
- * textAlign(CENTER);
- * text('EFGH', 50, 50);
- *
- * // Bottom line.
- * textAlign(LEFT);
- * text('IJKL', 50, 70);
- *
- * describe('The letters ABCD displayed at top-left, EFGH at center, and IJKL at bottom-right. A vertical line divides the canvas in half.');
- * }
- *
- *
- *
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * strokeWeight(0.5);
- *
- * // First line.
- * line(0, 12, width, 12);
- * textAlign(CENTER, TOP);
- * text('TOP', 50, 12);
- *
- * // Second line.
- * line(0, 37, width, 37);
- * textAlign(CENTER, CENTER);
- * text('CENTER', 50, 37);
- *
- * // Third line.
- * line(0, 62, width, 62);
- * textAlign(CENTER, BASELINE);
- * text('BASELINE', 50, 62);
- *
- * // Fourth line.
- * line(0, 97, width, 97);
- * textAlign(CENTER, BOTTOM);
- * text('BOTTOM', 50, 97);
- *
- * describe('The words "TOP", "CENTER", "BASELINE", and "BOTTOM" each drawn relative to a horizontal line. Their positions demonstrate different vertical alignments.');
- * }
- *
- *
- */
-/**
- * @method textAlign
- * @return {Object}
- */
-p5.prototype.textAlign = function(horizAlign, vertAlign) {
- p5._validateParameters('textAlign', arguments);
- return this._renderer.textAlign(...arguments);
-};
-
-/**
- * Sets the spacing between lines of text when
- * text() is called.
- *
- * Note: Spacing is measured in pixels.
- *
- * Calling `textLeading()` without an argument returns the current spacing.
- *
- * @method textLeading
- * @param {Number} leading spacing between lines of text in units of pixels.
- * @chainable
- *
- * @example
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // "\n" starts a new line of text.
- * let lines = 'one\ntwo';
- *
- * // Left.
- * text(lines, 10, 25);
- *
- * // Right.
- * textLeading(30);
- * text(lines, 70, 25);
- *
- * describe('The words "one" and "two" written on separate lines twice. The words on the left have less vertical spacing than the words on the right.');
- * }
- *
- *
- */
-/**
- * @method textLeading
- * @return {Number}
- */
-p5.prototype.textLeading = function(theLeading) {
- p5._validateParameters('textLeading', arguments);
- return this._renderer.textLeading(...arguments);
-};
-
-/**
- * Sets the font size when
- * text() is called.
- *
- * Note: Font size is measured in pixels.
- *
- * Calling `textSize()` without an arugment returns the current size.
- *
- * @method textSize
- * @param {Number} size size of the letters in units of pixels.
- * @chainable
- *
- * @example
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Top.
- * textSize(12);
- * text('Font Size 12', 10, 30);
- *
- * // Middle.
- * textSize(14);
- * text('Font Size 14', 10, 60);
- *
- * // Bottom.
- * textSize(16);
- * text('Font Size 16', 10, 90);
- *
- * describe('The text "Font Size 12" drawn small, "Font Size 14" drawn medium, and "Font Size 16" drawn large.');
- * }
- *
- *
- */
-/**
- * @method textSize
- * @return {Number}
- */
-p5.prototype.textSize = function(theSize) {
- p5._validateParameters('textSize', arguments);
- return this._renderer.textSize(...arguments);
-};
-
-/**
- * Sets the style for system fonts when
- * text() is called.
- *
- * The parameter, `style`, can be either `NORMAL`, `ITALIC`, `BOLD`, or
- * `BOLDITALIC`.
- *
- * `textStyle()` may be overridden by CSS styling. This function doesn't
- * affect fonts loaded with loadFont().
- *
- * @method textStyle
- * @param {(NORMAL|ITALIC|BOLD|BOLDITALIC)} style styling for text, either NORMAL,
- * ITALIC, BOLD or BOLDITALIC.
- * @chainable
- * @example
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the text.
- * textSize(12);
- * textAlign(CENTER);
- *
- * // First row.
- * textStyle(NORMAL);
- * text('Normal', 50, 15);
- *
- * // Second row.
- * textStyle(ITALIC);
- * text('Italic', 50, 40);
- *
- * // Third row.
- * textStyle(BOLD);
- * text('Bold', 50, 65);
- *
- * // Fourth row.
- * textStyle(BOLDITALIC);
- * text('Bold Italic', 50, 90);
- *
- * describe('The words "Normal" displayed normally, "Italic" in italic, "Bold" in bold, and "Bold Italic" in bold italics.');
- * }
- *
- *
- */
-/**
- * @method textStyle
- * @return {String}
- */
-p5.prototype.textStyle = function(theStyle) {
- p5._validateParameters('textStyle', arguments);
- return this._renderer.textStyle(...arguments);
-};
-
-/**
- * Calculates the maximum width of a string of text drawn when
- * text() is called.
- *
- * @method textWidth
- * @param {String} str string of text to measure.
- * @return {Number} width measured in units of pixels.
- * @example
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the text.
- * textSize(28);
- * strokeWeight(0.5);
- *
- * // Calculate the text width.
- * let s = 'yoyo';
- * let w = textWidth(s);
- *
- * // Display the text.
- * text(s, 22, 55);
- *
- * // Underline the text.
- * line(22, 55, 22 + w, 55);
- *
- * describe('The word "yoyo" underlined.');
- * }
- *
- *
- *
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the text.
- * textSize(28);
- * strokeWeight(0.5);
- *
- * // Calculate the text width.
- * // "\n" starts a new line.
- * let s = 'yo\nyo';
- * let w = textWidth(s);
- *
- * // Display the text.
- * text(s, 22, 55);
- *
- * // Underline the text.
- * line(22, 55, 22 + w, 55);
- *
- * describe('The word "yo" written twice, one copy beneath the other. The words are divided by a horizontal line.');
- * }
- *
- *
- */
-p5.prototype.textWidth = function (...args) {
- args[0] += '';
- p5._validateParameters('textWidth', args);
- if (args[0].length === 0) {
- return 0;
- }
-
- // Only use the line with the longest width, and replace tabs with double-space
- const textLines = args[0].replace(/\t/g, ' ').split(/\r?\n|\r|\n/g);
-
- const newArr = [];
-
- // Return the textWidth for every line
- for(let i=0; i
- *
- * let font;
- *
- * function preload() {
- * font = loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the text.
- * textFont(font);
- *
- * // Different for each font.
- * let fontScale = 0.8;
- *
- * let baseY = 75;
- * strokeWeight(0.5);
- *
- * // Draw small text.
- * textSize(24);
- * text('dp', 0, baseY);
- *
- * // Draw baseline and ascent.
- * let a = textAscent() * fontScale;
- * line(0, baseY, 23, baseY);
- * line(23, baseY - a, 23, baseY);
- *
- * // Draw large text.
- * textSize(48);
- * text('dp', 45, baseY);
- *
- * // Draw baseline and ascent.
- * a = textAscent() * fontScale;
- * line(45, baseY, 91, baseY);
- * line(91, baseY - a, 91, baseY);
- *
- * describe('The letters "dp" written twice in different sizes. Each version has a horizontal baseline. A vertical line extends upward from each baseline to the top of the "d".');
- * }
- *
- *
- * let font;
- *
- * function preload() {
- * font = loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the font.
- * textFont(font);
- *
- * // Different for each font.
- * let fontScale = 0.9;
- *
- * let baseY = 75;
- * strokeWeight(0.5);
- *
- * // Draw small text.
- * textSize(24);
- * text('dp', 0, baseY);
- *
- * // Draw baseline and descent.
- * let d = textDescent() * fontScale;
- * line(0, baseY, 23, baseY);
- * line(23, baseY, 23, baseY + d);
- *
- * // Draw large text.
- * textSize(48);
- * text('dp', 45, baseY);
- *
- * // Draw baseline and descent.
- * d = textDescent() * fontScale;
- * line(45, baseY, 91, baseY);
- * line(91, baseY, 91, baseY + d);
- *
- * describe('The letters "dp" written twice in different sizes. Each version has a horizontal baseline. A vertical line extends downward from each baseline to the bottom of the "p".');
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the text.
- * textSize(20);
- * textWrap(WORD);
- *
- * // Display the text.
- * text('Have a wonderful day', 0, 10, 100);
- *
- * describe('The text "Have a wonderful day" written across three lines.');
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the text.
- * textSize(20);
- * textWrap(CHAR);
- *
- * // Display the text.
- * text('Have a wonderful day', 0, 10, 100);
- *
- * describe('The text "Have a wonderful day" written across two lines.');
- * }
- *
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the text.
- * textSize(20);
- * textWrap(CHAR);
- *
- * // Display the text.
- * text('祝你有美好的一天', 0, 10, 100);
- *
- * describe('The text "祝你有美好的一天" written across two lines.');
- * }
- *
- *
- * let font;
- *
- * function preload() {
- * font = loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * fill('deeppink');
- * textFont(font);
- * textSize(36);
- * text('p5*js', 10, 50);
- *
- * describe('The text "p5*js" written in pink on a white background.');
- * }
- *
- *
- * function setup() {
- * loadFont('assets/inconsolata.otf', font => {
- * fill('deeppink');
- * textFont(font);
- * textSize(36);
- * text('p5*js', 10, 50);
- *
- * describe('The text "p5*js" written in pink on a white background.');
- * });
- * }
- *
- *
- * function setup() {
- * loadFont('assets/inconsolata.otf', success, failure);
- * }
- *
- * function success(font) {
- * fill('deeppink');
- * textFont(font);
- * textSize(36);
- * text('p5*js', 10, 50);
- *
- * describe('The text "p5*js" written in pink on a white background.');
- * }
- *
- * function failure(event) {
- * console.error('Oops!', event);
- * }
- *
- *
- * function preload() {
- * loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * let p = createP('p5*js');
- * p.style('color', 'deeppink');
- * p.style('font-family', 'Inconsolata');
- * p.style('font-size', '36px');
- * p.position(10, 50);
- *
- * describe('The text "p5*js" written in pink on a white background.');
- * }
- *
- *
- * function setup() {
- * background(200);
- * text('hi', 50, 50);
- *
- * describe('The text "hi" written in black in the middle of a gray square.');
- * }
- *
- *
- * function setup() {
- * background('skyblue');
- * textSize(100);
- * text('🌈', 0, 100);
- *
- * describe('A rainbow in a blue sky.');
- * }
- *
- *
- * function setup() {
- * textSize(32);
- * fill(255);
- * stroke(0);
- * strokeWeight(4);
- * text('hi', 50, 50);
- *
- * describe('The text "hi" written in white with a black outline.');
- * }
- *
- *
- * function setup() {
- * background('black');
- * textSize(22);
- * fill('yellow');
- * text('rainbows', 6, 20);
- * fill('cornflowerblue');
- * text('rainbows', 6, 45);
- * fill('tomato');
- * text('rainbows', 6, 70);
- * fill('limegreen');
- * text('rainbows', 6, 95);
- *
- * describe('The text "rainbows" written on several lines, each in a different color.');
- * }
- *
- *
- * function setup() {
- * background(200);
- * let s = 'The quick brown fox jumps over the lazy dog.';
- * text(s, 10, 10, 70, 80);
- *
- * describe('The sample text "The quick brown fox..." written in black across several lines.');
- * }
- *
- *
- * function setup() {
- * background(200);
- * rectMode(CENTER);
- * let s = 'The quick brown fox jumps over the lazy dog.';
- * text(s, 50, 50, 70, 80);
- *
- * describe('The sample text "The quick brown fox..." written in black across several lines.');
- * }
- *
- *
- * let font;
- *
- * function preload() {
- * font = loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- * textFont(font);
- * textSize(32);
- * textAlign(CENTER, CENTER);
- * }
- *
- * function draw() {
- * background(0);
- * rotateY(frameCount / 30);
- * text('p5*js', 0, 0);
- *
- * describe('The text "p5*js" written in white and spinning in 3D.');
- * }
- *
- *
- * function setup() {
- * background(200);
- * textFont('Courier New');
- * textSize(24);
- * text('hi', 35, 55);
- *
- * describe('The text "hi" written in a black, monospace font on a gray background.');
- * }
- *
- *
- * function setup() {
- * background('black');
- * fill('palegreen');
- * textFont('Courier New', 10);
- * text('You turn to the left and see a door. Do you enter?', 5, 5, 90, 90);
- * text('>', 5, 70);
- *
- * describe('A text prompt from a game is written in a green, monospace font on a black background.');
- * }
- *
- *
- * function setup() {
- * background(200);
- * textFont('Verdana');
- * let currentFont = textFont();
- * text(currentFont, 25, 50);
- *
- * describe('The text "Verdana" written in a black, sans-serif font on a gray background.');
- * }
- *
- *
- * let fontRegular;
- * let fontItalic;
- * let fontBold;
- *
- * function preload() {
- * fontRegular = loadFont('assets/Regular.otf');
- * fontItalic = loadFont('assets/Italic.ttf');
- * fontBold = loadFont('assets/Bold.ttf');
- * }
- *
- * function setup() {
- * background(200);
- * textFont(fontRegular);
- * text('I am Normal', 10, 30);
- * textFont(fontItalic);
- * text('I am Italic', 10, 50);
- * textFont(fontBold);
- * text('I am Bold', 10, 70);
- *
- * describe('The statements "I am Normal", "I am Italic", and "I am Bold" written in black on separate lines. The statements have normal, italic, and bold fonts, respectively.');
- * }
- *
- *
- * let font;
- *
- * function preload() {
- * // Creates a p5.Font object.
- * font = loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * // Style the text.
- * fill('deeppink');
- * textFont(font);
- * textSize(36);
- *
- * // Display the text.
- * text('p5*js', 10, 50);
- *
- * describe('The text "p5*js" written in pink on a gray background.');
- * }
- *
- *
- * let font;
- *
- * function preload() {
- * font = loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Display the bounding box.
- * let bbox = font.textBounds('p5*js', 35, 53);
- * rect(bbox.x, bbox.y, bbox.w, bbox.h);
- *
- * // Style the text.
- * textFont(font);
- *
- * // Display the text.
- * text('p5*js', 35, 53);
- *
- * describe('The text "p5*js" written in black inside a white rectangle.');
- * }
- *
- *
- * let font;
- *
- * function preload() {
- * font = loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Style the text.
- * textFont(font);
- * textSize(15);
- * textAlign(CENTER, CENTER);
- *
- * // Display the bounding box.
- * let bbox = font.textBounds('p5*js', 50, 50);
- * rect(bbox.x, bbox.y, bbox.w, bbox.h);
- *
- * // Display the text.
- * text('p5*js', 50, 50);
- *
- * describe('The text "p5*js" written in black inside a white rectangle.');
- * }
- *
- *
- * let font;
- *
- * function preload() {
- * font = loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Display the bounding box.
- * let bbox = font.textBounds('p5*js', 31, 53, 15);
- * rect(bbox.x, bbox.y, bbox.w, bbox.h);
- *
- * // Style the text.
- * textFont(font);
- * textSize(15);
- *
- * // Display the text.
- * text('p5*js', 31, 53);
- *
- * describe('The text "p5*js" written in black inside a white rectangle.');
- * }
- *
- *
- * let font;
- *
- * function preload() {
- * font = loadFont('assets/inconsolata.otf');
- * }
- *
- * function setup() {
- * createCanvas(100, 100);
- *
- * background(200);
- *
- * // Get the point array.
- * let points = font.textToPoints('p5*js', 6, 60, 35, { sampleFactor: 0.5 });
- *
- * // Draw a dot at each point.
- * for (let p of points) {
- * point(p.x, p.y);
- * }
- *
- * describe('A set of black dots outlining the text "p5*js" on a gray background.');
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * let shape;
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * // Start building the p5.Geometry object.
- * beginGeometry();
- *
- * // Add a cone.
- * cone();
- *
- * // Stop building the p5.Geometry object.
- * shape = endGeometry();
- *
- * describe('A white cone drawn on a gray background.');
- * }
- *
- * function draw() {
- * background(50);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Turn on the lights.
- * lights();
- *
- * // Style the p5.Geometry object.
- * noStroke();
- *
- * // Draw the p5.Geometry object.
- * model(shape);
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * let shape;
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * // Create the p5.Geometry object.
- * createArrow();
- *
- * describe('A white arrow drawn on a gray background.');
- * }
- *
- * function draw() {
- * background(50);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Turn on the lights.
- * lights();
- *
- * // Style the p5.Geometry object.
- * noStroke();
- *
- * // Draw the p5.Geometry object.
- * model(shape);
- * }
- *
- * function createArrow() {
- * // Start building the p5.Geometry object.
- * beginGeometry();
- *
- * // Add shapes.
- * push();
- * rotateX(PI);
- * cone(10);
- * translate(0, -10, 0);
- * cylinder(3, 20);
- * pop();
- *
- * // Stop building the p5.Geometry object.
- * shape = endGeometry();
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * let blueArrow;
- * let redArrow;
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * // Create the arrows.
- * redArrow = createArrow('red');
- * blueArrow = createArrow('blue');
- *
- * describe('A red arrow and a blue arrow drawn on a gray background. The blue arrow rotates slowly.');
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Turn on the lights.
- * lights();
- *
- * // Style the arrows.
- * noStroke();
- *
- * // Draw the red arrow.
- * model(redArrow);
- *
- * // Translate and rotate the coordinate system.
- * translate(30, 0, 0);
- * rotateZ(frameCount * 0.01);
- *
- * // Draw the blue arrow.
- * model(blueArrow);
- * }
- *
- * function createArrow(fillColor) {
- * // Start building the p5.Geometry object.
- * beginGeometry();
- *
- * fill(fillColor);
- *
- * // Add shapes to the p5.Geometry object.
- * push();
- * rotateX(PI);
- * cone(10);
- * translate(0, -10, 0);
- * cylinder(3, 20);
- * pop();
- *
- * // Stop building the p5.Geometry object.
- * let shape = endGeometry();
- *
- * return shape;
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * let button;
- * let particles;
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * // Create a button to reset the particle system.
- * button = createButton('Reset');
- *
- * // Call resetModel() when the user presses the button.
- * button.mousePressed(resetModel);
- *
- * // Add the original set of particles.
- * resetModel();
- * }
- *
- * function draw() {
- * background(50);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Turn on the lights.
- * lights();
- *
- * // Style the particles.
- * noStroke();
- *
- * // Draw the particles.
- * model(particles);
- * }
- *
- * function resetModel() {
- * // If the p5.Geometry object has already been created,
- * // free those resources.
- * if (particles) {
- * freeGeometry(particles);
- * }
- *
- * // Create a new p5.Geometry object with random spheres.
- * particles = createParticles();
- * }
- *
- * function createParticles() {
- * // Start building the p5.Geometry object.
- * beginGeometry();
- *
- * // Add shapes.
- * for (let i = 0; i < 60; i += 1) {
- * // Calculate random coordinates.
- * let x = randomGaussian(0, 20);
- * let y = randomGaussian(0, 20);
- * let z = randomGaussian(0, 20);
- *
- * push();
- * // Translate to the particle's coordinates.
- * translate(x, y, z);
- * // Draw the particle.
- * sphere(5);
- * pop();
- * }
- *
- * // Stop building the p5.Geometry object.
- * let shape = endGeometry();
- *
- * return shape;
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * let shape;
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * // Start building the p5.Geometry object.
- * beginGeometry();
- *
- * // Add a cone.
- * cone();
- *
- * // Stop building the p5.Geometry object.
- * shape = endGeometry();
- *
- * describe('A white cone drawn on a gray background.');
- * }
- *
- * function draw() {
- * background(50);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Turn on the lights.
- * lights();
- *
- * // Style the p5.Geometry object.
- * noStroke();
- *
- * // Draw the p5.Geometry object.
- * model(shape);
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * let shape;
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * // Create the p5.Geometry object.
- * createArrow();
- *
- * describe('A white arrow drawn on a gray background.');
- * }
- *
- * function draw() {
- * background(50);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Turn on the lights.
- * lights();
- *
- * // Style the p5.Geometry object.
- * noStroke();
- *
- * // Draw the p5.Geometry object.
- * model(shape);
- * }
- *
- * function createArrow() {
- * // Start building the p5.Geometry object.
- * beginGeometry();
- *
- * // Add shapes.
- * push();
- * rotateX(PI);
- * cone(10);
- * translate(0, -10, 0);
- * cylinder(3, 20);
- * pop();
- *
- * // Stop building the p5.Geometry object.
- * shape = endGeometry();
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * let blueArrow;
- * let redArrow;
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * // Create the arrows.
- * redArrow = createArrow('red');
- * blueArrow = createArrow('blue');
- *
- * describe('A red arrow and a blue arrow drawn on a gray background. The blue arrow rotates slowly.');
- * }
- *
- * function draw() {
- * background(200);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Turn on the lights.
- * lights();
- *
- * // Style the arrows.
- * noStroke();
- *
- * // Draw the red arrow.
- * model(redArrow);
- *
- * // Translate and rotate the coordinate system.
- * translate(30, 0, 0);
- * rotateZ(frameCount * 0.01);
- *
- * // Draw the blue arrow.
- * model(blueArrow);
- * }
- *
- * function createArrow(fillColor) {
- * // Start building the p5.Geometry object.
- * beginGeometry();
- *
- * fill(fillColor);
- *
- * // Add shapes to the p5.Geometry object.
- * push();
- * rotateX(PI);
- * cone(10);
- * translate(0, -10, 0);
- * cylinder(3, 20);
- * pop();
- *
- * // Stop building the p5.Geometry object.
- * let shape = endGeometry();
- *
- * return shape;
- * }
- *
- *
- * // Click and drag the mouse to view the scene from different angles.
- *
- * let button;
- * let particles;
- *
- * function setup() {
- * createCanvas(100, 100, WEBGL);
- *
- * // Create a button to reset the particle system.
- * button = createButton('Reset');
- *
- * // Call resetModel() when the user presses the button.
- * button.mousePressed(resetModel);
- *
- * // Add the original set of particles.
- * resetModel();
- * }
- *
- * function draw() {
- * background(50);
- *
- * // Enable orbiting with the mouse.
- * orbitControl();
- *
- * // Turn on the lights.
- * lights();
- *
- * // Style the particles.
- * noStroke();
- *
- * // Draw the particles.
- * model(particles);
- * }
- *
- * function resetModel() {
- * // If the p5.Geometry object has already been created,
- * // free those resources.
- * if (particles) {
- * freeGeometry(particles);
- * }
- *
- * // Create a new p5.Geometry object with random spheres.
- * particles = createParticles();
- * }
- *
- * function createParticles() {
- * // Start building the p5.Geometry object.
- * beginGeometry();
- *
- * // Add shapes.
- * for (let i = 0; i < 60; i += 1) {
- * // Calculate random coordinates.
- * let x = randomGaussian(0, 20);
- * let y = randomGaussian(0, 20);
- * let z = randomGaussian(0, 20);
- *
- * push();
- * // Translate to the particle's coordinates.
- * translate(x, y, z);
- * // Draw the particle.
- * sphere(5);
- * pop();
- * }
- *
- * // Stop building the p5.Geometry object.
- * let shape = endGeometry();
- *
- * return shape;
- * }
- *
- *
* function setup() {
* createCanvas(300, 300, WEBGL);
- *
- * describe('A sphere with red stroke and a red, wavy line on a gray background.');
+ *
+ * describe('A sphere with red stroke and a red, wavy line on a gray background.');
* }
- *
+ *
* function draw() {
* background(128);
* strokeMode(FULL); // Enables detailed rendering with caps, joins, and stroke color.
@@ -554,8 +46,8 @@ function primitives3D(p5, fn){
* strokeWeight(1);
* translate(0, -50, 0);
* sphere(50);
- * pop();
- *
+ * pop();
+ *
* noFill();
* strokeWeight(15);
* beginShape();
@@ -566,15 +58,15 @@ function primitives3D(p5, fn){
* }
*
*
* function setup() {
* createCanvas(300, 300, WEBGL);
- *
- * describe('A sphere with red stroke and a wavy line without full curve decorations without caps and color on a gray background.');
+ *
+ * describe('A sphere with red stroke and a wavy line without full curve decorations without caps and color on a gray background.');
* }
- *
+ *
* function draw() {
* background(128);
* strokeMode(SIMPLE); // Enables simple rendering without caps, joins, and stroke color.
@@ -582,8 +74,8 @@ function primitives3D(p5, fn){
* strokeWeight(1);
* translate(0, -50, 0);
* sphere(50);
- * pop();
- *
+ * pop();
+ *
* noFill();
* strokeWeight(15);
* beginShape();
@@ -595,7 +87,7 @@ function primitives3D(p5, fn){
*
*
+ * function setup() {
+ * createCanvas(100, 100);
+ *
+ * background(200);
+ *
+ * // Draw a black spline curve.
+ * noFill();
+ * strokeWeight(1);
+ * stroke(0);
+ * curve(5, 26, 73, 24, 73, 61, 15, 65);
+ *
+ * // Draw red spline curves from the anchor points to the control points.
+ * stroke(255, 0, 0);
+ * curve(5, 26, 5, 26, 73, 24, 73, 61);
+ * curve(73, 24, 73, 61, 15, 65, 15, 65);
+ *
+ * // Draw the anchor points in black.
+ * strokeWeight(5);
+ * stroke(0);
+ * point(73, 24);
+ * point(73, 61);
+ *
+ * // Draw the control points in red.
+ * stroke(255, 0, 0);
+ * point(5, 26);
+ * point(15, 65);
+ *
+ * describe(
+ * 'A gray square with a curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.'
+ * );
+ * }
+ *
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ *
+ * background(200);
+ *
+ * // Set the curveDetail() to 3.
+ * curveDetail(3);
+ *
+ * // Draw a black spline curve.
+ * noFill();
+ * strokeWeight(1);
+ * stroke(0);
+ * curve(-45, -24, 0, 23, -26, 0, 23, 11, 0, -35, 15, 0);
+ *
+ * // Draw red spline curves from the anchor points to the control points.
+ * stroke(255, 0, 0);
+ * curve(-45, -24, 0, -45, -24, 0, 23, -26, 0, 23, 11, 0);
+ * curve(23, -26, 0, 23, 11, 0, -35, 15, 0, -35, 15, 0);
+ *
+ * // Draw the anchor points in black.
+ * strokeWeight(5);
+ * stroke(0);
+ * point(23, -26);
+ * point(23, 11);
+ *
+ * // Draw the control points in red.
+ * stroke(255, 0, 0);
+ * point(-45, -24);
+ * point(-35, 15);
+ *
+ * describe(
+ * 'A gray square with a jagged curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.'
+ * );
+ * }
+ *
+ *
- * function setup() {
- * createCanvas(200, 400, WEBGL);
- * setAttributes('antialias', true);
- * }
- *
- * function draw() {
- * background(0);
- * noStroke();
- * translate(0, -100, 0);
- * stroke(240, 150, 150);
- * fill(100, 100, 240);
- * push();
- * strokeWeight(8);
- * rotateX(frameCount * 0.01);
- * rotateY(frameCount * 0.01);
- * sphere(75);
- * pop();
- * push();
- * translate(0, 200, 0);
- * strokeWeight(1);
- * rotateX(frameCount * 0.01);
- * rotateY(frameCount * 0.01);
- * sphere(75);
- * pop();
- * }
- *
- * Hello
'; - testElement = myp5.createDiv(testHTML); + const testElement = mockP5Prototype.createDiv(testHTML); assert.deepEqual(testElement.elt.innerHTML, testHTML); }); }); suite('p5.prototype.createP', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { - assert.isFunction(myp5.createP); + assert.isFunction(mockP5Prototype.createP); }); test('should return a p5.Element of p type', function() { - testElement = myp5.createP(); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createP(); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLParagraphElement); }); test('should set given param as innerHTML of p', function() { const testHTML = 'Hello'; - testElement = myp5.createP(testHTML); + const testElement = mockP5Prototype.createP(testHTML); assert.deepEqual(testElement.elt.innerHTML, testHTML); }); }); suite('p5.prototype.createSpan', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { - assert.isFunction(myp5.createSpan); + assert.isFunction(mockP5Prototype.createSpan); }); test('should return a p5.Element of span type', function() { - testElement = myp5.createSpan(); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createSpan(); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLSpanElement); }); test('should set given param as innerHTML of span', function() { const testHTML = 'Hello'; - testElement = myp5.createSpan(testHTML); + const testElement = mockP5Prototype.createSpan(testHTML); assert.deepEqual(testElement.elt.innerHTML, testHTML); }); }); suite('p5.prototype.createImg', function() { - let myp5; - let testElement; - const imagePath = 'unit/assets/cat.jpg'; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); + const imagePath = '/test/unit/assets/cat.jpg'; afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { - assert.isFunction(myp5.createImg); + assert.isFunction(mockP5Prototype.createImg); }); test('should return p5.Element of image type', function() { - testElement = myp5.createImg(imagePath, ''); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createImg(imagePath, ''); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLImageElement); }); test('should set src of image from params', function() { - testElement = myp5.createImg(imagePath, ''); + const testElement = mockP5Prototype.createImg(imagePath, ''); assert.isTrue(testElement.elt.src.endsWith(imagePath)); }); test('should set alt from params if given', function() { const alternativeText = 'Picture of a cat'; - testElement = myp5.createImg(imagePath, alternativeText); + const testElement = mockP5Prototype.createImg(imagePath, alternativeText); assert.deepEqual(testElement.elt.alt, alternativeText); }); test('should set crossOrigin from params if given', function() { const crossOrigin = 'anonymous'; - testElement = myp5.createImg(imagePath, '', crossOrigin); + const testElement = mockP5Prototype.createImg(imagePath, '', crossOrigin); assert.deepEqual(testElement.elt.crossOrigin, crossOrigin); }); - testSketchWithPromise( - 'should trigger callback when image is loaded', - function(sketch, resolve, reject) { - sketch.setup = function() { - testElement = sketch.createImg(imagePath, '', '', resolve); - testElement.elt.dispatchEvent(new Event('load')); - }; - } - ); + // testSketchWithPromise( + // 'should trigger callback when image is loaded', + // function(sketch, resolve, reject) { + // sketch.setup = function() { + // testElement = sketch.createImg(imagePath, '', '', resolve); + // testElement.elt.dispatchEvent(new Event('load')); + // }; + // } + // ); }); suite('p5.prototype.createA', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - testElement = null; - } + document.body.innerHTML = ""; }); test('should return a p5.Element of anchor type', () => { - testElement = myp5.createA('', ''); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createA('', ''); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLAnchorElement); }); test('creates anchor with given link & text', function() { const testUrl = 'http://p5js.org/'; const testText = 'p5js website'; - testElement = myp5.createA(testUrl, testText); + const testElement = mockP5Prototype.createA(testUrl, testText); assert.deepEqual(testElement.elt.href, testUrl); assert.deepEqual(testElement.elt.text, testText); @@ -535,110 +440,66 @@ suite('DOM', function() { test('creates anchor with given target', function() { const testTarget = 'blank'; - testElement = myp5.createA('http://p5js.org', 'p5js website', testTarget); + const testElement = mockP5Prototype.createA('http://p5js.org', 'p5js website', testTarget); assert.deepEqual(testElement.elt.target, testTarget); }); }); suite('p5.prototype.createSlider', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should return a p5.Element of slider type', () => { - testElement = myp5.createSlider(0, 100, 0, 1); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createSlider(0, 100, 0, 1); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLInputElement); assert.deepEqual(testElement.elt.type, 'range'); }); test('should set min and max values', function() { - let testElement = myp5.createSlider(20, 80); + const testElement = mockP5Prototype.createSlider(20, 80); assert.deepEqual(testElement.elt.min, '20'); assert.deepEqual(testElement.elt.max, '80'); }); test('should set slider position', function() { - let testElement = myp5.createSlider(20, 80, 50); + const testElement = mockP5Prototype.createSlider(20, 80, 50); assert.deepEqual(testElement.elt.value, '50'); }); test('should set step value', function() { - testElement = myp5.createSlider(20, 80, 10, 5); + const testElement = mockP5Prototype.createSlider(20, 80, 10, 5); assert.deepEqual(testElement.elt.step, '5'); }); }); suite('p5.prototype.createButton', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should return a p5.Element of button type', function() { - testElement = myp5.createButton('testButton', 'testButton'); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createButton('testButton', 'testButton'); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLButtonElement); }); - testSketchWithPromise( - 'should trigger callback when mouse is pressed', - function(sketch, resolve, reject) { - sketch.setup = function() { - const testElement = sketch.createButton('test'); - testElement.mousePressed(resolve); - testElement.elt.dispatchEvent(new Event('mousedown')); - }; - } - ); + // testSketchWithPromise( + // 'should trigger callback when mouse is pressed', + // function(sketch, resolve, reject) { + // sketch.setup = function() { + // const testElement = sketch.createButton('test'); + // testElement.mousePressed(resolve); + // testElement.elt.dispatchEvent(new Event('mousedown')); + // }; + // } + // ); }); - // Tests for createCheckbox suite('p5.prototype.createCheckbox', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); // helper functions @@ -653,75 +514,60 @@ suite('DOM', function() { : null; test('should be a function', function() { - assert.isFunction(myp5.createCheckbox); + assert.isFunction(mockP5Prototype.createCheckbox); }); test('should return a p5.Element with checkbox as descendant', function() { - testElement = myp5.createCheckbox(); + const testElement = mockP5Prototype.createCheckbox(); const checkboxElement = getCheckboxElement(testElement); - assert.instanceOf(testElement, p5.Element); + assert.instanceOf(testElement, Element); assert.instanceOf(checkboxElement, HTMLInputElement); }); test('calling createCheckbox(label) should create checkbox and set its label', function() { const labelValue = 'label'; - testElement = myp5.createCheckbox(labelValue); + const testElement = mockP5Prototype.createCheckbox(labelValue); const spanElement = getSpanElement(testElement); const testElementLabelValue = getSpanElement(testElement) ? getSpanElement(testElement).innerHTML : ''; - assert.instanceOf(testElement, p5.Element); + assert.instanceOf(testElement, Element); assert.instanceOf(spanElement, HTMLSpanElement); assert.deepEqual(testElementLabelValue, labelValue); }); test('calling createCheckbox(label, true) should create a checked checkbox and set its label', function() { const labelValue = 'label'; - testElement = myp5.createCheckbox(labelValue, true); + const testElement = mockP5Prototype.createCheckbox(labelValue, true); const spanElement = getSpanElement(testElement); const testElementLabelValue = getSpanElement(testElement) ? getSpanElement(testElement).innerHTML : ''; - assert.instanceOf(testElement, p5.Element); + assert.instanceOf(testElement, Element); assert.instanceOf(spanElement, HTMLSpanElement); assert.deepEqual(testElementLabelValue, labelValue); assert.isTrue(testElement.checked()); }); test('calling checked() should return checked value of checkbox', function() { - testElement = myp5.createCheckbox('', true); + const testElement = mockP5Prototype.createCheckbox('', true); assert.isTrue(testElement.checked()); }); test('calling checked(true) should set checked value of checkbox', function() { - testElement = myp5.createCheckbox('', false); + const testElement = mockP5Prototype.createCheckbox('', false); testElement.checked(true); assert.isTrue(testElement.checked()); }); }); suite('p5.prototype.createSelect', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - testElement = null; - } + document.body.innerHTML = ""; }); const createHTMLSelect = options => { @@ -736,24 +582,24 @@ suite('DOM', function() { }; test('should be a function', function() { - assert.isFunction(myp5.createSelect); + assert.isFunction(mockP5Prototype.createSelect); }); test('should return p5.Element of select HTML Element', function() { - testElement = myp5.createSelect(); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createSelect(); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLSelectElement); }); test('should return p5.Element when select element is passed', function() { const selectElement = createHTMLSelect(['option1', 'option2']); - testElement = myp5.createSelect(selectElement); + const testElement = mockP5Prototype.createSelect(selectElement); assert.deepEqual(testElement.elt, selectElement); }); test('calling option(newName) should add a new option', function() { const testOptions = ['John', 'Bethany', 'Dwayne']; - testElement = myp5.createSelect(); + const testElement = mockP5Prototype.createSelect(); for (const optionName of testOptions) testElement.option(optionName); const htmlOptions = []; @@ -767,7 +613,7 @@ suite('DOM', function() { test('calling option(name, newValue) should update value of option', function() { const optionName = 'john'; const testValues = [1, 'abc', true]; - testElement = myp5.createSelect(); + const testElement = mockP5Prototype.createSelect(); testElement.option(optionName, 0); for (const newValue of testValues) { @@ -778,7 +624,7 @@ suite('DOM', function() { }); test('calling value() should return current selected option', function() { - testElement = myp5.createSelect(); + const testElement = mockP5Prototype.createSelect(); testElement.option('Sunday'); testElement.option('Monday'); testElement.elt.options[1].selected = true; @@ -786,7 +632,7 @@ suite('DOM', function() { }); test('calling selected() should return all selected options', function() { - testElement = myp5.createSelect(true); + const testElement = mockP5Prototype.createSelect(true); const options = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; for (const optionName of options) testElement.option(optionName); @@ -802,7 +648,7 @@ suite('DOM', function() { }); test('should update select value when HTML special characters are in the name', function() { - testElement = myp5.createSelect(true); + const testElement = mockP5Prototype.createSelect(true); testElement.option('&', 'foo'); assert.equal(testElement.elt.options.length, 1); assert.equal(testElement.elt.options[0].value, 'foo'); @@ -811,7 +657,7 @@ suite('DOM', function() { }); test('calling selected(value) should updated selectedIndex', function() { - testElement = myp5.createSelect(true); + const testElement = mockP5Prototype.createSelect(true); const options = ['Sunday', 'Monday', 'Tuesday', 'Friday']; for (const optionName of options) testElement.option(optionName); @@ -825,14 +671,14 @@ suite('DOM', function() { }); test('calling disable() should disable the whole dropdown', function() { - testElement = myp5.createSelect(true); + const testElement = mockP5Prototype.createSelect(true); testElement.disable(); assert.isTrue(testElement.elt.disabled); }); test('should disable an option when disable() method invoked with option name', function() { - testElement = myp5.createSelect(true); + const testElement = mockP5Prototype.createSelect(true); const options = ['Sunday', 'Monday', 'Tuesday', 'Friday']; for (const optionName of options) testElement.option(optionName); @@ -844,23 +690,8 @@ suite('DOM', function() { }); suite('p5.prototype.createRadio', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - testElement = null; - } + document.body.innerHTML = ""; }); // Helper functions @@ -889,19 +720,19 @@ suite('DOM', function() { .map(el => (isRadioInput(el) ? el : el.firstElementChild)); test('should be a function', function() { - assert.isFunction(myp5.createRadio); + assert.isFunction(mockP5Prototype.createRadio); }); test('should return p5.Element of radio type', function() { - testElement = myp5.createRadio(); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createRadio(); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLDivElement); }); test('should return p5.Element from existing radio Element', function() { const options = ['Saturday', 'Sunday']; const radioElement = createRadioElement(options); - testElement = myp5.createRadio(radioElement); + const testElement = mockP5Prototype.createRadio(radioElement); assert.deepEqual(testElement.elt, radioElement); }); @@ -909,7 +740,7 @@ suite('DOM', function() { test('calling option(value) should return existing radio element', function() { const options = ['Saturday', 'Sunday']; const radioElement = createRadioElement(options); - testElement = myp5.createRadio(radioElement); + const testElement = mockP5Prototype.createRadio(radioElement); for (const radioInput of getChildren(radioElement)) { const optionEl = testElement.option(radioInput.value); assert.deepEqual(radioInput, optionEl); @@ -920,7 +751,7 @@ suite('DOM', function() { test('calling option(newValue) should create a new radio input', function() { const testName = 'defaultRadio'; const options = ['Saturday', 'Sunday']; - testElement = myp5.createRadio(testName); + const testElement = mockP5Prototype.createRadio(testName); let count = 0; for (const option of options) { const optionEl = testElement.option(option); @@ -938,7 +769,7 @@ suite('DOM', function() { test('calling option(value, label) should set label of option', function() { const testName = 'defaultRadio'; const options = ['Saturday', 'Sunday']; - testElement = myp5.createRadio(testName); + const testElement = mockP5Prototype.createRadio(testName); for (const option of options) { const optionLabel = `${option}-label`; const optionEl = testElement.option(option, optionLabel); @@ -953,7 +784,7 @@ suite('DOM', function() { const testName = 'defaultRadio'; const options = ['Saturday', 'Sunday']; const radioElement = createRadioElement(options); - testElement = myp5.createRadio(radioElement, testName); + const testElement = mockP5Prototype.createRadio(radioElement, testName); for (const option of options) { const optionEl = testElement.option(option); @@ -964,7 +795,7 @@ suite('DOM', function() { test('calling remove(value) should remove option', function() { const options = ['Monday', 'Friday', 'Saturday', 'Sunday']; const radioElement = createRadioElement(options); - testElement = myp5.createRadio(radioElement); + const testElement = mockP5Prototype.createRadio(radioElement); // Remove element const testValue = 'Friday'; @@ -980,7 +811,7 @@ suite('DOM', function() { test('calling value() should return selected value', function() { const options = ['Monday', 'Friday', 'Saturday', 'Sunday']; const selectedValue = options[1]; - testElement = myp5.createRadio(); + const testElement = mockP5Prototype.createRadio(); for (const option of options) testElement.option(option); testElement.selected(selectedValue); assert.deepEqual(testElement.value(), selectedValue); @@ -988,7 +819,7 @@ suite('DOM', function() { test('calling selected(value) should select a value and return it', function() { const options = ['Monday', 'Friday', 'Saturday', 'Sunday']; - testElement = myp5.createRadio(); + const testElement = mockP5Prototype.createRadio(); for (const option of options) testElement.option(option); for (const option of options) { @@ -1000,7 +831,7 @@ suite('DOM', function() { test('calling selected() should return the currently selected option', function() { const options = ['Monday', 'Friday', 'Saturday', 'Sunday']; - testElement = myp5.createRadio(); + const testElement = mockP5Prototype.createRadio(); for (const option of options) testElement.option(option); const testOption = getChildren(testElement.elt)[1]; @@ -1012,7 +843,7 @@ suite('DOM', function() { test('calling disable() should disable all the radio inputs', function() { const options = ['Monday', 'Friday', 'Saturday', 'Sunday']; - testElement = myp5.createRadio(); + const testElement = mockP5Prototype.createRadio(); for (const option of options) testElement.option(option); testElement.disable(); @@ -1023,98 +854,69 @@ suite('DOM', function() { }); suite('p5.prototype.createColorPicker', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { - assert.isFunction(myp5.createColorPicker); + assert.isFunction(mockP5Prototype.createColorPicker); }); test('should return p5.Element of input[color] type', function() { - testElement = myp5.createColorPicker(); + const testElement = mockP5Prototype.createColorPicker(); - assert.instanceOf(testElement, p5.Element); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLInputElement); assert.deepEqual(testElement.elt.type, 'color'); }); + // TODO: pending finalization of p5.Color implementation test.todo('should accept a p5.Color as param', function() { - const testColor = myp5.color('red'); - testElement = myp5.createColorPicker(testColor); + const testColor = mockP5Prototype.color('red'); + const testElement = mockP5Prototype.createColorPicker(testColor); assert.deepEqual(testElement.elt.value, testColor.toString('#rrggbb')); }); test.todo('should accept a string as param', function() { const testColorString = '#f00f00'; - testElement = myp5.createColorPicker(testColorString); + const testElement = mockP5Prototype.createColorPicker(testColorString); assert.deepEqual(testElement.elt.value, testColorString); }); test.todo('calling color() should return the current color as p5.color', function() { const testColorString = 'red'; - const testColor = myp5.color(testColorString); - testElement = myp5.createColorPicker(testColorString); + const testColor = mockP5Prototype.color(testColorString); + const testElement = mockP5Prototype.createColorPicker(testColorString); assert.deepEqual(testElement.color(), testColor); }); test.todo('calling value() should return hex string of color', function() { - const testColor = myp5.color('aqua'); - testElement = myp5.createColorPicker(testColor); + const testColor = mockP5Prototype.color('aqua'); + const testElement = mockP5Prototype.createColorPicker(testColor); assert.deepEqual(testElement.value(), testColor.toString('#rrggbb')); }); }); suite('p5.prototype.createInput', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { - assert.isFunction(myp5.createInput); + assert.isFunction(mockP5Prototype.createInput); }); test('should return p5.Element of input type', function() { - testElement = myp5.createInput(); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createInput(); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLInputElement); }); test('should set given value as input', function() { const testValues = ['123', '', 'Hello world']; for (const value of testValues) { - testElement = myp5.createInput(value); + const testElement = mockP5Prototype.createInput(value); assert.deepEqual(testElement.elt.value, value); } }); @@ -1122,36 +924,15 @@ suite('DOM', function() { test('should create input of given type and value', function() { const testType = 'password'; const testValue = '1234056789'; - testElement = myp5.createInput(testValue, testType); + const testElement = mockP5Prototype.createInput(testValue, testType); assert.deepEqual(testElement.elt.type, testType); assert.deepEqual(testElement.elt.value, testValue); }); }); suite('p5.prototype.createFileInput', function() { - if (!(window.File && window.FileReader && window.FileList && window.Blob)) { - throw Error( - 'File API not supported in test environment. Cannot run tests' - ); - } - - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; - myp5.remove(); + document.body.innerHTML = ""; }); const emptyCallback = () => {}; @@ -1162,370 +943,67 @@ suite('DOM', function() { }; test('should be a function', function() { - assert.isFunction(myp5.createFileInput); + assert.isFunction(mockP5Prototype.createFileInput); }); test('should return input of file input', function() { - testElement = myp5.createFileInput(emptyCallback); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createFileInput(emptyCallback); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLInputElement); assert.deepEqual(testElement.elt.type, 'file'); }); - testSketchWithPromise( - 'should trigger callback on input change event', - function(sketch, resolve, reject) { - sketch.setup = function() { - testElement = myp5.createFileInput(resolve); - const testFile = createDummyFile('file'); - testElement.files = testFile; - - const mockedEvent = new Event('change'); - const mockedDataTransfer = new DataTransfer(); - mockedDataTransfer.items.add(testFile); - testElement.elt.files = mockedDataTransfer.files; - testElement.elt.dispatchEvent(mockedEvent); - }; - } - ); + // testSketchWithPromise( + // 'should trigger callback on input change event', + // function(sketch, resolve, reject) { + // sketch.setup = function() { + // testElement = mockP5Prototype.createFileInput(resolve); + // const testFile = createDummyFile('file'); + // testElement.files = testFile; + + // const mockedEvent = new Event('change'); + // const mockedDataTransfer = new DataTransfer(); + // mockedDataTransfer.items.add(testFile); + // testElement.elt.files = mockedDataTransfer.files; + // testElement.elt.dispatchEvent(mockedEvent); + // }; + // } + // ); test('should accept multiple files if specified', function() { - testElement = myp5.createFileInput(emptyCallback, true); + const testElement = mockP5Prototype.createFileInput(emptyCallback, true); assert.isTrue(testElement.elt.multiple); }); - testSketchWithPromise( - 'should trigger callback for each file if multiple files are given', - function(sketch, resolve, reject) { - sketch.setup = function() { - let totalTriggers = 0; - let filesCount = 7; - - const handleFiles = event => { - totalTriggers += 1; - if (totalTriggers === filesCount) resolve(); - }; - - const mockedEvent = new Event('change'); - const mockedDataTransfer = new DataTransfer(); - for (let i = 0; i < filesCount; i += 1) { - mockedDataTransfer.items.add(createDummyFile(i.toString())); - } - - testElement = myp5.createFileInput(handleFiles, true); - testElement.elt.files = mockedDataTransfer.files; - testElement.elt.dispatchEvent(mockedEvent); - }; - } - ); - }); - - suite('p5.prototype.createVideo', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - testElement = null; - } - }); - - const mediaSources = [ - '/test/unit/assets/nyan_cat.gif', - '/test/unit/assets/target.gif' - ]; - - test('should be a function', function() { - assert.isFunction(myp5.createVideo); - }); - - test('should return p5.Element of HTMLVideoElement', function() { - testElement = myp5.createVideo(''); - assert.instanceOf(testElement, p5.MediaElement); - assert.instanceOf(testElement.elt, HTMLVideoElement); - }); - - test('should accept a singular media source', function() { - const mediaSource = mediaSources[0]; - testElement = myp5.createVideo(mediaSource); - const sourceEl = testElement.elt.children[0]; - - assert.deepEqual(testElement.elt.childElementCount, 1); - assert.instanceOf(sourceEl, HTMLSourceElement); - assert.isTrue(sourceEl.src.endsWith(mediaSource)); - }); - - test('should accept multiple media sources', function() { - testElement = myp5.createVideo(mediaSources); - - assert.deepEqual(testElement.elt.childElementCount, mediaSources.length); - for (let index = 0; index < mediaSources.length; index += 1) { - const sourceEl = testElement.elt.children[index]; - assert.instanceOf(sourceEl, HTMLSourceElement); - assert.isTrue(sourceEl.src.endsWith(mediaSources[index])); - } - }); - - testSketchWithPromise( - 'should trigger callback on canplaythrough event', - function(sketch, resolve, reject) { - sketch.setup = function() { - testElement = myp5.createVideo(mediaSources, resolve); - testElement.elt.dispatchEvent(new Event('canplaythrough')); - }; - } - ); - - test('should work with tint()', function(done) { - const imgElt = myp5.createImg('/test/unit/assets/cat.jpg', ''); - testElement = myp5.createVideo('/test/unit/assets/cat.webm', () => { - // Workaround for headless tests, where the video data isn't loading - // correctly: mock the video element using an image for this test - const prevElt = testElement.elt; - testElement.elt = imgElt.elt; - - myp5.background(255); - myp5.tint(255, 0, 0); - myp5.image(testElement, 0, 0); - - testElement.elt = prevElt; - imgElt.remove(); - - myp5.loadPixels(); - testElement.loadPixels(); - assert.equal(myp5.pixels[0], testElement.pixels[0]); - assert.equal(myp5.pixels[1], 0); - assert.equal(myp5.pixels[2], 0); - done(); - }); - }); - - test('should work with updatePixels()', function(done) { - let loaded = false; - let prevElt; - const imgElt = myp5.createImg('/test/unit/assets/cat.jpg', ''); - testElement = myp5.createVideo('/test/unit/assets/cat.webm', () => { - loaded = true; - // Workaround for headless tests, where the video data isn't loading - // correctly: mock the video element using an image for this test - prevElt = testElement.elt; - testElement.elt = imgElt.elt; - }); - - let drewUpdatedPixels = false; - myp5.draw = function() { - if (!loaded) return; - myp5.background(255); - - if (!drewUpdatedPixels) { - // First, update pixels and check that it draws the updated - // pixels correctly - testElement.loadPixels(); - for (let i = 0; i < testElement.pixels.length; i += 4) { - // Set every pixel to red - testElement.pixels[i] = 255; - testElement.pixels[i + 1] = 0; - testElement.pixels[i + 2] = 0; - testElement.pixels[i + 3] = 255; - } - testElement.updatePixels(); - myp5.image(testElement, 0, 0); - - // The element should have drawn using the updated red pixels - myp5.loadPixels(); - assert.deepEqual([...myp5.pixels.slice(0, 4)], [255, 0, 0, 255]); - - // Mark that we've done the first check so we can see whether - // the video still updates on the next frame - drewUpdatedPixels = true; - } else { - // Next, make sure it still updates with the real pixels from - // the next frame of the video on the next frame of animation - myp5.image(testElement, 0, 0); - - myp5.loadPixels(); - testElement.loadPixels(); - expect([...testElement.pixels.slice(0, 4)]) - .to.not.deep.equal([255, 0, 0, 255]); - assert.deepEqual( - [...myp5.pixels.slice(0, 4)], - [...testElement.pixels.slice(0, 4)] - ); - testElement.elt = prevElt; - imgElt.remove(); - done(); - } - }; - }); - }); - - suite('p5.prototype.createAudio', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - testElement = null; - } - }); - - const mediaSources = [ - '/test/unit/assets/beat.mp3', - '/test/unit/assets/beat.mp3' - ]; - - test('should be a function', function() { - assert.isFunction(myp5.createAudio); - }); - - test('should return p5.Element of HTMLAudioElement', function() { - testElement = myp5.createAudio(''); - assert.instanceOf(testElement, p5.MediaElement); - assert.instanceOf(testElement.elt, HTMLAudioElement); - }); - - test('should accept a singular media source', function() { - const mediaSource = mediaSources[0]; - testElement = myp5.createAudio(mediaSource); - const sourceEl = testElement.elt.children[0]; - - assert.deepEqual(testElement.elt.childElementCount, 1); - assert.instanceOf(sourceEl, HTMLSourceElement); - assert.isTrue(sourceEl.src.endsWith(mediaSource)); - }); - - test('should accept multiple media sources', function() { - testElement = myp5.createAudio(mediaSources); - - assert.deepEqual(testElement.elt.childElementCount, mediaSources.length); - for (let index = 0; index < mediaSources.length; index += 1) { - const sourceEl = testElement.elt.children[index]; - assert.instanceOf(sourceEl, HTMLSourceElement); - assert.isTrue(sourceEl.src.endsWith(mediaSources[index])); - } - }); - - testSketchWithPromise( - 'should trigger callback on canplaythrough event', - function(sketch, resolve, reject) { - sketch.setup = function() { - testElement = myp5.createAudio(mediaSources, resolve); - testElement.elt.dispatchEvent(new Event('canplaythrough')); - }; - } - ); - }); - - suite.todo('p5.prototype.createCapture', function() { - // to run these tests, getUserMedia is required to be supported - if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) { - throw Error( - 'Cannot run tests as getUserMedia not supported in this browser' - ); - } - - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - - afterEach(function() { - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; - myp5.remove(); - }); - - test('should be a function', function() { - assert.isFunction(myp5.createCapture); - }); - - test('should return p5.Element of video type', function() { - testElement = myp5.createCapture(myp5.VIDEO); - assert.instanceOf(testElement, p5.Element); - assert.instanceOf(testElement.elt, HTMLVideoElement); - }); - - test('should throw error if getUserMedia is not supported', function() { - // Remove getUserMedia method and test - const backup = navigator.mediaDevices.getUserMedia; - navigator.mediaDevices.getUserMedia = undefined; - try { - testElement = myp5.createCapture(myp5.VIDEO); - assert.fail(); - } catch (error) { - assert.instanceOf(error, DOMException); - } - - // Restore backup, very important. - navigator.mediaDevices.getUserMedia = backup; - }); - - // NOTE: play() failed because the user didn't interact with the document first. - testSketchWithPromise( - 'triggers the callback after loading metadata', - function(sketch, resolve, reject) { - sketch.setup = function() { - testElement = myp5.createCapture(myp5.VIDEO, resolve); - const mockedEvent = new Event('loadedmetadata'); - testElement.elt.dispatchEvent(mockedEvent); - }; - } - ); - - // Required for ios 11 devices - test('should have playsinline attribute to empty string on DOM element', function() { - testElement = myp5.createCapture(myp5.VIDEO); - // Weird check, setter accepts : playinline, getter accepts playInline - assert.isTrue(testElement.elt.playsInline); - }); + // testSketchWithPromise( + // 'should trigger callback for each file if multiple files are given', + // function(sketch, resolve, reject) { + // sketch.setup = function() { + // let totalTriggers = 0; + // let filesCount = 7; + + // const handleFiles = event => { + // totalTriggers += 1; + // if (totalTriggers === filesCount) resolve(); + // }; + + // const mockedEvent = new Event('change'); + // const mockedDataTransfer = new DataTransfer(); + // for (let i = 0; i < filesCount; i += 1) { + // mockedDataTransfer.items.add(createDummyFile(i.toString())); + // } + + // testElement = mockP5Prototype.createFileInput(handleFiles, true); + // testElement.elt.files = mockedDataTransfer.files; + // testElement.elt.dispatchEvent(mockedEvent); + // }; + // } + // ); }); suite('p5.prototype.createElement', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - testElement = null; - } + document.body.innerHTML = ""; }); const testData = { @@ -1537,54 +1015,62 @@ suite('DOM', function() { }; test('should be a function', function() { - assert.isFunction(myp5.createElement); + assert.isFunction(mockP5Prototype.createElement); }); test('should return a p5.Element of appropriate type', function() { for (const [tag, domElName] of Object.entries(testData)) { - testElement = myp5.createElement(tag); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createElement(tag); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, domElName); } }); test('should set given content as innerHTML', function() { const testContent = 'Lorem ipsum'; - testElement = myp5.createElement('div', testContent); + const testElement = mockP5Prototype.createElement('div', testContent); assert.deepEqual(testElement.elt.innerHTML, testContent); }); }); - // p5.Element.prototype.addClass - suite('p5.Element.prototype.addClass', function() { - let myp5; - let testElement; + suite('p5.prototype.removeElements', function() { + afterEach(function() { + document.body.innerHTML = ""; + }); - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); + test('should remove all elements created by p5 except Canvas', function() { + // creates 6 elements one of which is a canvas, then calls + // removeElements and tests if only canvas is left. + const tags = ['a', 'button', 'canvas', 'div', 'p', 'video']; + for (const tag of tags) { + mockP5Prototype.createElement(tag); + } + // Check if all elements are created. + assert.deepEqual(document.body.childElementCount, tags.length); + + // Call removeElements and check if only canvas is remaining + mockP5Prototype.removeElements(); + assert.deepEqual(document.body.childElementCount, 1); + const remainingElement = document.body.children[0]; + assert.instanceOf(remainingElement, HTMLCanvasElement); }); + }); + // p5.Element.prototype.addClass + suite('p5.Element.prototype.addClass', function() { afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { // Create any p5.Element - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); assert.isFunction(testElement.addClass); }); test('should add provided string to class names', function() { const testClassName = 'jumbotron'; - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); testElement.addClass(testClassName); assert.deepEqual(testElement.elt.className, testClassName); }); @@ -1594,7 +1080,7 @@ suite('DOM', function() { const testClassName2 = 'container-fluid'; const expectedClassName = testClassName1 + ' ' + testClassName2; - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); testElement.addClass(testClassName1); testElement.addClass(testClassName2); @@ -1606,28 +1092,13 @@ suite('DOM', function() { // p5.Element.prototype.removeClass suite('p5.Element.prototype.removeClass', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { // Create any p5.Element - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); assert.isFunction(testElement.removeClass); }); @@ -1635,7 +1106,7 @@ suite('DOM', function() { const defaultClassNames = 'col-md-9 col-sm-12'; const testClassName = 'jumbotron'; - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); testElement.addClass(defaultClassNames); testElement.addClass(testClassName); @@ -1648,7 +1119,7 @@ suite('DOM', function() { const testClassName1 = 'jumbotron'; const testClassName2 = 'container-fluid'; - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); testElement.addClass(testClassName1); // Handling the curse of 'this' @@ -1660,28 +1131,13 @@ suite('DOM', function() { // p5.Element.prototype.hasClass suite('p5.Element.prototype.hasClass', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { // Create any p5.Element - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); assert.isFunction(testElement.hasClass); }); @@ -1689,7 +1145,7 @@ suite('DOM', function() { const defaultClassNames = 'col-md-9 jumbotron'; const testClassName = 'jumbotron'; - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); testElement.addClass(defaultClassNames); assert.isTrue(testElement.hasClass(testClassName)); @@ -1699,7 +1155,7 @@ suite('DOM', function() { const defaultClassNames = 'col-md-9 jumbotron'; const testClassName = 'container-fluid'; - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); testElement.addClass(defaultClassNames); assert.isFalse(testElement.hasClass(testClassName)); @@ -1708,28 +1164,13 @@ suite('DOM', function() { // p5.Element.prototype.toggleClass suite('p5.Element.prototype.toggleClass', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { // Create any p5.Element - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); assert.isFunction(testElement.toggleClass); }); @@ -1737,7 +1178,7 @@ suite('DOM', function() { const defaultClassName = 'container-fluid'; const testClassName = 'jumbotron'; - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); testElement.addClass(defaultClassName); testElement.addClass(testClassName); @@ -1749,7 +1190,7 @@ suite('DOM', function() { const defaultClassName = 'container-fluid'; const testClassName = 'jumbotron'; - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); testElement.addClass(defaultClassName); testElement.toggleClass(testClassName); @@ -1762,33 +1203,18 @@ suite('DOM', function() { // p5.Element.prototype.child suite('p5.Element.prototype.child', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); assert.isFunction(testElement.child); }); test('should return all child nodes by default', function() { - testElement = myp5.createElement('div'); - const childElement = myp5.createElement('p'); + const testElement = mockP5Prototype.createElement('div'); + const childElement = mockP5Prototype.createElement('p'); // Add child element by using DOM API testElement.elt.appendChild(childElement.elt); @@ -1802,8 +1228,8 @@ suite('DOM', function() { }); test('should append p5 element as child', function() { - testElement = myp5.createElement('div'); - const childElement = myp5.createElement('p'); + const testElement = mockP5Prototype.createElement('div'); + const childElement = mockP5Prototype.createElement('p'); testElement.child(childElement); const childNodes = Array.from(testElement.elt.children); @@ -1811,8 +1237,8 @@ suite('DOM', function() { }); test('should append dom element as child', function() { - testElement = myp5.createElement('div'); - const childElement = myp5.createElement('p'); + const testElement = mockP5Prototype.createElement('div'); + const childElement = mockP5Prototype.createElement('p'); testElement.child(childElement.elt); const childNodes = Array.from(testElement.elt.children); @@ -1820,9 +1246,9 @@ suite('DOM', function() { }); test('should append element as child from a given id', function() { - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); const childId = 'testChildElement'; - const childElement = myp5.createElement('p'); + const childElement = mockP5Prototype.createElement('p'); childElement.id(childId); testElement.child(childId); @@ -1831,7 +1257,7 @@ suite('DOM', function() { }); test('should not throw error if mathcing element is not found from a given id', function() { - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); const randomChildId = 'testChildElement'; expect(() => testElement.child(randomChildId)).to.not.throw(); }); @@ -1839,27 +1265,12 @@ suite('DOM', function() { // p5.Element.prototype.center suite('p5.Element.prototype.center', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); assert.isFunction(testElement.center); }); @@ -1878,33 +1289,18 @@ suite('DOM', function() { // p5.Element.prototype.html suite('p5.Element.prototype.html', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { // Create any p5.Element - testElement = myp5.createElement('a'); + const testElement = mockP5Prototype.createElement('a'); assert.isFunction(testElement.position); }); test('should return the inner HTML of element if no argument is given', function() { - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); const testHTML = 'Hello World
'; testElement.elt.innerHTML = testHTML; @@ -1912,7 +1308,7 @@ suite('DOM', function() { }); test('should replace the inner HTML of element', function() { - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); const initialtestHTML = 'Hello World
'; const modifiedtestHTML = 'Hello World !!!
'; @@ -1924,7 +1320,7 @@ suite('DOM', function() { }); test('should append to the inner HTML if second param is true', function() { - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); const testHTML1 = 'Hello World
'; const testHTML2 = 'Hello World !!!
'; @@ -1936,7 +1332,7 @@ suite('DOM', function() { }); test('should replace the inner HTML if second param is false', function() { - testElement = myp5.createElement('div'); + const testElement = mockP5Prototype.createElement('div'); const testHTML1 = 'Hello World
'; const testHTML2 = 'Hello World !!!
'; @@ -1950,46 +1346,31 @@ suite('DOM', function() { // p5.Element.prototype.position suite('p5.Element.prototype.position', function() { - let myp5; - let testElement; - - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - afterEach(function() { - myp5.remove(); - if (testElement && testElement.parentNode) { - testElement.parentNode.removeChild(testElement); - } - testElement = null; + document.body.innerHTML = ""; }); test('should be a function', function() { // Create any p5.Element - testElement = myp5.createElement('a'); + const testElement = mockP5Prototype.createElement('a'); assert.isFunction(testElement.position); }); test('should return current position if no args are given', function() { - testElement = myp5.createButton('testButton'); + const testElement = mockP5Prototype.createButton('testButton'); const position = testElement.position(); assert.deepEqual(position.x, testElement.elt.offsetLeft); assert.deepEqual(position.y, testElement.elt.offsetTop); }); test('should set default position as absolute', function() { - testElement = myp5.createButton('testButton'); + const testElement = mockP5Prototype.createButton('testButton'); testElement.position(20, 70); assert.deepEqual(testElement.elt.style.position, 'absolute'); }); test('should set given params as properties', function() { - let testElement = myp5.createButton('testButton'); + const testElement = mockP5Prototype.createButton('testButton'); testElement.position(20, 80, 'static'); assert.deepEqual(testElement.elt.style.position, 'static'); @@ -2018,42 +1399,6 @@ suite('DOM', function() { // p5.Element.prototype.remove - suite('p5.prototype.drop', function() { - testSketchWithPromise('drop fires multiple events', function( - sketch, - resolve, - reject - ) { - let testElement; - let fileFnCounter = 0; - let eventFnCounter = 0; - sketch.setup = function() { - testElement = sketch.createDiv('Drop files inside'); - - // Setup test functions and constants - const file1 = new File(['foo'], 'foo.txt', { type: 'text/plain' }); - const file2 = new File(['foo'], 'foo.txt', { type: 'text/plain' }); - const hasFinished = () => { - if (fileFnCounter > 1 && eventFnCounter === 1) resolve(); - }; - const testFileFn = () => { - fileFnCounter += 1; - hasFinished(); - }; - const testEventFn = () => { - eventFnCounter += 1; - hasFinished(); - }; - testElement.drop(testFileFn, testEventFn); - - // Fire a mock drop and test the method - const mockedEvent = new Event('drop'); - mockedEvent.dataTransfer = { files: [file1, file2] }; - testElement.elt.dispatchEvent(mockedEvent); - }; - }); - }); - // p5.MediaElement // p5.MediaElement.play diff --git a/test/unit/core/p5.Element.js b/test/unit/dom/p5.Element.js similarity index 83% rename from test/unit/core/p5.Element.js rename to test/unit/dom/p5.Element.js index aaae36ee3e..1c94d2b81c 100644 --- a/test/unit/core/p5.Element.js +++ b/test/unit/dom/p5.Element.js @@ -1,27 +1,33 @@ -import p5 from '../../../src/app.js'; +// import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import dom from '../../../src/dom/dom'; suite('p5.Element', function() { - const myp5 = new p5(function(sketch) { - sketch.setup = function() {}; - sketch.draw = function() {}; - }); - - let elt; - - afterAll(function() { - if (elt && elt.parentNode) { - elt.parentNode.removeChild(elt); - elt = null; - } - myp5.remove(); + // const mockP5Prototype = new p5(function(sketch) { + // sketch.setup = function() {}; + // sketch.draw = function() {}; + // }); + + // let elt; + + // afterAll(function() { + // if (elt && elt.parentNode) { + // elt.parentNode.removeChild(elt); + // elt = null; + // } + // mockP5Prototype.remove(); + // }); + + beforeAll(() => { + dom(mockP5, mockP5Prototype); }); suite('p5.Element.prototype.parent', function() { let div0, div1; beforeEach(() => { - div0 = myp5.createDiv('this is the parent'); - div1 = myp5.createDiv('this is the child'); + div0 = mockP5Prototype.createDiv('this is the parent'); + div1 = mockP5Prototype.createDiv('this is the child'); }); afterEach(() => { @@ -53,29 +59,29 @@ suite('p5.Element', function() { div1.setAttribute('id', 'child'); div0.appendChild(div1); document.body.appendChild(div0); - assert.equal(myp5.select('#child').parent(), div0); + assert.equal(mockP5Prototype.select('#child').parent(), div0); }); }); suite('p5.Element.prototype.id', function() { test('attaches child to parent', function() { - elt = myp5.createDiv(); + const elt = mockP5Prototype.createDiv(); elt.id('test'); assert.equal(document.getElementById('test'), elt.elt); }); test('returns the id', function() { - elt = document.createElement('div'); + const elt = document.createElement('div'); elt.setAttribute('id', 'test'); document.body.appendChild(elt); - assert.equal(myp5.select('#child').id(), 'child'); + assert.equal(mockP5Prototype.select('#child').id(), 'child'); }); }); - suite('p5.Element.prototype.mousePressed', function() { + suite.todo('p5.Element.prototype.mousePressed', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -89,7 +95,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -111,7 +117,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.mouseClicked', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -125,7 +131,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -145,7 +151,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -162,7 +168,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.doubleClicked', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -176,7 +182,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -196,7 +202,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -213,7 +219,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.mouseWheel', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function(event) { if (event.deltaX > 0) { @@ -229,7 +235,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -251,7 +257,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.touchStarted', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function(event) { myFnCounter++; @@ -265,7 +271,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -285,7 +291,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -302,7 +308,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.touchMoved', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function(event) { myFnCounter++; @@ -316,7 +322,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -336,7 +342,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -353,7 +359,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.touchEnded', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function(event) { myFnCounter++; @@ -367,7 +373,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -387,7 +393,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -404,7 +410,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.mouseReleased', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -418,7 +424,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -438,7 +444,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -455,7 +461,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.mouseMoved', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -469,7 +475,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -489,7 +495,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -506,7 +512,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.mouseOver', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -520,7 +526,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -540,7 +546,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -557,7 +563,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.mouseOut', function() { test('attaches and gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -571,7 +577,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -591,7 +597,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -607,7 +613,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.dragOver', function() { test('attaches and gets events', function() { - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -621,7 +627,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -641,7 +647,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -657,7 +663,7 @@ suite('p5.Element', function() { suite('p5.Element.prototype.dragLeave', function() { test('attaches and gets events', function() { - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -671,7 +677,7 @@ suite('p5.Element', function() { test('attaches multiple handlers and only latest gets events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -691,7 +697,7 @@ suite('p5.Element', function() { test('detaches and does not get events', function() { // setup - elt = myp5.createDiv('hello'); + const elt = mockP5Prototype.createDiv('hello'); var myFnCounter = 0; var myFn = function() { myFnCounter++; @@ -720,7 +726,7 @@ suite('p5.Element', function() { elt.setAttribute('id', 'testdiv'); document.body.appendChild(elt); - myp5.select('#testdiv').addClass('testclass'); + mockP5Prototype.select('#testdiv').addClass('testclass'); assert.strictEqual(elt.getAttribute('class'), 'testclass'); }); @@ -729,7 +735,7 @@ suite('p5.Element', function() { elt.setAttribute('class', 'testclass'); document.body.appendChild(elt); - myp5.select('#testdiv').removeClass('testclass'); + mockP5Prototype.select('#testdiv').removeClass('testclass'); assert.strictEqual(elt.getAttribute('class'), ''); }); @@ -738,7 +744,7 @@ suite('p5.Element', function() { elt.setAttribute('class', 'testclass1 testclass2 testclass3'); document.body.appendChild(elt); - myp5.select('#testdiv').removeClass('testclass2'); + mockP5Prototype.select('#testdiv').removeClass('testclass2'); assert.strictEqual(elt.getAttribute('class'), 'testclass1 testclass3'); }); @@ -747,7 +753,7 @@ suite('p5.Element', function() { elt.setAttribute('class', 'testclass1 testclass2 testclass3'); document.body.appendChild(elt); - assert.strictEqual(myp5.select('#testdiv').hasClass('testclass2'), true); + assert.strictEqual(mockP5Prototype.select('#testdiv').hasClass('testclass2'), true); }); test('should return false if element has not specified class', function() { @@ -755,7 +761,7 @@ suite('p5.Element', function() { elt.setAttribute('class', 'testclass1 testclass3'); document.body.appendChild(elt); - assert.strictEqual(myp5.select('#testdiv').hasClass('testclass2'), false); + assert.strictEqual(mockP5Prototype.select('#testdiv').hasClass('testclass2'), false); }); test('should return false if element has class that is partially similar as specified class', function() { @@ -763,10 +769,10 @@ suite('p5.Element', function() { elt.setAttribute('class', 'testclass slideshow newtestsclas'); document.body.appendChild(elt); - assert.strictEqual(myp5.select('#testdiv').hasClass('show'), false); - assert.strictEqual(myp5.select('#testdiv').hasClass('slide'), false); - assert.strictEqual(myp5.select('#testdiv').hasClass('test'), false); - assert.strictEqual(myp5.select('#testdiv').hasClass('class'), false); + assert.strictEqual(mockP5Prototype.select('#testdiv').hasClass('show'), false); + assert.strictEqual(mockP5Prototype.select('#testdiv').hasClass('slide'), false); + assert.strictEqual(mockP5Prototype.select('#testdiv').hasClass('test'), false); + assert.strictEqual(mockP5Prototype.select('#testdiv').hasClass('class'), false); }); test('should toggle specified class on element', function() { @@ -774,10 +780,10 @@ suite('p5.Element', function() { elt.setAttribute('class', 'testclass1 testclass2'); document.body.appendChild(elt); - myp5.select('#testdiv').toggleClass('testclass2'); + mockP5Prototype.select('#testdiv').toggleClass('testclass2'); assert.strictEqual(elt.getAttribute('class'), 'testclass1'); - myp5.select('#testdiv').toggleClass('testclass2'); + mockP5Prototype.select('#testdiv').toggleClass('testclass2'); assert.strictEqual(elt.getAttribute('class'), 'testclass1 testclass2'); }); }); diff --git a/test/unit/dom/p5.MediaElement.js b/test/unit/dom/p5.MediaElement.js new file mode 100644 index 0000000000..bfaaba88c0 --- /dev/null +++ b/test/unit/dom/p5.MediaElement.js @@ -0,0 +1,238 @@ +import { vi } from 'vitest'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import { default as media, MediaElement } from '../../../src/dom/p5.MediaElement'; +import { Element } from '../../../src/dom/p5.Element'; + +suite('p5.MediaElement', () => { + beforeAll(() => { + media(mockP5, mockP5Prototype); + navigator.mediaDevices.getUserMedia = vi.fn() + .mockResolvedValue("stream-value"); + }); + + afterAll(() => { + vi.restoreAllMocks(); + }); + + suite('p5.prototype.createVideo', function() { + afterEach(function() { + document.body.innerHTML = ""; + }); + + const mediaSources = [ + '/test/unit/assets/nyan_cat.gif', + '/test/unit/assets/target.gif' + ]; + + test('should be a function', function() { + assert.isFunction(mockP5Prototype.createVideo); + }); + + test('should return p5.Element of HTMLVideoElement', function() { + const testElement = mockP5Prototype.createVideo(''); + assert.instanceOf(testElement, MediaElement); + assert.instanceOf(testElement.elt, HTMLVideoElement); + }); + + test('should accept a singular media source', function() { + const mediaSource = mediaSources[0]; + const testElement = mockP5Prototype.createVideo(mediaSource); + const sourceEl = testElement.elt.children[0]; + + assert.deepEqual(testElement.elt.childElementCount, 1); + assert.instanceOf(sourceEl, HTMLSourceElement); + assert.isTrue(sourceEl.src.endsWith(mediaSource)); + }); + + test('should accept multiple media sources', function() { + const testElement = mockP5Prototype.createVideo(mediaSources); + + assert.deepEqual(testElement.elt.childElementCount, mediaSources.length); + for (let index = 0; index < mediaSources.length; index += 1) { + const sourceEl = testElement.elt.children[index]; + assert.instanceOf(sourceEl, HTMLSourceElement); + assert.isTrue(sourceEl.src.endsWith(mediaSources[index])); + } + }); + + // testSketchWithPromise( + // 'should trigger callback on canplaythrough event', + // function(sketch, resolve, reject) { + // sketch.setup = function() { + // testElement = myp5.createVideo(mediaSources, resolve); + // testElement.elt.dispatchEvent(new Event('canplaythrough')); + // }; + // } + // ); + + // TODO: integration test + test.todo('should work with tint()', function(done) { + const imgElt = myp5.createImg('/test/unit/assets/cat.jpg', ''); + const testElement = myp5.createVideo('/test/unit/assets/cat.webm', () => { + // Workaround for headless tests, where the video data isn't loading + // correctly: mock the video element using an image for this test + const prevElt = testElement.elt; + testElement.elt = imgElt.elt; + + myp5.background(255); + myp5.tint(255, 0, 0); + myp5.image(testElement, 0, 0); + + testElement.elt = prevElt; + imgElt.remove(); + + myp5.loadPixels(); + testElement.loadPixels(); + assert.equal(myp5.pixels[0], testElement.pixels[0]); + assert.equal(myp5.pixels[1], 0); + assert.equal(myp5.pixels[2], 0); + done(); + }); + }); + + test.todo('should work with updatePixels()', function(done) { + let loaded = false; + let prevElt; + const imgElt = myp5.createImg('/test/unit/assets/cat.jpg', ''); + const testElement = myp5.createVideo('/test/unit/assets/cat.webm', () => { + loaded = true; + // Workaround for headless tests, where the video data isn't loading + // correctly: mock the video element using an image for this test + prevElt = testElement.elt; + testElement.elt = imgElt.elt; + }); + + let drewUpdatedPixels = false; + myp5.draw = function() { + if (!loaded) return; + myp5.background(255); + + if (!drewUpdatedPixels) { + // First, update pixels and check that it draws the updated + // pixels correctly + testElement.loadPixels(); + for (let i = 0; i < testElement.pixels.length; i += 4) { + // Set every pixel to red + testElement.pixels[i] = 255; + testElement.pixels[i + 1] = 0; + testElement.pixels[i + 2] = 0; + testElement.pixels[i + 3] = 255; + } + testElement.updatePixels(); + myp5.image(testElement, 0, 0); + + // The element should have drawn using the updated red pixels + myp5.loadPixels(); + assert.deepEqual([...myp5.pixels.slice(0, 4)], [255, 0, 0, 255]); + + // Mark that we've done the first check so we can see whether + // the video still updates on the next frame + drewUpdatedPixels = true; + } else { + // Next, make sure it still updates with the real pixels from + // the next frame of the video on the next frame of animation + myp5.image(testElement, 0, 0); + + myp5.loadPixels(); + testElement.loadPixels(); + expect([...testElement.pixels.slice(0, 4)]) + .to.not.deep.equal([255, 0, 0, 255]); + assert.deepEqual( + [...myp5.pixels.slice(0, 4)], + [...testElement.pixels.slice(0, 4)] + ); + testElement.elt = prevElt; + imgElt.remove(); + done(); + } + }; + }); + }); + + suite('p5.prototype.createAudio', function() { + afterEach(function() { + document.body.innerHTML = ""; + }); + + const mediaSources = [ + '/test/unit/assets/beat.mp3', + '/test/unit/assets/beat.mp3' + ]; + + test('should be a function', function() { + assert.isFunction(mockP5Prototype.createAudio); + }); + + test('should return p5.Element of HTMLAudioElement', function() { + const testElement = mockP5Prototype.createAudio(''); + assert.instanceOf(testElement, MediaElement); + assert.instanceOf(testElement.elt, HTMLAudioElement); + }); + + test('should accept a singular media source', function() { + const mediaSource = mediaSources[0]; + const testElement = mockP5Prototype.createAudio(mediaSource); + const sourceEl = testElement.elt.children[0]; + + assert.deepEqual(testElement.elt.childElementCount, 1); + assert.instanceOf(sourceEl, HTMLSourceElement); + assert.isTrue(sourceEl.src.endsWith(mediaSource)); + }); + + test('should accept multiple media sources', function() { + const testElement = mockP5Prototype.createAudio(mediaSources); + + assert.deepEqual(testElement.elt.childElementCount, mediaSources.length); + for (let index = 0; index < mediaSources.length; index += 1) { + const sourceEl = testElement.elt.children[index]; + assert.instanceOf(sourceEl, HTMLSourceElement); + assert.isTrue(sourceEl.src.endsWith(mediaSources[index])); + } + }); + + // testSketchWithPromise( + // 'should trigger callback on canplaythrough event', + // function(sketch, resolve, reject) { + // sketch.setup = function() { + // testElement = mockP5Prototype.createAudio(mediaSources, resolve); + // testElement.elt.dispatchEvent(new Event('canplaythrough')); + // }; + // } + // ); + }); + + suite('p5.prototype.createCapture', function() { + afterEach(function() { + document.body.innerHTML = ""; + }); + + test('should be a function', function() { + assert.isFunction(mockP5Prototype.createCapture); + }); + + test('should return p5.Element of video type', function() { + const testElement = mockP5Prototype.createCapture(mockP5Prototype.VIDEO); + assert.instanceOf(testElement, Element); + assert.instanceOf(testElement.elt, HTMLVideoElement); + }); + + // NOTE: play() failed because the user didn't interact with the document first. + // testSketchWithPromise( + // 'triggers the callback after loading metadata', + // function(sketch, resolve, reject) { + // sketch.setup = function() { + // testElement = myp5.createCapture(myp5.VIDEO, resolve); + // const mockedEvent = new Event('loadedmetadata'); + // testElement.elt.dispatchEvent(mockedEvent); + // }; + // } + // ); + + // Required for ios 11 devices + test('should have playsinline attribute to empty string on DOM element', function() { + const testElement = mockP5Prototype.createCapture(mockP5Prototype.VIDEO); + // Weird check, setter accepts : playinline, getter accepts playInline + assert.isTrue(testElement.elt.playsInline); + }); + }); +}); diff --git a/test/unit/events/mouse.js b/test/unit/events/mouse.js index 0165d4199a..baa956925c 100644 --- a/test/unit/events/mouse.js +++ b/test/unit/events/mouse.js @@ -47,8 +47,8 @@ suite.todo('Mouse Events', function() { myp5.remove(); }); - let mouseEvent1 = new MouseEvent('mousemove', { clientX: 100, clientY: 100 }); - let mouseEvent2 = new MouseEvent('mousemove', { clientX: 200, clientY: 200 }); + let mouseEvent1 = new PointerEvent('pointermove', { clientX: 100, clientY: 100 }); + let mouseEvent2 = new PointerEvent('pointermove', { clientX: 200, clientY: 200 }); suite('p5.prototype._hasMouseInteracted', function() { test('_hasMouseInteracted should be a boolean', function() { @@ -223,17 +223,17 @@ suite.todo('Mouse Events', function() { }); test('mouseButton should be "left" on left mouse button click', function() { - window.dispatchEvent(new MouseEvent('mousedown', { button: 0 })); + window.dispatchEvent(new PointerEvent('pointerdown', { button: 0 })); assert.strictEqual(myp5.mouseButton, 'left'); }); test('mouseButton should be "center" on auxillary mouse button click', function() { - window.dispatchEvent(new MouseEvent('mousedown', { button: 1 })); + window.dispatchEvent(new PointerEvent('pointerdown', { button: 1 })); assert.strictEqual(myp5.mouseButton, 'center'); }); test('mouseButton should be "right" on right mouse button click', function() { - window.dispatchEvent(new MouseEvent('mousedown', { button: 2 })); + window.dispatchEvent(new PointerEvent('pointerdown', { button: 2 })); assert.strictEqual(myp5.mouseButton, 'right'); }); }); @@ -280,7 +280,7 @@ suite.todo('Mouse Events', function() { }; let sketches = parallelSketches([sketchFn, sketchFn]); //create two sketches await sketches.setup; //wait for all sketches to setup - window.dispatchEvent(new MouseEvent('mousemove')); //dispatch a mouse event to trigger the mouseMoved functions + window.dispatchEvent(new PointerEvent('pointermove')); //dispatch a mouse event to trigger the mouseMoved functions sketches.end(); //resolve all sketches by calling their finish functions let counts = await sketches.result; //get array holding number of times mouseMoved was called. Rejected sketches also thrown here assert.deepEqual(counts, [1, 1]); @@ -295,8 +295,8 @@ suite.todo('Mouse Events', function() { count += 1; }; - window.dispatchEvent(new MouseEvent('mousedown')); //dispatch a mousedown event - window.dispatchEvent(new MouseEvent('mousemove')); //dispatch mousemove event while mouse is down to trigger mouseDragged + window.dispatchEvent(new PointerEvent('pointerdown')); //dispatch a mousedown event + window.dispatchEvent(new PointerEvent('pointermove')); //dispatch mousemove event while mouse is down to trigger mouseDragged assert.deepEqual(count, 1); }); @@ -314,8 +314,8 @@ suite.todo('Mouse Events', function() { }; let sketches = parallelSketches([sketchFn, sketchFn]); //create two sketches await sketches.setup; //wait for all sketches to setup - window.dispatchEvent(new MouseEvent('mousedown')); //dispatch a mousedown event - window.dispatchEvent(new MouseEvent('mousemove')); //dispatch mousemove event while mouse is down to trigger mouseDragged + window.dispatchEvent(new PointerEvent('pointerdown')); //dispatch a mousedown event + window.dispatchEvent(new PointerEvent('pointermove')); //dispatch mousemove event while mouse is down to trigger mouseDragged sketches.end(); //resolve all sketches by calling their finish functions let counts = await sketches.result; //get array holding number of times mouseDragged was called. Rejected sketches also thrown here assert.deepEqual(counts, [1, 1]); @@ -330,7 +330,7 @@ suite.todo('Mouse Events', function() { count += 1; }; - window.dispatchEvent(new MouseEvent('mousedown')); + window.dispatchEvent(new PointerEvent('pointerdown')); assert.deepEqual(count, 1); }); @@ -348,7 +348,7 @@ suite.todo('Mouse Events', function() { }; let sketches = parallelSketches([sketchFn, sketchFn]); //create two sketches await sketches.setup; //wait for all sketches to setup - window.dispatchEvent(new MouseEvent('mousedown')); + window.dispatchEvent(new PointerEvent('pointerdown')); sketches.end(); //resolve all sketches by calling their finish functions let counts = await sketches.result; //get array holding number of times mouseDragged was called. Rejected sketches also thrown here assert.deepEqual(counts, [1, 1]); @@ -363,7 +363,7 @@ suite.todo('Mouse Events', function() { count += 1; }; - window.dispatchEvent(new MouseEvent('mouseup')); + window.dispatchEvent(new PointerEvent('pointerup')); assert.deepEqual(count, 1); }); @@ -381,7 +381,7 @@ suite.todo('Mouse Events', function() { }; let sketches = parallelSketches([sketchFn, sketchFn]); //create two sketches await sketches.setup; //wait for all sketches to setup - window.dispatchEvent(new MouseEvent('mouseup')); + window.dispatchEvent(new PointerEvent('pointerup')); sketches.end(); //resolve all sketches by calling their finish functions let counts = await sketches.result; //get array holding number of times mouseReleased was called. Rejected sketches also thrown here assert.deepEqual(counts, [1, 1]); diff --git a/test/unit/events/touch.js b/test/unit/events/touch.js index 72d3dd20c6..54677a59cc 100644 --- a/test/unit/events/touch.js +++ b/test/unit/events/touch.js @@ -4,9 +4,6 @@ import { parallelSketches } from '../../js/p5_helpers'; suite('Touch Events', function() { let myp5; - let canvas; - let touchObj1; - let touchObj2; let touchEvent1; let touchEvent2; @@ -14,24 +11,19 @@ suite('Touch Events', function() { new p5(function(p) { p.setup = function() { myp5 = p; - canvas = myp5._curElement.elt; - touchObj1 = new Touch({ - target: canvas, + touchEvent1 = new PointerEvent('pointerdown', { + pointerId: 1, clientX: 100, clientY: 100, - identifier: 36 + pointerType: 'touch' }); - touchObj2 = new Touch({ - target: canvas, + + // Simulate second touch event + touchEvent2 = new PointerEvent('pointerdown', { + pointerId: 2, clientX: 200, clientY: 200, - identifier: 35 - }); - touchEvent1 = new TouchEvent('touchmove', { - touches: [touchObj1, touchObj2] - }); - touchEvent2 = new TouchEvent('touchmove', { - touches: [touchObj2] + pointerType: 'touch' }); }; }); @@ -48,136 +40,12 @@ suite('Touch Events', function() { test('should be an array of multiple touches', function() { window.dispatchEvent(touchEvent1); + window.dispatchEvent(touchEvent2); assert.strictEqual(myp5.touches.length, 2); }); test('should contain the touch registered', function() { - window.dispatchEvent(touchEvent2); - assert.strictEqual(myp5.touches[0].id, 35); - }); - }); - - suite('touchStarted', function() { - test('touchStarted should be fired when a touch is registered', function() { - let count = 0; - myp5.touchStarted = function() { - count += 1; - }; - window.dispatchEvent(new TouchEvent('touchstart')); - assert.strictEqual(count, 1); - }); - - test('should be fired when a touch starts over the element', function() { - let count = 0; - let div = myp5.createDiv(); - let divTouchStarted = function() { - count += 1; - }; - div.touchStarted(divTouchStarted); - div.elt.dispatchEvent(new TouchEvent('touchstart')); - assert.strictEqual(count, 1); - }); - - // NOTE: Required review of parallel sketches test method - test('touchStarted functions on multiple instances must run once', async function() { - let sketchFn = function(sketch, resolve, reject) { - let count = 0; - sketch.touchStarted = function() { - count += 1; - }; - - sketch.finish = function() { - resolve(count); - }; - }; - let sketches = parallelSketches([sketchFn, sketchFn]); //create two sketches - await sketches.setup; //wait for all sketches to setup - window.dispatchEvent(new TouchEvent('touchstart')); - sketches.end(); //resolve all sketches by calling their finish functions - let counts = await sketches.result; - assert.deepEqual(counts, [1, 1]); - }); - }); - - suite('touchMoved', function() { - test('touchMoved should be fired when a touchmove is registered', function() { - let count = 0; - myp5.touchMoved = function() { - count += 1; - }; - window.dispatchEvent(touchEvent2); - assert.strictEqual(count, 1); - }); - - test('should be fired when a touchmove is registered over the element', function() { - let count = 0; - let div = myp5.createDiv(); - let divTouchMoved = function() { - count += 1; - }; - div.touchMoved(divTouchMoved); - div.elt.dispatchEvent(touchEvent2); - assert.strictEqual(count, 1); - }); - - test('touchMoved functions on multiple instances must run once', async function() { - let sketchFn = function(sketch, resolve, reject) { - let count = 0; - sketch.touchMoved = function() { - count += 1; - }; - - sketch.finish = function() { - resolve(count); - }; - }; - let sketches = parallelSketches([sketchFn, sketchFn]); //create two sketches - await sketches.setup; //wait for all sketches to setup - window.dispatchEvent(touchEvent2); - sketches.end(); //resolve all sketches by calling their finish functions - let counts = await sketches.result; - assert.deepEqual(counts, [1, 1]); - }); - }); - - suite('touchEnded', function() { - test('touchEnded must run when a touch is registered', function() { - let count = 0; - myp5.touchEnded = function() { - count += 1; - }; - window.dispatchEvent(new TouchEvent('touchend')); - assert.strictEqual(count, 1); - }); - - test('should be fired when a touch starts over the element', function() { - let count = 0; - let div = myp5.createDiv(); - let divTouchEnded = function() { - count += 1; - }; - div.touchEnded(divTouchEnded); - div.elt.dispatchEvent(new TouchEvent('touchend')); - assert.strictEqual(count, 1); - }); - - test('touchEnded functions on multiple instances must run once', async function() { - let sketchFn = function(sketch, resolve, reject) { - let count = 0; - sketch.touchEnded = function() { - count += 1; - }; - - sketch.finish = function() { - resolve(count); - }; - }; - let sketches = parallelSketches([sketchFn, sketchFn]); //create two sketches - await sketches.setup; //wait for all sketches to setup - window.dispatchEvent(new TouchEvent('touchend')); - sketches.end(); //resolve all sketches by calling their finish functions - let counts = await sketches.result; - assert.deepEqual(counts, [1, 1]); + assert.strictEqual(myp5.touches[0].id, 1); }); }); }); diff --git a/test/unit/typography/attributes.js b/test/unit/type/attributes.js similarity index 50% rename from test/unit/typography/attributes.js rename to test/unit/type/attributes.js index 73252efe15..01d9a39551 100644 --- a/test/unit/typography/attributes.js +++ b/test/unit/type/attributes.js @@ -1,73 +1,77 @@ import p5 from '../../../src/app.js'; suite('Typography Attributes', function() { - let myp5; + var myp5; - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; + beforeEach(function () { + myp5 = new p5(function (p) { + p.setup = function () { }; + p.draw = function () { }; }); }); - afterAll(function() { + afterEach(function () { myp5.remove(); }); - suite('p5.prototype.textLeading', function() { - test('sets and gets the spacing value', function() { + suite('textLeading', function() { + test('sets and gets the leading value', function() { myp5.textLeading(20); assert.strictEqual(myp5.textLeading(), 20); }); - test('should work for negative spacing value', function() { + test('should work for negative leadings', function() { myp5.textLeading(-20); assert.strictEqual(myp5.textLeading(), -20); }); }); - suite('p5.prototype.textSize', function() { - test('sets and gets the font size', function() { + suite('textSize', function() { + test('sets and gets the text size', function() { myp5.textSize(24); assert.strictEqual(myp5.textSize(), 24); }); }); - suite('p5.prototype.textStyle', function() { - test('sets and gets the font style', function() { + suite('textStyle', function() { + test('sets and gets the text style', function() { myp5.textStyle(myp5.ITALIC); assert.strictEqual(myp5.textStyle(), myp5.ITALIC); }); }); - suite('p5.prototype.textWidth', function() { + suite('textWidth', function() { test('should return a number for char input', function() { assert.isNumber(myp5.textWidth('P')); }); - test('should return a number for string input.', function() { + test('should return a number for string input.', function () { assert.isNumber(myp5.textWidth('p5.js')); }); // Either should not throw error test('should return a number for number input', function() { - assert.isNumber(myp5.textWidth('p5.js')); + assert.isNumber(myp5.textWidth(100)); }); }); - suite('p5.prototype.textAscent', function() { + suite('textAscent', function() { test('should return a number', function() { assert.isNumber(myp5.textAscent()); }); }); - suite('p5.prototype.textDescent', function() { + suite('textDescent', function() { test('should return a number', function() { assert.isNumber(myp5.textDescent()); }); }); - suite('p5.prototype.textWrap', function() { - test('returns textWrap text attribute', function() { - assert.strictEqual(myp5.textWrap(myp5.WORD), myp5.WORD); + suite('textWrap', function() { + test('gets the default text wrap attribute', function() { + assert.strictEqual(myp5.textWrap(), myp5.WORD); + }); + test('sets and gets the text wrap value', function() { + myp5.textWrap(myp5.CHAR); + assert.strictEqual(myp5.textWrap(), myp5.CHAR); }); + }); }); diff --git a/test/unit/type/loading.js b/test/unit/type/loading.js new file mode 100644 index 0000000000..c9c7d0ea5d --- /dev/null +++ b/test/unit/type/loading.js @@ -0,0 +1,47 @@ +import p5 from '../../../src/app.js'; + +suite('Loading Fonts', function () { + var myp5; + + beforeEach(function () { + myp5 = new p5(function (p) { + p.setup = function () { }; + p.draw = function () { }; + }); + }); + + afterEach(function () { + myp5.remove(); + }); + + // tests //////////////////////////////////////////////// + const fontFile = '/unit/assets/acmesa.ttf'; + + test('loadFont.await', async () => { + const pFont = await myp5.loadFont(fontFile, 'fredTheFont'); + assert.ok(pFont, 'acmesa.ttf loaded'); + assert.equal(pFont.name, 'fredTheFont'); + assert.isTrue(pFont instanceof p5.Font); + }); + + test('loadFont.then', async () => new Promise(done => { + + myp5.loadFont(fontFile, 'acmesa').then(pFont => { + assert.ok(pFont, 'acmesa.ttf loaded'); + assert.equal(pFont.name, 'acmesa'); + assert.isTrue(pFont instanceof p5.Font); + done(); + }); + + })); + + test.skip('loadFont.callback', async () => new Promise(done => { + myp5.loadFont(fontFile, (pFont) => { + assert.ok(pFont, 'acmesa.ttf loaded'); + assert.equal(pFont.name, 'A.C.M.E. Secret Agent'); + assert.isTrue(pFont instanceof p5.Font); + done(); + }); + })); + +}); diff --git a/test/unit/type/p5.Font.js b/test/unit/type/p5.Font.js new file mode 100644 index 0000000000..4428df7f86 --- /dev/null +++ b/test/unit/type/p5.Font.js @@ -0,0 +1,42 @@ +import p5 from '../../../src/app.js'; + +suite('p5.Font', function () { + var myp5; + + beforeEach(function () { + myp5 = new p5(function (p) { + p.setup = function () { }; + p.draw = function () { }; + }); + }); + + afterEach(function () { + myp5.remove(); + }); + + // tests //////////////////////////////////////////////// + const fontFile = '/unit/assets/acmesa.ttf'; + const textString = 'Lorem ipsum dolor sit amet.'; + + test('textBounds', async () => { + const pFont = await myp5.loadFont(fontFile); + let bbox = pFont.textBounds(textString, 10, 30, 12); + //console.log(bbox); + assert.isObject(bbox); + assert.property(bbox, 'x'); + assert.property(bbox, 'y'); + assert.property(bbox, 'w'); + assert.property(bbox, 'h'); + }); + + test('fontBounds', async () => { + const pFont = await myp5.loadFont(fontFile); + let bbox = pFont.fontBounds(textString, 10, 30, 12); + //console.log(bbox); + assert.isObject(bbox); + assert.property(bbox, 'x'); + assert.property(bbox, 'y'); + assert.property(bbox, 'w'); + assert.property(bbox, 'h'); + }); +}); diff --git a/test/unit/typography/loadFont.js b/test/unit/typography/loadFont.js deleted file mode 100644 index 0037403d76..0000000000 --- a/test/unit/typography/loadFont.js +++ /dev/null @@ -1,144 +0,0 @@ -import p5 from '../../../src/app.js'; -import { testSketchWithPromise, promisedSketch } from '../../js/p5_helpers'; - -suite('Loading Displaying Fonts', function() { - var myp5; - - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - - afterAll(function() { - myp5.remove(); - }); - - suite.todo('p5.prototype.loadFont', function() { - var invalidFile = '404file'; - var fontFile = 'manual-test-examples/p5.Font/acmesa.ttf'; - - testSketchWithPromise('error prevents sketch continuing', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadFont(invalidFile, reject, function() { - setTimeout(resolve, 50); - }); - }; - - sketch.setup = function() { - reject(new Error('Setup called')); - }; - - sketch.draw = function() { - reject(new Error('Draw called')); - }; - }); - - testSketchWithPromise('error callback is called', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadFont( - invalidFile, - function() { - reject(new Error('Success callback executed.')); - }, - function() { - // Wait a bit so that if both callbacks are executed we will get an error. - setTimeout(resolve, 50); - } - ); - }; - }); - - testSketchWithPromise('loading correctly triggers setup', function( - sketch, - resolve, - reject - ) { - sketch.preload = function() { - sketch.loadFont(fontFile); - }; - - sketch.setup = function() { - resolve(); - }; - }); - - testSketchWithPromise('success callback is called', function( - sketch, - resolve, - reject - ) { - var hasBeenCalled = false; - sketch.preload = function() { - sketch.loadFont( - fontFile, - function() { - hasBeenCalled = true; - }, - function(err) { - reject(new Error('Error callback was entered: ' + err)); - } - ); - }; - - sketch.setup = function() { - if (!hasBeenCalled) { - reject(new Error('Setup called prior to success callback')); - } else { - setTimeout(resolve, 50); - } - }; - }); - - test('returns a p5.Font object', async function() { - const font = await promisedSketch(function(sketch, resolve, reject) { - let _font; - sketch.preload = function() { - _font = sketch.loadFont(fontFile, function() {}, reject); - }; - - sketch.setup = function() { - resolve(_font); - }; - }); - assert.instanceOf(font, p5.Font); - }); - - test('passes a p5.Font object to success callback', async function() { - const font = await promisedSketch(function(sketch, resolve, reject) { - sketch.preload = function() { - sketch.loadFont(fontFile, resolve, reject); - }; - }); - assert.isObject(font); - }); - }); - - suite('p5.prototype.textFont', function() { - test('sets the current font as Georgia', function() { - myp5.textFont('Georgia'); - assert.strictEqual(myp5.textFont(), 'Georgia'); - }); - - test('sets the current font as Helvetica', function() { - myp5.textFont('Helvetica'); - assert.strictEqual(myp5.textFont(), 'Helvetica'); - }); - - test('sets the current font and text size', function() { - myp5.textFont('Courier New', 24); - assert.strictEqual(myp5.textFont(), 'Courier New'); - assert.strictEqual(myp5.textSize(), 24); - }); - }); -}); diff --git a/test/unit/typography/p5.Font.js b/test/unit/typography/p5.Font.js deleted file mode 100644 index 5b92936075..0000000000 --- a/test/unit/typography/p5.Font.js +++ /dev/null @@ -1,65 +0,0 @@ -import p5 from '../../../src/app.js'; -import { promisedSketch } from '../../js/p5_helpers'; - -suite.todo('p5.Font', function() { - var myp5; - - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - - afterAll(function() { - myp5.remove(); - }); - - suite('p5.Font.prototype.textBounds', function() { - test('returns a tight bounding box for the given text string', async function() { - let fontFile = 'manual-test-examples/p5.Font/acmesa.ttf'; - const bbox = await promisedSketch(function(sketch, resolve, reject) { - let _font; - let textString = 'Lorem ipsum dolor sit amet.'; - sketch.preload = function() { - _font = sketch.loadFont(fontFile, function() {}, reject); - }; - sketch.setup = function() { - let _bbox = _font.textBounds(textString, 10, 30, 12); - resolve(_bbox); - }; - }); - assert.isObject(bbox); - assert.property(bbox, 'x'); - assert.property(bbox, 'y'); - assert.property(bbox, 'w'); - assert.property(bbox, 'h'); - }); - }); - - suite('p5.Font.prototype.textToPoints', function() { - test('returns array of points', async function() { - let fontFile = 'manual-test-examples/p5.Font/acmesa.ttf'; - const points = await promisedSketch(function(sketch, resolve, reject) { - let _font; - sketch.preload = function() { - _font = sketch.loadFont(fontFile, function() {}, reject); - }; - sketch.setup = function() { - let _points = _font.textToPoints('p5', 0, 0, 10, { - sampleFactor: 5, - simplifyThreshold: 0 - }); - resolve(_points); - }; - }); - assert.isArray(points); - points.forEach(p => { - assert.property(p, 'x'); - assert.property(p, 'y'); - assert.property(p, 'alpha'); - }); - }); - }); -}); diff --git a/test/unit/utilities/array_functions.js b/test/unit/utilities/array_functions.js index 12ce6f0edc..c34e92715b 100644 --- a/test/unit/utilities/array_functions.js +++ b/test/unit/utilities/array_functions.js @@ -1,61 +1,53 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import arrayFunctions from '../../../src/utilities/array_functions'; +import random from '../../../src/math/random'; suite('Array', function() { - var myp5; - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - - afterAll(function() { - myp5.remove(); + arrayFunctions(mockP5, mockP5Prototype); + random(mockP5, mockP5Prototype); }); - var result; - suite('p5.prototype.append', function() { test('should be a function', function() { - assert.ok(myp5.append); - assert.typeOf(myp5.append, 'function'); + assert.ok(mockP5Prototype.append); + assert.typeOf(mockP5Prototype.append, 'function'); }); + test('should return an array with appended value', function() { - result = myp5.append([], 1); + const result = mockP5Prototype.append([], 1); assert.typeOf(result, 'Array'); assert.deepEqual(result, [1]); }); }); suite('p5.prototype.arrayCopy', function() { - var src, dest; + let src, dest; beforeEach(function() { src = [1, 2, 3, 4, 5]; dest = [6, 7, 8]; }); test('should be a function', function() { - assert.ok(myp5.arrayCopy); - assert.typeOf(myp5.arrayCopy, 'function'); + assert.ok(mockP5Prototype.arrayCopy); + assert.typeOf(mockP5Prototype.arrayCopy, 'function'); }); suite('src, dst', function() { test('should return fully copied array', function() { - myp5.arrayCopy(src, dest); + mockP5Prototype.arrayCopy(src, dest); assert.deepEqual(dest, src); }); }); suite('src, dst, len', function() { test('should return an array with first 2 elements copied over', function() { - myp5.arrayCopy(src, dest, 2); + mockP5Prototype.arrayCopy(src, dest, 2); assert.deepEqual(dest, [1, 2, 8]); }); test('should return an array with first 4 elements copied over', function() { - myp5.arrayCopy(src, dest, 4); + mockP5Prototype.arrayCopy(src, dest, 4); assert.deepEqual(dest, [1, 2, 3, 4]); }); }); @@ -63,17 +55,17 @@ suite('Array', function() { suite('src, srcPosition, dst, dstPosition, length', function() { // src[1 - 2] is src[1] and src[2] test('should copy src[1 - 2] to dst[0 - 1]', function() { - myp5.arrayCopy(src, 1, dest, 0, 2); + mockP5Prototype.arrayCopy(src, 1, dest, 0, 2); assert.deepEqual(dest, [2, 3, 8]); }); test('should copy src[1 - 2] to dst [1 - 2]', function() { - myp5.arrayCopy(src, 1, dest, 1, 2); + mockP5Prototype.arrayCopy(src, 1, dest, 1, 2); assert.deepEqual(dest, [6, 2, 3]); }); test('should copy src[3 - 4] to dst[0 - 1]', function() { - myp5.arrayCopy(src, 3, dest, 0, 2); + mockP5Prototype.arrayCopy(src, 3, dest, 0, 2); assert.deepEqual(dest, [4, 5, 8]); }); }); @@ -81,44 +73,44 @@ suite('Array', function() { suite('p5.prototype.concat', function() { test('should concat empty arrays', function() { - result = myp5.concat([], []); + const result = mockP5Prototype.concat([], []); assert.deepEqual(result, []); }); test('should concat arrays', function() { - result = myp5.concat([1], [2, 3]); + const result = mockP5Prototype.concat([1], [2, 3]); assert.deepEqual(result, [1, 2, 3]); }); }); suite('p5.prototype.reverse', function() { test('should reverse empty array', function() { - result = myp5.reverse([]); + const result = mockP5Prototype.reverse([]); assert.deepEqual(result, []); }); test('should reverse array', function() { - result = myp5.reverse([1, 2, 3]); + const result = mockP5Prototype.reverse([1, 2, 3]); assert.deepEqual(result, [3, 2, 1]); }); }); suite('p5.prototype.shorten', function() { test('should not have error for shortening empty array', function() { - result = myp5.shorten([]); + const result = mockP5Prototype.shorten([]); assert.deepEqual(result, []); }); test('should shorten array', function() { - result = myp5.shorten([1, 2, 3]); + const result = mockP5Prototype.shorten([1, 2, 3]); assert.deepEqual(result, [1, 2]); }); }); suite('p5.prototype.shuffle', function() { test('should contain all the elements of the original array', function() { - let regularArr = ['ABC', 'def', myp5.createVector(), myp5.TAU, Math.E]; - let newArr = myp5.shuffle(regularArr); + let regularArr = ['ABC', 'def', {}, Math.PI * 2, Math.E]; + let newArr = mockP5Prototype.shuffle(regularArr); let flag = true; for (let i = 0; i < regularArr.length; i++) { if (!newArr.includes(regularArr[i])) { @@ -134,46 +126,46 @@ suite('Array', function() { suite('p5.prototype.sort', function() { test('should not have error for sorting empty array', function() { - result = myp5.sort([]); + const result = mockP5Prototype.sort([]); assert.deepEqual(result, []); }); test('should sort alphabetic array lexicographically', function() { - result = myp5.sort(['c', 'b', 'a']); + const result = mockP5Prototype.sort(['c', 'b', 'a']); assert.deepEqual(result, ['a', 'b', 'c']); }); test('should sort numerical array from smallest to largest', function() { - result = myp5.sort([2, 1, 11]); + const result = mockP5Prototype.sort([2, 1, 11]); assert.deepEqual(result, [1, 2, 11]); }); test('should sort numerical array from smallest to largest for only first 2 elements', function() { - result = myp5.sort([3, 1, 2, 0], 2); + const result = mockP5Prototype.sort([3, 1, 2, 0], 2); assert.deepEqual(result, [1, 3, 2, 0]); }); }); suite('p5.prototype.splice', function() { test('should insert 4 into position 1', function() { - result = myp5.splice([1, 2, 3], 4, 1); + const result = mockP5Prototype.splice([1, 2, 3], 4, 1); assert.deepEqual(result, [1, 4, 2, 3]); }); test('should splice in array of values', function() { - result = myp5.splice([1, 2, 3], [4, 5], 1); + const result = mockP5Prototype.splice([1, 2, 3], [4, 5], 1); assert.deepEqual(result, [1, 4, 5, 2, 3]); }); }); suite('p5.prototype.subset', function() { test('should get subset from index 1 to end', function() { - result = myp5.subset([1, 2, 3], 1); + const result = mockP5Prototype.subset([1, 2, 3], 1); assert.deepEqual(result, [2, 3]); }); test('should subset arr[1 - 2]', function() { - result = myp5.subset([1, 2, 3, 4], 1, 2); + const result = mockP5Prototype.subset([1, 2, 3, 4], 1, 2); assert.deepEqual(result, [2, 3]); }); }); diff --git a/test/unit/utilities/conversion.js b/test/unit/utilities/conversion.js index 1d0318264a..a4a217cb9e 100644 --- a/test/unit/utilities/conversion.js +++ b/test/unit/utilities/conversion.js @@ -1,51 +1,40 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import conversion from '../../../src/utilities/conversion'; suite('Conversion', function() { - var myp5; - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); + conversion(mockP5, mockP5Prototype); }); - afterAll(function() { - myp5.remove(); - }); - - var result; - suite('p5.prototype.float', function() { test('should be a function', function() { - assert.ok(myp5.float); - assert.typeOf(myp5.float, 'function'); + assert.ok(mockP5Prototype.float); + assert.typeOf(mockP5Prototype.float, 'function'); }); test('should convert a string to its floating point representation', function() { - result = myp5.float('56.99998'); + const result = mockP5Prototype.float('56.99998'); assert.typeOf(result, 'Number'); assert.strictEqual(result, 56.99998); }); test('should return NaN for invalid string', function() { - result = myp5.float('cat'); + const result = mockP5Prototype.float('cat'); assert.isNaN(result); }); test('should return Infinity for Infinity', function() { - result = myp5.float(Infinity); + const result = mockP5Prototype.float(Infinity); assert.strictEqual(result, Infinity); }); test('should return -Infinity for -Infinity', function() { - result = myp5.float(-Infinity); + const result = mockP5Prototype.float(-Infinity); assert.strictEqual(result, -Infinity); }); test('should return array of floating points and Nan', function() { - result = myp5.float(['1', '2.0', '3.1', 'giraffe']); + const result = mockP5Prototype.float(['1', '2.0', '3.1', 'giraffe']); assert.typeOf(result, 'Array'); assert.deepEqual(result, [1, 2.0, 3.1, NaN]); }); @@ -53,48 +42,48 @@ suite('Conversion', function() { suite('p5.prototype.int', function() { test('should be a function', function() { - assert.ok(myp5.int); - assert.typeOf(myp5.int, 'function'); + assert.ok(mockP5Prototype.int); + assert.typeOf(mockP5Prototype.int, 'function'); }); test('should convert false to its integer representation i.e. 0', function() { - result = myp5.int(false); + const result = mockP5Prototype.int(false); assert.typeOf(result, 'Number'); assert.strictEqual(result, 0); }); test('should convert true to its integer representation i.e. 1', function() { - result = myp5.int(true); + const result = mockP5Prototype.int(true); assert.strictEqual(result, 1); }); test('should convert a string to its integer representation', function() { - result = myp5.int('1001'); + const result = mockP5Prototype.int('1001'); assert.strictEqual(result, 1001); }); test('should return NaN for invalid string', function() { - result = myp5.int('cat'); + const result = mockP5Prototype.int('cat'); assert.isNaN(result); }); test('should return Infinity for Infinity', function() { - result = myp5.int(Infinity); + const result = mockP5Prototype.int(Infinity); assert.strictEqual(result, Infinity); }); test('should return -Infinity for -Infinity', function() { - result = myp5.int(-Infinity); + const result = mockP5Prototype.int(-Infinity); assert.strictEqual(result, -Infinity); }); test('should convert float to its integer representation', function() { - result = myp5.int('-1001.9'); + const result = mockP5Prototype.int('-1001.9'); assert.strictEqual(result, -1001); }); test('should return array of integers and NaN', function() { - result = myp5.int(['1', '2.3', '-3.5', 'giraffe', false, 4.7]); + const result = mockP5Prototype.int(['1', '2.3', '-3.5', 'giraffe', false, 4.7]); assert.typeOf(result, 'Array'); assert.deepEqual(result, [1, 2, -3, NaN, 0, 4]); }); @@ -102,28 +91,28 @@ suite('Conversion', function() { suite('p5.prototype.str', function() { test('should be a function', function() { - assert.ok(myp5.str); - assert.typeOf(myp5.str, 'function'); + assert.ok(mockP5Prototype.str); + assert.typeOf(mockP5Prototype.str, 'function'); }); test('should convert false to string', function() { - result = myp5.str(false); + const result = mockP5Prototype.str(false); assert.typeOf(result, 'String'); assert.strictEqual(result, 'false'); }); test('should convert true to string', function() { - result = myp5.str(true); + const result = mockP5Prototype.str(true); assert.strictEqual(result, 'true'); }); test('should convert a number to string', function() { - result = myp5.str(45); + const result = mockP5Prototype.str(45); assert.strictEqual(result, '45'); }); test('should return array of strings', function() { - result = myp5.str([1, 2.3, true, -4.5]); + const result = mockP5Prototype.str([1, 2.3, true, -4.5]); assert.typeOf(result, 'Array'); assert.deepEqual(result, ['1', '2.3', 'true', '-4.5']); }); @@ -131,52 +120,52 @@ suite('Conversion', function() { suite('p5.prototype.boolean', function() { test('should be a function', function() { - assert.ok(myp5.boolean); - assert.typeOf(myp5.boolean, 'function'); + assert.ok(mockP5Prototype.boolean); + assert.typeOf(mockP5Prototype.boolean, 'function'); }); test('should convert 1 to true', function() { - result = myp5.boolean(1); + const result = mockP5Prototype.boolean(1); assert.strictEqual(result, true); }); test('should convert a number to true', function() { - result = myp5.boolean(154); + const result = mockP5Prototype.boolean(154); assert.strictEqual(result, true); }); test('should return true for Infinity', function() { - result = myp5.boolean(Infinity); + const result = mockP5Prototype.boolean(Infinity); assert.strictEqual(result, true); }); test('should convert 0 to false', function() { - result = myp5.boolean(0); + const result = mockP5Prototype.boolean(0); assert.strictEqual(result, false); }); test('should convert a string to false', function() { - result = myp5.boolean('1'); + const result = mockP5Prototype.boolean('1'); assert.strictEqual(result, false); }); test('should convert a string to false', function() { - result = myp5.boolean('0'); + const result = mockP5Prototype.boolean('0'); assert.strictEqual(result, false); }); test('should convert "true" to true', function() { - result = myp5.boolean('true'); + const result = mockP5Prototype.boolean('true'); assert.strictEqual(result, true); }); test('should return false for empty string', function() { - result = myp5.boolean(''); + const result = mockP5Prototype.boolean(''); assert.strictEqual(result, false); }); test('should return array of boolean', function() { - result = myp5.boolean([1, true, -4.5, Infinity, 'cat', '23']); + const result = mockP5Prototype.boolean([1, true, -4.5, Infinity, 'cat', '23']); assert.typeOf(result, 'Array'); assert.deepEqual(result, [true, true, true, true, false, false]); }); @@ -184,42 +173,42 @@ suite('Conversion', function() { suite('p5.prototype.byte', function() { test('should be a function', function() { - assert.ok(myp5.byte); - assert.typeOf(myp5.byte, 'function'); + assert.ok(mockP5Prototype.byte); + assert.typeOf(mockP5Prototype.byte, 'function'); }); test('should return 127 for 127', function() { - result = myp5.byte(127); + const result = mockP5Prototype.byte(127); assert.strictEqual(result, 127); }); test('should return -128 for 128', function() { - result = myp5.byte(128); + const result = mockP5Prototype.byte(128); assert.strictEqual(result, -128); }); test('should return 23 for 23.4', function() { - result = myp5.byte(23.4); + const result = mockP5Prototype.byte(23.4); assert.strictEqual(result, 23); }); test('should return 1 for true', function() { - result = myp5.byte(true); + const result = mockP5Prototype.byte(true); assert.strictEqual(result, 1); }); test('should return 23 for "23.4"', function() { - result = myp5.byte('23.4'); + const result = mockP5Prototype.byte('23.4'); assert.strictEqual(result, 23); }); test('should return NaN for invalid string', function() { - result = myp5.byte('cat'); + const result = mockP5Prototype.byte('cat'); assert.isNaN(result); }); test('should return array', function() { - result = myp5.byte([0, 255, '100']); + const result = mockP5Prototype.byte([0, 255, '100']); assert.typeOf(result, 'Array'); assert.deepEqual(result, [0, -1, 100]); }); @@ -227,23 +216,23 @@ suite('Conversion', function() { suite('p5.prototype.char', function() { test('should be a function', function() { - assert.ok(myp5.char); - assert.typeOf(myp5.char, 'function'); + assert.ok(mockP5Prototype.char); + assert.typeOf(mockP5Prototype.char, 'function'); }); test('should return the char representation of the number', function() { - result = myp5.char(65); + const result = mockP5Prototype.char(65); assert.typeOf(result, 'String'); assert.strictEqual(result, 'A'); }); test('should return the char representation of the string', function() { - result = myp5.char('65'); + const result = mockP5Prototype.char('65'); assert.strictEqual(result, 'A'); }); test('should return array', function() { - result = myp5.char([65, 66, '67']); + const result = mockP5Prototype.char([65, 66, '67']); assert.typeOf(result, 'Array'); assert.deepEqual(result, ['A', 'B', 'C']); }); @@ -251,18 +240,18 @@ suite('Conversion', function() { suite('p5.prototype.unchar', function() { test('should be a function', function() { - assert.ok(myp5.unchar); - assert.typeOf(myp5.unchar, 'function'); + assert.ok(mockP5Prototype.unchar); + assert.typeOf(mockP5Prototype.unchar, 'function'); }); test('should return the integer representation of char', function() { - result = myp5.unchar('A'); + const result = mockP5Prototype.unchar('A'); assert.typeOf(result, 'Number'); assert.strictEqual(result, 65); }); test('should return array of numbers', function() { - result = myp5.unchar(['A', 'B', 'C']); + const result = mockP5Prototype.unchar(['A', 'B', 'C']); assert.typeOf(result, 'Array'); assert.deepEqual(result, [65, 66, 67]); }); @@ -270,30 +259,30 @@ suite('Conversion', function() { suite('p5.prototype.hex', function() { test('should be a function', function() { - assert.ok(myp5.hex); - assert.typeOf(myp5.hex, 'function'); + assert.ok(mockP5Prototype.hex); + assert.typeOf(mockP5Prototype.hex, 'function'); }); test('should return the hex representation of the number', function() { - result = myp5.hex(65); + const result = mockP5Prototype.hex(65); assert.typeOf(result, 'String'); assert.strictEqual(result, '00000041'); }); test('should return FFFFFFFF for Infinity', function() { - result = myp5.hex(Infinity); + const result = mockP5Prototype.hex(Infinity); assert.typeOf(result, 'String'); assert.strictEqual(result, 'FFFFFFFF'); }); test('should return 00000000 for -Infinity', function() { - result = myp5.hex(-Infinity); + const result = mockP5Prototype.hex(-Infinity); assert.typeOf(result, 'String'); assert.strictEqual(result, '00000000'); }); test('should return array', function() { - result = myp5.hex([65, 66, 67]); + const result = mockP5Prototype.hex([65, 66, 67]); assert.typeOf(result, 'Array'); assert.deepEqual(result, ['00000041', '00000042', '00000043']); }); @@ -301,28 +290,28 @@ suite('Conversion', function() { suite('p5.prototype.unhex', function() { test('should be a function', function() { - assert.ok(myp5.unhex); - assert.typeOf(myp5.unchar, 'function'); + assert.ok(mockP5Prototype.unhex); + assert.typeOf(mockP5Prototype.unchar, 'function'); }); test('should return the integer representation of hex', function() { - result = myp5.unhex('00000041'); + const result = mockP5Prototype.unhex('00000041'); assert.typeOf(result, 'Number'); assert.strictEqual(result, 65); }); test('should return the NaN for empty string', function() { - result = myp5.unhex(''); + const result = mockP5Prototype.unhex(''); assert.isNaN(result); }); test('should return the NaN for invalid hex string', function() { - result = myp5.unhex('lorem'); + const result = mockP5Prototype.unhex('lorem'); assert.isNaN(result); }); test('should return array of numbers', function() { - result = myp5.unhex(['00000041', '00000042', '00000043']); + const result = mockP5Prototype.unhex(['00000041', '00000042', '00000043']); assert.typeOf(result, 'Array'); assert.deepEqual(result, [65, 66, 67]); }); diff --git a/test/unit/utilities/string_functions.js b/test/unit/utilities/string_functions.js index 232b11c4d0..a17eadfac3 100644 --- a/test/unit/utilities/string_functions.js +++ b/test/unit/utilities/string_functions.js @@ -1,213 +1,192 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import stringFunctions from '../../../src/utilities/string_functions'; suite('String functions', function() { - var myp5; - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); + stringFunctions(mockP5, mockP5Prototype); }); - afterAll(function() { - myp5.remove(); - }); - - var result; - suite('p5.prototype.join', function() { - var join = p5.prototype.join; test('should be a function', function() { - assert.ok(join); + assert.ok(mockP5Prototype.join); }); test('should return joined string', function() { var arr = ['foo', 'bar']; var sep = '-'; - result = myp5.join(arr, sep); + const result = mockP5Prototype.join(arr, sep); assert.equal(result, 'foo-bar'); }); }); suite('p5.prototype.match', function() { - var match = p5.prototype.match; test('should be a function', function() { - assert.ok(match); + assert.ok(mockP5Prototype.match); }); test('should return correct index of match strings', function() { var str = 'Where is the duckling in this ducky duck string?'; var regexp = 'duck'; - result = myp5.match(str, regexp); + const result = mockP5Prototype.match(str, regexp); assert.equal(result.index, 13); }); }); suite('p5.prototype.matchAll', function() { - var matchAll = p5.prototype.matchAll; test('should be a function', function() { - assert.ok(matchAll); + assert.ok(mockP5Prototype.matchAll); }); test('should return correct array of strings', function() { var str = 'Where is the duckling in this ducky duck string?'; var regexp = 'duck'; - result = myp5.matchAll(str, regexp); + const result = mockP5Prototype.matchAll(str, regexp); assert.equal(result.length, 3); }); }); suite('p5.prototype.nf', function() { - var nf = p5.prototype.nf; test('should be a function', function() { - assert.ok(nf); + assert.ok(mockP5Prototype.nf); }); test('should return correct string', function() { var num = 1234; - result = myp5.nf(num, 3); + const result = mockP5Prototype.nf(num, 3); assert.equal(result, '1234'); }); test('should return correct string', function() { var num = 1234; - result = myp5.nf(num, 5); + const result = mockP5Prototype.nf(num, 5); assert.equal(result, '01234'); }); test('should return correct string', function() { var num = 1234; - result = myp5.nf(num, 3, 3); + const result = mockP5Prototype.nf(num, 3, 3); assert.equal(result, '1234.000'); }); test('should return correct string', function() { var num = 3.141516; - result = myp5.nf(num, '2'); // automatic conversion? + const result = mockP5Prototype.nf(num, '2'); // automatic conversion? assert.equal(result, '03.141516'); }); test('should return correct string', function() { var num = 3.141516; - result = myp5.nf(num, '2', '2'); // automatic conversion? + const result = mockP5Prototype.nf(num, '2', '2'); // automatic conversion? assert.equal(result, '03.14'); }); test('should return correct string', function() { var num = 3.141516e-2; - result = myp5.nf(num, '3', '4'); // automatic conversion? + const result = mockP5Prototype.nf(num, '3', '4'); // automatic conversion? assert.equal(result, '000.0314'); }); test('should return correct string', function() { var num = 3.141516e7; - result = myp5.nf(num, '3', '4'); // automatic conversion? + const result = mockP5Prototype.nf(num, '3', '4'); // automatic conversion? assert.equal(result, '31415160.0000'); }); test('should return correct string', function() { var num = 123.45; - result = myp5.nf(num, 3, 0); + const result = mockP5Prototype.nf(num, 3, 0); assert.equal(result, '123'); }); }); suite('p5.prototype.nfc', function() { - var nfc = p5.prototype.nfc; test('should be a function', function() { - assert.ok(nfc); + assert.ok(mockP5Prototype.nfc); }); test('should return correct string', function() { var num = 32000; - result = myp5.nfc(num, 3); + const result = mockP5Prototype.nfc(num, 3); assert.equal(result, '32,000.000'); }); test('should return correct string', function() { var num = 32000; - result = myp5.nfc(num, '3'); // automatic conversion? + const result = mockP5Prototype.nfc(num, '3'); // automatic conversion? assert.equal(result, '32,000.000'); }); }); suite('p5.prototype.nfp', function() { - var nfp = p5.prototype.nfp; test('should be a function', function() { - assert.ok(nfp); + assert.ok(mockP5Prototype.nfp); }); test('should return correct string', function() { var num = -32000; - result = myp5.nfp(num, 3); + const result = mockP5Prototype.nfp(num, 3); assert.equal(result, '-32000'); }); test('should return correct string', function() { var num = 32000; - result = myp5.nfp(num, 3); // automatic conversion? + const result = mockP5Prototype.nfp(num, 3); // automatic conversion? assert.equal(result, '+32000'); }); }); suite('p5.prototype.nfs', function() { - var nfs = p5.prototype.nfs; test('should be a function', function() { - assert.ok(nfs); + assert.ok(mockP5Prototype.nfs); }); test('should return correct string', function() { var num = -32000; - result = myp5.nfs(num, 3); + const result = mockP5Prototype.nfs(num, 3); assert.equal(result, '-32000'); }); test('should return correct string', function() { var num = 32000; - result = myp5.nfs(num, 3); // automatic conversion? + const result = mockP5Prototype.nfs(num, 3); // automatic conversion? assert.equal(result, ' 32000'); }); }); suite('p5.prototype.split', function() { - var split = p5.prototype.split; test('should be a function', function() { - assert.ok(split); + assert.ok(mockP5Prototype.split); }); test('should return correct index of match strings', function() { var str = 'parsely, sage, rosemary, thyme'; var regexp = ','; - result = myp5.split(str, regexp); + const result = mockP5Prototype.split(str, regexp); assert.equal(result.length, 4); }); }); suite('p5.prototype.splitTokens', function() { - var splitTokens = p5.prototype.splitTokens; test('should be a function', function() { - assert.ok(splitTokens); + assert.ok(mockP5Prototype.splitTokens); }); test('should return correct index of match strings', function() { var str = 'parsely, sage, rosemary, thyme'; var regexp = ','; - result = myp5.splitTokens(str, regexp); + const result = mockP5Prototype.splitTokens(str, regexp); assert.equal(result.length, 4); }); }); suite('p5.prototype.trim', function() { - var trim = p5.prototype.trim; test('should be a function', function() { - assert.ok(trim); + assert.ok(mockP5Prototype.trim); }); test('should return correct strings', function() { var str = ' oh so roomy '; - result = myp5.trim(str); + const result = mockP5Prototype.trim(str); assert.equal(result, 'oh so roomy'); }); }); diff --git a/test/unit/utilities/time_date.js b/test/unit/utilities/time_date.js index b59e5b0696..189d7c8e68 100644 --- a/test/unit/utilities/time_date.js +++ b/test/unit/utilities/time_date.js @@ -1,30 +1,19 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import timeDate from '../../../src/utilities/time_date'; suite('time and date', function() { - var myp5; - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); + timeDate(mockP5, mockP5Prototype); }); - afterAll(function() { - myp5.remove(); - }); - - var result; - suite('p5.prototype.year', function() { test('should be a function', function() { - assert.ok(myp5.year); - assert.typeOf(myp5.year, 'function'); + assert.ok(mockP5Prototype.year); + assert.typeOf(mockP5Prototype.year, 'function'); }); test('should return this year', function() { - result = myp5.year(); + const result = mockP5Prototype.year(); var jsYear = new Date().getFullYear(); assert.equal(result, jsYear); }); @@ -32,25 +21,25 @@ suite('time and date', function() { suite('p5.prototype.day', function() { test('should be a function', function() { - assert.ok(myp5.day); - assert.typeOf(myp5.day, 'function'); + assert.ok(mockP5Prototype.day); + assert.typeOf(mockP5Prototype.day, 'function'); }); test('should return todays day', function() { var jsDay = new Date().getDate(); - result = myp5.day(); + const result = mockP5Prototype.day(); assert.equal(result, jsDay); }); }); suite('p5.prototype.month', function() { test('should be a function', function() { - assert.ok(myp5.month); - assert.typeOf(myp5.month, 'function'); + assert.ok(mockP5Prototype.month); + assert.typeOf(mockP5Prototype.month, 'function'); }); test("should return today's month", function() { - result = myp5.month(); + const result = mockP5Prototype.month(); var jsMonth = new Date().getMonth() + 1; assert.equal(result, jsMonth); }); @@ -58,39 +47,39 @@ suite('time and date', function() { suite('p5.prototype.hour', function() { test('should be a function', function() { - assert.ok(myp5.hour); - assert.typeOf(myp5.hour, 'function'); + assert.ok(mockP5Prototype.hour); + assert.typeOf(mockP5Prototype.hour, 'function'); }); test('should return this hour', function() { var jsHour = new Date().getHours(); - result = myp5.hour(); + const result = mockP5Prototype.hour(); assert.equal(result, jsHour); }); }); suite('p5.prototype.second', function() { test('should be a function', function() { - assert.ok(myp5.second); - assert.typeOf(myp5.second, 'function'); + assert.ok(mockP5Prototype.second); + assert.typeOf(mockP5Prototype.second, 'function'); }); test('should return this second', function() { var jsSecond = new Date().getSeconds(); - result = myp5.second(); + const result = mockP5Prototype.second(); assert.equal(result, jsSecond); //(Math.abs(jsSecond - result), '==', 0, 'everything is ok'); // in my testing, found this might be off by 1 second }); }); suite('p5.prototype.minute', function() { test('should be a function', function() { - assert.ok(myp5.minute); - assert.typeOf(myp5.minute, 'function'); + assert.ok(mockP5Prototype.minute); + assert.typeOf(mockP5Prototype.minute, 'function'); }); test('should return a number that is this minute', function() { var jsMinute = new Date().getMinutes(); - result = myp5.minute(); + const result = mockP5Prototype.minute(); assert.isNumber(result); assert.isNumber(jsMinute); assert.equal(result, jsMinute); @@ -99,33 +88,34 @@ suite('time and date', function() { suite('p5.prototype.millis', function() { test('should be a function', function() { - assert.ok(myp5.millis); - assert.typeOf(myp5.millis, 'function'); + assert.ok(mockP5Prototype.millis); + assert.typeOf(mockP5Prototype.millis, 'function'); }); test('result should be a number', function() { - assert.isNumber(myp5.millis()); + assert.isNumber(mockP5Prototype.millis()); }); - test('result should be greater than running time', function() { + // TODO: need to move internal state to module + test.todo('result should be greater than running time', function() { var runningTime = 50; var init_date = window.performance.now(); // wait :\ while (window.performance.now() - init_date <= runningTime) { /* no-op */ } - assert.operator(myp5.millis(), '>', runningTime, 'everything is ok'); + assert.operator(mockP5Prototype.millis(), '>', runningTime, 'everything is ok'); }); - test('result should be > newResult', function() { + test.todo('result should be > newResult', function() { var runningTime = 50; var init_date = Date.now(); - var result = myp5.millis(); + const result = mockP5Prototype.millis(); // wait :\ while (Date.now() - init_date <= runningTime) { /* no-op */ } - var newResult = myp5.millis(); + const newResult = mockP5Prototype.millis(); assert.operator(newResult, '>', result, 'everything is ok'); }); }); diff --git a/test/unit/visual/cases/shapes.js b/test/unit/visual/cases/shapes.js index 76eee44f0b..61f8c0d3d9 100644 --- a/test/unit/visual/cases/shapes.js +++ b/test/unit/visual/cases/shapes.js @@ -105,28 +105,60 @@ visualSuite('Shape drawing', function() { visualTest('Drawing with curves', function(p5, screenshot) { setup(p5); p5.beginShape(); - p5.curveVertex(10, 10); - p5.curveVertex(10, 10); - p5.curveVertex(15, 40); - p5.curveVertex(40, 35); - p5.curveVertex(25, 15); - p5.curveVertex(15, 25); - p5.curveVertex(15, 25); + p5.splineVertex(10, 10); + p5.splineVertex(15, 40); + p5.splineVertex(40, 35); + p5.splineVertex(25, 15); + p5.splineVertex(15, 25); p5.endShape(); screenshot(); }); + visualTest('Drawing with curves in the middle of other shapes', function(p5, screenshot) { + setup(p5); + p5.beginShape(); + p5.vertex(10, 10); + p5.vertex(40, 10); + p5.splineVertex(40, 40); + p5.splineVertex(10, 40); + p5.endShape(p5.CLOSE); + screenshot(); + }); + + visualTest('Drawing with curves with hidden ends', function(p5, screenshot) { + setup(p5); + p5.beginShape(); + p5.splineEnds(p5.EXCLUDE); + p5.splineVertex(10, 10); + p5.splineVertex(15, 40); + p5.splineVertex(40, 35); + p5.splineVertex(25, 15); + p5.splineVertex(15, 25); + p5.endShape(); + screenshot(); + }); + + visualTest('Drawing closed curves', function(p5, screenshot) { + setup(p5); + p5.beginShape(); + p5.splineVertex(10, 10); + p5.splineVertex(15, 40); + p5.splineVertex(40, 35); + p5.splineVertex(25, 15); + p5.splineVertex(15, 25); + p5.endShape(p5.CLOSE); + screenshot(); + }); + visualTest('Drawing with curves with tightness', function(p5, screenshot) { setup(p5); p5.curveTightness(0.5); p5.beginShape(); - p5.curveVertex(10, 10); - p5.curveVertex(10, 10); - p5.curveVertex(15, 40); - p5.curveVertex(40, 35); - p5.curveVertex(25, 15); - p5.curveVertex(15, 25); - p5.curveVertex(15, 25); + p5.splineVertex(10, 10); + p5.splineVertex(15, 40); + p5.splineVertex(40, 35); + p5.splineVertex(25, 15); + p5.splineVertex(15, 25); p5.endShape(); screenshot(); }); @@ -134,15 +166,16 @@ visualSuite('Shape drawing', function() { visualTest('Drawing closed curve loops', function(p5, screenshot) { setup(p5); p5.beginShape(); - p5.curveVertex(10, 10); - p5.curveVertex(15, 40); - p5.curveVertex(40, 35); - p5.curveVertex(25, 15); - p5.curveVertex(15, 25); + p5.splineEnds(p5.EXCLUDE); + p5.splineVertex(10, 10); + p5.splineVertex(15, 40); + p5.splineVertex(40, 35); + p5.splineVertex(25, 15); + p5.splineVertex(15, 25); // Repeat first 3 points - p5.curveVertex(10, 10); - p5.curveVertex(15, 40); - p5.curveVertex(40, 35); + p5.splineVertex(10, 10); + p5.splineVertex(15, 40); + p5.splineVertex(40, 35); p5.endShape(); screenshot(); }); @@ -168,6 +201,31 @@ visualSuite('Shape drawing', function() { screenshot(); }); + visualTest('Combining quadratic and cubic beziers', function (p5, screenshot) { + setup(p5); + p5.strokeWeight(5); + p5.beginShape(); + p5.vertex(10, 10); + p5.vertex(30, 10); + + // Default cubic + p5.bezierVertex(35, 10); + p5.bezierVertex(40, 15); + p5.bezierVertex(40, 20); + + p5.vertex(40, 30); + + p5.bezierOrder(2); + p5.bezierVertex(40, 40); + p5.bezierVertex(30, 40); + + p5.vertex(10, 40); + + p5.endShape(p5.CLOSE); + + screenshot(); + }); + visualTest('Drawing with points', function(p5, screenshot) { setup(p5); p5.strokeWeight(5); @@ -199,8 +257,8 @@ visualSuite('Shape drawing', function() { p5.vertex(15, 40); p5.vertex(40, 35); p5.vertex(25, 15); - p5.vertex(15, 25); p5.vertex(10, 10); + p5.vertex(15, 25); p5.endShape(); screenshot(); }); @@ -220,6 +278,65 @@ visualSuite('Shape drawing', function() { screenshot(); }); + visualTest('Drawing with a single closed contour', function(p5, screenshot) { + setup(p5); + p5.beginShape(); + p5.vertex(10, 10); + p5.vertex(40, 10); + p5.vertex(40, 40); + p5.vertex(10, 40); + + p5.beginContour(); + p5.vertex(20, 20); + p5.vertex(20, 30); + p5.vertex(30, 30); + p5.vertex(30, 20); + p5.endContour(p5.CLOSE); + + p5.endShape(p5.CLOSE); + screenshot(); + }); + + visualTest('Drawing with a single unclosed contour', function(p5, screenshot) { + setup(p5); + p5.beginShape(); + p5.vertex(10, 10); + p5.vertex(40, 10); + p5.vertex(40, 40); + p5.vertex(10, 40); + + p5.beginContour(); + p5.vertex(20, 20); + p5.vertex(20, 30); + p5.vertex(30, 30); + p5.vertex(30, 20); + p5.endContour(); + + p5.endShape(p5.CLOSE); + screenshot(); + }); + + visualTest('Drawing with every subshape in a contour', function(p5, screenshot) { + setup(p5); + p5.beginShape(); + p5.beginContour(); + p5.vertex(10, 10); + p5.vertex(40, 10); + p5.vertex(40, 40); + p5.vertex(10, 40); + p5.endContour(p5.CLOSE); + + p5.beginContour(); + p5.vertex(20, 20); + p5.vertex(20, 30); + p5.vertex(30, 30); + p5.vertex(30, 20); + p5.endContour(p5.CLOSE); + + p5.endShape(); + screenshot(); + }); + if (mode === 'WebGL') { visualTest('3D vertex coordinates', function(p5, screenshot) { setup(p5); @@ -287,7 +404,7 @@ visualSuite('Shape drawing', function() { screenshot(); }); - visualTest('Per-vertex fills', async function(p5, screenshot) { + visualTest('Per-vertex fills', function(p5, screenshot) { setup(p5); p5.beginShape(p5.QUAD_STRIP); p5.fill(0); @@ -303,7 +420,7 @@ visualSuite('Shape drawing', function() { screenshot(); }); - visualTest('Per-vertex strokes', async function(p5, screenshot) { + visualTest('Per-vertex strokes', function(p5, screenshot) { setup(p5); p5.strokeWeight(5); p5.beginShape(p5.QUAD_STRIP); @@ -320,7 +437,7 @@ visualSuite('Shape drawing', function() { screenshot(); }); - visualTest('Per-vertex normals', async function(p5, screenshot) { + visualTest('Per-vertex normals', function(p5, screenshot) { setup(p5); p5.normalMaterial(); p5.beginShape(p5.QUAD_STRIP); @@ -336,6 +453,41 @@ visualSuite('Shape drawing', function() { screenshot(); }); + + visualTest('Per-control point fills', function (p5, screenshot) { + setup(p5); + + p5.noStroke(); + p5.beginShape(); + p5.bezierOrder(2); + p5.fill('red'); + p5.vertex(10, 10); + p5.fill('lime'); + p5.bezierVertex(40, 25); + p5.fill('blue'); + p5.bezierVertex(10, 40); + p5.endShape(); + + screenshot(); + }); + + visualTest('Per-control point strokes', function (p5, screenshot) { + setup(p5); + + p5.noFill(); + p5.strokeWeight(5); + p5.beginShape(); + p5.bezierOrder(2); + p5.stroke('red'); + p5.vertex(10, 10); + p5.stroke('lime'); + p5.bezierVertex(40, 25); + p5.stroke('blue'); + p5.bezierVertex(10, 40); + p5.endShape(); + + screenshot(); + }); } }); } diff --git a/test/unit/visual/cases/typography.js b/test/unit/visual/cases/typography.js index 6066acfe83..826325d644 100644 --- a/test/unit/visual/cases/typography.js +++ b/test/unit/visual/cases/typography.js @@ -1,21 +1,321 @@ -import { visualSuite, visualTest } from '../visualTest'; +import { visualSuite, visualTest } from "../visualTest"; -visualSuite('Typography', function() { - visualSuite('textFont() with default fonts', function() { - visualTest('With the default font', function (p5, screenshot) { +visualSuite("Typography", function () { + visualSuite("textFont", function () { + visualTest("with the default font", function (p5, screenshot) { p5.createCanvas(50, 50); p5.textSize(20); p5.textAlign(p5.LEFT, p5.TOP); - p5.text('test', 0, 0); + p5.text("test", 0, 0); screenshot(); }); - visualTest('With the default monospace font', function (p5, screenshot) { + visualTest("with the default monospace font", function (p5, screenshot) { p5.createCanvas(50, 50); p5.textSize(20); - p5.textFont('monospace'); + p5.textFont("monospace"); p5.textAlign(p5.LEFT, p5.TOP); - p5.text('test', 0, 0); + p5.text("test", 0, 0); + screenshot(); + }); + }); + + visualSuite("textAlign", function () { // TEMPORARY SKIP + /*visualTest.skip("all alignments with single word", function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM }, + ]; + + p5.createCanvas(300, 80); + p5.textSize(60); + alignments.forEach((alignment) => { + p5.textAlign(alignment.alignX, alignment.alignY); + p5.text("Single Line", 0, 0); + const bb = p5.textBounds("Single Line", 0, 0); + p5.noFill(); + p5.stroke("red"); + p5.rect(bb.x, bb.y, bb.w, bb.h); + }) + screenshot(); + }); + + visualTest.skip("all alignments with single line", function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM }, + ]; + + p5.createCanvas(300, 80); + p5.textSize(60); + alignments.forEach((alignment) => { + p5.textAlign(alignment.alignX, alignment.alignY); + p5.text("Single Line", 0, 0); + const bb = p5.textBounds("Single Line", 0, 0); + p5.noFill(); + p5.stroke("red"); + p5.rect(bb.x, bb.y, bb.w, bb.h); + }); + screenshot(); + });*/ + + visualTest("all alignments with multi-lines and wrap word", + function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM }, + ]; + + p5.createCanvas(300, 200); + p5.textSize(20); + p5.textWrap(p5.WORD); + + let xPos = 20; + let yPos = 20; + const boxWidth = 100; + const boxHeight = 60; + + alignments.forEach((alignment, i) => { + if (i % 3 === 0 && i !== 0) { + yPos += 70; + xPos = 20; + } + + p5.textAlign(alignment.alignX, alignment.alignY); + + p5.noFill(); + p5.stroke(200); + p5.rect(xPos, yPos, boxWidth, boxHeight); + + p5.fill(0); + p5.text( + "A really long text that should wrap automatically as it reaches the end of the box", + xPos, + yPos, + boxWidth, + boxHeight + ); + const bb = p5.textBounds( + "A really long text that should wrap automatically as it reaches the end of the box", + xPos, + yPos, + boxWidth, + boxHeight + ); + p5.noFill(); + p5.stroke("red"); + p5.rect(bb.x, bb.y, bb.w, bb.h); + + xPos += 120; + }); + screenshot(); + } + ); + + visualTest( + "all alignments with multi-lines and wrap char", + function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM }, + ]; + + p5.createCanvas(300, 200); + p5.textSize(20); + p5.textWrap(p5.CHAR); + + let xPos = 20; + let yPos = 20; + const boxWidth = 100; + const boxHeight = 60; + + alignments.forEach((alignment, i) => { + if (i % 3 === 0 && i !== 0) { + yPos += 70; + xPos = 20; + } + + p5.textAlign(alignment.alignX, alignment.alignY); + + p5.noFill(); + p5.stroke(200); + p5.rect(xPos, yPos, boxWidth, boxHeight); + + p5.fill(0); + p5.text( + "A really long text that should wrap automatically as it reaches the end of the box", + xPos, + yPos, + boxWidth, + boxHeight + ); + const bb = p5.textBounds( + "A really long text that should wrap automatically as it reaches the end of the box", + xPos, + yPos, + boxWidth, + boxHeight + ); + p5.noFill(); + p5.stroke("red"); + p5.rect(bb.x, bb.y, bb.w, bb.h); + + xPos += 120; + }); + screenshot(); + } + ); + + visualTest( + "all alignments with multi-line manual text", + function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM }, + ]; + + p5.createCanvas(300, 200); + p5.textSize(20); + + let xPos = 20; + let yPos = 20; + const boxWidth = 100; + const boxHeight = 60; + + alignments.forEach((alignment, i) => { + if (i % 3 === 0 && i !== 0) { + yPos += 70; + xPos = 20; + } + + p5.textAlign(alignment.alignX, alignment.alignY); + + p5.noFill(); + p5.stroke(200); + p5.rect(xPos, yPos, boxWidth, boxHeight); + + p5.fill(0); + p5.text("Line 1\nLine 2\nLine 3", xPos, yPos, boxWidth, boxHeight); + const bb = p5.textBounds( + "Line 1\nLine 2\nLine 3", + xPos, + yPos, + boxWidth, + boxHeight + ); + p5.noFill(); + p5.stroke("red"); + p5.rect(bb.x, bb.y, bb.w, bb.h); + + xPos += 120; + }); + screenshot(); + } + ); + }); + + visualSuite("textStyle", function () { + visualTest("all text styles", function (p5, screenshot) { + p5.createCanvas(300, 100); + p5.textSize(20); + p5.textAlign(p5.LEFT, p5.TOP); + + p5.text("Regular Text", 0, 0); + p5.textStyle(p5.BOLD); + p5.text("Bold Text", 0, 30); + p5.textStyle(p5.ITALIC); + p5.text("Italic Text", 0, 60); + p5.textStyle(p5.BOLDITALIC); + p5.text("Bold Italic Text", 0, 90); + screenshot(); + }); + }); + + visualSuite("textSize", function () { + const units = ["px"]; + const sizes = [12, 16, 20, 24, 30]; + + visualTest("text sizes comparison", function (p5, screenshot) { + p5.createCanvas(300, 200); + let yOffset = 0; + + units.forEach((unit) => { + sizes.forEach((size) => { + p5.textSize(size); + p5.textAlign(p5.LEFT, p5.TOP); + p5.text(`Size: ${size}${unit}`, 0, yOffset); + yOffset += 30; + }); + }); + screenshot(); + }); + }); + + visualSuite("textLeading", function () { + visualTest("text leading with different values", function (p5, screenshot) { + p5.createCanvas(300, 200); + const leadingValues = [10, 20, 30]; + let yOffset = 0; + + p5.textSize(20); + p5.textAlign(p5.LEFT, p5.TOP); + + leadingValues.forEach((leading) => { + p5.textLeading(leading); + p5.text(`Leading: ${leading}`, 0, yOffset); + p5.text("This is a line of text.", 0, yOffset + 30); + p5.text("This is another line of text.", 0, yOffset + 30 + leading); + yOffset += 30 + leading; + }); + screenshot(); + }); + }); + + visualSuite("textWidth", function () { + visualTest("verify width of a string", function (p5, screenshot) { + p5.createCanvas(300, 100); + p5.textSize(20); + const text = "Width Test"; + const width = p5.textWidth(text); + p5.text(text, 0, 50); + p5.noFill(); + p5.stroke("red"); + p5.rect(0, 50 - 20, width, 20); screenshot(); }); }); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 2a3059a204..91bc52ddd2 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -151,12 +151,12 @@ visualSuite('WebGL', function() { outColor = vec4(vCol, 1.0); }`; visualTest( - 'on TESS shape mode', function(p5, screenshot) { + 'on PATH shape mode', function(p5, screenshot) { p5.createCanvas(50, 50, p5.WEBGL); p5.background('white'); const myShader = p5.createShader(vertSrc, fragSrc); p5.shader(myShader); - p5.beginShape(p5.TESS); + p5.beginShape(p5.PATH); p5.noStroke(); for (let i = 0; i < 20; i++){ let x = 20 * p5.sin(i/20*p5.TWO_PI); @@ -272,9 +272,9 @@ visualSuite('WebGL', function() { p5.strokeWeight(15); p5.line( -p5.width / 3, - p5.sin(p5.millis() * 0.001) * p5.height / 4, + p5.sin(0.2) * p5.height / 4, p5.width / 3, - p5.sin(p5.millis() * 0.001 + 1) * p5.height / 4 + p5.sin(1.2) * p5.height / 4 ); screenshot(); }); diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Combining quadratic and cubic beziers/000.png b/test/unit/visual/screenshots/Shape drawing/2D mode/Combining quadratic and cubic beziers/000.png new file mode 100644 index 0000000000..88a283cca1 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/2D mode/Combining quadratic and cubic beziers/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/vertexProperty/on TESS shape mode/metadata.json b/test/unit/visual/screenshots/Shape drawing/2D mode/Combining quadratic and cubic beziers/metadata.json similarity index 100% rename from test/unit/visual/screenshots/WebGL/vertexProperty/on TESS shape mode/metadata.json rename to test/unit/visual/screenshots/Shape drawing/2D mode/Combining quadratic and cubic beziers/metadata.json diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing closed curves/000.png b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing closed curves/000.png new file mode 100644 index 0000000000..9260c77d47 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing closed curves/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing closed curves/metadata.json b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing closed curves/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing closed curves/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single closed contour/000.png b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single closed contour/000.png new file mode 100644 index 0000000000..551b1d0380 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single closed contour/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single closed contour/metadata.json b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single closed contour/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single closed contour/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single unclosed contour/000.png b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single unclosed contour/000.png new file mode 100644 index 0000000000..e2c2b8aa95 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single unclosed contour/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single unclosed contour/metadata.json b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single unclosed contour/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with a single unclosed contour/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves in the middle of other shapes/000.png b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves in the middle of other shapes/000.png new file mode 100644 index 0000000000..3645b1eabc Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves in the middle of other shapes/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves in the middle of other shapes/metadata.json b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves in the middle of other shapes/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves in the middle of other shapes/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves with hidden ends/000.png b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves with hidden ends/000.png new file mode 100644 index 0000000000..7956ab38f6 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves with hidden ends/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves with hidden ends/metadata.json b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves with hidden ends/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves with hidden ends/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with every subshape in a contour/000.png b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with every subshape in a contour/000.png new file mode 100644 index 0000000000..551b1d0380 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with every subshape in a contour/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with every subshape in a contour/metadata.json b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with every subshape in a contour/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with every subshape in a contour/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Combining quadratic and cubic beziers/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Combining quadratic and cubic beziers/000.png new file mode 100644 index 0000000000..cff0b299d6 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Combining quadratic and cubic beziers/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Combining quadratic and cubic beziers/metadata.json b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Combining quadratic and cubic beziers/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Combining quadratic and cubic beziers/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing closed curves/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing closed curves/000.png new file mode 100644 index 0000000000..3a361da3d2 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing closed curves/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing closed curves/metadata.json b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing closed curves/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing closed curves/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single closed contour/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single closed contour/000.png new file mode 100644 index 0000000000..9d5d7ff4cf Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single closed contour/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single closed contour/metadata.json b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single closed contour/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single closed contour/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single unclosed contour/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single unclosed contour/000.png new file mode 100644 index 0000000000..fbc9111225 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single unclosed contour/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single unclosed contour/metadata.json b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single unclosed contour/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with a single unclosed contour/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves in the middle of other shapes/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves in the middle of other shapes/000.png new file mode 100644 index 0000000000..141baf4e29 Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves in the middle of other shapes/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves in the middle of other shapes/metadata.json b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves in the middle of other shapes/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves in the middle of other shapes/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with hidden ends/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with hidden ends/000.png new file mode 100644 index 0000000000..88984a153a Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with hidden ends/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with hidden ends/metadata.json b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with hidden ends/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with hidden ends/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with tightness/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with tightness/000.png index 70d1c096b2..8d123f745f 100644 Binary files a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with tightness/000.png and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with curves with tightness/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with every subshape in a contour/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with every subshape in a contour/000.png new file mode 100644 index 0000000000..9d5d7ff4cf Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with every subshape in a contour/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with every subshape in a contour/metadata.json b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with every subshape in a contour/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Drawing with every subshape in a contour/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point fills/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point fills/000.png new file mode 100644 index 0000000000..e07875af6e Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point fills/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point fills/metadata.json b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point fills/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point fills/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point strokes/000.png b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point strokes/000.png new file mode 100644 index 0000000000..b5d899f13a Binary files /dev/null and b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point strokes/000.png differ diff --git a/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point strokes/metadata.json b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point strokes/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Shape drawing/WebGL mode/Per-control point strokes/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-line manual text/000.png b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-line manual text/000.png new file mode 100644 index 0000000000..7687835897 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-line manual text/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-line manual text/metadata.json b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-line manual text/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-line manual text/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap char/000.png b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap char/000.png new file mode 100644 index 0000000000..6c7d403104 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap char/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap char/metadata.json b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap char/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap char/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap word/000.png b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap word/000.png new file mode 100644 index 0000000000..ccf569cf57 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap word/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap word/metadata.json b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap word/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textAlign/all alignments with multi-lines and wrap word/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textAlign/all alignments with single line/metadata.json b/test/unit/visual/screenshots/Typography/textAlign/all alignments with single line/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textAlign/all alignments with single line/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textAlign/all alignments with single word/000.png b/test/unit/visual/screenshots/Typography/textAlign/all alignments with single word/000.png new file mode 100644 index 0000000000..2c1e6d3bca Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textAlign/all alignments with single word/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textAlign/all alignments with single word/metadata.json b/test/unit/visual/screenshots/Typography/textAlign/all alignments with single word/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textAlign/all alignments with single word/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textFont/with the default font/000.png b/test/unit/visual/screenshots/Typography/textFont/with the default font/000.png new file mode 100644 index 0000000000..9412be3440 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textFont/with the default font/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textFont/with the default font/metadata.json b/test/unit/visual/screenshots/Typography/textFont/with the default font/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textFont/with the default font/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textFont/with the default monospace font/000.png b/test/unit/visual/screenshots/Typography/textFont/with the default monospace font/000.png new file mode 100644 index 0000000000..a88804ae71 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textFont/with the default monospace font/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textFont/with the default monospace font/metadata.json b/test/unit/visual/screenshots/Typography/textFont/with the default monospace font/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textFont/with the default monospace font/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textLeading/text leading with different values/000.png b/test/unit/visual/screenshots/Typography/textLeading/text leading with different values/000.png new file mode 100644 index 0000000000..40e44165a0 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textLeading/text leading with different values/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textLeading/text leading with different values/metadata.json b/test/unit/visual/screenshots/Typography/textLeading/text leading with different values/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textLeading/text leading with different values/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textSize/text sizes comparison/000.png b/test/unit/visual/screenshots/Typography/textSize/text sizes comparison/000.png new file mode 100644 index 0000000000..c98bbf4e94 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textSize/text sizes comparison/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textSize/text sizes comparison/metadata.json b/test/unit/visual/screenshots/Typography/textSize/text sizes comparison/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textSize/text sizes comparison/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textStyle/all text styles/000.png b/test/unit/visual/screenshots/Typography/textStyle/all text styles/000.png new file mode 100644 index 0000000000..e8d25e8f75 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textStyle/all text styles/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textStyle/all text styles/metadata.json b/test/unit/visual/screenshots/Typography/textStyle/all text styles/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textStyle/all text styles/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textWidth/verify width of a string/000.png b/test/unit/visual/screenshots/Typography/textWidth/verify width of a string/000.png new file mode 100644 index 0000000000..ea34acbf31 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textWidth/verify width of a string/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textWidth/verify width of a string/metadata.json b/test/unit/visual/screenshots/Typography/textWidth/verify width of a string/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textWidth/verify width of a string/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/3DModel/OBJ model with MTL file displays diffuse colors correctly/000.png b/test/unit/visual/screenshots/WebGL/3DModel/OBJ model with MTL file displays diffuse colors correctly/000.png index d3f8182293..ddc88b1044 100644 Binary files a/test/unit/visual/screenshots/WebGL/3DModel/OBJ model with MTL file displays diffuse colors correctly/000.png and b/test/unit/visual/screenshots/WebGL/3DModel/OBJ model with MTL file displays diffuse colors correctly/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/3DModel/Object with different texture coordinates per use of vertex keeps the coordinates intact/000.png b/test/unit/visual/screenshots/WebGL/3DModel/Object with different texture coordinates per use of vertex keeps the coordinates intact/000.png index e928fe3cb0..bbf407af53 100644 Binary files a/test/unit/visual/screenshots/WebGL/3DModel/Object with different texture coordinates per use of vertex keeps the coordinates intact/000.png and b/test/unit/visual/screenshots/WebGL/3DModel/Object with different texture coordinates per use of vertex keeps the coordinates intact/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/Lights/Fill color and default ambient material/000.png b/test/unit/visual/screenshots/WebGL/Lights/Fill color and default ambient material/000.png index a6ba205f7c..f6b1feb5b8 100644 Binary files a/test/unit/visual/screenshots/WebGL/Lights/Fill color and default ambient material/000.png and b/test/unit/visual/screenshots/WebGL/Lights/Fill color and default ambient material/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/000.png index bda8586d1c..135079c7f7 100644 Binary files a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/000.png and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/ImageShader/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/000.png b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/000.png index 0c8494d309..398d95eca4 100644 Binary files a/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/000.png and b/test/unit/visual/screenshots/WebGL/ShaderFunctionality/StrokeShader/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/vertexProperty/on PATH shape mode/000.png b/test/unit/visual/screenshots/WebGL/vertexProperty/on PATH shape mode/000.png new file mode 100644 index 0000000000..3dcaf9bc31 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/vertexProperty/on PATH shape mode/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/vertexProperty/on PATH shape mode/metadata.json b/test/unit/visual/screenshots/WebGL/vertexProperty/on PATH shape mode/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/vertexProperty/on PATH shape mode/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/vertexProperty/on TESS shape mode/000.png b/test/unit/visual/screenshots/WebGL/vertexProperty/on TESS shape mode/000.png deleted file mode 100644 index 7e43d3f58c..0000000000 Binary files a/test/unit/visual/screenshots/WebGL/vertexProperty/on TESS shape mode/000.png and /dev/null differ diff --git a/test/unit/visual/screenshots/WebGL/vertexProperty/on buildGeometry outputs containing 3D primitives/000.png b/test/unit/visual/screenshots/WebGL/vertexProperty/on buildGeometry outputs containing 3D primitives/000.png index 9a8353804d..065fd3b563 100644 Binary files a/test/unit/visual/screenshots/WebGL/vertexProperty/on buildGeometry outputs containing 3D primitives/000.png and b/test/unit/visual/screenshots/WebGL/vertexProperty/on buildGeometry outputs containing 3D primitives/000.png differ diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 5843e36d2f..25ccfe22fa 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -141,21 +141,21 @@ suite('p5.RendererGL', function() { test('check activate and deactivating fill and stroke', function() { myp5.noStroke(); assert( - !myp5._renderer.states.doStroke, + !myp5._renderer.states.strokeColor, 'stroke shader still active after noStroke()' ); - assert.isTrue( - myp5._renderer.states.doFill, + assert( + !myp5._renderer.states.doFill, 'fill shader deactivated by noStroke()' ); myp5.stroke(0); myp5.noFill(); assert( - myp5._renderer.states.doStroke, + !!myp5._renderer.states.strokeColor, 'stroke shader not active after stroke()' ); assert.isTrue( - !myp5._renderer.states.doFill, + !myp5._renderer.states.fillColor, 'fill shader still active after noFill()' ); }); @@ -626,18 +626,30 @@ suite('p5.RendererGL', function() { myp5.endContour(); myp5.endShape(myp5.CLOSE); myp5.loadPixels(); - return [...myp5.pixels]; + const img = myp5._renderer.canvas.toDataURL(); + return { pixels: [...myp5.pixels], img }; }; - assert.deepEqual(getColors(myp5.P2D), getColors(myp5.WEBGL)); + let ok = true; + const colors2D = getColors(myp5.P2D); + const colorsGL = getColors(myp5.WEBGL); + for (let i = 0; i < colors2D.pixels.length; i++) { + if (colors2D.pixels[i] !== colorsGL.pixels[i]) { + ok = false; + break; + } + } + if (!ok) { + throw new Error(`Expected match:\n\n2D: ${colors2D.img}\n\nWebGL: ${colorsGL.img}`); + } }); suite('text shader', function() { - test('rendering looks the same in WebGL1 and 2', function() { - myp5.loadFont('manual-test-examples/p5.Font/Inconsolata-Bold.ttf', function(font) { + test.todo('rendering looks the same in WebGL1 and 2', function() { + myp5.loadFont('/test/unit/assets/Inconsolata-Bold.ttf', function(font) { const webgl2 = myp5.createGraphics(100, 20, myp5.WEBGL); const webgl1 = myp5.createGraphics(100, 20, myp5.WEBGL); - webgl1.setAttributes({ version: 1 }); + webgl1.setAttributes({ version: 1 }); // no longer exists ? for (const graphic of [webgl1, webgl2]) { graphic.background(255); @@ -1453,33 +1465,33 @@ suite('p5.RendererGL', function() { test('QUADS mode converts into triangles', function() { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); myp5.textureMode(myp5.NORMAL); - renderer.beginShape(myp5.QUADS); - renderer.fill(255, 0, 0); - renderer.normal(0, 1, 2); - renderer.vertex(0, 0, 0, 0, 0); - renderer.fill(0, 255, 0); - renderer.normal(3, 4, 5); - renderer.vertex(0, 1, 1, 0, 1); - renderer.fill(0, 0, 255); - renderer.normal(6, 7, 8); - renderer.vertex(1, 0, 2, 1, 0); - renderer.fill(255, 0, 255); - renderer.normal(9, 10, 11); - renderer.vertex(1, 1, 3, 1, 1); - - renderer.fill(255, 0, 0); - renderer.normal(12, 13, 14); - renderer.vertex(2, 0, 4, 0, 0); - renderer.fill(0, 255, 0); - renderer.normal(15, 16, 17); - renderer.vertex(2, 1, 5, 0, 1); - renderer.fill(0, 0, 255); - renderer.normal(18, 19, 20); - renderer.vertex(3, 0, 6, 1, 0); - renderer.fill(255, 0, 255); - renderer.normal(21, 22, 23); - renderer.vertex(3, 1, 7, 1, 1); - renderer.endShape(); + myp5.beginShape(myp5.QUADS); + myp5.fill(255, 0, 0); + myp5.normal(0, 1, 2); + myp5.vertex(0, 0, 0, 0, 0); + myp5.fill(0, 255, 0); + myp5.normal(3, 4, 5); + myp5.vertex(0, 1, 1, 0, 1); + myp5.fill(0, 0, 255); + myp5.normal(6, 7, 8); + myp5.vertex(1, 0, 2, 1, 0); + myp5.fill(255, 0, 255); + myp5.normal(9, 10, 11); + myp5.vertex(1, 1, 3, 1, 1); + + myp5.fill(255, 0, 0); + myp5.normal(12, 13, 14); + myp5.vertex(2, 0, 4, 0, 0); + myp5.fill(0, 255, 0); + myp5.normal(15, 16, 17); + myp5.vertex(2, 1, 5, 0, 1); + myp5.fill(0, 0, 255); + myp5.normal(18, 19, 20); + myp5.vertex(3, 0, 6, 1, 0); + myp5.fill(255, 0, 255); + myp5.normal(21, 22, 23); + myp5.vertex(3, 1, 7, 1, 1); + myp5.endShape(); const expectedVerts = [ [0, 0, 0], @@ -1579,33 +1591,33 @@ suite('p5.RendererGL', function() { test('QUADS mode makes edges for quad outlines', function() { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); - renderer.beginShape(myp5.QUADS); - renderer.vertex(0, 0); - renderer.vertex(0, 1); - renderer.vertex(1, 0); - renderer.vertex(1, 1); - - renderer.vertex(2, 0); - renderer.vertex(2, 1); - renderer.vertex(3, 0); - renderer.vertex(3, 1); - renderer.endShape(); + myp5.beginShape(myp5.QUADS); + myp5.vertex(0, 0); + myp5.vertex(0, 1); + myp5.vertex(1, 0); + myp5.vertex(1, 1); + + myp5.vertex(2, 0); + myp5.vertex(2, 1); + myp5.vertex(3, 0); + myp5.vertex(3, 1); + myp5.endShape(); assert.equal(renderer.shapeBuilder.geometry.edges.length, 8); }); test('QUAD_STRIP mode makes edges for strip outlines', function() { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); - renderer.beginShape(myp5.QUAD_STRIP); - renderer.vertex(0, 0); - renderer.vertex(0, 1); - renderer.vertex(1, 0); - renderer.vertex(1, 1); - renderer.vertex(2, 0); - renderer.vertex(2, 1); - renderer.vertex(3, 0); - renderer.vertex(3, 1); - renderer.endShape(); + myp5.beginShape(myp5.QUAD_STRIP); + myp5.vertex(0, 0); + myp5.vertex(0, 1); + myp5.vertex(1, 0); + myp5.vertex(1, 1); + myp5.vertex(2, 0); + myp5.vertex(2, 1); + myp5.vertex(3, 0); + myp5.vertex(3, 1); + myp5.endShape(); // Two full quads (2 * 4) plus two edges connecting them assert.equal(renderer.shapeBuilder.geometry.edges.length, 10); @@ -1618,39 +1630,39 @@ suite('p5.RendererGL', function() { // x--x--x // \ | / // x - renderer.beginShape(myp5.TRIANGLE_FAN); - renderer.vertex(0, 0); - renderer.vertex(0, -5); - renderer.vertex(5, 0); - renderer.vertex(0, 5); - renderer.vertex(-5, 0); - renderer.endShape(); + myp5.beginShape(myp5.TRIANGLE_FAN); + myp5.vertex(0, 0); + myp5.vertex(0, -5); + myp5.vertex(5, 0); + myp5.vertex(0, 5); + myp5.vertex(-5, 0); + myp5.endShape(); assert.equal(renderer.shapeBuilder.geometry.edges.length, 7); }); - test('TESS preserves vertex data', function() { + test('PATH preserves vertex data', function() { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); myp5.textureMode(myp5.NORMAL); - renderer.beginShape(myp5.TESS); - renderer.fill(255, 255, 255); - renderer.normal(-1, -1, 1); - renderer.vertexProperty('aCustom', [1, 1, 1]) - renderer.vertex(-10, -10, 0, 0); - renderer.fill(255, 0, 0); - renderer.normal(1, -1, 1); - renderer.vertexProperty('aCustom', [1, 0, 0]) - renderer.vertex(10, -10, 1, 0); - renderer.fill(0, 255, 0); - renderer.normal(1, 1, 1); - renderer.vertexProperty('aCustom', [0, 1, 0]) - renderer.vertex(10, 10, 1, 1); - renderer.fill(0, 0, 255); - renderer.normal(-1, 1, 1); - renderer.vertexProperty('aCustom', [0, 0, 1]) - renderer.vertex(-10, 10, 0, 1); - renderer.endShape(myp5.CLOSE); + myp5.beginShape(myp5.PATH); + myp5.fill(255, 255, 255); + myp5.normal(-1, -1, 1); + myp5.vertexProperty('aCustom', [1, 1, 1]) + myp5.vertex(-10, -10, 0, 0); + myp5.fill(255, 0, 0); + myp5.normal(1, -1, 1); + myp5.vertexProperty('aCustom', [1, 0, 0]) + myp5.vertex(10, -10, 1, 0); + myp5.fill(0, 255, 0); + myp5.normal(1, 1, 1); + myp5.vertexProperty('aCustom', [0, 1, 0]) + myp5.vertex(10, 10, 1, 1); + myp5.fill(0, 0, 255); + myp5.normal(-1, 1, 1); + myp5.vertexProperty('aCustom', [0, 0, 1]) + myp5.vertex(-10, 10, 0, 1); + myp5.endShape(myp5.CLOSE); assert.equal(renderer.shapeBuilder.geometry.vertices.length, 6); assert.deepEqual( @@ -1732,55 +1744,59 @@ suite('p5.RendererGL', function() { ]); }); - test('TESS does not affect stroke colors', function() { + test('PATH does not affect stroke colors', function() { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); myp5.textureMode(myp5.NORMAL); - renderer.beginShape(myp5.TESS); + myp5.beginShape(myp5.PATH); myp5.noFill(); - renderer.stroke(255, 255, 255); - renderer.vertex(-10, -10, 0, 0); - renderer.stroke(255, 0, 0); - renderer.vertex(10, -10, 1, 0); - renderer.stroke(0, 255, 0); - renderer.vertex(10, 10, 1, 1); - renderer.stroke(0, 0, 255); - renderer.vertex(-10, 10, 0, 1); - renderer.endShape(myp5.CLOSE); - - // Vertex colors are not run through tessy + myp5.stroke(255, 255, 255); + myp5.vertex(-10, -10, 0, 0); + myp5.stroke(255, 0, 0); + myp5.vertex(10, -10, 1, 0); + myp5.stroke(0, 255, 0); + myp5.vertex(10, 10, 1, 1); + myp5.stroke(0, 0, 255); + myp5.vertex(-10, 10, 0, 1); + myp5.endShape(myp5.CLOSE); + + // Vertex stroke colors are not run through libtess assert.deepEqual(renderer.shapeBuilder.geometry.vertexStrokeColors, [ 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, - 0, 0, 1, 1 + 0, 0, 1, 1, + 1, 1, 1, 1, ]); }); - test('TESS does not affect texture coordinates', function() { + test('PATH does not affect texture coordinates', function() { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); const texture = new p5.Image(25, 25); myp5.textureMode(myp5.IMAGE); myp5.texture(texture); - renderer.beginShape(myp5.TESS); + myp5.beginShape(myp5.PATH); myp5.noFill(); - renderer.vertex(-10, -10, 0, 0); - renderer.vertex(10, -10, 25, 0); - renderer.vertex(10, 10, 25, 25); - renderer.vertex(-10, 10, 0, 25); - renderer.endShape(myp5.CLOSE); + myp5.vertex(-10, -10, 0, 0); + myp5.vertex(10, -10, 25, 0); + myp5.vertex(10, 10, 25, 25); + myp5.vertex(-10, 10, 0, 25); + myp5.endShape(myp5.CLOSE); - // UVs are correctly translated through tessy + // UVs are correctly translated through libtess assert.deepEqual(renderer.shapeBuilder.geometry.uvs, [ + 1, 0, + 0, 1, 0, 0, + + 0, 1, 1, 0, - 1, 1, - 0, 1 + 1, 1 ]); }); - test('TESS interpolates vertex data at intersections', function() { + test('PATH interpolates vertex data at intersections', function() { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); // Hourglass shape: @@ -1794,20 +1810,20 @@ suite('p5.RendererGL', function() { // // Tessy will add a vertex in the middle myp5.textureMode(myp5.NORMAL); - renderer.beginShape(myp5.TESS); - renderer.fill(255, 255, 255); - renderer.normal(-1, -1, 1); - renderer.vertex(-10, -10, 0, 0); - renderer.fill(0, 255, 0); - renderer.normal(1, 1, 1); - renderer.vertex(10, 10, 1, 1); - renderer.fill(255, 0, 0); - renderer.normal(1, -1, 1); - renderer.vertex(10, -10, 1, 0); - renderer.fill(0, 0, 255); - renderer.normal(-1, 1, 1); - renderer.vertex(-10, 10, 0, 1); - renderer.endShape(myp5.CLOSE); + myp5.beginShape(myp5.PATH); + myp5.fill(255, 255, 255); + myp5.normal(-1, -1, 1); + myp5.vertex(-10, -10, 0, 0); + myp5.fill(0, 255, 0); + myp5.normal(1, 1, 1); + myp5.vertex(10, 10, 1, 1); + myp5.fill(255, 0, 0); + myp5.normal(1, -1, 1); + myp5.vertex(10, -10, 1, 0); + myp5.fill(0, 0, 255); + myp5.normal(-1, 1, 1); + myp5.vertex(-10, 10, 0, 1); + myp5.endShape(myp5.CLOSE); assert.equal(renderer.shapeBuilder.geometry.vertices.length, 6); assert.deepEqual( @@ -1880,16 +1896,16 @@ suite('p5.RendererGL', function() { ]); }); - test('TESS handles vertex data perpendicular to the camera', function() { + test('PATH handles vertex data perpendicular to the camera', function() { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); myp5.textureMode(myp5.NORMAL); - renderer.beginShape(myp5.TESS); - renderer.vertex(-10, 0, -10); - renderer.vertex(10, 0, -10); - renderer.vertex(10, 0, 10); - renderer.vertex(-10, 0, 10); - renderer.endShape(myp5.CLOSE); + myp5.beginShape(myp5.PATH); + myp5.vertex(-10, 0, -10); + myp5.vertex(10, 0, -10); + myp5.vertex(10, 0, 10); + myp5.vertex(-10, 0, 10); + myp5.endShape(myp5.CLOSE); assert.equal(renderer.shapeBuilder.geometry.vertices.length, 6); assert.deepEqual( @@ -1927,91 +1943,19 @@ suite('p5.RendererGL', function() { // far right color: (42, 36, 240) // expected middle color: (142, 136, 140) - renderer.strokeWeight(4); - renderer.beginShape(); - renderer.stroke(242, 236, 40); - renderer.vertex(-256, 0); - renderer.stroke(42, 36, 240); - renderer.vertex(256, 0); - renderer.endShape(); + myp5.strokeWeight(4); + myp5.beginShape(); + myp5.stroke(242, 236, 40); + myp5.vertex(-256, 0); + myp5.stroke(42, 36, 240); + myp5.vertex(256, 0); + myp5.endShape(); assert.deepEqual(myp5.get(0, 2), [242, 236, 40, 255]); assert.deepEqual(myp5.get(256, 2), [142, 136, 140, 255]); assert.deepEqual(myp5.get(511, 2), [42, 36, 240, 255]); }); - test('bezierVertex() should interpolate curFillColor', function() { - const renderer = myp5.createCanvas(256, 256, myp5.WEBGL); - - // start color: (255, 255, 255) - // end color: (255, 0, 0) - // Intermediate values are expected to be approximately half the value. - - renderer.beginShape(); - renderer.fill(255); - renderer.vertex(-128, -128); - renderer.fill(255, 0, 0); - renderer.bezierVertex(128, -128, 128, 128, -128, 128); - renderer.endShape(); - - assert.deepEqual(myp5.get(128, 127), [255, 129, 129, 255]); - }); - - test('bezierVertex() should interpolate curStrokeColor', function() { - const renderer = myp5.createCanvas(256, 256, myp5.WEBGL); - - // start color: (255, 255, 255) - // end color: (255, 0, 0) - // Intermediate values are expected to be approximately half the value. - - renderer.strokeWeight(5); - renderer.beginShape(); - myp5.noFill(); - renderer.stroke(255); - renderer.vertex(-128, -128); - renderer.stroke(255, 0, 0); - renderer.bezierVertex(128, -128, 128, 128, -128, 128); - renderer.endShape(); - - assert.arrayApproximately(myp5.get(190, 127), [255, 128, 128, 255], 10); - }); - - test('quadraticVertex() should interpolate curFillColor', function() { - const renderer = myp5.createCanvas(256, 256, myp5.WEBGL); - - // start color: (255, 255, 255) - // end color: (255, 0, 0) - // Intermediate values are expected to be approximately half the value. - - renderer.beginShape(); - renderer.fill(255); - renderer.vertex(-128, -128); - renderer.fill(255, 0, 0); - renderer.quadraticVertex(256, 0, -128, 128); - renderer.endShape(); - - assert.arrayApproximately(myp5.get(128, 127), [255, 128, 128, 255], 10); - }); - - test('quadraticVertex() should interpolate curStrokeColor', function() { - const renderer = myp5.createCanvas(256, 256, myp5.WEBGL); - - // start color: (255, 255, 255) - // end color: (255, 0, 0) - // Intermediate values are expected to be approximately half the value. - - renderer.strokeWeight(5); - renderer.beginShape(); - myp5.noFill(); - renderer.stroke(255); - renderer.vertex(-128, -128); - renderer.stroke(255, 0, 0); - renderer.quadraticVertex(256, 0, -128, 128); - renderer.endShape(); - - assert.deepEqual(myp5.get(190, 127), [255, 128, 128, 255]); - }); - test('geometry without stroke colors use curStrokeColor', function() { const renderer = myp5.createCanvas(256, 256, myp5.WEBGL); myp5.background(255); @@ -2568,13 +2512,18 @@ suite('p5.RendererGL', function() { function() { myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.noStroke(); myp5.beginShape(); myp5.vertexProperty('aCustom', 1); myp5.vertexProperty('aCustomVec3', [1, 2, 3]); myp5.vertex(0,0,0); + myp5.vertex(0,1,0); + myp5.vertex(1,1,0); + myp5.endShape(); + expect(myp5._renderer.shapeBuilder.geometry.userVertexProperties.aCustom).to.containSubset({ name: 'aCustom', - currentData: 1, + currentData: [1], dataSize: 1 }); expect(myp5._renderer.shapeBuilder.geometry.userVertexProperties.aCustomVec3).to.containSubset({ @@ -2582,23 +2531,6 @@ suite('p5.RendererGL', function() { currentData: [1, 2, 3], dataSize: 3 }); - assert.deepEqual(myp5._renderer.shapeBuilder.geometry.aCustomSrc, [1]); - assert.deepEqual(myp5._renderer.shapeBuilder.geometry.aCustomVec3Src, [1,2,3]); - expect(myp5._renderer.buffers.user).to.containSubset([ - { - size: 1, - src: 'aCustomSrc', - dst: 'aCustomBuffer', - attr: 'aCustom', - }, - { - size: 3, - src: 'aCustomVec3Src', - dst: 'aCustomVec3Buffer', - attr: 'aCustomVec3', - } - ]); - myp5.endShape(); } ); test('Immediate mode data and buffers deleted after beginShape', @@ -2612,28 +2544,27 @@ suite('p5.RendererGL', function() { myp5.endShape(); myp5.beginShape(); + myp5.endShape(); assert.isUndefined(myp5._renderer.shapeBuilder.geometry.aCustomSrc); assert.isUndefined(myp5._renderer.shapeBuilder.geometry.aCustomVec3Src); assert.deepEqual(myp5._renderer.shapeBuilder.geometry.userVertexProperties, {}); assert.deepEqual(myp5._renderer.buffers.user, []); - myp5.endShape(); } ); test('Data copied over from beginGeometry', function() { myp5.createCanvas(50, 50, myp5.WEBGL); - myp5.beginGeometry(); - myp5.beginShape(); - myp5.vertexProperty('aCustom', 1); - myp5.vertexProperty('aCustomVec3', [1,2,3]); - myp5.vertex(0,1,0); - myp5.vertex(-1,0,0); - myp5.vertex(1,0,0); - const immediateCopy = myp5._renderer.shapeBuilder.geometry; - myp5.endShape(); - const myGeo = myp5.endGeometry(); - assert.deepEqual(immediateCopy.aCustomSrc, myGeo.aCustomSrc); - assert.deepEqual(immediateCopy.aCustomVec3Src, myGeo.aCustomVec3Src); + const myGeo = myp5.buildGeometry(() => { + myp5.beginShape(); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); + myp5.vertex(0,1,0); + myp5.vertex(-1,0,0); + myp5.vertex(1,0,0); + myp5.endShape(); + }); + assert.deepEqual(myGeo.aCustomSrc, [1,1,1]); + assert.deepEqual(myGeo.aCustomVec3Src, [1,2,3,1,2,3,1,2,3]); } ); test('Retained mode buffers are created for rendering', @@ -2663,15 +2594,15 @@ suite('p5.RendererGL', function() { } try { - myp5.beginGeometry(); - myp5.beginShape(); - myp5.vertexProperty('aCustom', 1); - myp5.vertexProperty('aCustomVec3', [1,2,3]); - myp5.vertex(0,0,0); - myp5.vertex(1,0,0); - myp5.vertex(1,1,0); - myp5.endShape(); - const myGeo = myp5.endGeometry(); + const myGeo = myp5.buildGeometry(() => { + myp5.beginShape(); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); + myp5.vertex(0,0,0); + myp5.vertex(1,0,0); + myp5.vertex(1,1,0); + myp5.endShape(); + }); myp5.model(myGeo); expect(called).to.equal(true); } finally { @@ -2682,15 +2613,15 @@ suite('p5.RendererGL', function() { test('Retained mode buffers deleted after rendering', function() { myp5.createCanvas(50, 50, myp5.WEBGL); - myp5.beginGeometry(); - myp5.beginShape(); - myp5.vertexProperty('aCustom', 1); - myp5.vertexProperty('aCustomVec3', [1,2,3]); - myp5.vertex(0,0,0); - myp5.vertex(1,0,0); - myp5.vertex(1,1,0); - myp5.endShape(); - const myGeo = myp5.endGeometry(); + const myGeo = myp5.buildGeometry(() => { + myp5.beginShape(); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); + myp5.vertex(0,0,0); + myp5.vertex(1,0,0); + myp5.vertex(1,1,0); + myp5.endShape(); + }); myp5.model(myGeo); assert.equal(myp5._renderer.buffers.user.length, 0); }