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.'); + * } + * + *
+ */ + fn.createElement = function (tag, content) { + p5._validateParameters('createElement', arguments); + const elt = document.createElement(tag); + if (typeof content !== 'undefined') { + elt.innerHTML = content; + } + return addElement(elt, this); + }; + /** * Removes all elements created by p5.js, including any event handlers. * @@ -1186,7 +1244,7 @@ function dom(p5, fn){ p5._validateParameters('createSelect', args); let self; let arg = args[0]; - if (arg instanceof p5.Element && arg.elt instanceof HTMLSelectElement) { + if (arg instanceof Element && arg.elt instanceof HTMLSelectElement) { // If given argument is p5.Element of select type self = arg; this.elt = arg.elt; @@ -1444,7 +1502,7 @@ function dom(p5, fn){ let name; const arg0 = args[0]; if ( - arg0 instanceof p5.Element && + arg0 instanceof Element && (arg0.elt instanceof HTMLDivElement || arg0.elt instanceof HTMLSpanElement) ) { // If given argument is p5.Element of div/span type diff --git a/src/dom/p5.MediaElement.js b/src/dom/p5.MediaElement.js index 3d85047d39..d263617046 100644 --- a/src/dom/p5.MediaElement.js +++ b/src/dom/p5.MediaElement.js @@ -1744,65 +1744,6 @@ function media(p5, fn){ return videoEl; }; - - /** - * 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.'); - * } - * - *
- */ - fn.createElement = function (tag, content) { - p5._validateParameters('createElement', arguments); - const elt = document.createElement(tag); - if (typeof content !== 'undefined') { - elt.innerHTML = content; - } - return addElement(elt, this); - }; - // ============================================================================= // p5.MediaElement additions // ============================================================================= diff --git a/src/events/index.js b/src/events/index.js index 773424d42b..447f3d0d10 100644 --- a/src/events/index.js +++ b/src/events/index.js @@ -1,11 +1,9 @@ import acceleration from './acceleration.js'; import keyboard from './keyboard.js'; -import mouse from './mouse.js'; -import touch from './touch.js'; +import pointer from './pointer.js'; export default function(p5){ p5.registerAddon(acceleration); p5.registerAddon(keyboard); - p5.registerAddon(mouse); - p5.registerAddon(touch); + p5.registerAddon(pointer); } diff --git a/src/events/mouse.js b/src/events/pointer.js similarity index 92% rename from src/events/mouse.js rename to src/events/pointer.js index 6e9f4a661e..5d75349899 100644 --- a/src/events/mouse.js +++ b/src/events/pointer.js @@ -1,6 +1,6 @@ /** * @module Events - * @submodule Mouse + * @submodule Pointer * @for p5 * @requires core * @requires constants @@ -8,7 +8,7 @@ import * as constants from '../core/constants'; -function mouse(p5, fn){ +function pointer(p5, fn){ /** * A `Number` system variable that tracks the mouse's horizontal movement. * @@ -758,6 +758,92 @@ function mouse(p5, fn){ */ fn.mouseButton = 0; + /** + * An `Array` of all the current touch points on a touchscreen device. + * + * The `touches` array is empty by default. When the user touches their + * screen, a new touch point is tracked and added to the array. Touch points + * are `Objects` with the following properties: + * + * ```js + * // Iterate over the touches array. + * for (let touch of touches) { + * // x-coordinate relative to the top-left + * // corner of the canvas. + * console.log(touch.x); + * + * // y-coordinate relative to the top-left + * // corner of the canvas. + * console.log(touch.y); + * + * // x-coordinate relative to the top-left + * // corner of the browser. + * console.log(touch.winX); + * + * // y-coordinate relative to the top-left + * // corner of the browser. + * console.log(touch.winY); + * + * // ID number + * console.log(touch.id); + * } + * ``` + * + * @property {Object[]} touches + * @readOnly + * + * @example + *
+ * + * // 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); + * } + * } + * + *
+ */ + fn.touches = []; + fn._activeTouches = new Map(); + /** * A `Boolean` system variable that's `true` if the mouse is pressed and * `false` if not. @@ -817,27 +903,34 @@ function mouse(p5, fn){ */ fn.mouseIsPressed = false; - fn._updateNextMouseCoords = function(e) { - if (this._curElement !== null && (!e.touches || e.touches.length > 0)) { - const mousePos = getMousePos( - this._curElement.elt, - this.width, - this.height, - e - ); - this.movedX = e.movementX; - this.movedY = e.movementY; - this.mouseX = mousePos.x; - this.mouseY = mousePos.y; - this.winMouseX = mousePos.winX; - this.winMouseY = mousePos.winY; - } - if (!this._hasMouseInteracted) { - // For first draw, make previous and next equal - this._updateMouseCoords(); - this._hasMouseInteracted = true; + fn._updatePointerCoords = function (e) { + if (this._curElement !== null) { + const canvas = this._curElement.elt; + const sx = canvas.scrollWidth / this.width || 1; + const sy = canvas.scrollHeight / this.height || 1; + + if (e.pointerType == 'touch') { + const touches = []; + for (const touch of this._activeTouches.values()) { + touches.push(getTouchInfo(canvas, sx, sy, touch)); + } + this.touches = touches; + } else { + const mousePos = getMouseInfo(canvas, sx, sy, e); + this.movedX = e.movementX || 0; + this.movedY = e.movementY || 0; + this.mouseX = mousePos.x; + this.mouseY = mousePos.y; + this.winMouseX = mousePos.winX; + this.winMouseY = mousePos.winY; + } + + if (!this._hasMouseInteracted) { + this._updateMouseCoords(); + this._hasMouseInteracted = true; + } } - }; + }; fn._updateMouseCoords = function() { this.pmouseX = this.mouseX; @@ -847,26 +940,26 @@ function mouse(p5, fn){ this._pmouseWheelDeltaY = this._mouseWheelDeltaY; }; - function getMousePos(canvas, w, h, evt) { - if (evt && !evt.clientX) { - // use touches if touch and not mouse - if (evt.touches) { - evt = evt.touches[0]; - } else if (evt.changedTouches) { - evt = evt.changedTouches[0]; - } - } + function getMouseInfo(canvas, sx, sy, evt) { const rect = canvas.getBoundingClientRect(); - const sx = canvas.scrollWidth / w || 1; - const sy = canvas.scrollHeight / h || 1; return { - x: (evt.clientX - rect.left) / sx, - y: (evt.clientY - rect.top) / sy, - winX: evt.clientX, - winY: evt.clientY, - id: evt.identifier + x: (evt.clientX - rect.left) / sx, + y: (evt.clientY - rect.top) / sy, + winX: evt.clientX, + winY: evt.clientY, }; - } + } + + function getTouchInfo(canvas, sx, sy, touch) { + const rect = canvas.getBoundingClientRect(); + return { + x: (touch.clientX - rect.left) / sx, + y: (touch.clientY - rect.top) / sy, + winX: touch.clientX, + winY: touch.clientY, + id: touch.pointerId, + }; +} fn._setMouseButton = function(e) { if (e.button === 1) { @@ -1004,10 +1097,7 @@ function mouse(p5, fn){ * ``` * * On touchscreen devices, `mouseDragged()` will run when a user moves a touch - * point if touchMoved() isn’t declared. If - * touchMoved() is declared, then - * touchMoved() will run when a user moves a - * touch point and `mouseDragged()` won’t. + * point. * * Browsers may have default behaviors attached to various mouse events. For * example, some browsers highlight text when the user moves the mouse while @@ -1054,30 +1144,26 @@ function mouse(p5, fn){ * * */ - fn._onmousemove = function(e) { + fn._onpointermove = function(e) { const context = this._isGlobal ? window : this; let executeDefault; - this._updateNextMouseCoords(e); - if (!this.mouseIsPressed) { - if (typeof context.mouseMoved === 'function') { + this._updatePointerCoords(e); + + if(e.pointerType === 'touch') { + this._activeTouches.set(e.pointerId, e); + } + + if (!this.mouseIsPressed && typeof context.mouseMoved === 'function') { executeDefault = context.mouseMoved(e); if (executeDefault === false) { e.preventDefault(); } - } - } else { - if (typeof context.mouseDragged === 'function') { + } else if (this.mouseIsPressed && typeof context.mouseDragged === 'function') { executeDefault = context.mouseDragged(e); if (executeDefault === false) { e.preventDefault(); } - } else if (typeof context.touchMoved === 'function') { - executeDefault = context.touchMoved(e); - if (executeDefault === false) { - e.preventDefault(); - } - } - } + } }; /** @@ -1120,10 +1206,7 @@ function mouse(p5, fn){ * ``` * * On touchscreen devices, `mousePressed()` will run when a user’s touch - * begins if touchStarted() isn’t declared. If - * touchStarted() is declared, then - * touchStarted() will run when a user’s touch - * begins and `mousePressed()` won’t. + * begins. * * Browsers may have default behaviors attached to various mouse events. For * example, some browsers highlight text when the user moves the mouse while @@ -1225,31 +1308,25 @@ function mouse(p5, fn){ * * */ - fn._onmousedown = function(e) { + fn._onpointerdown = function(e) { const context = this._isGlobal ? window : this; let executeDefault; this.mouseIsPressed = true; - this._setMouseButton(e); - this._updateNextMouseCoords(e); - // _ontouchstart triggers first and sets this._touchstart - if (this._touchstart) { - return; - } + if (e.pointerType === 'touch') { + this._activeTouches.set(e.pointerId, e); + } else { + this._setMouseButton(e); + } + + this._updatePointerCoords(e); if (typeof context.mousePressed === 'function') { executeDefault = context.mousePressed(e); if (executeDefault === false) { e.preventDefault(); } - } else if (typeof context.touchStarted === 'function') { - executeDefault = context.touchStarted(e); - if (executeDefault === false) { - e.preventDefault(); - } - } - - this._touchstart = false; + } }; /** @@ -1293,10 +1370,7 @@ function mouse(p5, fn){ * ``` * * On touchscreen devices, `mouseReleased()` will run when a user’s touch - * ends if touchEnded() isn’t declared. If - * touchEnded() is declared, then - * touchEnded() will run when a user’s touch - * ends and `mouseReleased()` won’t. + * ends. * * Browsers may have default behaviors attached to various mouse events. For * example, some browsers highlight text when the user moves the mouse while @@ -1398,32 +1472,27 @@ function mouse(p5, fn){ * * */ - fn._onmouseup = function(e) { + fn._onpointerup = function(e) { const context = this._isGlobal ? window : this; let executeDefault; this.mouseIsPressed = false; - // _ontouchend triggers first and sets this._touchend - if (this._touchend) { - return; + if(e.pointerType == 'touch'){ + this._activeTouches.delete(e.pointerId); } + this._updatePointerCoords(e); + if (typeof context.mouseReleased === 'function') { executeDefault = context.mouseReleased(e); if (executeDefault === false) { e.preventDefault(); } - } else if (typeof context.touchEnded === 'function') { - executeDefault = context.touchEnded(e); - if (executeDefault === false) { - e.preventDefault(); - } } - this._touchend = false; }; - fn._ondragend = fn._onmouseup; - fn._ondragover = fn._onmousemove; + fn._ondragend = fn._onpointerup; + fn._ondragover = fn._onpointermove; /** * A function that's called once after a mouse button is pressed and released. @@ -1466,10 +1535,7 @@ function mouse(p5, fn){ * ``` * * On touchscreen devices, `mouseClicked()` will run when a user’s touch - * ends if touchEnded() isn’t declared. If - * touchEnded() is declared, then - * touchEnded() will run when a user’s touch - * ends and `mouseClicked()` won’t. + * ends. * * Browsers may have default behaviors attached to various mouse events. For * example, some browsers highlight text when the user moves the mouse while @@ -1989,8 +2055,8 @@ function mouse(p5, fn){ }; } -export default mouse; +export default pointer; if(typeof p5 !== 'undefined'){ - mouse(p5, p5.prototype); -} + pointer(p5, p5.prototype); +} \ No newline at end of file diff --git a/src/events/touch.js b/src/events/touch.js deleted file mode 100644 index 9a9254a511..0000000000 --- a/src/events/touch.js +++ /dev/null @@ -1,641 +0,0 @@ -/** - * @module Events - * @submodule Touch - * @for p5 - * @requires core - */ - -function touch(p5, fn){ - /** - * An `Array` of all the current touch points on a touchscreen device. - * - * The `touches` array is empty by default. When the user touches their - * screen, a new touch point is tracked and added to the array. Touch points - * are `Objects` with the following properties: - * - * ```js - * // Iterate over the touches array. - * for (let touch of touches) { - * // x-coordinate relative to the top-left - * // corner of the canvas. - * console.log(touch.x); - * - * // y-coordinate relative to the top-left - * // corner of the canvas. - * console.log(touch.y); - * - * // x-coordinate relative to the top-left - * // corner of the browser. - * console.log(touch.winX); - * - * // y-coordinate relative to the top-left - * // corner of the browser. - * console.log(touch.winY); - * - * // ID number - * console.log(touch.id); - * } - * ``` - * - * @property {Object[]} touches - * @readOnly - * - * @example - *
- * - * // 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); - * } - * } - * - *
- */ - fn.touches = []; - - fn._updateTouchCoords = function(e) { - if (this._curElement !== null) { - const touches = []; - for (let i = 0; i < e.touches.length; i++) { - touches[i] = getTouchInfo( - this._curElement.elt, - this.width, - this.height, - e, - i - ); - } - this.touches = touches; - } - }; - - function getTouchInfo(canvas, w, h, e, i = 0) { - const rect = canvas.getBoundingClientRect(); - const sx = canvas.scrollWidth / w || 1; - const sy = canvas.scrollHeight / h || 1; - const touch = e.touches[i] || e.changedTouches[i]; - return { - x: (touch.clientX - rect.left) / sx, - y: (touch.clientY - rect.top) / sy, - winX: touch.clientX, - winY: touch.clientY, - id: touch.identifier - }; - } - - /** - * A function that's called once each time the user touches the screen. - * - * Declaring a function called `touchStarted()` sets a code block to run - * automatically each time the user begins touching a touchscreen device: - * - * ```js - * function touchStarted() { - * // Code to run. - * } - * ``` - * - * The touches array will be updated with the most - * recent touch points when `touchStarted()` is called by p5.js: - * - * ```js - * function touchStarted() { - * // Paint over the background. - * background(200); - * - * // Mark each touch point once with a circle. - * for (let touch of touches) { - * circle(touch.x, touch.y, 40); - * } - * } - * ``` - * - * The parameter, event, is optional. `touchStarted()` will be passed a - * TouchEvent - * object with properties that describe the touch event: - * - * ```js - * function touchStarted(event) { - * // Code to run that uses the event. - * console.log(event); - * } - * ``` - * - * On touchscreen devices, mousePressed() will - * run when a user’s touch starts if `touchStarted()` isn’t declared. If - * `touchStarted()` is declared, then `touchStarted()` will run when a user’s - * touch starts and mousePressed() won’t. - * - * Note: `touchStarted()`, touchEnded(), and - * touchMoved() are all related. - * `touchStarted()` runs as soon as the user touches a touchscreen device. - * touchEnded() runs as soon as the user ends a - * touch. touchMoved() runs repeatedly as the - * user moves any touch points. - * - * @method touchStarted - * @param {TouchEvent} [event] optional `TouchEvent` argument. - * - * @example - *
- * - * // 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; - * } - * } - * - *
- */ - fn._ontouchstart = function(e) { - const context = this._isGlobal ? window : this; - let executeDefault; - this.mouseIsPressed = true; - this._updateTouchCoords(e); - this._updateNextMouseCoords(e); - this._updateMouseCoords(); // reset pmouseXY at the start of each touch event - - if (typeof context.touchStarted === 'function') { - executeDefault = context.touchStarted(e); - if (executeDefault === false) { - e.preventDefault(); - } - this._touchstart = true; - } - }; - - /** - * A function that's called when the user touches the screen and moves. - * - * Declaring the function `touchMoved()` sets a code block to run - * automatically when the user touches a touchscreen device and moves: - * - * ```js - * function touchMoved() { - * // Code to run. - * } - * ``` - * - * The touches array will be updated with the most - * recent touch points when `touchMoved()` is called by p5.js: - * - * ```js - * function touchMoved() { - * // Paint over the background. - * background(200); - * - * // Mark each touch point while the user moves. - * for (let touch of touches) { - * circle(touch.x, touch.y, 40); - * } - * } - * ``` - * - * The parameter, event, is optional. `touchMoved()` will be passed a - * TouchEvent - * object with properties that describe the touch event: - * - * ```js - * function touchMoved(event) { - * // Code to run that uses the event. - * console.log(event); - * } - * ``` - * - * On touchscreen devices, mouseDragged() will - * run when the user’s touch points move if `touchMoved()` isn’t declared. If - * `touchMoved()` is declared, then `touchMoved()` will run when a user’s - * touch points move and mouseDragged() won’t. - * - * Note: touchStarted(), - * touchEnded(), and - * `touchMoved()` are all related. - * touchStarted() runs as soon as the user - * touches a touchscreen device. touchEnded() - * runs as soon as the user ends a touch. `touchMoved()` runs repeatedly as - * the user moves any touch points. - * - * @method touchMoved - * @param {TouchEvent} [event] optional TouchEvent argument. - * - * @example - *
- * - * // 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; - * } - * } - * - *
- */ - fn._ontouchmove = function(e) { - const context = this._isGlobal ? window : this; - let executeDefault; - this._updateTouchCoords(e); - this._updateNextMouseCoords(e); - if (typeof context.touchMoved === 'function') { - executeDefault = context.touchMoved(e); - if (executeDefault === false) { - e.preventDefault(); - } - } else if (typeof context.mouseDragged === 'function') { - executeDefault = context.mouseDragged(e); - if (executeDefault === false) { - e.preventDefault(); - } - } - }; - - /** - * A function that's called once each time a screen touch ends. - * - * Declaring the function `touchEnded()` sets a code block to run - * automatically when the user stops touching a touchscreen device: - * - * ```js - * function touchEnded() { - * // Code to run. - * } - * ``` - * - * The touches array will be updated with the most - * recent touch points when `touchEnded()` is called by p5.js: - * - * ```js - * function touchEnded() { - * // Paint over the background. - * background(200); - * - * // Mark each remaining touch point when the user stops - * // a touch. - * for (let touch of touches) { - * circle(touch.x, touch.y, 40); - * } - * } - * ``` - * - * The parameter, event, is optional. `touchEnded()` will be passed a - * TouchEvent - * object with properties that describe the touch event: - * - * ```js - * function touchEnded(event) { - * // Code to run that uses the event. - * console.log(event); - * } - * ``` - * - * On touchscreen devices, mouseReleased() will - * run when the user’s touch ends if `touchEnded()` isn’t declared. If - * `touchEnded()` is declared, then `touchEnded()` will run when a user’s - * touch ends and mouseReleased() won’t. - * - * Note: touchStarted(), - * `touchEnded()`, and touchMoved() are all - * related. touchStarted() runs as soon as the - * user touches a touchscreen device. `touchEnded()` runs as soon as the user - * ends a touch. touchMoved() runs repeatedly as - * the user moves any touch points. - * - * @method touchEnded - * @param {TouchEvent} [event] optional `TouchEvent` argument. - * - * @example - *
- * - * // 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; - * } - * } - * - *
- */ - fn._ontouchend = function(e) { - this.mouseIsPressed = false; - this._updateTouchCoords(e); - this._updateNextMouseCoords(e); - const context = this._isGlobal ? window : this; - let executeDefault; - if (typeof context.touchEnded === 'function') { - executeDefault = context.touchEnded(e); - if (executeDefault === false) { - e.preventDefault(); - } - this._touchend = true; - } - }; -} - -export default touch; - -if(typeof p5 !== 'undefined'){ - touch(p5, p5.prototype); -} diff --git a/src/image/image.js b/src/image/image.js index 9c1e9f9ee1..7db4af9ab4 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -327,7 +327,6 @@ function image(p5, fn){ } htmlCanvas.toBlob(blob => { - console.log("here"); fn.downloadFile(blob, filename, extension); if(temporaryGraphics) temporaryGraphics.remove(); }, mimeType); diff --git a/src/shape/2d_primitives.js b/src/shape/2d_primitives.js index 6e4f551322..3f1e0a3506 100644 --- a/src/shape/2d_primitives.js +++ b/src/shape/2d_primitives.js @@ -313,7 +313,7 @@ function primitives(p5, fn){ // if the current stroke and fill settings wouldn't result in something // visible, exit immediately - if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { + if (!this._renderer.states.strokeColor && !this._renderer.states.fillColor) { return this; } @@ -540,7 +540,7 @@ function primitives(p5, fn){ fn._renderEllipse = function(x, y, w, h, detailX) { // if the current stroke and fill settings wouldn't result in something // visible, exit immediately - if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { + if (!this._renderer.states.strokeColor && !this._renderer.states.fillColor) { return this; } @@ -712,7 +712,7 @@ function primitives(p5, fn){ fn.line = function(...args) { p5._validateParameters('line', args); - if (this._renderer.states.doStroke) { + if (this._renderer.states.strokeColor) { this._renderer.line(...args); } @@ -896,7 +896,7 @@ function primitives(p5, fn){ fn.point = function(...args) { p5._validateParameters('point', args); - if (this._renderer.states.doStroke) { + if (this._renderer.states.strokeColor) { if (args.length === 1 && args[0] instanceof p5.Vector) { this._renderer.point.call( this._renderer, @@ -1057,7 +1057,7 @@ function primitives(p5, fn){ fn.quad = function(...args) { p5._validateParameters('quad', args); - if (this._renderer.states.doStroke || this._renderer.states.doFill) { + if (this._renderer.states.strokeColor || this._renderer.states.fillColor) { if (this._renderer.isP3D && args.length < 12) { // if 3D and we weren't passed 12 args, assume Z is 0 this._renderer.quad.call( @@ -1334,7 +1334,7 @@ function primitives(p5, fn){ // internal method to have renderer draw a rectangle fn._renderRect = function() { - if (this._renderer.states.doStroke || this._renderer.states.doFill) { + if (this._renderer.states.strokeColor || this._renderer.states.fillColor) { // duplicate width for height in case only 3 arguments is provided if (arguments.length === 3) { arguments[3] = arguments[2]; @@ -1433,7 +1433,7 @@ function primitives(p5, fn){ fn.triangle = function(...args) { p5._validateParameters('triangle', args); - if (this._renderer.states.doStroke || this._renderer.states.doFill) { + if (this._renderer.states.strokeColor || this._renderer.states.fillColor) { this._renderer.triangle(args); } diff --git a/src/shape/attributes.js b/src/shape/attributes.js index 9160e009f3..bf787d1df7 100644 --- a/src/shape/attributes.js +++ b/src/shape/attributes.js @@ -297,7 +297,7 @@ function attributes(p5, fn){ ) { this._renderer.states.rectMode = m; } - return this; + return this; // return current rectMode ? }; /** diff --git a/src/shape/curves.js b/src/shape/curves.js index 949f60e8ec..30f7287d9d 100644 --- a/src/shape/curves.js +++ b/src/shape/curves.js @@ -205,7 +205,7 @@ function curves(p5, fn){ // if the current stroke and fill settings wouldn't result in something // visible, exit immediately - if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { + if (!this._renderer.states.strokeColor && !this._renderer.states.fillColor) { return this; } @@ -758,119 +758,16 @@ function curves(p5, fn){ fn.curve = function(...args) { p5._validateParameters('curve', args); - if (this._renderer.states.doStroke) { + if (this._renderer.states.strokeColor) { this._renderer.curve(...args); } return this; }; - /** - * Sets the number of segments used to draw spline curves in WebGL mode. - * - * In WebGL mode, smooth shapes are drawn using many flat segments. Adding - * more flat segments makes shapes appear smoother. - * - * The parameter, `detail`, is the number of segments to use while drawing a - * spline curve. For example, calling `curveDetail(5)` will use 5 segments to - * draw curves with the curve() function. By - * default,`detail` is 20. - * - * Note: `curveDetail()` has no effect in 2D mode. - * - * @method curveDetail - * @param {Number} resolution number of segments to use. Defaults to 20. - * @chainable - * - * @example - *
- * - * 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.' - * ); - * } - * - *
- */ - fn.curveDetail = function(d) { - p5._validateParameters('curveDetail', arguments); - if (d < 3) { - this._curveDetail = 3; - } else { - this._curveDetail = d; - } - return this; - }; - /** * Adjusts the way curve() and - * curveVertex() draw. + * splineVertex() draw. * * Spline curves are like cables that are attached to a set of points. * `curveTightness()` adjusts how tightly the cable is attached to the points. @@ -906,12 +803,12 @@ function curves(p5, fn){ * // Draw the curve. * noFill(); * beginShape(); - * curveVertex(10, 26); - * curveVertex(10, 26); - * curveVertex(83, 24); - * curveVertex(83, 61); - * curveVertex(25, 65); - * curveVertex(25, 65); + * splineVertex(10, 26); + * splineVertex(10, 26); + * splineVertex(83, 24); + * splineVertex(83, 61); + * splineVertex(25, 65); + * splineVertex(25, 65); * endShape(); * } * diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index fac21ddd9d..ce718f9f3c 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -6,27 +6,2111 @@ * @requires constants */ -// declare MyClass +// REMINDER: remove .js extension (currently using it to run file locally) +import { Color } from '../color/p5.Color'; +import { Vector } from '../math/p5.Vector'; +import * as constants from '../core/constants'; + +// ---- UTILITY FUNCTIONS ---- +function polylineLength(vertices) { + let length = 0; + for (let i = 1; i < vertices.length; i++) { + length += vertices[i-1].position.dist(vertices[i].position); + } + return length; +} + +// ---- GENERAL BUILDING BLOCKS ---- + +class Vertex { + constructor(properties) { + for (const [key, value] of Object.entries(properties)) { + this[key] = value; + } + } + /* + get array() { + // convert to 1D array + // call `toArray()` if value is an object with a toArray() method + // handle primitive values separately + // maybe handle object literals too, with Object.values()? + // probably don’t need anything else for now? + } + */ + // TODO: make sure name of array conversion method is + // consistent with any modifications to the names of corresponding + // properties of p5.Vector and p5.Color +} + +class ShapePrimitive { + vertices; + _shape = null; + _primitivesIndex = null; + _contoursIndex = null; + isClosing = false; + + constructor(...vertices) { + if (this.constructor === ShapePrimitive) { + throw new Error('ShapePrimitive is an abstract class: it cannot be instantiated.'); + } + if (vertices.length > 0) { + this.vertices = vertices; + } + else { + throw new Error('At least one vertex must be passed to the constructor.'); + } + } + + get vertexCount() { + return this.vertices.length; + } + + get vertexCapacity() { + throw new Error('Getter vertexCapacity must be implemented.'); + } + + get _firstInterpolatedVertex() { + return this.startVertex(); + } + + get canOverrideAnchor() { + return false; + } + + accept(visitor) { + throw new Error('Method accept() must be implemented.'); + } + + addToShape(shape) { + /* + TODO: + Refactor? + Test this method once more primitives are implemented. + Test segments separately (Segment adds an extra step to this method). + */ + let lastContour = shape.at(-1); + + if (lastContour.primitives.length === 0) { + lastContour.primitives.push(this); + } else { + // last primitive in shape + let lastPrimitive = shape.at(-1, -1); + let hasSameType = lastPrimitive instanceof this.constructor; + let spareCapacity = lastPrimitive.vertexCapacity - + lastPrimitive.vertexCount; + + // this primitive + let pushableVertices; + let remainingVertices; + + if (hasSameType && spareCapacity > 0) { + + pushableVertices = this.vertices.splice(0, spareCapacity); + remainingVertices = this.vertices; + lastPrimitive.vertices.push(...pushableVertices); + + if (remainingVertices.length > 0) { + lastContour.primitives.push(this); + } + } + else { + lastContour.primitives.push(this); + } + } + + // if primitive itself was added + // (i.e. its individual vertices weren't all added to an existing primitive) + // give it a reference to the shape and store its location within the shape + let addedToShape = this.vertices.length > 0; + if (addedToShape) { + let lastContour = shape.at(-1); + this._primitivesIndex = lastContour.primitives.length - 1; + this._contoursIndex = shape.contours.length - 1; + this._shape = shape; + } + + return shape.at(-1, -1); + } + + get _nextPrimitive() { + return this._belongsToShape ? + this._shape.at(this._contoursIndex, this._primitivesIndex + 1) : + null; + } + + get _belongsToShape() { + return this._shape !== null; + } + + handlesClose() { + return false; + } + + close(vertex) { + throw new Error('Unimplemented!'); + } +} + +class Contour { + #kind; + primitives; + + constructor(kind = constants.PATH) { + this.#kind = kind; + this.primitives = []; + } + + get kind() { + const isEmpty = this.primitives.length === 0; + const isPath = this.#kind === constants.PATH; + return isEmpty && isPath ? constants.EMPTY_PATH : this.#kind; + } + + accept(visitor) { + for (const primitive of this.primitives) { + primitive.accept(visitor); + } + } +} + +// ---- PATH PRIMITIVES ---- + +class Anchor extends ShapePrimitive { + #vertexCapacity = 1; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitAnchor(this); + } + + getEndVertex() { + return this.vertices[0]; + } +} + +// abstract class +class Segment extends ShapePrimitive { + constructor(...vertices) { + super(...vertices); + if (this.constructor === Segment) { + throw new Error('Segment is an abstract class: it cannot be instantiated.'); + } + } + + // segments in a shape always have a predecessor + // (either an anchor or another segment) + get _previousPrimitive() { + return this._belongsToShape ? + this._shape.at(this._contoursIndex, this._primitivesIndex - 1) : + null; + } + + getStartVertex() { + return this._previousPrimitive.getEndVertex(); + } + + getEndVertex() { + return this.vertices.at(-1); + } +} + +class LineSegment extends Segment { + #vertexCapacity = 1; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitLineSegment(this); + } +} + +class BezierSegment extends Segment { + #order; + #vertexCapacity; + + constructor(order, ...vertices) { + super(...vertices); + + // Order m may sometimes be passed as an array [m], since arrays + // may be used elsewhere to store order of + // Bezier curves and surfaces in a common format + + let numericalOrder = Array.isArray(order) ? order[0] : order; + this.#order = numericalOrder; + this.#vertexCapacity = numericalOrder; + } + + get order() { + return this.#order; + } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + #_hullLength; + hullLength() { + if (this.#_hullLength === undefined) { + this.#_hullLength = polylineLength([ + this.getStartVertex(), + ...this.vertices + ]); + } + return this.#_hullLength; + } + + accept(visitor) { + visitor.visitBezierSegment(this); + } +} + +/* +To-do: Consider type and end modes -- see #6766 +may want to use separate classes, but maybe not + +For now, the implementation overrides +super.getEndVertex() in order to preserve current p5 +endpoint behavior, but we're considering defaulting +to interpolated endpoints (a breaking change) +*/ +class SplineSegment extends Segment { + #vertexCapacity = Infinity; + _splineEnds = constants.INCLUDE; + _splineTightness = 0; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitSplineSegment(this); + } + + get _comesAfterSegment() { + return this._previousPrimitive instanceof Segment; + } + + get canOverrideAnchor() { + return this._splineEnds === constants.EXCLUDE; + } + + // assuming for now that the first interpolated vertex is always + // the second vertex passed to splineVertex() + // if this spline segment doesn't follow another segment, + // the first vertex is in an anchor + get _firstInterpolatedVertex() { + if (this._splineEnds === constants.EXCLUDE) { + return this._comesAfterSegment ? + this.vertices[1] : + this.vertices[0]; + } else { + return this.getStartVertex() + } + } + + get _chainedToSegment() { + if (this._belongsToShape && this._comesAfterSegment) { + let interpolatedStartPosition = this._firstInterpolatedVertex.position; + let predecessorEndPosition = this.getStartVertex().position; + return predecessorEndPosition.equals(interpolatedStartPosition); + } + else { + return false; + } + } + + // extend addToShape() with a warning in case second vertex + // doesn't line up with end of last segment + addToShape(shape) { + const added = super.addToShape(shape); + this._splineEnds = shape._splineEnds; + this._splineTightness = shape._splineTightness; + + if (this._splineEnds !== constants.EXCLUDE) return added; + + let verticesPushed = !this._belongsToShape; + let lastPrimitive = shape.at(-1, -1); + + let message = (array1, array2) => + `Spline does not start where previous path segment ends: + second spline vertex at (${array1}) + expected to be at (${array2}).`; + + if (verticesPushed && + // Only check once the first interpolated vertex has been added + lastPrimitive.vertices.length === 2 && + lastPrimitive._comesAfterSegment && + !lastPrimitive._chainedToSegment + ) { + let interpolatedStart = lastPrimitive._firstInterpolatedVertex.position; + let predecessorEnd = lastPrimitive.getStartVertex().position; + + console.warn( + message(interpolatedStart.array(), predecessorEnd.array()) + ); + } + + // Note: Could add a warning in an else-if case for when this spline segment + // is added directly to the shape instead of pushing its vertices to + // an existing spline segment. However, if we assume addToShape() is called by + // splineVertex(), it'd add a new spline segment with only one vertex in that case, + // and the check wouldn't be needed yet. + + // TODO: Consider case where positions match but other vertex properties don't. + return added; + } + + // override method on base class + getEndVertex() { + if (this._splineEnds === constants.INCLUDE) { + return super.getEndVertex(); + } else if (this._splineEnds === constants.EXCLUDE) { + return this.vertices.at(-2); + } else { + return this.getStartVertex(); + } + } + + getControlPoints() { + let points = []; + + if (this._comesAfterSegment) { + points.push(this.getStartVertex()); + } + points.push(this.getStartVertex()); + + for (const vertex of this.vertices) { + points.push(vertex); + } + + const prevVertex = this.getStartVertex(); + if (this._splineEnds === constants.INCLUDE) { + points.unshift(prevVertex); + points.push(this.vertices.at(-1)); + } else if (this._splineEnds === constants.JOIN) { + points.unshift(this.vertices.at(-1), prevVertex); + points.push(prevVertex, this.vertices.at(0)); + } + + return points; + } + + handlesClose() { + if (!this._belongsToShape) return false; + + // Only handle closing if the spline is the only thing in its contour after + // the anchor + const contour = this._shape.at(this._contoursIndex); + return contour.primitives.length === 2 && this._primitivesIndex === 1; + } + + close() { + this._splineEnds = constants.JOIN; + } +} + +// ---- ISOLATED PRIMITIVES ---- + +class Point extends ShapePrimitive { + #vertexCapacity = 1; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitPoint(this); + } +} + +class Line extends ShapePrimitive { + #vertexCapacity = 2; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitLine(this); + } +} + +class Triangle extends ShapePrimitive { + #vertexCapacity = 3; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitTriangle(this); + } +} + +class Quad extends ShapePrimitive { + #vertexCapacity = 4; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitQuad(this); + } +} + +// ---- TESSELLATION PRIMITIVES ---- + +class TriangleFan extends ShapePrimitive { + #vertexCapacity = Infinity; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitTriangleFan(this); + } +} + +class TriangleStrip extends ShapePrimitive { + #vertexCapacity = Infinity; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitTriangleStrip(this); + } +} + +class QuadStrip extends ShapePrimitive { + #vertexCapacity = Infinity; + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitQuadStrip(this); + } +} + +// ---- PRIMITIVE SHAPE CREATORS ---- + +class PrimitiveShapeCreators { + // TODO: make creators private? + // That'd probably be better, but for now, it may be convenient to use + // native Map properties like size, e.g. for testing, and it's simpler to + // not have to wrap all the properties that might be useful + creators; + + constructor() { + let creators = new Map(); + + /* TODO: REFACTOR BASED ON THE CODE BELOW, + ONCE CONSTANTS ARE IMPLEMENTED AS SYMBOLS + + // Store Symbols as strings for use in Map keys + const EMPTY_PATH = constants.EMPTY_PATH.description; + const PATH = constants.PATH.description; + //etc. + + creators.set(`vertex-${EMPTY_PATH}`, (...vertices) => new Anchor(...vertices)); + // etc. + + get(vertexKind, shapeKind) { + const key = `${vertexKind}-${shapeKind.description}`; + return this.creators.get(key); + } + // etc. + */ + + // vertex + creators.set(`vertex-${constants.EMPTY_PATH}`, (...vertices) => new Anchor(...vertices)); + creators.set(`vertex-${constants.PATH}`, (...vertices) => new LineSegment(...vertices)); + creators.set(`vertex-${constants.POINTS}`, (...vertices) => new Point(...vertices)); + creators.set(`vertex-${constants.LINES}`, (...vertices) => new Line(...vertices)); + creators.set(`vertex-${constants.TRIANGLES}`, (...vertices) => new Triangle(...vertices)); + creators.set(`vertex-${constants.QUADS}`, (...vertices) => new Quad(...vertices)); + creators.set(`vertex-${constants.TRIANGLE_FAN}`, (...vertices) => new TriangleFan(...vertices)); + creators.set(`vertex-${constants.TRIANGLE_STRIP}`, (...vertices) => new TriangleStrip(...vertices)); + creators.set(`vertex-${constants.QUAD_STRIP}`, (...vertices) => new QuadStrip(...vertices)); + + // bezierVertex (constructors all take order and vertices so they can be called in a uniform way) + creators.set(`bezierVertex-${constants.EMPTY_PATH}`, (order, ...vertices) => new Anchor(...vertices)); + creators.set(`bezierVertex-${constants.PATH}`, (order, ...vertices) => new BezierSegment(order, ...vertices)); + + // splineVertex + creators.set(`splineVertex-${constants.EMPTY_PATH}`, (...vertices) => new Anchor(...vertices)); + creators.set(`splineVertex-${constants.PATH}`, (...vertices) => new SplineSegment(...vertices)); + + this.creators = creators; + } + + get(vertexKind, shapeKind) { + const key = `${vertexKind}-${shapeKind}`; + return this.creators.get(key); + } + + set(vertexKind, shapeKind, creator) { + const key = `${vertexKind}-${shapeKind}`; + this.creators.set(key, creator); + } + + clear() { + this.creators.clear(); + } +} + +// ---- SHAPE ---- + +/* Note: It's assumed that Shape instances are always built through + * their beginShape()/endShape() methods. For example, this ensures + * that a segment is never the first primitive in a contour (paths + * always start with an anchor), which simplifies code elsewhere. + */ +class Shape { + #vertexProperties; + #initialVertexProperties; + #primitiveShapeCreators; + #bezierOrder = 3; + _splineTightness = 0; + kind = null; + contours = []; + _splineEnds = constants.INCLUDE; + userVertexProperties = null; + + constructor( + vertexProperties, + primitiveShapeCreators = new PrimitiveShapeCreators() + ) { + this.#initialVertexProperties = vertexProperties; + this.#vertexProperties = vertexProperties; + this.#primitiveShapeCreators = primitiveShapeCreators; + + for (const key in this.#vertexProperties) { + if (key !== 'position' && key !== 'textureCoordinates') { + this[key] = function(value) { + this.#vertexProperties[key] = value; + }; + } + } + } + + serializeToArray(val) { + if (val === null) { + return []; + } if (val instanceof Number) { + return [val]; + } else if (val instanceof Array) { + return val; + } else if (val.array instanceof Function) { + return val.array(); + } else { + throw new Error(`Can't convert ${val} to array!`); + } + } + + vertexToArray(vertex) { + const array = []; + for (const key in this.#vertexProperties) { + if (this.userVertexProperties && key in this.userVertexProperties) + continue; + const val = vertex[key]; + array.push(...this.serializeToArray(val)); + } + for (const key in this.userVertexProperties) { + if (key in vertex) { + array.push(...this.serializeToArray(vertex[key])); + } else { + array.push(...new Array(this.userVertexProperties[key]).fill(0)); + } + } + return array; + } + + hydrateValue(queue, original) { + if (original === null) { + return null; + } else if (original instanceof Number) { + return queue.shift(); + } else if (original instanceof Array) { + const array = []; + for (let i = 0; i < original.length; i++) { + array.push(queue.shift()); + } + return array; + } else if (original instanceof Vector) { + return new Vector(queue.shift(), queue.shift(), queue.shift()); + } else if (original instanceof Color) { + const array = [ + queue.shift(), + queue.shift(), + queue.shift(), + queue.shift() + ]; + return new Color( + array.map((v, i) => v * original.maxes[original.mode][i]), + original.mode, + original.maxes + ); + } + } + + arrayToVertex(array) { + const vertex = {}; + const queue = [...array]; + + for (const key in this.#vertexProperties) { + if (this.userVertexProperties && key in this.userVertexProperties) + continue; + const original = this.#vertexProperties[key]; + vertex[key] = this.hydrateValue(queue, original); + } + for (const key in this.userVertexProperties) { + const original = this.#vertexProperties[key]; + vertex[key] = this.hydrateValue(queue, original); + } + return vertex; + } + + arrayScale(array, scale) { + return array.map(v => v * scale); + } + + arraySum(first, ...rest) { + return first.map((v, i) => { + let result = v; + for (let j = 0; j < rest.length; j++) { + result += rest[j][i]; + } + return result; + }); + } + + arrayMinus(a, b) { + return a.map((v, i) => v - b[i]); + } + + evaluateCubicBezier([a, b, c, d], t) { + return this.arraySum( + this.arrayScale(a, Math.pow(1 - t, 3)), + this.arrayScale(b, 3 * Math.pow(1 - t, 2) * t), + this.arrayScale(c, 3 * (1 - t) * Math.pow(t, 2)), + this.arrayScale(d, Math.pow(t, 3)) + ); + } + + evaluateQuadraticBezier([a, b, c], t) { + return this.arraySum( + this.arrayScale(a, Math.pow(1 - t, 2)), + this.arrayScale(b, 2 * (1 - t) * t), + this.arrayScale(c, t * t) + ); + } + + /* + catmullRomToBezier(vertices, tightness) + + Abbreviated description: + Converts a Catmull-Rom spline to a sequence of Bezier curveTo points. + + Parameters: + vertices -> Array [v0, v1, v2, v3, ...] of at least four vertices + tightness -> Number affecting shape of curve + + Returns: + array of Bezier curveTo control points, each represented as [c1, c2, c3][] + + TODO: + 1. It seems p5 contains code for converting from Catmull-Rom to Bezier in at least two places: + + catmullRomToBezier() is based on code in the legacy endShape() function: + https://github.com/processing/p5.js/blob/1b66f097761d3c2057c0cec4349247d6125f93ca/src/core/p5.Renderer2D.js#L859C1-L886C1 + + A different conversion can be found elsewhere in p5: + https://github.com/processing/p5.js/blob/17304ce9e9ef3f967bd828102a51b62a2d39d4f4/src/typography/p5.Font.js#L1179 + + A more careful review and comparison of both implementations would be helpful. They're different. I put + catmullRomToBezier() together quickly without checking the math/algorithm, when I made the proof of concept + for the refactor. + + 2. It may be possible to replace the code in p5.Font.js with the code here, to reduce duplication. + */ + catmullRomToBezier(vertices, tightness) { + let s = 1 - tightness; + let bezArrays = []; + + for (let i = 0; i + 3 < vertices.length; i++) { + const [a, b, c, d] = vertices.slice(i, i + 4); + const bezB = this.arraySum( + b, + this.arrayScale(this.arrayMinus(c, a), s / 6) + ); + const bezC = this.arraySum( + c, + this.arrayScale(this.arrayMinus(b, d), s / 6) + ); + const bezD = c; + + bezArrays.push([bezB, bezC, bezD]); + } + return bezArrays; + } + + // TODO for at() method: + + // RENAME? + // -at() indicates it works like Array.prototype.at(), e.g. with negative indices + // -get() may work better if we want to add a corresponding set() method + // -a set() method could maybe check for problematic usage (e.g. inserting a Triangle into a PATH) + // -renaming or removing would necessitate changes at call sites (it's already in use) + + // REFACTOR? + + // TEST + at(contoursIndex, primitivesIndex, verticesIndex) { + let contour; + let primitive; + + contour = this.contours.at(contoursIndex); + + switch(arguments.length) { + case 1: + return contour; + case 2: + return contour.primitives.at(primitivesIndex); + case 3: + primitive = contour.primitives.at(primitivesIndex); + return primitive.vertices.at(verticesIndex); + } + } + + // maybe call this clear() for consistency with PrimitiveShapeCreators.clear()? + // note: p5.Geometry has a reset() method, but also clearColors() + // looks like reset() isn't in the public reference, so maybe we can switch + // everything to clear()? Not sure if reset/clear is used in other classes, + // but it'd be good if geometries and shapes are consistent + reset() { + this.#vertexProperties = { ...this.#initialVertexProperties }; + this.kind = null; + this.contours = []; + this.userVertexProperties = null; + } + + vertexProperty(name, data) { + this.userVertexProperties = this.userVertexProperties || {}; + const key = this.vertexPropertyKey(name); + if (!this.userVertexProperties[key]) { + this.userVertexProperties[key] = data.length ? data.length : 1; + } + this.#vertexProperties[key] = data; + } + vertexPropertyName(key) { + return key.replace(/Src$/, ''); + } + vertexPropertyKey(name) { + return name + 'Src'; + } + + /* + Note: Internally, #bezierOrder is stored as an array, in order to accommodate + primitives including Bezier segments, Bezier triangles, and Bezier quads. For example, + a segment may have #bezierOrder [m], whereas a quad may have #bezierOrder [m, n]. + */ + + bezierOrder(...order) { + this.#bezierOrder = order; + } + + splineEnds(mode) { + this._splineEnds = mode; + } + + splineTightness(tightness) { + this._splineTightness = tightness; + } + + /* + To-do: Maybe refactor #createVertex() since this has side effects that aren't advertised + in the method name? + */ + #createVertex(position, textureCoordinates) { + this.#vertexProperties.position = position; + + if (textureCoordinates !== undefined) { + this.#vertexProperties.textureCoordinates = textureCoordinates; + } + + return new Vertex(this.#vertexProperties); + } + + #createPrimitiveShape(vertexKind, shapeKind, ...vertices) { + let primitiveShapeCreator = this.#primitiveShapeCreators.get( + vertexKind, shapeKind + ); + + return vertexKind === 'bezierVertex' ? + primitiveShapeCreator(this.#bezierOrder, ...vertices) : + primitiveShapeCreator(...vertices); + } + + /* + #generalVertex() is reused by the special vertex functions, + including vertex(), bezierVertex(), splineVertex(), and arcVertex(): + + It creates a vertex, builds a primitive including that + vertex, and has the primitive add itself to the shape. + */ + #generalVertex(kind, position, textureCoordinates) { + let vertexKind = kind; + let lastContourKind = this.at(-1).kind; + let vertex = this.#createVertex(position, textureCoordinates); + + let primitiveShape = this.#createPrimitiveShape( + vertexKind, + lastContourKind, + vertex + ); + + return primitiveShape.addToShape(this); + } + + vertex(position, textureCoordinates, { isClosing = false } = {}) { + const added = this.#generalVertex('vertex', position, textureCoordinates); + added.isClosing = isClosing; + } + + bezierVertex(position, textureCoordinates) { + this.#generalVertex('bezierVertex', position, textureCoordinates); + } + + splineVertex(position, textureCoordinates) { + this.#generalVertex('splineVertex', position, textureCoordinates); + } + + arcVertex(position, textureCoordinates) { + this.#generalVertex('arcVertex', position, textureCoordinates); + } + + beginContour(shapeKind = constants.PATH) { + if (this.at(-1)?.kind === constants.EMPTY_PATH) { + this.contours.pop(); + } + this.contours.push(new Contour(shapeKind)); + } + + endContour(closeMode = constants.OPEN, _index = this.contours.length - 1) { + const contour = this.at(_index); + if (closeMode === constants.CLOSE) { + // shape characteristics + const isPath = contour.kind === constants.PATH; + + // anchor characteristics + const anchorVertex = this.at(_index, 0, 0); + const anchorHasPosition = Object.hasOwn(anchorVertex, 'position'); + const lastSegment = this.at(_index, -1); + + // close path + if (isPath && anchorHasPosition) { + if (lastSegment.handlesClose()) { + lastSegment.close(anchorVertex); + } else { + // Temporarily remove contours after the current one so that we add to the original + // contour again + const rest = this.contours.splice( + _index + 1, + this.contours.length - _index - 1 + ); + const prevVertexProperties = this.#vertexProperties; + this.#vertexProperties = { ...prevVertexProperties }; + for (const key in anchorVertex) { + if (['position', 'textureCoordinates'].includes(key)) continue; + this.#vertexProperties[key] = anchorVertex[key]; + } + this.vertex( + anchorVertex.position, + anchorVertex.textureCoordinates, + { isClosing: true } + ); + this.#vertexProperties = prevVertexProperties; + this.contours.push(...rest); + } + } + } + } + + beginShape(shapeKind = constants.PATH) { + this.kind = shapeKind; + // Implicitly start a contour + this.beginContour(shapeKind); + } + /* TO-DO: + Refactor? + - Might not need anchorHasPosition. + - Might combine conditions at top, and rely on shortcircuiting. + Does nothing if shape is not a path or has multiple contours. Might discuss this. + */ + endShape(closeMode = constants.OPEN) { + if (closeMode === constants.CLOSE) { + // Close the first contour, the one implicitly used for shape data + // added without an explicit contour + this.endContour(closeMode, 0); + } + } + + accept(visitor) { + for (const contour of this.contours) { + contour.accept(visitor); + } + } +} + +// ---- PRIMITIVE VISITORS ---- + +// abstract class +class PrimitiveVisitor { + constructor() { + if (this.constructor === PrimitiveVisitor) { + throw new Error('PrimitiveVisitor is an abstract class: it cannot be instantiated.'); + } + } + // path primitives + visitAnchor(anchor) { + throw new Error('Method visitAnchor() has not been implemented.'); + } + visitLineSegment(lineSegment) { + throw new Error('Method visitLineSegment() has not been implemented.'); + } + visitBezierSegment(bezierSegment) { + throw new Error('Method visitBezierSegment() has not been implemented.'); + } + visitSplineSegment(curveSegment) { + throw new Error('Method visitSplineSegment() has not been implemented.'); + } + visitArcSegment(arcSegment) { + throw new Error('Method visitArcSegment() has not been implemented.'); + } + + // isolated primitives + visitPoint(point) { + throw new Error('Method visitPoint() has not been implemented.'); + } + visitLine(line) { + throw new Error('Method visitLine() has not been implemented.'); + } + visitTriangle(triangle) { + throw new Error('Method visitTriangle() has not been implemented.'); + } + visitQuad(quad) { + throw new Error('Method visitQuad() has not been implemented.'); + } + + // tessellation primitives + visitTriangleFan(triangleFan) { + throw new Error('Method visitTriangleFan() has not been implemented.'); + } + visitTriangleStrip(triangleStrip) { + throw new Error('Method visitTriangleStrip() has not been implemented.'); + } + visitQuadStrip(quadStrip) { + throw new Error('Method visitQuadStrip() has not been implemented.'); + } +} + +// requires testing +class PrimitiveToPath2DConverter extends PrimitiveVisitor { + path = new Path2D(); + strokeWeight; + + constructor({ strokeWeight }) { + super(); + this.strokeWeight = strokeWeight; + } + + // path primitives + visitAnchor(anchor) { + let vertex = anchor.getEndVertex(); + this.path.moveTo(vertex.position.x, vertex.position.y); + } + visitLineSegment(lineSegment) { + if (lineSegment.isClosing) { + // The same as lineTo, but it adds a stroke join between this + // and the starting vertex rather than having two caps + this.path.closePath(); + } else { + let vertex = lineSegment.getEndVertex(); + this.path.lineTo(vertex.position.x, vertex.position.y); + } + } + visitBezierSegment(bezierSegment) { + let [v1, v2, v3] = bezierSegment.vertices; + + switch (bezierSegment.order) { + case 2: + this.path.quadraticCurveTo( + v1.position.x, + v1.position.y, + v2.position.x, + v2.position.y + ); + break; + case 3: + this.path.bezierCurveTo( + v1.position.x, + v1.position.y, + v2.position.x, + v2.position.y, + v3.position.x, + v3.position.y + ); + break; + } + } + visitSplineSegment(splineSegment) { + const shape = splineSegment._shape; + + if ( + splineSegment._splineEnds === constants.EXCLUDE && + !splineSegment._comesAfterSegment + ) { + let startVertex = splineSegment._firstInterpolatedVertex; + this.path.moveTo(startVertex.position.x, startVertex.position.y); + } + + const arrayVertices = splineSegment.getControlPoints().map( + v => shape.vertexToArray(v) + ); + let bezierArrays = shape.catmullRomToBezier( + arrayVertices, + splineSegment._splineTightness + ).map(arr => arr.map(vertArr => shape.arrayToVertex(vertArr))); + for (const array of bezierArrays) { + const points = array.flatMap(vert => [vert.position.x, vert.position.y]); + this.path.bezierCurveTo(...points); + } + } + visitPoint(point) { + const { x, y } = point.vertices[0].position; + this.path.moveTo(x, y); + // Hack: to draw just strokes and not fills, draw a very very tiny line + this.path.lineTo(x + 0.00001, y); + } + visitLine(line) { + const { x: x0, y: y0 } = line.vertices[0].position; + const { x: x1, y: y1 } = line.vertices[1].position; + this.path.moveTo(x0, y0); + this.path.lineTo(x1, y1); + } + visitTriangle(triangle) { + const [v0, v1, v2] = triangle.vertices; + this.path.moveTo(v0.position.x, v0.position.y); + this.path.lineTo(v1.position.x, v1.position.y); + this.path.lineTo(v2.position.x, v2.position.y); + this.path.closePath(); + } + visitQuad(quad) { + const [v0, v1, v2, v3] = quad.vertices; + this.path.moveTo(v0.position.x, v0.position.y); + this.path.lineTo(v1.position.x, v1.position.y); + this.path.lineTo(v2.position.x, v2.position.y); + this.path.lineTo(v3.position.x, v3.position.y); + this.path.closePath(); + } + visitTriangleFan(triangleFan) { + const [v0, ...rest] = triangleFan.vertices; + for (let i = 0; i < rest.length - 1; i++) { + const v1 = rest[i]; + const v2 = rest[i + 1]; + this.path.moveTo(v0.position.x, v0.position.y); + this.path.lineTo(v1.position.x, v1.position.y); + this.path.lineTo(v2.position.x, v2.position.y); + this.path.closePath(); + } + } + visitTriangleStrip(triangleStrip) { + for (let i = 0; i < triangleStrip.vertices.length - 2; i++) { + const v0 = triangleStrip.vertices[i]; + const v1 = triangleStrip.vertices[i + 1]; + const v2 = triangleStrip.vertices[i + 2]; + this.path.moveTo(v0.position.x, v0.position.y); + this.path.lineTo(v1.position.x, v1.position.y); + this.path.lineTo(v2.position.x, v2.position.y); + this.path.closePath(); + } + } + visitQuadStrip(quadStrip) { + for (let i = 0; i < quadStrip.vertices.length - 3; i += 2) { + const v0 = quadStrip.vertices[i]; + const v1 = quadStrip.vertices[i + 1]; + const v2 = quadStrip.vertices[i + 2]; + const v3 = quadStrip.vertices[i + 3]; + this.path.moveTo(v0.position.x, v0.position.y); + this.path.lineTo(v1.position.x, v1.position.y); + // These are intentionally out of order to go around the quad + this.path.lineTo(v3.position.x, v3.position.y); + this.path.lineTo(v2.position.x, v2.position.y); + this.path.closePath(); + } + } +} + +class PrimitiveToVerticesConverter extends PrimitiveVisitor { + contours = []; + curveDetail; + + constructor({ curveDetail = 1 } = {}) { + super(); + this.curveDetail = curveDetail; + } + + lastContour() { + return this.contours[this.contours.length - 1]; + } + + visitAnchor(anchor) { + this.contours.push([]); + // Weird edge case: if the next segment is a spline, we might + // need to jump to a different vertex. + const next = anchor._nextPrimitive; + if (next?.canOverrideAnchor) { + this.lastContour().push(next._firstInterpolatedVertex); + } else { + this.lastContour().push(anchor.getEndVertex()); + } + } + visitLineSegment(lineSegment) { + this.lastContour().push(lineSegment.getEndVertex()); + } + visitBezierSegment(bezierSegment) { + const contour = this.lastContour(); + const numPoints = Math.max( + 1, + Math.ceil(bezierSegment.hullLength() * this.curveDetail) + ); + const vertexArrays = [ + bezierSegment.getStartVertex(), + ...bezierSegment.vertices + ].map(v => bezierSegment._shape.vertexToArray(v)); + for (let i = 0; i < numPoints; i++) { + const t = (i + 1) / numPoints; + contour.push( + bezierSegment._shape.arrayToVertex( + bezierSegment.order === 3 + ? bezierSegment._shape.evaluateCubicBezier(vertexArrays, t) + : bezierSegment._shape.evaluateQuadraticBezier(vertexArrays, t) + ) + ); + } + } + visitSplineSegment(splineSegment) { + const shape = splineSegment._shape; + const contour = this.lastContour(); + + const arrayVertices = splineSegment.getControlPoints().map( + v => shape.vertexToArray(v) + ); + let bezierArrays = shape.catmullRomToBezier( + arrayVertices, + splineSegment._splineTightness + ); + let startVertex = shape.vertexToArray(splineSegment._firstInterpolatedVertex); + for (const array of bezierArrays) { + const bezierControls = [startVertex, ...array]; + const numPoints = Math.max( + 1, + Math.ceil( + polylineLength(bezierControls.map(v => shape.arrayToVertex(v))) * + this.curveDetail + ) + ); + for (let i = 0; i < numPoints; i++) { + const t = (i + 1) / numPoints; + contour.push( + shape.arrayToVertex(shape.evaluateCubicBezier(bezierControls, t)) + ); + } + startVertex = array[2]; + } + } + visitPoint(point) { + this.contours.push(point.vertices.slice()); + } + visitLine(line) { + this.contours.push(line.vertices.slice()); + } + visitTriangle(triangle) { + this.contours.push(triangle.vertices.slice()); + } + visitQuad(quad) { + this.contours.push(quad.vertices.slice()); + } + visitTriangleFan(triangleFan) { + // WebGL itself interprets the vertices as a fan, no reformatting needed + this.contours.push(triangleFan.vertices.slice()); + } + visitTriangleStrip(triangleStrip) { + // WebGL itself interprets the vertices as a strip, no reformatting needed + this.contours.push(triangleStrip.vertices.slice()); + } + visitQuadStrip(quadStrip) { + // WebGL itself interprets the vertices as a strip, no reformatting needed + this.contours.push(quadStrip.vertices.slice()); + } +} + +class PointAtLengthGetter extends PrimitiveVisitor { + constructor() { + super(); + } +} function customShapes(p5, fn) { - - // ---- FUNCTIONS ---- - - // documentation here + // ---- GENERAL CLASSES ---- + + /** + * @private + * A class to describe a custom shape made with `beginShape()`/`endShape()`. + * + * Every `Shape` has a `kind`. The kind takes any value that + * can be passed to beginShape(): + * + * - `PATH` + * - `POINTS` + * - `LINES` + * - `TRIANGLES` + * - `QUADS` + * - `TRIANGLE_FAN` + * - `TRIANGLE_STRIP` + * - `QUAD_STRIP` + * + * A `Shape` of any kind consists of `contours`, which can be thought of as + * subshapes (shapes inside another shape). Each `contour` is built from + * basic shapes called primitives, and each primitive consists of one or more vertices. + * + * For example, a square can be made from a single path contour with four line-segment + * primitives. Each line segment contains a vertex that indicates its endpoint. A square + * with a circular hole in it contains the circle in a separate contour. + * + * By default, each vertex only has a position, but a shape's vertices may have other + * properties such as texture coordinates, a normal vector, a fill color, and a stroke color. + * The properties every vertex should have may be customized by passing `vertexProperties` to + * `createShape()`. + * + * Once a shape is created and given a name like `myShape`, it can be built up with + * methods such as `myShape.beginShape()`, `myShape.vertex()`, and `myShape.endShape()`. + * + * Vertex functions such as `vertex()` or `bezierVertex()` are used to set the `position` + * property of vertices, as well as the `textureCoordinates` property if applicable. Those + * properties only apply to a single vertex. + * + * If `vertexProperties` includes other properties, they are each set by a method of the + * same name. For example, if vertices in `myShape` have a `fill`, then that is set with + * `myShape.fill()`. In the same way that a fill() may be applied + * to one or more shapes, `myShape.fill()` may be applied to one or more vertices. + * + * @class p5.Shape + * @param {Object} [vertexProperties={position: createVector(0, 0)}] vertex properties and their initial values. + */ + + p5.Shape = Shape; + + /** + * @private + * A class to describe a contour made with `beginContour()`/`endContour()`. + * + * Contours may be thought of as shapes inside of other shapes. + * For example, a contour may be used to create a hole in a shape that is created + * with beginShape()/endShape(). + * Multiple contours may be included inside a single shape. + * + * Contours can have any `kind` that a shape can have: + * + * - `PATH` + * - `POINTS` + * - `LINES` + * - `TRIANGLES` + * - `QUADS` + * - `TRIANGLE_FAN` + * - `TRIANGLE_STRIP` + * - `QUAD_STRIP` + * + * By default, a contour has the same kind as the shape that contains it, but this + * may be changed by passing a different `kind` to beginContour(). + * + * A `Contour` of any kind consists of `primitives`, which are the most basic + * shapes that can be drawn. For example, if a contour is a hexagon, then + * it's made from six line-segment primitives. + * + * @class p5.Contour + */ + + p5.Contour = Contour; - // fn.myFunction = function() { - // this.background('yellow'); // call an existing p5 function - // }; + /** + * @private + * A base class to describe a shape primitive (a basic shape drawn with + * `beginShape()`/`endShape()`). + * + * Shape primitives are the most basic shapes that can be drawn with + * beginShape()/endShape(): + * + * - segment primitives: line segments, bezier segments, spline segments, and arc segments + * - isolated primitives: points, lines, triangles, and quads + * - tessellation primitives: triangle fans, triangle strips, and quad strips + * + * More complex shapes may be created by combining many primitives, possibly of different kinds, + * into a single shape. + * + * In a similar way, every shape primitive is built from one or more vertices. + * For example, a point consists of a single vertex, while a triangle consists of three vertices. + * Each type of shape primitive has a `vertexCapacity`, which may be `Infinity` (for example, a + * spline may consist of any number of vertices). A primitive's `vertexCount` is the number of + * vertices it currently contains. + * + * Each primitive can add itself to a shape with an `addToShape()` method. + * + * It can also accept visitor objects with an `accept()` method. When a primitive accepts a visitor, + * it gives the visitor access to its vertex data. For example, one visitor to a segment might turn + * the data into 2D drawing instructions. Another might find a point at a given distance + * along the segment. + * + * @class p5.ShapePrimitive + * @abstract + */ - // ---- CLASSES ---- + p5.ShapePrimitive = ShapePrimitive; - // documentation here + /** + * @private + * A class to describe a vertex (a point on a shape), in 2D or 3D. + * + * Vertices are the basic building blocks of all `p5.Shape` objects, including + * shapes made with vertex(), arcVertex(), + * bezierVertex(), and splineVertex(). + * + * Like a point on an object in the real world, a vertex may have different properties. + * These may include coordinate properties such as `position`, `textureCoordinates`, and `normal`, + * color properties such as `fill` and `stroke`, and more. + * + * A vertex called `myVertex` with position coordinates `(2, 3, 5)` and a green stroke may be created + * like this: + * + * ```js + * let myVertex = new p5.Vertex({ + * position: createVector(2, 3, 5), + * stroke: color('green') + * }); + * ``` + * + * Any property names may be used. The `p5.Shape` class assumes that if a vertex has a + * position or texture coordinates, they are stored in `position` and `textureCoordinates` + * properties. + * + * Property values may be any + * JavaScript primitive, any + * object literal, + * or any object with an `array` property. + * + * For example, if a position is stored as a `p5.Vector` object and a stroke is stored as a `p5.Color` object, + * then the `array` properties of those objects will be used by the vertex's own `array` property, which provides + * all the vertex data in a single array. + * + * @class p5.Vertex + * @param {Object} [properties={position: createVector(0, 0)}] vertex properties. + */ - // p5.MyClass = MyClass; + p5.Vertex = Vertex; + + // ---- PATH PRIMITIVES ---- + + /** + * @private + * A class responsible for... + * + * @class p5.Anchor + * @extends p5.ShapePrimitive + * @param {p5.Vertex} vertex the vertex to include in the anchor. + */ + + p5.Anchor = Anchor; + + /** + * @private + * A class responsible for... + * + * Note: When a segment is added to a shape, it's attached to an anchor or another segment. + * Adding it to another shape may result in unexpected behavior. + * + * @class p5.Segment + * @extends p5.ShapePrimitive + * @param {...p5.Vertex} vertices the vertices to include in the segment. + */ + + p5.Segment = Segment; + + /** + * @private + * A class responsible for... + * + * @class p5.LineSegment + * @param {p5.Vertex} vertex the vertex to include in the anchor. + */ + + p5.LineSegment = LineSegment; + + /** + * @private + * A class responsible for... + */ + + p5.BezierSegment = BezierSegment; + + /** + * @private + * A class responsible for... + */ + + p5.SplineSegment = SplineSegment; + + // ---- ISOLATED PRIMITIVES ---- + + /** + * @private + * A class responsible for... + */ + + p5.Point = Point; + + /** + * @private + * A class responsible for... + * + * @class p5.Line + * @param {...p5.Vertex} vertices the vertices to include in the line. + */ + + p5.Line = Line; + + /** + * @private + * A class responsible for... + */ + + p5.Triangle = Triangle; + + /** + * @private + * A class responsible for... + */ + + p5.Quad = Quad; + + // ---- TESSELLATION PRIMITIVES ---- + + /** + * @private + * A class responsible for... + */ + + p5.TriangleFan = TriangleFan; + + /** + * @private + * A class responsible for... + */ + + p5.TriangleStrip = TriangleStrip; + + /** + * @private + * A class responsible for... + */ + + p5.QuadStrip = QuadStrip; + + // ---- PRIMITIVE VISITORS ---- + + /** + * @private + * A class responsible for... + */ + + p5.PrimitiveVisitor = PrimitiveVisitor; + + /** + * @private + * A class responsible for... + * + * Notes: + * 1. Assumes vertex positions are stored as p5.Vector instances. + * 2. Currently only supports position properties of vectors. + */ + + p5.PrimitiveToPath2DConverter = PrimitiveToPath2DConverter; + + /** + * @private + * A class responsible for... + */ + + p5.PrimitiveToVerticesConverter = PrimitiveToVerticesConverter; + + /** + * @private + * A class responsible for... + */ + + p5.PointAtLengthGetter = PointAtLengthGetter; + + // ---- FUNCTIONS ---- + + /** + * TODO: documentation + */ + fn.bezierOrder = function(order) { + return this._renderer.bezierOrder(order); + }; + + /** + * TODO: documentation + */ + fn.splineVertex = function(...args) { + let x = 0, y = 0, z = 0, u = 0, v = 0; + if (args.length === 2) { + [x, y] = args; + } else if (args.length === 4) { + [x, y, u, v] = args; + } else if (args.length === 3) { + [x, y, z] = args; + } else if (args.length === 5) { + [x, y, z, u, v] = args; + } + this._renderer.splineVertex(x, y, z, u, v); + }; + + /** + * TODO: documentation + * @param {SHOW|HIDE} mode + */ + fn.splineEnds = function(mode) { + return this._renderer.splineEnds(mode); + }; + + /** + * Adds a vertex to a custom shape. + * + * `vertex()` sets the coordinates of vertices drawn between the + * beginShape() and + * endShape() functions. + * + * The first two parameters, `x` and `y`, set the x- and y-coordinates of the + * vertex. + * + * The third parameter, `z`, is optional. It sets the z-coordinate of the + * vertex in WebGL mode. By default, `z` is 0. + * + * The fourth and fifth parameters, `u` and `v`, are also optional. They set + * the u- and v-coordinates for the vertex’s texture when used with + * endShape(). By default, `u` and `v` are both 0. + * + * @method vertex + * @param {Number} x x-coordinate of the vertex. + * @param {Number} y y-coordinate of the vertex. + * + * @example + *
+ * + * 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(); + * } + * + *
+ */ + /** + * @method vertex + * @param {Number} x + * @param {Number} y + * @param {Number} [z] z-coordinate of the vertex. Defaults to 0. + */ + /** + * @method vertex + * @param {Number} x + * @param {Number} y + * @param {Number} [z] + * @param {Number} [u] u-coordinate of the vertex's texture. Defaults to 0. + * @param {Number} [v] v-coordinate of the vertex's texture. Defaults to 0. + */ + fn.vertex = function(x, y) { + let z, u, v; + + // default to (x, y) mode: all other arguments assumed to be 0. + z = u = v = 0; + + if (arguments.length === 3) { + // (x, y, z) mode: (u, v) assumed to be 0. + z = arguments[2]; + } else if (arguments.length === 4) { + // (x, y, u, v) mode: z assumed to be 0. + u = arguments[2]; + v = arguments[3]; + } else if (arguments.length === 5) { + // (x, y, z, u, v) mode + z = arguments[2]; + u = arguments[3]; + v = arguments[4]; + } + this._renderer.vertex(x, y, z, u, v); + return; + }; + + // Note: Code is commented out for now, to avoid conflicts with the existing implementation. + + /** + * Top-line description + * + * More details... + */ + + // fn.beginShape = function() { + + // }; + + /** + * Top-line description + * + * More details... + */ + + // fn.bezierVertex = function() { + + // }; + + /** + * Top-line description + * + * More details... + */ + + // fn.curveVertex = function() { + + // }; + + /** + * Begins 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 beginContour + * + * @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); + * } + * + *
+ */ + fn.beginContour = function(kind) { + this._renderer.beginContour(kind); + }; + + /** + * 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 + * + * @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); + * } + * + *
+ */ + fn.endContour = function(mode = constants.OPEN) { + this._renderer.endContour(mode); + }; + + /** + * Top-line description + * + * More details... + */ + + // fn.endShape = function() { + + // }; + + /** + * Top-line description + * + * More details... + */ + + // fn.vertex = function() { + + // }; + + /** + * Top-line description + * + * More details... + */ + + // fn.normal = function() { + + // }; + + /** + * Top-line description + * + * More details... + */ + + // fn.vertexProperty = function() { + + // }; } export default customShapes; +export { + Shape, + Contour, + ShapePrimitive, + Vertex, + Anchor, + Segment, + LineSegment, + BezierSegment, + SplineSegment, + Point, + Line, + Triangle, + Quad, + TriangleFan, + TriangleStrip, + QuadStrip, + PrimitiveVisitor, + PrimitiveToPath2DConverter, + PrimitiveToVerticesConverter, + PointAtLengthGetter +}; if (typeof p5 !== 'undefined') { - customShapes(p5, p5.prototype); -} \ No newline at end of file + customShapes(p5, p5.prototype); +} diff --git a/src/shape/vertex.js b/src/shape/vertex.js index 73afcc1653..34f452f45f 100644 --- a/src/shape/vertex.js +++ b/src/shape/vertex.js @@ -9,124 +9,6 @@ import * as constants from '../core/constants'; function vertex(p5, fn){ - let shapeKind = null; - let vertices = []; - let contourVertices = []; - let isBezier = false; - let isCurve = false; - let isQuadratic = false; - let isContour = false; - let isFirstContour = true; - - /** - * Begins 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 beginContour - * @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); - * } - * - *
- */ - fn.beginContour = function() { - if (this._renderer.isP3D) { - this._renderer.beginContour(); - } else { - contourVertices = []; - isContour = true; - } - return this; - }; - /** * Begins adding vertices to a custom shape. * @@ -135,9 +17,9 @@ function vertex(p5, fn){ * vertices to a custom shape and endShape() stops * adding them. * - * The parameter, `kind`, sets the kind of shape to make. By default, any - * irregular polygon can be drawn. The available modes for kind are: + * The parameter, `kind`, sets the kind of shape to make. The available kinds are: * + * - `PATH` (the default) to draw shapes by tracing out the path along their edges. * - `POINTS` to draw a series of points. * - `LINES` to draw a series of unconnected line segments. * - `TRIANGLES` to draw a series of separate triangles. @@ -145,13 +27,12 @@ function vertex(p5, fn){ * - `TRIANGLE_STRIP` to draw a series of connected triangles in strip fashion. * - `QUADS` to draw a series of separate quadrilaterals (quads). * - `QUAD_STRIP` to draw quad strip using adjacent edges to form the next quad. - * - `TESS` to create a filling curve by explicit tessellation (WebGL only). * * After calling `beginShape()`, shapes can be built by calling * vertex(), * bezierVertex(), - * quadraticVertex(), and/or - * curveVertex(). Calling + * bezierVertex(), and/or + * splineVertex(). Calling * endShape() will stop adding vertices to the * shape. Each shape will be outlined with the current stroke color and filled * with the current fill color. @@ -165,8 +46,8 @@ function vertex(p5, fn){ * endShape(). * * @method beginShape - * @param {(POINTS|LINES|TRIANGLES|TRIANGLE_FAN|TRIANGLE_STRIP|QUADS|QUAD_STRIP|TESS)} [kind] either POINTS, LINES, TRIANGLES, TRIANGLE_FAN - * TRIANGLE_STRIP, QUADS, QUAD_STRIP or TESS. + * @param {(POINTS|LINES|TRIANGLES|TRIANGLE_FAN|TRIANGLE_STRIP|QUADS|QUAD_STRIP|PATH)} [kind=PATH] either POINTS, LINES, TRIANGLES, TRIANGLE_FAN + * TRIANGLE_STRIP, QUADS, QUAD_STRIP or PATH. Defaults to PATH. * @chainable * * @example @@ -452,7 +333,7 @@ function vertex(p5, fn){ * * // Start drawing the shape. * // Draw a series of quadrilaterals. - * beginShape(TESS); + * beginShape(PATH); * * // Add the vertices. * vertex(-30, -30, 0); @@ -491,7 +372,7 @@ function vertex(p5, fn){ * * // Start drawing the shape. * // Draw a series of quadrilaterals. - * beginShape(TESS); + * beginShape(PATH); * * // Add the vertices. * fill('red'); @@ -518,27 +399,7 @@ function vertex(p5, fn){ */ fn.beginShape = function(kind) { p5._validateParameters('beginShape', arguments); - if (this._renderer.isP3D) { - this._renderer.beginShape(...arguments); - } else { - if ( - kind === constants.POINTS || - kind === constants.LINES || - kind === constants.TRIANGLES || - kind === constants.TRIANGLE_FAN || - kind === constants.TRIANGLE_STRIP || - kind === constants.QUADS || - kind === constants.QUAD_STRIP - ) { - shapeKind = kind; - } else { - shapeKind = null; - } - - vertices = []; - contourVertices = []; - } - return this; + this._renderer.beginShape(...arguments); }; /** @@ -574,7 +435,6 @@ function vertex(p5, fn){ * @param {Number} y3 y-coordinate of the second control point. * @param {Number} x4 x-coordinate of the anchor point. * @param {Number} y4 y-coordinate of the anchor point. - * @chainable * * @example *
@@ -801,47 +661,36 @@ function vertex(p5, fn){ * @param {Number} x4 * @param {Number} y4 * @param {Number} z4 z-coordinate of the anchor point. - * @chainable */ fn.bezierVertex = function(...args) { - p5._validateParameters('bezierVertex', args); - if (this._renderer.isP3D) { - this._renderer.bezierVertex(...args); - } else { - if (vertices.length === 0) { - p5._friendlyError( - 'vertex() must be used once before calling bezierVertex()', - 'bezierVertex' - ); - } else { - isBezier = true; - const vert = []; - for (let i = 0; i < args.length; i++) { - vert[i] = args[i]; - } - vert.isVert = false; - if (isContour) { - contourVertices.push(vert); - } else { - vertices.push(vert); - } + if (args.length === 2 * 3 || args.length === 3 * 3) { + // Handle the legacy case where all bezier control points are provided + // at once. We'll translate them into 3 individual calls. + const stride = args.length / 3; + + const prevOrder = this._renderer.bezierOrder(); + this._renderer.bezierOrder(3); + for (let i = 0; i < args.length; i += stride) { + this._renderer.bezierVertex(...args.slice(i, i + stride)); } + this._renderer.bezierOrder(prevOrder); + } else { + this._renderer.bezierVertex(...args); } - return this; }; /** * Adds a spline curve segment to a custom shape. * - * `curveVertex()` adds a curved segment to custom shapes. The spline curves + * `splineVertex()` adds a curved segment to custom shapes. The spline curves * it creates are defined like those made by the - * curve() function. `curveVertex()` must be called + * curve() function. `splineVertex()` must be called * between the beginShape() and * endShape() functions. * * Spline curves can form shapes and curves that slope gently. They’re like * cables that are attached to a set of points. Splines are defined by two - * anchor points and two control points. `curveVertex()` must be called at + * anchor points and two control points. `splineVertex()` must be called at * least four times between * beginShape() and * endShape() in order to draw a curve: @@ -850,14 +699,14 @@ function vertex(p5, fn){ * beginShape(); * * // Add the first control point. - * curveVertex(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); * * endShape(); * @@ -865,37 +714,37 @@ function vertex(p5, fn){ * The code snippet above would only draw the curve between the anchor points, * similar to the curve() function. The segments * between the control and anchor points can be drawn by calling - * `curveVertex()` with the coordinates of the control points: + * `splineVertex()` with the coordinates of the control points: * * * 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); - * } - * - *
- */ - fn.endContour = function() { - if (this._renderer.isP3D) { - return this; - } - - const vert = contourVertices[0].slice(); // copy all data - vert.isVert = contourVertices[0].isVert; - vert.moveTo = false; - contourVertices.push(vert); - - // prevent stray lines with multiple contours - if (isFirstContour) { - vertices.push(vertices[0]); - isFirstContour = false; - } - - for (let i = 0; i < contourVertices.length; i++) { - vertices.push(contourVertices[i]); - } + this._renderer.splineVertex(...args); return this; }; @@ -1366,7 +1090,7 @@ function vertex(p5, fn){ * built by calling vertex(), * bezierVertex(), * quadraticVertex(), and/or - * curveVertex(). Calling + * splineVertex(). Calling * `endShape()` will stop adding vertices to the * shape. Each shape will be outlined with the current stroke color and filled * with the current fill color. @@ -1512,59 +1236,7 @@ function vertex(p5, fn){ count = 1; } - if (this._renderer.isP3D) { - this._renderer.endShape( - mode, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind, - count - ); - } else { - if (count !== 1) { - console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); - } - if (vertices.length === 0) { - return this; - } - if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { - return this; - } - - const closeShape = mode === constants.CLOSE; - - // if the shape is closed, the first element is also the last element - if (closeShape && !isContour) { - vertices.push(vertices[0]); - } - - this._renderer.endShape( - mode, - vertices, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind - ); - - // Reset some settings - isCurve = false; - isBezier = false; - isQuadratic = false; - isContour = false; - isFirstContour = true; - - // If the shape is closed, the first element was added as last element. - // We must remove it again to prevent the list of vertices from growing - // over successive calls to endShape(CLOSE) - if (closeShape) { - vertices.pop(); - } - } - return this; + this._renderer.endShape(mode, count); }; /** @@ -1809,258 +1481,18 @@ function vertex(p5, fn){ * @param {Number} z3 z-coordinate of the anchor point. */ fn.quadraticVertex = function(...args) { - p5._validateParameters('quadraticVertex', args); - if (this._renderer.isP3D) { - this._renderer.quadraticVertex(...args); + let x1, y1, z1, x2, y2, z2 = 0; + if (args.length === 4) { + [x1, y1, x2, y2] = args; } else { - //if we're drawing a contour, put the points into an - // array for inside drawing - if (this._contourInited) { - const pt = {}; - pt.x = args[0]; - pt.y = args[1]; - pt.x3 = args[2]; - pt.y3 = args[3]; - pt.type = constants.QUADRATIC; - this._contourVertices.push(pt); - - return this; - } - if (vertices.length > 0) { - isQuadratic = true; - const vert = []; - for (let i = 0; i < args.length; i++) { - vert[i] = args[i]; - } - vert.isVert = false; - if (isContour) { - contourVertices.push(vert); - } else { - vertices.push(vert); - } - } else { - p5._friendlyError( - 'vertex() must be used once before calling quadraticVertex()', - 'quadraticVertex' - ); - } - } - return this; - }; - - /** - * Adds a vertex to a custom shape. - * - * `vertex()` sets the coordinates of vertices drawn between the - * beginShape() and - * endShape() functions. - * - * The first two parameters, `x` and `y`, set the x- and y-coordinates of the - * vertex. - * - * The third parameter, `z`, is optional. It sets the z-coordinate of the - * vertex in WebGL mode. By default, `z` is 0. - * - * The fourth and fifth parameters, `u` and `v`, are also optional. They set - * the u- and v-coordinates for the vertex’s texture when used with - * endShape(). By default, `u` and `v` are both 0. - * - * @method vertex - * @param {Number} x x-coordinate of the vertex. - * @param {Number} y y-coordinate of the vertex. - * @chainable - * - * @example - *
- * - * 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(); - * } - * - *
- */ - /** - * @method vertex - * @param {Number} x - * @param {Number} y - * @param {Number} [z] z-coordinate of the vertex. Defaults to 0. - * @chainable - */ - /** - * @method vertex - * @param {Number} x - * @param {Number} y - * @param {Number} [z] - * @param {Number} [u] u-coordinate of the vertex's texture. Defaults to 0. - * @param {Number} [v] v-coordinate of the vertex's texture. Defaults to 0. - * @chainable - */ - fn.vertex = function(x, y, moveTo, u, v) { - if (this._renderer.isP3D) { - this._renderer.vertex(...arguments); - } else { - const vert = []; - vert.isVert = true; - vert[0] = x; - vert[1] = y; - vert[2] = 0; - vert[3] = 0; - vert[4] = 0; - vert[5] = this._renderer._getFill(); - vert[6] = this._renderer._getStroke(); - - if (moveTo) { - vert.moveTo = moveTo; - } - if (isContour) { - if (contourVertices.length === 0) { - vert.moveTo = true; - } - contourVertices.push(vert); - } else { - vertices.push(vert); - } + [x1, y1, z1, x2, y2, z2] = args; } + p5._validateParameters('quadraticVertex', args); + const prevOrder = this.bezierOrder(); + this.bezierOrder(2); + this.bezierVertex(x1, y1, z1); + this.bezierVertex(x2, y2, z2); + this.bezierOrder(prevOrder); return this; }; @@ -2255,27 +1687,27 @@ function vertex(p5, fn){ }; /** Sets the shader's vertex property or attribute variables. - * + * * An vertex property or vertex attribute is a variable belonging to a vertex in a shader. p5.js provides some * default properties, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are - * set using vertex(), normal() + * set using vertex(), normal() * and fill() respectively. Custom properties can also - * be defined within beginShape() and + * be defined within beginShape() and * endShape(). - * + * * The first parameter, `propertyName`, is a string with the property's name. * This is the same variable name which should be declared in the shader, such as * `in vec3 aProperty`, similar to .`setUniform()`. - * - * The second parameter, `data`, is the value assigned to the shader variable. This - * value will be applied to subsequent vertices created with + * + * The second parameter, `data`, is the value assigned to the shader variable. This + * value will be applied to subsequent vertices created with * vertex(). It can be a Number or an array of numbers, * and in the shader program the type can be declared according to the WebGL * specification. Common types include `float`, `vec2`, `vec3`, `vec4` or matrices. - * - * See also the vertexProperty() method on + * + * See also the vertexProperty() method on * Geometry objects. - * + * * @example *
* @@ -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".'); - * } - * - *
- */ -p5.prototype.textAscent = function(...args) { - p5._validateParameters('textAscent', args); - return this._renderer.textAscent(); -}; - -/** - * Calculates the descent of the current font at its current size. - * - * The descent represents the distance, in pixels, of the character with the - * longest descender below the baseline. - * - * @method textDescent - * @return {Number} descent measured in units of pixels. - * @example - *
- * - * 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".'); - * } - * - *
- */ -p5.prototype.textDescent = function(...args) { - p5._validateParameters('textDescent', args); - return this._renderer.textDescent(); -}; - -/** - * Helper function to measure ascent and descent. - */ -p5.prototype._updateTextMetrics = function() { - return this._renderer._updateTextMetrics(); -}; - -/** - * Sets the style for wrapping text when - * text() is called. - * - * The parameter, `style`, can be one of the following values: - * - * `WORD` starts new lines of text at spaces. If a string of text doesn't - * have spaces, it may overflow the text box and the canvas. This is the - * default style. - * - * `CHAR` starts new lines as needed to stay within the text box. - * - * `textWrap()` only works when the maximum width is set for a text box. For - * example, calling `text('Have a wonderful day', 0, 10, 100)` sets the - * maximum width to 100 pixels. - * - * Calling `textWrap()` without an argument returns the current style. - * - * @method textWrap - * @param {(WORD|CHAR)} style text wrapping style, either WORD or CHAR. - * @return {String} style - * @example - *
- * - * 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.'); - * } - * - *
- */ -p5.prototype.textWrap = function(wrapStyle) { - p5._validateParameters('textWrap', [wrapStyle]); - - return this._renderer.textWrap(wrapStyle); -}; - -export default p5; diff --git a/src/typography/loading_displaying.js b/src/typography/loading_displaying.js deleted file mode 100644 index 47e7fc5812..0000000000 --- a/src/typography/loading_displaying.js +++ /dev/null @@ -1,445 +0,0 @@ -/** - * @module Typography - * @submodule Loading & Displaying - * @for p5 - * @requires core - */ - -import p5 from '../core/main'; -import * as constants from '../core/constants'; -import * as opentype from 'opentype.js'; - -import '../core/friendly_errors/validate_params'; -import '../core/friendly_errors/file_errors'; -import '../core/friendly_errors/fes_core'; - -/** - * Loads a font and creates a p5.Font object. - * `loadFont()` can load fonts in either .otf or .ttf format. Loaded fonts can - * be used to style text on the canvas and in HTML elements. - * - * The first parameter, `path`, is the path to a font file. - * Paths to local files should be relative. For example, - * `'assets/inconsolata.otf'`. The Inconsolata font used in the following - * examples can be downloaded for free - * here. - * Paths to remote files should be URLs. For example, - * `'https://example.com/inconsolata.otf'`. URLs may be blocked due to browser - * security. - * - * The second parameter, `successCallback`, is optional. If a function is - * passed, it will be called once the font has loaded. The callback function - * may use the new p5.Font object if needed. - * - * The third parameter, `failureCallback`, is also optional. If a function is - * passed, it will be called if the font fails to load. The callback function - * may use the error - * Event - * object if needed. - * - * Fonts can take time to load. Calling `loadFont()` in - * preload() ensures fonts load before they're - * used in setup() or - * draw(). - * - * @method loadFont - * @param {String} path path of the font to be loaded. - * @param {Function} [successCallback] function called with the - * p5.Font object after it - * loads. - * @param {Function} [failureCallback] function called with the error - * Event - * object if the font fails to load. - * @return {p5.Font} p5.Font object. - * @example - * - *
- * - * 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.'); - * } - * - *
- */ -p5.prototype.loadFont = async function(path, onSuccess, onError) { - p5._validateParameters('loadFont', arguments); - const p5Font = new p5.Font(this); - - await new Promise(resolve => opentype.load(path, (err, font) => { - if (err) { - p5._friendlyFileLoadError(4, path); - if (typeof onError !== 'undefined') { - return onError(err); - } - console.error(err, path); - return; - } - - p5Font.font = font; - - if (typeof onSuccess !== 'undefined') { - onSuccess(p5Font); - } - resolve(); - - // check that we have an acceptable font type - const validFontTypes = ['ttf', 'otf', 'woff', 'woff2']; - - const fileNoPath = path - .split('\\') - .pop() - .split('/') - .pop(); - - const lastDotIdx = fileNoPath.lastIndexOf('.'); - let fontFamily; - let newStyle; - const fileExt = lastDotIdx < 1 ? null : fileNoPath.slice(lastDotIdx + 1); - - // if so, add it to the DOM (name-only) for use with DOM module - if (validFontTypes.includes(fileExt)) { - fontFamily = fileNoPath.slice(0, lastDotIdx !== -1 ? lastDotIdx : 0); - newStyle = document.createElement('style'); - newStyle.appendChild( - document.createTextNode( - `\n@font-face {\nfont-family: ${fontFamily};\nsrc: url(${path});\n}\n` - ) - ); - document.head.appendChild(newStyle); - } - })); - - return p5Font; -}; - -/** - * Draws text to the canvas. - * - * The first parameter, `str`, is the text to be drawn. The second and third - * parameters, `x` and `y`, set the coordinates of the text's bottom-left - * corner. See textAlign() for other ways to - * align text. - * - * The fourth and fifth parameters, `maxWidth` and `maxHeight`, are optional. - * They set the dimensions of the invisible rectangle containing the text. By - * default, they set its maximum width and height. See - * rectMode() for other ways to define the - * rectangular text box. Text will wrap to fit within the text box. Text - * outside of the box won't be drawn. - * - * Text can be styled a few ways. Call the fill() - * function to set the text's fill color. Call - * stroke() and - * strokeWeight() to set the text's outline. - * Call textSize() and - * textFont() to set the text's size and font, - * respectively. - * - * Note: `WEBGL` mode only supports fonts loaded with - * loadFont(). Calling - * stroke() has no effect in `WEBGL` mode. - * - * @method text - * @param {String|Object|Array|Number|Boolean} str text to be displayed. - * @param {Number} x x-coordinate of the text box. - * @param {Number} y y-coordinate of the text box. - * @param {Number} [maxWidth] maximum width of the text box. See - * rectMode() for - * other options. - * @param {Number} [maxHeight] maximum height of the text box. See - * rectMode() for - * other options. - * - * @chainable - * @example - *
- * - * 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.'); - * } - * - *
- */ -p5.prototype.text = function(str, x, y, maxWidth, maxHeight) { - p5._validateParameters('text', arguments); - return !(this._renderer.states.doFill || this._renderer.states.doStroke) - ? this - : this._renderer.text(...arguments); -}; - -/** - * Sets the font used by the text() function. - * - * The first parameter, `font`, sets the font. `textFont()` recognizes either - * a p5.Font object or a string with the name of a - * system font. For example, `'Courier New'`. - * - * The second parameter, `size`, is optional. It sets the font size in pixels. - * This has the same effect as calling textSize(). - * - * Note: `WEBGL` mode only supports fonts loaded with - * loadFont(). - * - * @method textFont - * @return {Object} current font or p5 Object. - * - * @example - *
- * - * 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.'); - * } - * - *
- */ -/** - * @method textFont - * @param {Object|String} font font as a p5.Font object or a string. - * @param {Number} [size] font size in pixels. - * @chainable - */ -p5.prototype.textFont = function(theFont, theSize) { - p5._validateParameters('textFont', arguments); - if (arguments.length) { - if (!theFont) { - throw new Error('null font passed to textFont'); - } - - this._renderer.states.textFont = theFont; - - if (theSize) { - this._renderer.states.textSize = theSize; - if (!this._renderer.states.leadingSet) { - // only use a default value if not previously set (#5181) - this._renderer.states._textLeading = theSize * constants._DEFAULT_LEADMULT; - } - } - - return this._renderer._applyTextProperties(); - } - - return this._renderer.states.textFont; -}; - -export default p5; diff --git a/src/typography/p5.Font.js b/src/typography/p5.Font.js deleted file mode 100644 index a91712e2ed..0000000000 --- a/src/typography/p5.Font.js +++ /dev/null @@ -1,1384 +0,0 @@ -/** - * This module defines the p5.Font class and functions for - * drawing text to the display canvas. - * @module Typography - * @submodule Loading & Displaying - * @requires core - * @requires constants - */ - -import p5 from '../core/main'; -import * as constants from '../core/constants'; - -/** - * A class to describe fonts. - * - * @class p5.Font - * @param {p5} [pInst] pointer to p5 instance. - * @example - *
- * - * 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.'); - * } - * - *
- */ -p5.Font = class Font { - constructor(p){ - this.parent = p; - - this.cache = {}; - - this.font = undefined; - } - - /** - * Returns the bounding box for a string of text written using the font. - * - * The bounding box is the smallest rectangle that can contain a string of - * text. `font.textBounds()` returns an object with the bounding box's - * location and size. For example, calling `font.textBounds('p5*js', 5, 20)` - * returns an object in the format - * `{ x: 5.7, y: 12.1 , w: 9.9, h: 28.6 }`. The `x` and `y` properties are - * always the coordinates of the bounding box's top-left corner. - * - * The first parameter, `str`, is a string of text. The second and third - * parameters, `x` and `y`, are the text's position. By default, they set the - * coordinates of the bounding box's bottom-left corner. See - * textAlign() for more ways to align text. - * - * The fourth parameter, `fontSize`, is optional. It sets the font size used to - * determine the bounding box. By default, `font.textBounds()` will use the - * current textSize(). - * - * @param {String} str string of text. - * @param {Number} x x-coordinate of the text. - * @param {Number} y y-coordinate of the text. - * @param {Number} [fontSize] font size. Defaults to the current - * textSize(). - * @return {Object} object describing the bounding box with - * properties x, y, w, and h. - * - * @example - *
- * - * 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.'); - * } - * - *
- */ - textBounds(str, x = 0, y = 0, fontSize, opts) { - // Check cache for existing bounds. Take into consideration the text alignment - // settings. Default alignment should match opentype's origin: left-aligned & - // alphabetic baseline. - const p = (opts && opts.renderer && opts.renderer._pInst) || this.parent; - - const ctx = p._renderer.drawingContext; - const alignment = ctx.textAlign || constants.LEFT; - const baseline = ctx.textBaseline || constants.BASELINE; - const cacheResults = false; - let result; - let key; - - fontSize = fontSize || p._renderer.states.textSize; - - // NOTE: cache disabled for now pending further discussion of #3436 - if (cacheResults) { - key = cacheKey('textBounds', str, x, y, fontSize, alignment, baseline); - result = this.cache[key]; - } - - if (!result) { - let minX = []; - let minY; - let maxX = []; - let maxY; - let pos; - const xCoords = []; - xCoords[0] = []; - const yCoords = []; - const scale = this._scale(fontSize); - const lineHeight = p._renderer.textLeading(); - let lineCount = 0; - - this.font.forEachGlyph( - str, - x, - y, - fontSize, - opts, - (glyph, gX, gY, gFontSize) => { - const gm = glyph.getMetrics(); - if (glyph.index === 0) { - lineCount += 1; - xCoords[lineCount] = []; - } else { - xCoords[lineCount].push(gX + gm.xMin * scale); - xCoords[lineCount].push(gX + gm.xMax * scale); - yCoords.push(gY + lineCount * lineHeight + -gm.yMin * scale); - yCoords.push(gY + lineCount * lineHeight + -gm.yMax * scale); - } - } - ); - - if (xCoords[lineCount].length > 0) { - minX[lineCount] = Math.min.apply(null, xCoords[lineCount]); - maxX[lineCount] = Math.max.apply(null, xCoords[lineCount]); - } - - let finalMaxX = 0; - for (let i = 0; i <= lineCount; i++) { - minX[i] = Math.min.apply(null, xCoords[i]); - maxX[i] = Math.max.apply(null, xCoords[i]); - const lineLength = maxX[i] - minX[i]; - if (lineLength > finalMaxX) { - finalMaxX = lineLength; - } - } - - const finalMinX = Math.min.apply(null, minX); - minY = Math.min.apply(null, yCoords); - maxY = Math.max.apply(null, yCoords); - - result = { - x: finalMinX, - y: minY, - h: maxY - minY, - w: finalMaxX, - advance: finalMinX - x - }; - - // Bounds are now calculated, so shift the x & y to match alignment settings - pos = this._handleAlignment( - p._renderer, - str, - result.x, - result.y, - result.w + result.advance - ); - - result.x = pos.x; - result.y = pos.y; - - if (cacheResults) { - this.cache[key] = result; - } - } - - return result; - } - - /** - * Returns an array of points outlining a string of text written using the - * font. - * - * Each point object in the array has three properties that describe the - * point's location and orientation, called its path angle. For example, - * `{ x: 10, y: 20, alpha: 450 }`. - * - * The first parameter, `str`, is a string of text. The second and third - * parameters, `x` and `y`, are the text's position. By default, they set the - * coordinates of the bounding box's bottom-left corner. See - * textAlign() for more ways to align text. - * - * The fourth parameter, `fontSize`, is optional. It sets the text's font - * size. By default, `font.textToPoints()` will use the current - * textSize(). - * - * The fifth parameter, `options`, is also optional. `font.textToPoints()` - * expects an object with the following properties: - * - * `sampleFactor` is the ratio of the text's path length to the number of - * samples. It defaults to 0.1. Higher values produce more points along the - * path and are more precise. - * - * `simplifyThreshold` removes collinear points if it's set to a number other - * than 0. The value represents the threshold angle to use when determining - * whether two edges are collinear. - * - * @param {String} str string of text. - * @param {Number} x x-coordinate of the text. - * @param {Number} y y-coordinate of the text. - * @param {Number} [fontSize] font size. Defaults to the current - * textSize(). - * @param {Object} [options] object with sampleFactor and simplifyThreshold - * properties. - * @return {Array} array of point objects, each with x, y, and alpha (path angle) properties. - * - * @example - *
- * - * 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.'); - * } - * - *
- */ - textToPoints(txt, x, y, fontSize, options) { - const xOriginal = x; - const result = []; - - let lines = txt.split(/\r?\n|\r|\n/g); - fontSize = fontSize || this.parent._renderer.states.textSize; - - function isSpace(i, text, glyphsLine) { - return ( - (glyphsLine[i].name && glyphsLine[i].name === 'space') || - (text.length === glyphsLine.length && text[i] === ' ') //|| - //(glyphs[i].index && glyphs[i].index === 3) - ); - } - - for (let i = 0; i < lines.length; i++) { - let xoff = 0; - x = xOriginal; - let line = lines[i]; - - line = line.replace('\t', ' '); - const glyphs = this._getGlyphs(line); - - for (let j = 0; j < glyphs.length; j++) { - if (!isSpace(j, line, glyphs)) { - // fix to #1817, #2069 - - const gpath = glyphs[j].getPath(x, y, fontSize), - paths = splitPaths(gpath.commands); - - for (let k = 0; k < paths.length; k++) { - const pts = pathToPoints(paths[k], options); - - for (let l = 0; l < pts.length; l++) { - pts[l].x += xoff; - result.push(pts[l]); - } - } - } - - xoff += glyphs[j].advanceWidth * this._scale(fontSize); - } - - y = y + this.parent._renderer.states.textLeading; - } - - return result; - } - - // ----------------------------- End API ------------------------------ - - /** - * Returns the set of opentype glyphs for the supplied string. - * - * Note that there is not a strict one-to-one mapping between characters - * and glyphs, so the list of returned glyphs can be larger or smaller - * than the length of the given string. - * - * @private - * @param {String} str the string to be converted - * @return {Array} the opentype glyphs - */ - _getGlyphs(str) { - return this.font.stringToGlyphs(str); - } - - /** - * Returns an opentype path for the supplied string and position. - * - * @private - * @param {String} line a line of text - * @param {Number} x x-position - * @param {Number} y y-position - * @param {Object} options opentype options (optional) - * @return {Object} the opentype path - */ - _getPath(line, x, y, options) { - const p = - (options && options.renderer && options.renderer._pInst) || this.parent, - renderer = p._renderer, - pos = this._handleAlignment(renderer, line, x, y); - - return this.font.getPath(line, pos.x, pos.y, renderer.states.textSize, options); - } - - /* - * Creates an SVG-formatted path-data string - * (See http://www.w3.org/TR/SVG/paths.html#PathData) - * from the given opentype path or string/position - * - * @param {Object} path an opentype path, OR the following: - * - * @param {String} line a line of text - * @param {Number} x x-position - * @param {Number} y y-position - * @param {Object} options opentype options (optional), set options.decimals - * to set the decimal precision of the path-data - * - * @return {Object} this p5.Font object - */ - _getPathData(line, x, y, options) { - let decimals = 3; - - // create path from string/position - if (typeof line === 'string' && arguments.length > 2) { - line = this._getPath(line, x, y, options); - } else if (typeof x === 'object') { - // handle options specified in 2nd arg - options = x; - } - - // handle svg arguments - if (options && typeof options.decimals === 'number') { - decimals = options.decimals; - } - - return line.toPathData(decimals); - } - - /* - * Creates an SVG element, as a string, - * from the given opentype path or string/position - * - * @param {Object} path an opentype path, OR the following: - * - * @param {String} line a line of text - * @param {Number} x x-position - * @param {Number} y y-position - * @param {Object} options opentype options (optional), set options.decimals - * to set the decimal precision of the path-data in the element, - * options.fill to set the fill color for the element, - * options.stroke to set the stroke color for the element, - * options.strokeWidth to set the strokeWidth for the element. - * - * @return {Object} this p5.Font object - */ - _getSVG(line, x, y, options) { - let decimals = 3; - - // create path from string/position - if (typeof line === 'string' && arguments.length > 2) { - line = this._getPath(line, x, y, options); - } else if (typeof x === 'object') { - // handle options specified in 2nd arg - options = x; - } - - // handle svg arguments - if (options) { - if (typeof options.decimals === 'number') { - decimals = options.decimals; - } - if (typeof options.strokeWidth === 'number') { - line.strokeWidth = options.strokeWidth; - } - if (typeof options.fill !== 'undefined') { - line.fill = options.fill; - } - if (typeof options.stroke !== 'undefined') { - line.stroke = options.stroke; - } - } - - return line.toSVG(decimals); - } - - /* - * Renders an opentype path or string/position - * to the current graphics context - * - * @param {Object} path an opentype path, OR the following: - * - * @param {String} line a line of text - * @param {Number} x x-position - * @param {Number} y y-position - * @param {Object} options opentype options (optional) - * - * @return {p5.Font} this p5.Font object - */ - _renderPath(line, x, y, options) { - let pdata; - const pg = (options && options.renderer) || this.parent._renderer; - const ctx = pg.drawingContext; - - if (typeof line === 'object' && line.commands) { - pdata = line.commands; - } else { - //pos = handleAlignment(p, ctx, line, x, y); - pdata = this._getPath(line, x, y, options).commands; - } - - if (!pg._clipping) ctx.beginPath(); - - for (const cmd of pdata) { - if (cmd.type === 'M') { - ctx.moveTo(cmd.x, cmd.y); - } else if (cmd.type === 'L') { - ctx.lineTo(cmd.x, cmd.y); - } else if (cmd.type === 'C') { - ctx.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); - } else if (cmd.type === 'Q') { - ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y); - } else if (cmd.type === 'Z') { - ctx.closePath(); - } - } - - // only draw stroke if manually set by user - if (pg.states.doStroke && pg.states.strokeSet && !pg._clipping) { - ctx.stroke(); - } - - if (pg.states.doFill && !pg._clipping) { - // if fill hasn't been set by user, use default-text-fill - if (!pg.states.fillSet) { - pg._setFill(constants._DEFAULT_TEXT_FILL); - } - ctx.fill(); - } - - return this; - } - - _textWidth(str, fontSize) { - return this.font.getAdvanceWidth(str, fontSize); - } - - _textAscent(fontSize) { - return this.font.ascender * this._scale(fontSize); - } - - _textDescent(fontSize) { - return -this.font.descender * this._scale(fontSize); - } - - _scale(fontSize) { - return ( - 1 / this.font.unitsPerEm * (fontSize || this.parent._renderer.states.textSize) - ); - } - - _handleAlignment(renderer, line, x, y, textWidth) { - const fontSize = renderer.states.textSize; - - if (typeof textWidth === 'undefined') { - textWidth = this._textWidth(line, fontSize); - } - - switch (renderer.states.textAlign) { - case constants.CENTER: - x -= textWidth / 2; - break; - case constants.RIGHT: - x -= textWidth; - break; - } - - switch (renderer.states.textBaseline) { - case constants.TOP: - y += this._textAscent(fontSize); - break; - case constants.CENTER: - y += this._textAscent(fontSize) / 2; - break; - case constants.BOTTOM: - y -= this._textDescent(fontSize); - break; - } - - return { x, y }; - } -}; - -/** - * The font's underlying - * opentype.js - * font object. - * - * @for p5.Font - * @property font - * @name font - */ - -// path-utils - -function pathToPoints(cmds, options) { - const opts = parseOpts(options, { - sampleFactor: 0.1, - simplifyThreshold: 0 - }); - - const // total-length - len = pointAtLength(cmds, 0, 1), - t = len / (len * opts.sampleFactor), - pts = []; - - for (let i = 0; i < len; i += t) { - pts.push(pointAtLength(cmds, i)); - } - - if (opts.simplifyThreshold) { - simplify(pts, opts.simplifyThreshold); - } - - return pts; -} - -function simplify(pts, 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)) { - // Remove the middle point - pts.splice(i % pts.length, 1); - num++; - } - } - return num; -} - -function splitPaths(cmds) { - const paths = []; - let current; - for (let i = 0; i < cmds.length; i++) { - if (cmds[i].type === 'M') { - if (current) { - paths.push(current); - } - current = []; - } - current.push(cmdToArr(cmds[i])); - } - paths.push(current); - - return paths; -} - -function cmdToArr(cmd) { - const arr = [cmd.type]; - if (cmd.type === 'M' || cmd.type === 'L') { - // moveto or lineto - arr.push(cmd.x, cmd.y); - } else if (cmd.type === 'C') { - arr.push(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); - } else if (cmd.type === 'Q') { - arr.push(cmd.x1, cmd.y1, cmd.x, cmd.y); - } - // else if (cmd.type === 'Z') { /* no-op */ } - return arr; -} - -function 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; -} - -//////////////////////// Helpers //////////////////////////// - -function at(v, i) { - const s = v.length; - return v[i < 0 ? i % s + s : i % s]; -} - -function collinear(a, b, c, thresholdAngle) { - if (!thresholdAngle) { - return areaTriangle(a, b, c) === 0; - } - - if (typeof collinear.tmpPoint1 === 'undefined') { - collinear.tmpPoint1 = []; - collinear.tmpPoint2 = []; - } - - const ab = collinear.tmpPoint1, - bc = collinear.tmpPoint2; - ab.x = b.x - a.x; - ab.y = b.y - a.y; - bc.x = c.x - b.x; - bc.y = c.y - b.y; - - const dot = ab.x * bc.x + ab.y * bc.y, - magA = Math.sqrt(ab.x * ab.x + ab.y * ab.y), - magB = Math.sqrt(bc.x * bc.x + bc.y * bc.y), - angle = Math.acos(dot / (magA * magB)); - - return angle < thresholdAngle; -} - -function areaTriangle(a, b, c) { - return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]); -} - -// Portions of below code copyright 2008 Dmitry Baranovskiy (via MIT license) - -function 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 - }; -} - -function 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) - ); -} - -function pointAtLength(path, length, istotal) { - path = path2curve(path); - let x; - let y; - let p; - let l; - let sp = ''; - const subpaths = {}; - let point; - let len = 0; - 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; -} - -function 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; - - const 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; -} - -function 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; - let ny; - const 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; -} - -function a2c(x1, y1, rx, ry, angle, lac, sweep_flag, x2, y2, recursive) { - // for more information of where this Math came from visit: - // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes - const PI = Math.PI; - - const _120 = PI * 120 / 180; - let f1; - let f2; - let cx; - let cy; - const rad = PI / 180 * (+angle || 0); - let res = []; - let xy; - - 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 - ]; -} - -function bezlen(x1, y1, x2, y2, x3, y3, x4, y4, z) { - if (z == null) { - z = 1; - } - z = z > 1 ? 1 : z < 0 ? 0 : z; - const z2 = z / 2; - const n = 12; - 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 - ]; - - let sum = 0; - 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; -} - -function 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; - let step = t / 2; - let t2 = t - step; - let l; - const e = 0.01; - 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; -} - -function 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; -} - -function cacheKey(...args) { - let hash = ''; - for (let i = args.length - 1; i >= 0; --i) { - hash += `?${args[i]}`; - } - return hash; -} - -export default p5; diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 96b0593a1b..8f8a4f5252 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -13,514 +13,6 @@ import { Geometry } from './p5.Geometry'; import { Matrix } from './p5.Matrix'; function primitives3D(p5, fn){ - /** - * Begins adding shapes to a new - * p5.Geometry object. - * - * The `beginGeometry()` and endGeometry() - * functions help with creating complex 3D shapes from simpler ones such as - * sphere(). `beginGeometry()` begins adding shapes - * to a custom p5.Geometry object and - * endGeometry() stops adding them. - * - * `beginGeometry()` and endGeometry() can help - * to make sketches more performant. For example, if a complex 3D shape - * doesn’t change while a sketch runs, then it can be created with - * `beginGeometry()` and endGeometry(). - * Creating a p5.Geometry object once and then - * drawing it will run faster than repeatedly drawing the individual pieces. - * - * See buildGeometry() for another way to - * build 3D shapes. - * - * Note: `beginGeometry()` can only be used in WebGL mode. - * - * @method beginGeometry - * - * @example - *
- * - * // 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; - * } - * - *
- */ - fn.beginGeometry = function() { - return this._renderer.beginGeometry(); - }; - - /** - * Stops adding shapes to a new - * p5.Geometry object and returns the object. - * - * The `beginGeometry()` and endGeometry() - * functions help with creating complex 3D shapes from simpler ones such as - * sphere(). `beginGeometry()` begins adding shapes - * to a custom p5.Geometry object and - * endGeometry() stops adding them. - * - * `beginGeometry()` and endGeometry() can help - * to make sketches more performant. For example, if a complex 3D shape - * doesn’t change while a sketch runs, then it can be created with - * `beginGeometry()` and endGeometry(). - * Creating a p5.Geometry object once and then - * drawing it will run faster than repeatedly drawing the individual pieces. - * - * See buildGeometry() for another way to - * build 3D shapes. - * - * Note: `endGeometry()` can only be used in WebGL mode. - * - * @method endGeometry - * @returns {p5.Geometry} new 3D shape. - * - * @example - *
- * - * // 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; - * } - * - *
- */ - fn.endGeometry = function() { - return this._renderer.endGeometry(); - }; - - /** * Sets the stroke rendering mode to balance performance and visual features when drawing lines. * @@ -543,10 +35,10 @@ function primitives3D(p5, fn){ * * 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){ * *
*/ - + fn.strokeMode = function (mode) { if (mode === undefined) { return this._renderer._simpleLines ? constants.SIMPLE : constants.FULL; @@ -2337,7 +1829,7 @@ function primitives3D(p5, fn){ if (detail <= 50) { arcGeom._edgesToVertices(arcGeom); - } else if (this.states.doStroke) { + } else if (this.states.strokeColor) { console.log( `Cannot apply a stroke to an ${shape} with more than 50 detail` ); @@ -2456,40 +1948,44 @@ function primitives3D(p5, fn){ let x2 = c; let y2 = d; + const prevMode = this.states.textureMode; + this.states.textureMode = constants.NORMAL; + const prevOrder = this.bezierOrder(); + this.bezierOrder(2); this.beginShape(); + const addUVs = (x, y) => [x, y, (x - x1)/width, (y - y1)/height]; if (tr !== 0) { - this.vertex(x2 - tr, y1); - this.quadraticVertex(x2, y1, x2, y1 + tr); + this.vertex(...addUVs(x2 - tr, y1)); + this.bezierVertex(...addUVs(x2, y1)) + this.bezierVertex(...addUVs(x2, y1 + tr)); } else { - this.vertex(x2, y1); + this.vertex(...addUVs(x2, y1)); } if (br !== 0) { - this.vertex(x2, y2 - br); - this.quadraticVertex(x2, y2, x2 - br, y2); + this.vertex(...addUVs(x2, y2 - br)); + this.bezierVertex(...addUVs(x2, y2)); + this.bezierVertex(...addUVs(x2 - br, y2)) } else { - this.vertex(x2, y2); + this.vertex(...addUVs(x2, y2)); } if (bl !== 0) { - this.vertex(x1 + bl, y2); - this.quadraticVertex(x1, y2, x1, y2 - bl); + this.vertex(...addUVs(x1 + bl, y2)); + this.bezierVertex(...addUVs(x1, y2)); + this.bezierVertex(...addUVs(x1, y2 - bl)); } else { - this.vertex(x1, y2); + this.vertex(...addUVs(x1, y2)); } if (tl !== 0) { - this.vertex(x1, y1 + tl); - this.quadraticVertex(x1, y1, x1 + tl, y1); + this.vertex(...addUVs(x1, y1 + tl)); + this.bezierVertex(...addUVs(x1, y1)); + this.bezierVertex(...addUVs(x1 + tl, y1)); } else { - this.vertex(x1, y1); - } - - this.shapeBuilder.geometry.uvs.length = 0; - for (const vert of this.shapeBuilder.geometry.vertices) { - const u = (vert.x - x1) / width; - const v = (vert.y - y1) / height; - this.shapeBuilder.geometry.uvs.push(u, v); + this.vertex(...addUVs(x1, y1)); } this.endShape(constants.CLOSE); + this.states.textureMode = prevMode; + this.bezierOrder(prevOrder); } return this; }; @@ -2582,21 +2078,15 @@ function primitives3D(p5, fn){ x2 = z1; z1 = z2 = z3 = z4 = 0; } - const bezierDetail = this._pInst._bezierDetail || 20; //value of Bezier detail + // TODO: handle quadratic? + const prevOrder = this.bezierOrder(); + this.bezierOrder(3); this.beginShape(); - for (let i = 0; i <= bezierDetail; i++) { - const c1 = Math.pow(1 - i / bezierDetail, 3); - const c2 = 3 * (i / bezierDetail) * Math.pow(1 - i / bezierDetail, 2); - const c3 = 3 * Math.pow(i / bezierDetail, 2) * (1 - i / bezierDetail); - const c4 = Math.pow(i / bezierDetail, 3); - this.vertex( - x1 * c1 + x2 * c2 + x3 * c3 + x4 * c4, - y1 * c1 + y2 * c2 + y3 * c3 + y4 * c4, - z1 * c1 + z2 * c2 + z3 * c3 + z4 * c4 - ); - } + this.vertex(x1, y1, z1); + this.bezierVertex(x2, y2, z2); + this.bezierVertex(x3, y3, z3); + this.bezierVertex(x4, y4, z4); this.endShape(); - return this; }; // pretier-ignore @@ -2623,32 +2113,12 @@ function primitives3D(p5, fn){ y2 = x2; z1 = z2 = z3 = z4 = 0; } - const curveDetail = this._pInst._curveDetail; this.beginShape(); - for (let i = 0; i <= curveDetail; i++) { - const c1 = Math.pow(i / curveDetail, 3) * 0.5; - const c2 = Math.pow(i / curveDetail, 2) * 0.5; - const c3 = i / curveDetail * 0.5; - const c4 = 0.5; - const vx = - c1 * (-x1 + 3 * x2 - 3 * x3 + x4) + - c2 * (2 * x1 - 5 * x2 + 4 * x3 - x4) + - c3 * (-x1 + x3) + - c4 * (2 * x2); - const vy = - c1 * (-y1 + 3 * y2 - 3 * y3 + y4) + - c2 * (2 * y1 - 5 * y2 + 4 * y3 - y4) + - c3 * (-y1 + y3) + - c4 * (2 * y2); - const vz = - c1 * (-z1 + 3 * z2 - 3 * z3 + z4) + - c2 * (2 * z1 - 5 * z2 + 4 * z3 - z4) + - c3 * (-z1 + z3) + - c4 * (2 * z2); - this.vertex(vx, vy, vz); - } + this.splineVertex(x1, y1, z1); + this.splineVertex(x2, y2, z2); + this.splineVertex(x3, y3, z3); + this.splineVertex(x4, y4, z4); this.endShape(); - return this; }; /** @@ -2682,6 +2152,7 @@ function primitives3D(p5, fn){ */ RendererGL.prototype.line = function(...args) { if (args.length === 6) { + // TODO shapes refactor this.beginShape(constants.LINES); this.vertex(args[0], args[1], args[2]); this.vertex(args[3], args[4], args[5]); @@ -2695,559 +2166,6 @@ function primitives3D(p5, fn){ return this; }; - RendererGL.prototype.bezierVertex = function(...args) { - if (this.shapeBuilder._bezierVertex.length === 0) { - throw Error('vertex() must be used once before calling bezierVertex()'); - } else { - let w_x = []; - let w_y = []; - let w_z = []; - let t, _x, _y, _z, i, k, m; - // variable i for bezierPoints, k for components, and m for anchor points. - const argLength = args.length; - - t = 0; - - if ( - this._lookUpTableBezier.length === 0 || - this._lutBezierDetail !== this._pInst._curveDetail - ) { - this._lookUpTableBezier = []; - this._lutBezierDetail = this._pInst._curveDetail; - const step = 1 / this._lutBezierDetail; - let start = 0; - let end = 1; - let j = 0; - while (start < 1) { - t = parseFloat(start.toFixed(6)); - this._lookUpTableBezier[j] = this._bezierCoefficients(t); - if (end.toFixed(6) === step.toFixed(6)) { - t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); - ++j; - this._lookUpTableBezier[j] = this._bezierCoefficients(t); - break; - } - start += step; - end -= step; - ++j; - } - } - - const LUTLength = this._lookUpTableBezier.length; - const immediateGeometry = this.shapeBuilder.geometry; - - // fillColors[0]: start point color - // fillColors[1],[2]: control point color - // fillColors[3]: end point color - const fillColors = []; - for (m = 0; m < 4; m++) fillColors.push([]); - fillColors[0] = immediateGeometry.vertexColors.slice(-4); - fillColors[3] = this.states.curFillColor.slice(); - - // Do the same for strokeColor. - const strokeColors = []; - for (m = 0; m < 4; m++) strokeColors.push([]); - strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); - strokeColors[3] = this.states.curStrokeColor.slice(); - - // Do the same for custom vertex properties - const userVertexProperties = {}; - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - userVertexProperties[propName] = []; - for (m = 0; m < 4; m++) userVertexProperties[propName].push([]); - userVertexProperties[propName][0] = prop.getSrcArray().slice(-size); - userVertexProperties[propName][3] = prop.getCurrentData(); - } - - if (argLength === 6) { - this.isBezier = true; - - w_x = [this.shapeBuilder._bezierVertex[0], args[0], args[2], args[4]]; - w_y = [this.shapeBuilder._bezierVertex[1], args[1], args[3], args[5]]; - // The ratio of the distance between the start point, the two control- - // points, and the end point determines the intermediate color. - let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1]); - let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2]); - let d2 = Math.hypot(w_x[2]-w_x[3], w_y[2]-w_y[3]); - const totalLength = d0 + d1 + d2; - d0 /= totalLength; - d2 /= totalLength; - for (k = 0; k < 4; k++) { - fillColors[1].push( - fillColors[0][k] * (1-d0) + fillColors[3][k] * d0 - ); - fillColors[2].push( - fillColors[0][k] * d2 + fillColors[3][k] * (1-d2) - ); - strokeColors[1].push( - strokeColors[0][k] * (1-d0) + strokeColors[3][k] * d0 - ); - strokeColors[2].push( - strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) - ); - } - for (const propName in immediateGeometry.userVertexProperties){ - const size = immediateGeometry.userVertexProperties[propName].getDataSize(); - for (k = 0; k < size; k++){ - userVertexProperties[propName][1].push( - userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 - ); - userVertexProperties[propName][2].push( - userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 - ); - } - } - - for (let i = 0; i < LUTLength; i++) { - // Interpolate colors using control points - this.states.curFillColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 0]; - _x = _y = 0; - for (let m = 0; m < 4; m++) { - for (let k = 0; k < 4; k++) { - this.states.curFillColor[k] += - this._lookUpTableBezier[i][m] * fillColors[m][k]; - this.states.curStrokeColor[k] += - this._lookUpTableBezier[i][m] * strokeColors[m][k]; - } - _x += w_x[m] * this._lookUpTableBezier[i][m]; - _y += w_y[m] * this._lookUpTableBezier[i][m]; - } - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - let newValues = Array(size).fill(0); - for (let m = 0; m < 4; m++){ - for (let k = 0; k < size; k++){ - newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; - } - } - prop.setCurrentData(newValues); - } - this.vertex(_x, _y); - } - // so that we leave currentColor with the last value the user set it to - this.states.curFillColor = fillColors[3]; - this.states.curStrokeColor = strokeColors[3]; - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - prop.setCurrentData(userVertexProperties[propName][2]); - } - this.shapeBuilder._bezierVertex[0] = args[4]; - this.shapeBuilder._bezierVertex[1] = args[5]; - } else if (argLength === 9) { - this.isBezier = true; - - w_x = [this.shapeBuilder._bezierVertex[0], args[0], args[3], args[6]]; - w_y = [this.shapeBuilder._bezierVertex[1], args[1], args[4], args[7]]; - w_z = [this.shapeBuilder._bezierVertex[2], args[2], args[5], args[8]]; - // The ratio of the distance between the start point, the two control- - // points, and the end point determines the intermediate color. - let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1], w_z[0]-w_z[1]); - let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2], w_z[1]-w_z[2]); - let d2 = Math.hypot(w_x[2]-w_x[3], w_y[2]-w_y[3], w_z[2]-w_z[3]); - const totalLength = d0 + d1 + d2; - d0 /= totalLength; - d2 /= totalLength; - for (let k = 0; k < 4; k++) { - fillColors[1].push( - fillColors[0][k] * (1-d0) + fillColors[3][k] * d0 - ); - fillColors[2].push( - fillColors[0][k] * d2 + fillColors[3][k] * (1-d2) - ); - strokeColors[1].push( - strokeColors[0][k] * (1-d0) + strokeColors[3][k] * d0 - ); - strokeColors[2].push( - strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) - ); - } - for (const propName in immediateGeometry.userVertexProperties){ - const size = immediateGeometry.userVertexProperties[propName].getDataSize(); - for (k = 0; k < size; k++){ - userVertexProperties[propName][1].push( - userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 - ); - userVertexProperties[propName][2].push( - userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 - ); - } - } - for (let i = 0; i < LUTLength; i++) { - // Interpolate colors using control points - this.states.curFillColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 0]; - _x = _y = _z = 0; - for (m = 0; m < 4; m++) { - for (k = 0; k < 4; k++) { - this.states.curFillColor[k] += - this._lookUpTableBezier[i][m] * fillColors[m][k]; - this.states.curStrokeColor[k] += - this._lookUpTableBezier[i][m] * strokeColors[m][k]; - } - _x += w_x[m] * this._lookUpTableBezier[i][m]; - _y += w_y[m] * this._lookUpTableBezier[i][m]; - _z += w_z[m] * this._lookUpTableBezier[i][m]; - } - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - let newValues = Array(size).fill(0); - for (let m = 0; m < 4; m++){ - for (let k = 0; k < size; k++){ - newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; - } - } - prop.setCurrentData(newValues); - } - this.vertex(_x, _y, _z); - } - // so that we leave currentColor with the last value the user set it to - this.states.curFillColor = fillColors[3]; - this.states.curStrokeColor = strokeColors[3]; - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - prop.setCurrentData(userVertexProperties[propName][2]); - } - this.shapeBuilder._bezierVertex[0] = args[6]; - this.shapeBuilder._bezierVertex[1] = args[7]; - this.shapeBuilder._bezierVertex[2] = args[8]; - } - } - }; - - RendererGL.prototype.quadraticVertex = function(...args) { - if (this.shapeBuilder._quadraticVertex.length === 0) { - throw Error('vertex() must be used once before calling quadraticVertex()'); - } else { - let w_x = []; - let w_y = []; - let w_z = []; - let t, _x, _y, _z, i, k, m; - // variable i for bezierPoints, k for components, and m for anchor points. - const argLength = args.length; - - t = 0; - - if ( - this._lookUpTableQuadratic.length === 0 || - this._lutQuadraticDetail !== this._pInst._curveDetail - ) { - this._lookUpTableQuadratic = []; - this._lutQuadraticDetail = this._pInst._curveDetail; - const step = 1 / this._lutQuadraticDetail; - let start = 0; - let end = 1; - let j = 0; - while (start < 1) { - t = parseFloat(start.toFixed(6)); - this._lookUpTableQuadratic[j] = this._quadraticCoefficients(t); - if (end.toFixed(6) === step.toFixed(6)) { - t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); - ++j; - this._lookUpTableQuadratic[j] = this._quadraticCoefficients(t); - break; - } - start += step; - end -= step; - ++j; - } - } - - const LUTLength = this._lookUpTableQuadratic.length; - const immediateGeometry = this.shapeBuilder.geometry; - - // fillColors[0]: start point color - // fillColors[1]: control point color - // fillColors[2]: end point color - const fillColors = []; - for (m = 0; m < 3; m++) fillColors.push([]); - fillColors[0] = immediateGeometry.vertexColors.slice(-4); - fillColors[2] = this.states.curFillColor.slice(); - - // Do the same for strokeColor. - const strokeColors = []; - for (m = 0; m < 3; m++) strokeColors.push([]); - strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); - strokeColors[2] = this.states.curStrokeColor.slice(); - - // Do the same for user defined vertex properties - const userVertexProperties = {}; - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - userVertexProperties[propName] = []; - for (m = 0; m < 3; m++) userVertexProperties[propName].push([]); - userVertexProperties[propName][0] = prop.getSrcArray().slice(-size); - userVertexProperties[propName][2] = prop.getCurrentData(); - } - - if (argLength === 4) { - this.isQuadratic = true; - - w_x = [this.shapeBuilder._quadraticVertex[0], args[0], args[2]]; - w_y = [this.shapeBuilder._quadraticVertex[1], args[1], args[3]]; - - // The ratio of the distance between the start point, the control- - // point, and the end point determines the intermediate color. - let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1]); - let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2]); - const totalLength = d0 + d1; - d0 /= totalLength; - for (let k = 0; k < 4; k++) { - fillColors[1].push( - fillColors[0][k] * (1-d0) + fillColors[2][k] * d0 - ); - strokeColors[1].push( - strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 - ); - } - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - for (let k = 0; k < size; k++){ - userVertexProperties[propName][1].push( - userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 - ); - } - } - - for (let i = 0; i < LUTLength; i++) { - // Interpolate colors using control points - this.states.curFillColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 0]; - _x = _y = 0; - for (let m = 0; m < 3; m++) { - for (let k = 0; k < 4; k++) { - this.states.curFillColor[k] += - this._lookUpTableQuadratic[i][m] * fillColors[m][k]; - this.states.curStrokeColor[k] += - this._lookUpTableQuadratic[i][m] * strokeColors[m][k]; - } - _x += w_x[m] * this._lookUpTableQuadratic[i][m]; - _y += w_y[m] * this._lookUpTableQuadratic[i][m]; - } - - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - let newValues = Array(size).fill(0); - for (let m = 0; m < 3; m++){ - for (let k = 0; k < size; k++){ - newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; - } - } - prop.setCurrentData(newValues); - } - this.vertex(_x, _y); - } - - // so that we leave currentColor with the last value the user set it to - this.states.curFillColor = fillColors[2]; - this.states.curStrokeColor = strokeColors[2]; - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - prop.setCurrentData(userVertexProperties[propName][2]); - } - this.shapeBuilder._quadraticVertex[0] = args[2]; - this.shapeBuilder._quadraticVertex[1] = args[3]; - } else if (argLength === 6) { - this.isQuadratic = true; - - w_x = [this.shapeBuilder._quadraticVertex[0], args[0], args[3]]; - w_y = [this.shapeBuilder._quadraticVertex[1], args[1], args[4]]; - w_z = [this.shapeBuilder._quadraticVertex[2], args[2], args[5]]; - - // The ratio of the distance between the start point, the control- - // point, and the end point determines the intermediate color. - let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1], w_z[0]-w_z[1]); - let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2], w_z[1]-w_z[2]); - const totalLength = d0 + d1; - d0 /= totalLength; - for (k = 0; k < 4; k++) { - fillColors[1].push( - fillColors[0][k] * (1-d0) + fillColors[2][k] * d0 - ); - strokeColors[1].push( - strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 - ); - } - - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - for (let k = 0; k < size; k++){ - userVertexProperties[propName][1].push( - userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 - ); - } - } - - for (i = 0; i < LUTLength; i++) { - // Interpolate colors using control points - this.states.curFillColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 0]; - _x = _y = _z = 0; - for (m = 0; m < 3; m++) { - for (k = 0; k < 4; k++) { - this.states.curFillColor[k] += - this._lookUpTableQuadratic[i][m] * fillColors[m][k]; - this.states.curStrokeColor[k] += - this._lookUpTableQuadratic[i][m] * strokeColors[m][k]; - } - _x += w_x[m] * this._lookUpTableQuadratic[i][m]; - _y += w_y[m] * this._lookUpTableQuadratic[i][m]; - _z += w_z[m] * this._lookUpTableQuadratic[i][m]; - } - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - let newValues = Array(size).fill(0); - for (let m = 0; m < 3; m++){ - for (let k = 0; k < size; k++){ - newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; - } - } - prop.setCurrentData(newValues); - } - this.vertex(_x, _y, _z); - } - - // so that we leave currentColor with the last value the user set it to - this.states.curFillColor = fillColors[2]; - this.states.curStrokeColor = strokeColors[2]; - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - prop.setCurrentData(userVertexProperties[propName][2]); - } - this.shapeBuilder._quadraticVertex[0] = args[3]; - this.shapeBuilder._quadraticVertex[1] = args[4]; - this.shapeBuilder._quadraticVertex[2] = args[5]; - } - } - }; - - RendererGL.prototype.curveVertex = function(...args) { - let w_x = []; - let w_y = []; - let w_z = []; - let t, _x, _y, _z, i; - t = 0; - const argLength = args.length; - - if ( - this._lookUpTableBezier.length === 0 || - this._lutBezierDetail !== this._pInst._curveDetail - ) { - this._lookUpTableBezier = []; - this._lutBezierDetail = this._pInst._curveDetail; - const step = 1 / this._lutBezierDetail; - let start = 0; - let end = 1; - let j = 0; - while (start < 1) { - t = parseFloat(start.toFixed(6)); - this._lookUpTableBezier[j] = this._bezierCoefficients(t); - if (end.toFixed(6) === step.toFixed(6)) { - t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); - ++j; - this._lookUpTableBezier[j] = this._bezierCoefficients(t); - break; - } - start += step; - end -= step; - ++j; - } - } - - const LUTLength = this._lookUpTableBezier.length; - - if (argLength === 2) { - this.shapeBuilder._curveVertex.push(args[0]); - this.shapeBuilder._curveVertex.push(args[1]); - if (this.shapeBuilder._curveVertex.length === 8) { - this.isCurve = true; - w_x = this._bezierToCatmull([ - this.shapeBuilder._curveVertex[0], - this.shapeBuilder._curveVertex[2], - this.shapeBuilder._curveVertex[4], - this.shapeBuilder._curveVertex[6] - ]); - w_y = this._bezierToCatmull([ - this.shapeBuilder._curveVertex[1], - this.shapeBuilder._curveVertex[3], - this.shapeBuilder._curveVertex[5], - this.shapeBuilder._curveVertex[7] - ]); - for (i = 0; i < LUTLength; i++) { - _x = - w_x[0] * this._lookUpTableBezier[i][0] + - w_x[1] * this._lookUpTableBezier[i][1] + - w_x[2] * this._lookUpTableBezier[i][2] + - w_x[3] * this._lookUpTableBezier[i][3]; - _y = - w_y[0] * this._lookUpTableBezier[i][0] + - w_y[1] * this._lookUpTableBezier[i][1] + - w_y[2] * this._lookUpTableBezier[i][2] + - w_y[3] * this._lookUpTableBezier[i][3]; - this.vertex(_x, _y); - } - for (i = 0; i < argLength; i++) { - this.shapeBuilder._curveVertex.shift(); - } - } - } else if (argLength === 3) { - this.shapeBuilder._curveVertex.push(args[0]); - this.shapeBuilder._curveVertex.push(args[1]); - this.shapeBuilder._curveVertex.push(args[2]); - if (this.shapeBuilder._curveVertex.length === 12) { - this.isCurve = true; - w_x = this._bezierToCatmull([ - this.shapeBuilder._curveVertex[0], - this.shapeBuilder._curveVertex[3], - this.shapeBuilder._curveVertex[6], - this.shapeBuilder._curveVertex[9] - ]); - w_y = this._bezierToCatmull([ - this.shapeBuilder._curveVertex[1], - this.shapeBuilder._curveVertex[4], - this.shapeBuilder._curveVertex[7], - this.shapeBuilder._curveVertex[10] - ]); - w_z = this._bezierToCatmull([ - this.shapeBuilder._curveVertex[2], - this.shapeBuilder._curveVertex[5], - this.shapeBuilder._curveVertex[8], - this.shapeBuilder._curveVertex[11] - ]); - for (i = 0; i < LUTLength; i++) { - _x = - w_x[0] * this._lookUpTableBezier[i][0] + - w_x[1] * this._lookUpTableBezier[i][1] + - w_x[2] * this._lookUpTableBezier[i][2] + - w_x[3] * this._lookUpTableBezier[i][3]; - _y = - w_y[0] * this._lookUpTableBezier[i][0] + - w_y[1] * this._lookUpTableBezier[i][1] + - w_y[2] * this._lookUpTableBezier[i][2] + - w_y[3] * this._lookUpTableBezier[i][3]; - _z = - w_z[0] * this._lookUpTableBezier[i][0] + - w_z[1] * this._lookUpTableBezier[i][1] + - w_z[2] * this._lookUpTableBezier[i][2] + - w_z[3] * this._lookUpTableBezier[i][3]; - this.vertex(_x, _y, _z); - } - for (i = 0; i < argLength; i++) { - this.shapeBuilder._curveVertex.shift(); - } - } - } - }; - RendererGL.prototype.image = function( img, sx, @@ -3266,7 +2184,7 @@ function primitives3D(p5, fn){ this.push(); this.noLights(); - this.states.doStroke = false;; + this.states.strokeColor = null;; this.texture(img); this.states.textureMode = constants.NORMAL; @@ -3453,7 +2371,7 @@ function primitives3D(p5, fn){ planeGeom.computeFaces().computeNormals(); if (detailX <= 1 && detailY <= 1) { planeGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this.states.doStroke) { + } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on plane objects with more' + ' than 1 detailX or 1 detailY' @@ -3533,7 +2451,7 @@ function primitives3D(p5, fn){ boxGeom.computeNormals(); if (detailX <= 4 && detailY <= 4) { boxGeom._edgesToVertices(); - } else if (this.states.doStroke) { + } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on box objects with more' + ' than 4 detailX or 4 detailY' @@ -3589,7 +2507,7 @@ function primitives3D(p5, fn){ ellipsoidGeom.computeFaces(); if (detailX <= 24 && detailY <= 24) { ellipsoidGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this.states.doStroke) { + } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on ellipsoids with more' + ' than 24 detailX or 24 detailY' @@ -3627,7 +2545,7 @@ function primitives3D(p5, fn){ // normals are computed in call to _truncatedCone if (detailX <= 24 && detailY <= 16) { cylinderGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this.states.doStroke) { + } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on cylinder objects with more' + ' than 24 detailX or 16 detailY' @@ -3654,16 +2572,16 @@ function primitives3D(p5, fn){ this, 1, 0, - 1, + 1, detailX, detailY, - cap, - false + cap, + false ); }, this); if (detailX <= 24 && detailY <= 16) { coneGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this.states.doStroke) { + } else if (this.states.strokeColor) { console.log( 'Cannot draw stroke on cone objects with more' + ' than 24 detailX or 16 detailY' @@ -3726,7 +2644,7 @@ function primitives3D(p5, fn){ torusGeom.computeFaces(); if (detailX <= 24 && detailY <= 16) { torusGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this.states.doStroke) { + } else if (this.states.strokeColor) { console.log( 'Cannot draw strokes on torus object with more' + ' than 24 detailX or 16 detailY' @@ -3737,6 +2655,108 @@ function primitives3D(p5, fn){ } this._drawGeometryScaled(this.geometryBufferCache.getGeometryByID(gid), radius, radius, radius); } + + /** + * Sets the number of segments used to draw spline curves in WebGL mode. + * + * In WebGL mode, smooth shapes are drawn using many flat segments. Adding + * more flat segments makes shapes appear smoother. + * + * The parameter, `detail`, is the number of segments to use while drawing a + * spline curve. For example, calling `curveDetail(5)` will use 5 segments to + * draw curves with the curve() function. By + * default,`detail` is 20. + * + * Note: `curveDetail()` has no effect in 2D mode. + * + * @method curveDetail + * @param {Number} resolution number of segments to use. Defaults to 20. + * @chainable + * + * @example + *
+ * + * 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.' + * ); + * } + * + *
+ */ + fn.curveDetail = function(d) { + if (!(this._renderer instanceof RendererGL)) { + throw new Error( + 'curveDetail() only works in WebGL mode. Did you mean to call createCanvas(width, height, WEBGL)?' + ); + } + return this._renderer.curveDetail(d); + }; } export default primitives3D; diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 6d269282c2..4dd66c8fdd 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -87,12 +87,12 @@ class GeometryBuilder { this.geometry.vertexProperty(propName, data, size); } - if (this.renderer.states.doFill) { + if (this.renderer.states.fillColor) { this.geometry.faces.push( ...input.faces.map(f => f.map(idx => idx + startIdx)) ); } - if (this.renderer.states.doStroke) { + if (this.renderer.states.strokeColor) { this.geometry.edges.push( ...input.edges.map(edge => edge.map(idx => idx + startIdx)) ); @@ -111,7 +111,7 @@ class GeometryBuilder { addImmediate(geometry, shapeMode) { const faces = []; - if (this.renderer.states.doFill) { + if (this.renderer.states.fillColor) { if ( shapeMode === constants.TRIANGLE_STRIP || shapeMode === constants.QUAD_STRIP diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index 12d8798d63..41535345e7 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -20,7 +20,7 @@ const INITIAL_VERTEX_SIZE = export class ShapeBuilder { constructor(renderer) { this.renderer = renderer; - this.shapeMode = constants.TESS; + this.shapeMode = constants.PATH; this.geometry = new Geometry(undefined, undefined, undefined, this.renderer); this.geometry.gid = '__IMMEDIATE_MODE_GEOMETRY__'; @@ -40,203 +40,123 @@ export class ShapeBuilder { this.bufferStrides = { ...INITIAL_BUFFER_STRIDES }; } - beginShape(mode = constants.TESS) { - this.shapeMode = mode; - if (this._useUserVertexProperties === true){ + constructFromContours(shape, contours) { + if (this._useUserVertexProperties){ this._resetUserVertexProperties(); } this.geometry.reset(); this.contourIndices = []; - } - - endShape = function( - mode, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind, - count = 1 - ) { - if (this.shapeMode === constants.POINTS) { - // @TODO(dave) move to renderer directly - this.renderer._drawPoints( - this.geometry.vertices, - this.renderer.buffers.point - ); - return this; - } - // When we are drawing a shape then the shape mode is TESS, - // but in case of triangle we can skip the breaking into small triangle - // this can optimize performance by skipping the step of breaking it into triangles - if (this.geometry.vertices.length === 3 && - this.shapeMode === constants.TESS - ) { - this.shapeMode === constants.TRIANGLES; - } + // TODO: handle just some contours having non-PATH mode + this.shapeMode = shape.contours[0].kind; + const shouldProcessEdges = !!this.renderer.states.strokeColor; - this.isProcessingVertices = true; - this._processVertices(...arguments); - this.isProcessingVertices = false; - - // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we - // need to convert them to a supported format. In `vertex()`, we reformat - // the input data into the formats specified below. - if (this.shapeMode === constants.QUADS) { - this.shapeMode = constants.TRIANGLES; - } else if (this.shapeMode === constants.QUAD_STRIP) { - this.shapeMode = constants.TRIANGLE_STRIP; + const userVertexPropertyHelpers = {}; + if (shape.userVertexProperties) { + this._useUserVertexProperties = true; + for (const key in shape.userVertexProperties) { + const name = shape.vertexPropertyName(key); + const prop = this.geometry._userVertexPropertyHelper(name, [], shape.userVertexProperties[key]); + userVertexPropertyHelpers[key] = prop; + this.tessyVertexSize += prop.getDataSize(); + this.bufferStrides[prop.getSrcName()] = prop.getDataSize(); + this.renderer.buffers.user.push( + new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), name, this.renderer) + ); + } + } else { + this._useUserVertexProperties = false; } - this.isBezier = false; - this.isQuadratic = false; - this.isCurve = false; - this._bezierVertex.length = 0; - this._quadraticVertex.length = 0; - this._curveVertex.length = 0; - } - - beginContour() { - if (this.shapeMode !== constants.TESS) { - throw new Error('WebGL mode can only use contours with beginShape(TESS).'); - } - this.contourIndices.push( - this.geometry.vertices.length - ); - } + for (const contour of contours) { + this.contourIndices.push(this.geometry.vertices.length); + for (const vertex of contour) { + // WebGL doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn + // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra + // work to convert QUAD_STRIP here, since the only difference is in how edges + // are rendered.) + if (this.shapeMode === constants.QUADS) { + // A finished quad turned into triangles should leave 6 vertices in the + // buffer: + // 0--3 0 3--5 + // | | --> | \ \ | + // 1--2 1--2 4 + // When vertex index 3 is being added, add the necessary duplicates. + if (this.geometry.vertices.length % 6 === 3) { + for (const key in this.bufferStrides) { + const stride = this.bufferStrides[key]; + const buffer = this.geometry[key]; + buffer.push( + ...buffer.slice( + buffer.length - 3 * stride, + buffer.length - 2 * stride + ), + ...buffer.slice(buffer.length - stride, buffer.length), + ); + } + } + } - vertex(x, y) { - // WebGL doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn - // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra - // work to convert QUAD_STRIP here, since the only difference is in how edges - // are rendered.) - if (this.shapeMode === constants.QUADS) { - // A finished quad turned into triangles should leave 6 vertices in the - // buffer: - // 0--3 0 3--5 - // | | --> | \ \ | - // 1--2 1--2 4 - // When vertex index 3 is being added, add the necessary duplicates. - if (this.geometry.vertices.length % 6 === 3) { - for (const key in this.bufferStrides) { - const stride = this.bufferStrides[key]; - const buffer = this.geometry[key]; - buffer.push( - ...buffer.slice( - buffer.length - 3 * stride, - buffer.length - 2 * stride - ), - ...buffer.slice(buffer.length - stride, buffer.length) - ); + this.geometry.vertices.push(vertex.position); + this.geometry.vertexNormals.push(vertex.normal || new Vector(0, 0, 0)); + this.geometry.uvs.push(vertex.textureCoordinates.x, vertex.textureCoordinates.y); + if (this.renderer.states.fillColor) { + this.geometry.vertexColors.push(...vertex.fill.array()); + } else { + this.geometry.vertexColors.push(0, 0, 0, 0); + } + if (this.renderer.states.strokeColor) { + this.geometry.vertexStrokeColors.push(...vertex.stroke.array()); + } else { + this.geometry.vertexStrokeColors.push(0, 0, 0, 0); + } + for (const key in userVertexPropertyHelpers) { + const prop = userVertexPropertyHelpers[key]; + if (key in vertex) { + prop.setCurrentData(vertex[key]); + } + prop.pushCurrentData(); } } } - let z, u, v; - - // default to (x, y) mode: all other arguments assumed to be 0. - z = u = v = 0; - - if (arguments.length === 3) { - // (x, y, z) mode: (u, v) assumed to be 0. - z = arguments[2]; - } else if (arguments.length === 4) { - // (x, y, u, v) mode: z assumed to be 0. - u = arguments[2]; - v = arguments[3]; - } else if (arguments.length === 5) { - // (x, y, z, u, v) mode - z = arguments[2]; - u = arguments[3]; - v = arguments[4]; + if (shouldProcessEdges) { + this.geometry.edges = this._calculateEdges(this.shapeMode, this.geometry.vertices); } - const vert = new Vector(x, y, z); - this.geometry.vertices.push(vert); - this.geometry.vertexNormals.push(this.renderer.states._currentNormal); - - for (const propName in this.geometry.userVertexProperties){ - const geom = this.geometry; - const prop = geom.userVertexProperties[propName]; - const verts = geom.vertices; - if (prop.getSrcArray().length === 0 && verts.length > 1) { - const numMissingValues = prop.getDataSize() * (verts.length - 1); - const missingValues = Array(numMissingValues).fill(0); - prop.pushDirect(missingValues); - } - prop.pushCurrentData(); + if (shouldProcessEdges && !this.renderer.geometryBuilder) { + this.geometry._edgesToVertices(); } - const vertexColor = this.renderer.states.curFillColor || [0.5, 0.5, 0.5, 1.0]; - this.geometry.vertexColors.push( - vertexColor[0], - vertexColor[1], - vertexColor[2], - vertexColor[3] - ); - const lineVertexColor = this.renderer.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; - this.geometry.vertexStrokeColors.push( - lineVertexColor[0], - lineVertexColor[1], - lineVertexColor[2], - lineVertexColor[3] - ); - - if (this.renderer.states.textureMode === constants.IMAGE && !this.isProcessingVertices) { - if (this.renderer.states._tex !== null) { - if (this.renderer.states._tex.width > 0 && this.renderer.states._tex.height > 0) { - u /= this.renderer.states._tex.width; - v /= this.renderer.states._tex.height; - } - } else if ( - this.renderer.states.userFillShader !== undefined || - this.renderer.states.userStrokeShader !== undefined || - this.renderer.states.userPointShader !== undefined || - this.renderer.states.userImageShader !== undefined - ) { - // Do nothing if user-defined shaders are present - } else if ( - this.renderer.states._tex === null && - arguments.length >= 4 - ) { - // Only throw this warning if custom uv's have been provided - console.warn( - 'You must first call texture() before using' + - ' vertex() with image based u and v coordinates' - ); - } + if (this.shapeMode === constants.PATH) { + this.isProcessingVertices = true; + this._tesselateShape(); + this.isProcessingVertices = false; + } else if (this.shapeMode === constants.QUAD_STRIP) { + // The only difference between these two modes is which edges are + // displayed, so after we've updated the edges, we switch the mode + // to one that native WebGL knows how to render. + this.shapeMode = constants.TRIANGLE_STRIP; + } else if (this.shapeMode === constants.QUADS) { + // We translate QUADS to TRIANGLES when vertices are being added, + // since QUADS is just a p5 mode, whereas TRIANGLES is also a mode + // that native WebGL knows how to render. Once we've processed edges, + // everything should be set up for TRIANGLES mode. + this.shapeMode = constants.TRIANGLES; } - this.geometry.uvs.push(u, v); - - this._bezierVertex[0] = x; - this._bezierVertex[1] = y; - this._bezierVertex[2] = z; - - this._quadraticVertex[0] = x; - this._quadraticVertex[1] = y; - this._quadraticVertex[2] = z; - - return this; - } - - vertexProperty(propertyName, data) { - if (!this._useUserVertexProperties) { - this._useUserVertexProperties = true; - this.geometry.userVertexProperties = {}; - } - const propertyExists = this.geometry.userVertexProperties[propertyName]; - let prop; - if (propertyExists){ - prop = this.geometry.userVertexProperties[propertyName]; - } else { - prop = this.geometry._userVertexPropertyHelper(propertyName, data); - this.tessyVertexSize += prop.getDataSize(); - this.bufferStrides[prop.getSrcName()] = prop.getDataSize(); - this.renderer.buffers.user.push( - new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this.renderer) - ); + if ( + this.renderer.states.textureMode === constants.IMAGE && + this.renderer.states._tex !== null && + this.renderer.states._tex.width > 0 && + this.renderer.states._tex.height > 0 + ) { + this.geometry.uvs = this.geometry.uvs.map((val, i) => { + if (i % 2 === 0) { + return val / this.renderer.states._tex.width; + } else { + return val / this.renderer.states._tex.height; + } + }) } - prop.setCurrentData(data); } _resetUserVertexProperties() { @@ -251,50 +171,6 @@ export class ShapeBuilder { this.geometry.userVertexProperties = {}; } - /** - * Interpret the vertices of the current geometry according to - * the current shape mode, and convert them to something renderable (either - * triangles or lines.) - * @private - */ - _processVertices(mode) { - if (this.geometry.vertices.length === 0) return; - - const calculateStroke = this.renderer.states.doStroke; - const shouldClose = mode === constants.CLOSE; - if (calculateStroke) { - this.geometry.edges = this._calculateEdges( - this.shapeMode, - this.geometry.vertices, - shouldClose - ); - if (!this.renderer.geometryBuilder) { - this.geometry._edgesToVertices(); - } - } - - // For hollow shapes, user must set mode to TESS - const convexShape = this.shapeMode === constants.TESS; - // If the shape has a contour, we have to re-triangulate to cut out the - // contour region - const hasContour = this.contourIndices.length > 0; - // We tesselate when drawing curves or convex shapes - const shouldTess = - this.renderer.states.doFill && - ( - this.isBezier || - this.isQuadratic || - this.isCurve || - convexShape || - hasContour - ) && - this.shapeMode !== constants.LINES; - - if (shouldTess) { - this._tesselateShape(); - } - } - /** * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and * tesselates shapes when applicable. @@ -304,12 +180,11 @@ export class ShapeBuilder { _calculateEdges( shapeMode, verts, - shouldClose ) { const res = []; let i = 0; const contourIndices = this.contourIndices.slice(); - let contourStart = 0; + let contourStart = -1; switch (shapeMode) { case constants.TRIANGLE_STRIP: for (i = 0; i < verts.length - 2; i++) { @@ -345,8 +220,8 @@ export class ShapeBuilder { for (i = 0; i < verts.length - 5; i += 6) { res.push([i, i + 1]); res.push([i + 1, i + 2]); - res.push([i + 3, i + 5]); - res.push([i + 4, i + 5]); + res.push([i + 2, i + 5]); + res.push([i + 5, i]); } break; case constants.QUAD_STRIP: @@ -355,31 +230,27 @@ export class ShapeBuilder { // 1---3---5 for (i = 0; i < verts.length - 2; i += 2) { res.push([i, i + 1]); - res.push([i, i + 2]); res.push([i + 1, i + 3]); + res.push([i, i + 2]); } res.push([i, i + 1]); break; default: // TODO: handle contours in other modes too for (i = 0; i < verts.length; i++) { - // Handle breaks between contours - if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { - res.push([i, i + 1]); + if (i === contourIndices[0]) { + contourStart = contourIndices.shift(); + } else if ( + verts[contourStart] && + verts[i].equals(verts[contourStart]) + ) { + res.push([i - 1, contourStart]); } else { - if (shouldClose || contourStart) { - res.push([i, contourStart]); - } - if (contourIndices.length > 0) { - contourStart = contourIndices.shift(); - } + res.push([i - 1, i]); } } break; } - if (shapeMode !== constants.TESS && shouldClose) { - res.push([verts.length - 1, 0]); - } return res; } @@ -388,9 +259,10 @@ export class ShapeBuilder { * @private */ _tesselateShape() { - // TODO: handle non-TESS shape modes that have contours + // TODO: handle non-PATH shape modes that have contours this.shapeMode = constants.TRIANGLES; - const contours = [[]]; + // const contours = [[]]; + const contours = []; for (let i = 0; i < this.geometry.vertices.length; i++) { if ( this.contourIndices.length > 0 && @@ -438,19 +310,21 @@ export class ShapeBuilder { j = j + this.tessyVertexSize ) { colors.push(...polyTriangles.slice(j + 5, j + 9)); - this.renderer.normal(...polyTriangles.slice(j + 9, j + 12)); + this.geometry.vertexNormals.push(new Vector(...polyTriangles.slice(j + 9, j + 12))); { let offset = 12; for (const propName in this.geometry.userVertexProperties){ - const prop = this.geometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - const start = j + offset; - const end = start + size; - prop.setCurrentData(polyTriangles.slice(start, end)); - offset += size; + const prop = this.geometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + const start = j + offset; + const end = start + size; + prop.setCurrentData(polyTriangles.slice(start, end)); + prop.pushCurrentData(); + offset += size; } } - this.vertex(...polyTriangles.slice(j, j + 5)); + this.geometry.vertices.push(new Vector(...polyTriangles.slice(j, j + 3))); + this.geometry.uvs.push(...polyTriangles.slice(j + 3, j + 5)); } if (this.renderer.geometryBuilder) { // Tesselating the face causes the indices of edge vertices to stop being diff --git a/src/webgl/material.js b/src/webgl/material.js index 89c14de19a..1768e0cfaa 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -9,6 +9,7 @@ import * as constants from '../core/constants'; import { RendererGL } from './p5.RendererGL'; import { Shader } from './p5.Shader'; import { request } from '../io/files'; +import { Color } from '../color/p5.Color'; function material(p5, fn){ /** @@ -2928,7 +2929,7 @@ function material(p5, fn){ this._renderer.states.curAmbientColor = color._array; this._renderer.states._useNormalMaterial = false; this._renderer.states.enableLighting = true; - this._renderer.states.doFill = true; + this._renderer.states.fillColor = true; return this; }; @@ -3625,7 +3626,7 @@ function material(p5, fn){ this.states.drawMode = constants.TEXTURE; this.states._useNormalMaterial = false; this.states._tex = tex; - this.states.doFill = true; + this.states.fillColor = new Color(255); }; RendererGL.prototype.normalMaterial = function(...args) { @@ -3634,8 +3635,8 @@ function material(p5, fn){ this.states._useEmissiveMaterial = false; this.states._useNormalMaterial = true; this.states.curFillColor = [1, 1, 1, 1]; - this.states.doFill = true; - this.states.doStroke = false; + this.states.fillColor = new Color(255); + this.states.strokeColor = null; } // RendererGL.prototype.ambientMaterial = function(v1, v2, v3) { diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 9d78813469..2a5ced5f7a 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -1559,7 +1559,7 @@ class Framebuffer { this.renderer.states.imageMode = constants.CORNER; this.renderer.setCamera(this.filterCamera); this.renderer.resetMatrix(); - this.renderer.states.doStroke = false; + this.renderer.states.strokeColor = null; this.renderer.clear(); this.renderer._drawingFilter = true; this.renderer.image( diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index afaf4c3b7c..89701ced9c 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1818,6 +1818,9 @@ class Geometry { return this.name; }, getCurrentData(){ + if (this.currentData === undefined) { + this.currentData = new Array(this.getDataSize()).fill(0); + } return this.currentData; }, getDataSize() { diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index afce7c41ea..06d5be4b2a 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1,53 +1,54 @@ -import * as constants from '../core/constants'; -import GeometryBuilder from './GeometryBuilder'; -import { Renderer } from '../core/p5.Renderer'; -import { Matrix } from './p5.Matrix'; -import { Camera } from './p5.Camera'; -import { Vector } from '../math/p5.Vector'; -import { RenderBuffer } from './p5.RenderBuffer'; -import { DataArray } from './p5.DataArray'; -import { Shader } from './p5.Shader'; -import { Image } from '../image/p5.Image'; -import { Texture, MipmapTexture } from './p5.Texture'; -import { Framebuffer } from './p5.Framebuffer'; -import { Graphics } from '../core/p5.Graphics'; -import { Element } from '../dom/p5.Element'; -import { ShapeBuilder } from './ShapeBuilder'; -import { GeometryBufferCache } from './GeometryBufferCache'; - -import lightingShader from './shaders/lighting.glsl'; -import webgl2CompatibilityShader from './shaders/webgl2Compatibility.glsl'; -import normalVert from './shaders/normal.vert'; -import normalFrag from './shaders/normal.frag'; -import basicFrag from './shaders/basic.frag'; -import sphereMappingFrag from './shaders/sphereMapping.frag'; -import lightVert from './shaders/light.vert'; -import lightTextureFrag from './shaders/light_texture.frag'; -import phongVert from './shaders/phong.vert'; -import phongFrag from './shaders/phong.frag'; -import fontVert from './shaders/font.vert'; -import fontFrag from './shaders/font.frag'; -import lineVert from './shaders/line.vert'; -import lineFrag from './shaders/line.frag'; -import pointVert from './shaders/point.vert'; -import pointFrag from './shaders/point.frag'; -import imageLightVert from './shaders/imageLight.vert'; -import imageLightDiffusedFrag from './shaders/imageLightDiffused.frag'; -import imageLightSpecularFrag from './shaders/imageLightSpecular.frag'; - -import filterGrayFrag from './shaders/filters/gray.frag'; -import filterErodeFrag from './shaders/filters/erode.frag'; -import filterDilateFrag from './shaders/filters/dilate.frag'; -import filterBlurFrag from './shaders/filters/blur.frag'; -import filterPosterizeFrag from './shaders/filters/posterize.frag'; -import filterOpaqueFrag from './shaders/filters/opaque.frag'; -import filterInvertFrag from './shaders/filters/invert.frag'; -import filterThresholdFrag from './shaders/filters/threshold.frag'; -import filterShaderVert from './shaders/filters/default.vert'; +import * as constants from "../core/constants"; +import GeometryBuilder from "./GeometryBuilder"; +import { Renderer } from "../core/p5.Renderer"; +import { Matrix } from "./p5.Matrix"; +import { Camera } from "./p5.Camera"; +import { Vector } from "../math/p5.Vector"; +import { RenderBuffer } from "./p5.RenderBuffer"; +import { DataArray } from "./p5.DataArray"; +import { Shader } from "./p5.Shader"; +import { Image } from "../image/p5.Image"; +import { Texture, MipmapTexture } from "./p5.Texture"; +import { Framebuffer } from "./p5.Framebuffer"; +import { Graphics } from "../core/p5.Graphics"; +import { Element } from "../dom/p5.Element"; +import { ShapeBuilder } from "./ShapeBuilder"; +import { GeometryBufferCache } from "./GeometryBufferCache"; + +import lightingShader from "./shaders/lighting.glsl"; +import webgl2CompatibilityShader from "./shaders/webgl2Compatibility.glsl"; +import normalVert from "./shaders/normal.vert"; +import normalFrag from "./shaders/normal.frag"; +import basicFrag from "./shaders/basic.frag"; +import sphereMappingFrag from "./shaders/sphereMapping.frag"; +import lightVert from "./shaders/light.vert"; +import lightTextureFrag from "./shaders/light_texture.frag"; +import phongVert from "./shaders/phong.vert"; +import phongFrag from "./shaders/phong.frag"; +import fontVert from "./shaders/font.vert"; +import fontFrag from "./shaders/font.frag"; +import lineVert from "./shaders/line.vert"; +import lineFrag from "./shaders/line.frag"; +import pointVert from "./shaders/point.vert"; +import pointFrag from "./shaders/point.frag"; +import imageLightVert from "./shaders/imageLight.vert"; +import imageLightDiffusedFrag from "./shaders/imageLightDiffused.frag"; +import imageLightSpecularFrag from "./shaders/imageLightSpecular.frag"; + +import filterGrayFrag from "./shaders/filters/gray.frag"; +import filterErodeFrag from "./shaders/filters/erode.frag"; +import filterDilateFrag from "./shaders/filters/dilate.frag"; +import filterBlurFrag from "./shaders/filters/blur.frag"; +import filterPosterizeFrag from "./shaders/filters/posterize.frag"; +import filterOpaqueFrag from "./shaders/filters/opaque.frag"; +import filterInvertFrag from "./shaders/filters/invert.frag"; +import filterThresholdFrag from "./shaders/filters/threshold.frag"; +import filterShaderVert from "./shaders/filters/default.vert"; +import { PrimitiveToVerticesConverter } from "../shape/custom_shapes"; const STROKE_CAP_ENUM = {}; const STROKE_JOIN_ENUM = {}; -let lineDefs = ''; +let lineDefs = ""; const defineStrokeCapEnum = function (key, val) { lineDefs += `#define STROKE_CAP_${key} ${val}\n`; STROKE_CAP_ENUM[constants[key]] = val; @@ -57,40 +58,33 @@ const defineStrokeJoinEnum = function (key, val) { STROKE_JOIN_ENUM[constants[key]] = val; }; - // Define constants in line shaders for each type of cap/join, and also record // the values in JS objects -defineStrokeCapEnum('ROUND', 0); -defineStrokeCapEnum('PROJECT', 1); -defineStrokeCapEnum('SQUARE', 2); -defineStrokeJoinEnum('ROUND', 0); -defineStrokeJoinEnum('MITER', 1); -defineStrokeJoinEnum('BEVEL', 2); +defineStrokeCapEnum("ROUND", 0); +defineStrokeCapEnum("PROJECT", 1); +defineStrokeCapEnum("SQUARE", 2); +defineStrokeJoinEnum("ROUND", 0); +defineStrokeJoinEnum("MITER", 1); +defineStrokeJoinEnum("BEVEL", 2); const defaultShaders = { normalVert, normalFrag, basicFrag, sphereMappingFrag, - lightVert: - lightingShader + - lightVert, + lightVert: lightingShader + lightVert, lightTextureFrag, phongVert, - phongFrag: - lightingShader + - phongFrag, + phongFrag: lightingShader + phongFrag, fontVert, fontFrag, - lineVert: - lineDefs + lineVert, - lineFrag: - lineDefs + lineFrag, + lineVert: lineDefs + lineVert, + lineFrag: lineDefs + lineFrag, pointVert, pointFrag, imageLightVert, imageLightDiffusedFrag, - imageLightSpecularFrag + imageLightSpecularFrag, }; let sphereMapping = defaultShaders.sphereMappingFrag; for (const key in defaultShaders) { @@ -105,7 +99,7 @@ const filterShaderFrags = { [constants.POSTERIZE]: filterPosterizeFrag, [constants.OPAQUE]: filterOpaqueFrag, [constants.INVERT]: filterInvertFrag, - [constants.THRESHOLD]: filterThresholdFrag + [constants.THRESHOLD]: filterThresholdFrag, }; /** @@ -121,7 +115,7 @@ class RendererGL extends Renderer { super(pInst, w, h, isMainCanvas); // Create new canvas - this.canvas = this.elt = elt || document.createElement('canvas'); + this.canvas = this.elt = elt || document.createElement("canvas"); this._setAttributeDefaults(pInst); this._initContext(); // This redundant property is useful in reminding you that you are @@ -137,10 +131,10 @@ class RendererGL extends Renderer { this._pInst.canvas = this.canvas; } else { // hide if offscreen buffer by default - this.canvas.style.display = 'none'; + this.canvas.style.display = "none"; } - this.elt.id = 'defaultCanvas0'; - this.elt.classList.add('p5Canvas'); + this.elt.id = "defaultCanvas0"; + this.elt.classList.add("p5Canvas"); const dimensions = this._adjustDimensions(w, h); w = dimensions.adjustedWidth; @@ -156,12 +150,9 @@ class RendererGL extends Renderer { this.elt.style.height = `${h}px`; this._origViewport = { width: this.GL.drawingBufferWidth, - height: this.GL.drawingBufferHeight + height: this.GL.drawingBufferHeight, }; - this.viewport( - this._origViewport.width, - this._origViewport.height - ); + this.viewport(this._origViewport.width, this._origViewport.height); // Attach canvas element to DOM if (this._pInst._userNode) { @@ -169,12 +160,12 @@ class RendererGL extends Renderer { this._pInst._userNode.appendChild(this.elt); } else { //create main element - if (document.getElementsByTagName('main').length === 0) { - let m = document.createElement('main'); + if (document.getElementsByTagName("main").length === 0) { + let m = document.createElement("main"); document.body.appendChild(m); } //append canvas to main - document.getElementsByTagName('main')[0].appendChild(this.elt); + document.getElementsByTagName("main")[0].appendChild(this.elt); } this.isP3D = true; //lets us know we're in 3d mode @@ -187,8 +178,8 @@ class RendererGL extends Renderer { this.states.uViewMatrix = new Matrix(); this.states.uMVMatrix = new Matrix(); this.states.uPMatrix = new Matrix(); - this.states.uNMatrix = new Matrix('mat3'); - this.states.curMatrix = new Matrix('mat3'); + this.states.uNMatrix = new Matrix("mat3"); + this.states.curMatrix = new Matrix("mat3"); this.states.curCamera = new Camera(this); @@ -243,7 +234,7 @@ class RendererGL extends Renderer { this._isErasing = false; // simple lines - this._simpleLines = false; + this._simpleLines = false; // clipping this._clipDepths = []; @@ -265,7 +256,7 @@ class RendererGL extends Renderer { if (this.webglVersion === constants.WEBGL2) { this.blendExt = this.GL; } else { - this.blendExt = this.GL.getExtension('EXT_blend_minmax'); + this.blendExt = this.GL.getExtension("EXT_blend_minmax"); } this._isBlending = false; @@ -310,36 +301,97 @@ class RendererGL extends Renderer { this.states.userPointShader = undefined; this.states.userImageShader = undefined; + this.states.curveDetail = 1 / 4; + // Used by beginShape/endShape functions to construct a p5.Geometry this.shapeBuilder = new ShapeBuilder(this); this.buffers = { fill: [ - new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - new RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - new RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, (arr) => arr.flat()) + new RenderBuffer( + 3, + "vertices", + "vertexBuffer", + "aPosition", + this, + this._vToNArray, + ), + new RenderBuffer( + 3, + "vertexNormals", + "normalBuffer", + "aNormal", + this, + this._vToNArray, + ), + new RenderBuffer( + 4, + "vertexColors", + "colorBuffer", + "aVertexColor", + this, + ), + new RenderBuffer( + 3, + "vertexAmbients", + "ambientBuffer", + "aAmbientColor", + this, + ), + new RenderBuffer(2, "uvs", "uvBuffer", "aTexCoord", this, (arr) => + arr.flat(), + ), ], stroke: [ - new RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - new RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - new RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - new RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - new RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + new RenderBuffer( + 4, + "lineVertexColors", + "lineColorBuffer", + "aVertexColor", + this, + ), + new RenderBuffer( + 3, + "lineVertices", + "lineVerticesBuffer", + "aPosition", + this, + ), + new RenderBuffer( + 3, + "lineTangentsIn", + "lineTangentsInBuffer", + "aTangentIn", + this, + ), + new RenderBuffer( + 3, + "lineTangentsOut", + "lineTangentsOutBuffer", + "aTangentOut", + this, + ), + new RenderBuffer(1, "lineSides", "lineSidesBuffer", "aSide", this), ], text: [ - new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, (arr) => arr.flat()) + new RenderBuffer( + 3, + "vertices", + "vertexBuffer", + "aPosition", + this, + this._vToNArray, + ), + new RenderBuffer(2, "uvs", "uvBuffer", "aTexCoord", this, (arr) => + arr.flat(), + ), ], point: this.GL.createBuffer(), - user:[] - } + user: [], + }; this.geometryBufferCache = new GeometryBufferCache(this); - this.pointSize = 5.0; //default point size - this.curStrokeWeight = 1; this.curStrokeCap = constants.ROUND; this.curStrokeJoin = constants.ROUND; @@ -359,20 +411,10 @@ class RendererGL extends Renderer { this._curveTightness = 6; - // lookUpTable for coefficients needed to be calculated for bezierVertex, same are used for curveVertex - this._lookUpTableBezier = []; - // lookUpTable for coefficients needed to be calculated for quadraticVertex - this._lookUpTableQuadratic = []; - - // current curveDetail in the Bezier lookUpTable - this._lutBezierDetail = 0; - // current curveDetail in the Quadratic lookUpTable - this._lutQuadraticDetail = 0; - - this.fontInfos = {}; this._curShader = undefined; + this.drawShapeCount = 1; } ////////////////////////////////////////////// @@ -380,19 +422,22 @@ class RendererGL extends Renderer { ////////////////////////////////////////////// /** - * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added - * to the geometry and then returned when - * endGeometry() is called. One can also use - * buildGeometry() to pass a function that - * draws shapes. - * - * If you need to draw complex shapes every frame which don't change over time, - * combining them upfront with `beginGeometry()` and `endGeometry()` and then - * drawing that will run faster than repeatedly drawing the individual pieces. + * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added + * to the geometry and then returned when + * endGeometry() is called. One can also use + * buildGeometry() to pass a function that + * draws shapes. + * + * If you need to draw complex shapes every frame which don't change over time, + * combining them upfront with `beginGeometry()` and `endGeometry()` and then + * drawing that will run faster than repeatedly drawing the individual pieces. + * @private */ beginGeometry() { if (this.geometryBuilder) { - throw new Error('It looks like `beginGeometry()` is being called while another p5.Geometry is already being build.'); + throw new Error( + "It looks like `beginGeometry()` is being called while another p5.Geometry is already being build.", + ); } this.geometryBuilder = new GeometryBuilder(this); this.geometryBuilder.prevFillColor = [...this.states.curFillColor]; @@ -404,12 +449,15 @@ class RendererGL extends Renderer { * started using beginGeometry(). One can also * use buildGeometry() to pass a function that * draws shapes. + * @private * * @returns {p5.Geometry} The model that was built. */ endGeometry() { if (!this.geometryBuilder) { - throw new Error('Make sure you call beginGeometry() before endGeometry()!'); + throw new Error( + "Make sure you call beginGeometry() before endGeometry()!", + ); } const geometry = this.geometryBuilder.finish(); this.states.curFillColor = this.geometryBuilder.prevFillColor; @@ -439,23 +487,65 @@ class RendererGL extends Renderer { return this.endGeometry(); } - ////////////////////////////////////////////// // Shape drawing ////////////////////////////////////////////// beginShape(...args) { - this.shapeBuilder.beginShape(...args); + super.beginShape(...args); + // TODO remove when shape refactor is complete + // this.shapeBuilder.beginShape(...args); + } + + curveDetail(d) { + if (d === undefined) { + return this.states.curveDetail; + } else { + this.states.curveDetail = d; + } } - endShape( + drawShape(shape) { + const visitor = new PrimitiveToVerticesConverter({ + curveDetail: this.states.curveDetail, + }); + shape.accept(visitor); + this.shapeBuilder.constructFromContours(shape, visitor.contours); + + if (this.geometryBuilder) { + this.geometryBuilder.addImmediate( + this.shapeBuilder.geometry, + this.shapeBuilder.shapeMode, + ); + } else if (this.states.fillColor || this.states.strokeColor) { + if (this.shapeBuilder.shapeMode === constants.POINTS) { + this._drawPoints( + this.shapeBuilder.geometry.vertices, + this.buffers.point, + ); + } else { + this._drawGeometry(this.shapeBuilder.geometry, { + mode: this.shapeBuilder.shapeMode, + count: this.drawShapeCount, + }); + } + } + this.drawShapeCount = 1; + } + + endShape(mode, count) { + this.drawShapeCount = count; + super.endShape(mode, count); + } + + legacyEndShape( mode, isCurve, isBezier, isQuadratic, isContour, shapeKind, - count = 1 + count = 1, ) { this.shapeBuilder.endShape( mode, @@ -463,32 +553,28 @@ class RendererGL extends Renderer { isBezier, isQuadratic, isContour, - shapeKind + shapeKind, ); if (this.geometryBuilder) { this.geometryBuilder.addImmediate( this.shapeBuilder.geometry, - this.shapeBuilder.shapeMode - ); - } else if (this.states.doFill || this.states.doStroke) { - this._drawGeometry( - this.shapeBuilder.geometry, - { mode: this.shapeBuilder.shapeMode, count } + this.shapeBuilder.shapeMode, ); + } else if (this.states.fillColor || this.states.strokeColor) { + this._drawGeometry(this.shapeBuilder.geometry, { + mode: this.shapeBuilder.shapeMode, + count, + }); } } - beginContour(...args) { - this.shapeBuilder.beginContour(...args); - } - - vertex(...args) { + legacyVertex(...args) { this.shapeBuilder.vertex(...args); } vertexProperty(...args) { - this.shapeBuilder.vertexProperty(...args); + this.currentShape.vertexProperty(...args); } normal(xorv, y, z) { @@ -497,6 +583,7 @@ class RendererGL extends Renderer { } else { this.states._currentNormal = new Vector(xorv, y, z); } + this.updateShapeVertexProperties(); } ////////////////////////////////////////////// @@ -507,31 +594,32 @@ class RendererGL extends Renderer { for (const propName in geometry.userVertexProperties) { const prop = geometry.userVertexProperties[propName]; this.buffers.user.push( - new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) + new RenderBuffer( + prop.getDataSize(), + prop.getSrcName(), + prop.getDstName(), + prop.getName(), + this, + ), ); } if ( - this.states.doFill && + this.states.fillColor && geometry.vertices.length >= 3 && ![constants.LINES, constants.POINTS].includes(mode) ) { this._drawFills(geometry, { mode, count }); } - if (this.states.doStroke && geometry.lineVertices.length >= 1) { + if (this.states.strokeColor && geometry.lineVertices.length >= 1) { this._drawStrokes(geometry, { count }); } this.buffers.user = []; } - _drawGeometryScaled( - model, - scaleX, - scaleY, - scaleZ - ) { + _drawGeometryScaled(model, scaleX, scaleY, scaleZ) { let originalModelMatrix = this.states.uModelMatrix.copy(); try { this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); @@ -542,7 +630,6 @@ class RendererGL extends Renderer { this._drawGeometry(model); } } finally { - this.states.uModelMatrix = originalModelMatrix; } } @@ -550,9 +637,10 @@ class RendererGL extends Renderer { _drawFills(geometry, { count, mode } = {}) { this._useVertexColor = geometry.vertexColors.length > 0; - const shader = this._drawingFilter && this.states.userFillShader - ? this.states.userFillShader - : this._getFillShader(); + const shader = + this._drawingFilter && this.states.userFillShader + ? this.states.userFillShader + : this._getFillShader(); shader.bindShader(); this._setGlobalUniforms(shader); this._setFillUniforms(shader); @@ -566,7 +654,7 @@ class RendererGL extends Renderer { this._applyColorBlend( this.states.curFillColor, - geometry.hasFillTransparency() + geometry.hasFillTransparency(), ); this._drawBuffers(geometry, { mode, count }); @@ -593,25 +681,23 @@ class RendererGL extends Renderer { this._applyColorBlend( this.states.curStrokeColor, - geometry.hasStrokeTransparency() + geometry.hasStrokeTransparency(), ); if (count === 1) { - gl.drawArrays( - gl.TRIANGLES, - 0, - geometry.lineVertices.length / 3 - ); + gl.drawArrays(gl.TRIANGLES, 0, geometry.lineVertices.length / 3); } else { try { gl.drawArraysInstanced( gl.TRIANGLES, 0, geometry.lineVertices.length / 3, - count + count, ); } catch (e) { - console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + console.log( + "🌸 p5.js says: Instancing is only supported in WebGL2 mode", + ); } } @@ -631,7 +717,7 @@ class RendererGL extends Renderer { gl.ARRAY_BUFFER, this._vToNArray(vertices), Float32Array, - gl.STATIC_DRAW + gl.STATIC_DRAW, ); pointShader.enableAttrib(pointShader.attributes.aPosition, 3); @@ -651,9 +737,15 @@ class RendererGL extends Renderer { if (prop) { const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); if (adjustedLength > geometry.vertices.length) { - this._pInst.constructor._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + this._pInst.constructor._friendlyError( + `One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, + "vertexProperty()", + ); } else if (adjustedLength < geometry.vertices.length) { - this._pInst.constructor._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + this._pInst.constructor._friendlyError( + `One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, + "vertexProperty()", + ); } } } @@ -676,9 +768,9 @@ class RendererGL extends Renderer { this._pInst.webglVersion !== constants.WEBGL2 && glBuffers.indexBufferType === gl.UNSIGNED_INT ) { - if (!gl.getExtension('OES_element_index_uint')) { + if (!gl.getExtension("OES_element_index_uint")) { throw new Error( - 'Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.' + "Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.", ); } } @@ -688,7 +780,7 @@ class RendererGL extends Renderer { gl.TRIANGLES, geometry.faces.length * 3, glBuffers.indexBufferType, - 0 + 0, ); } else { try { @@ -697,29 +789,24 @@ class RendererGL extends Renderer { geometry.faces.length * 3, glBuffers.indexBufferType, 0, - count + count, ); } catch (e) { - console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + console.log( + "🌸 p5.js says: Instancing is only supported in WebGL2 mode", + ); } } } else { if (count === 1) { - gl.drawArrays( - mode, - 0, - geometry.vertices.length - ); + gl.drawArrays(mode, 0, geometry.vertices.length); } else { try { - gl.drawArraysInstanced( - mode, - 0, - geometry.vertices.length, - count - ); + gl.drawArraysInstanced(mode, 0, geometry.vertices.length, count); } catch (e) { - console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + console.log( + "🌸 p5.js says: Instancing is only supported in WebGL2 mode", + ); } } } @@ -735,7 +822,7 @@ class RendererGL extends Renderer { _setAttributeDefaults(pInst) { // See issue #3850, safer to enable AA in Safari - const applyAA = navigator.userAgent.toLowerCase().includes('safari'); + const applyAA = navigator.userAgent.toLowerCase().includes("safari"); const defaults = { alpha: true, depth: true, @@ -744,7 +831,7 @@ class RendererGL extends Renderer { premultipliedAlpha: true, preserveDrawingBuffer: true, perPixelLighting: true, - version: 2 + version: 2, }; if (pInst._glAttributes === null) { pInst._glAttributes = defaults; @@ -757,11 +844,14 @@ class RendererGL extends Renderer { _initContext() { if (this._pInst._glAttributes?.version !== 1) { // Unless WebGL1 is explicitly asked for, try to create a WebGL2 context - this.drawingContext = - this.canvas.getContext('webgl2', this._pInst._glAttributes); + this.drawingContext = this.canvas.getContext( + "webgl2", + this._pInst._glAttributes, + ); } - this.webglVersion = - this.drawingContext ? constants.WEBGL2 : constants.WEBGL; + this.webglVersion = this.drawingContext + ? constants.WEBGL2 + : constants.WEBGL; // If this is the main canvas, make sure the global `webglVersion` is set this._pInst.webglVersion = this.webglVersion; if (!this.drawingContext) { @@ -769,11 +859,11 @@ class RendererGL extends Renderer { // disabled via `setAttributes({ version: 1 })` or because the device // doesn't support it), fall back to a WebGL1 context this.drawingContext = - this.canvas.getContext('webgl', this._pInst._glAttributes) || - this.canvas.getContext('experimental-webgl', this._pInst._glAttributes); + this.canvas.getContext("webgl", this._pInst._glAttributes) || + this.canvas.getContext("experimental-webgl", this._pInst._glAttributes); } if (this.drawingContext === null) { - throw new Error('Error creating webgl context'); + throw new Error("Error creating webgl context"); } else { const gl = this.drawingContext; gl.enable(gl.DEPTH_TEST); @@ -784,7 +874,7 @@ class RendererGL extends Renderer { // be encoded the same way as textures from everything else. gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); this._viewport = this.drawingContext.getParameter( - this.drawingContext.VIEWPORT + this.drawingContext.VIEWPORT, ); } } @@ -801,19 +891,15 @@ class RendererGL extends Renderer { let maxTextureSize = this._maxTextureSize; let maxAllowedPixelDimensions = Math.floor( - maxTextureSize / this._pixelDensity - ); - let adjustedWidth = Math.min( - width, maxAllowedPixelDimensions - ); - let adjustedHeight = Math.min( - height, maxAllowedPixelDimensions + maxTextureSize / this._pixelDensity, ); + let adjustedWidth = Math.min(width, maxAllowedPixelDimensions); + let adjustedHeight = Math.min(height, maxAllowedPixelDimensions); if (adjustedWidth !== width || adjustedHeight !== height) { console.warn( - 'Warning: The requested width/height exceeds hardware limits. ' + - `Adjusting dimensions to width: ${adjustedWidth}, height: ${adjustedHeight}.` + "Warning: The requested width/height exceeds hardware limits. " + + `Adjusting dimensions to width: ${adjustedWidth}, height: ${adjustedHeight}.`, ); } @@ -832,7 +918,7 @@ class RendererGL extends Renderer { if (isPGraphics) { const pg = this._pInst; pg.canvas.parentNode.removeChild(pg.canvas); - pg.canvas = document.createElement('canvas'); + pg.canvas = document.createElement("canvas"); const node = pg._pInst._userNode || document.body; node.appendChild(pg.canvas); Element.call(pg, pg.canvas, pg._pInst); @@ -843,7 +929,7 @@ class RendererGL extends Renderer { if (c) { c.parentNode.removeChild(c); } - c = document.createElement('canvas'); + c = document.createElement("canvas"); c.id = defaultId; if (this._pInst._userNode) { this._pInst._userNode.appendChild(c); @@ -865,7 +951,7 @@ class RendererGL extends Renderer { renderer._applyDefaults(); - if (typeof callback === 'function') { + if (typeof callback === "function") { //setTimeout with 0 forces the task to the back of the queue, this ensures that //we finish switching out the renderer setTimeout(() => { @@ -874,7 +960,6 @@ class RendererGL extends Renderer { } } - _update() { // reset model view and apply initial camera transform // (containing only look at info; no projection). @@ -932,7 +1017,6 @@ class RendererGL extends Renderer { return modelMatrix.copy().mult(viewMatrix); } - ////////////////////////////////////////////// // COLOR ////////////////////////////////////////////// @@ -970,7 +1054,7 @@ class RendererGL extends Renderer { super.fill(...args); //see material.js for more info on color blending in webgl // const color = fn.color.apply(this._pInst, arguments); - const color = this._pInst.color(...args); + const color = this.states.fillColor; this.states.curFillColor = color._array; this.states.drawMode = constants.FILL; this.states._useNormalMaterial = false; @@ -1009,8 +1093,22 @@ class RendererGL extends Renderer { stroke(...args) { super.stroke(...args); // const color = fn.color.apply(this._pInst, arguments); - const color = this._pInst.color(...args); - this.states.curStrokeColor = color._array; + this.states.curStrokeColor = this.states.strokeColor._array; + } + + getCommonVertexProperties() { + return { + ...super.getCommonVertexProperties(), + stroke: this.states.strokeColor, + fill: this.states.fillColor, + normal: this.states._currentNormal, + }; + } + + getSupportedIndividualVertexProperties() { + return { + textureCoordinates: true, + }; } strokeCap(cap) { @@ -1050,12 +1148,12 @@ class RendererGL extends Renderer { // use internal shader for filter constants BLUR, INVERT, etc let filterParameter = undefined; let operation = undefined; - if (typeof args[0] === 'string') { + if (typeof args[0] === "string") { operation = args[0]; let defaults = { [constants.BLUR]: 3, [constants.POSTERIZE]: 4, - [constants.THRESHOLD]: 0.5 + [constants.THRESHOLD]: 0.5, }; let useDefaultParam = operation in defaults && args[1] === undefined; filterParameter = useDefaultParam ? defaults[operation] : args[1]; @@ -1067,11 +1165,10 @@ class RendererGL extends Renderer { this.defaultFilterShaders[operation] = new Shader( fbo.renderer, filterShaderVert, - filterShaderFrags[operation] + filterShaderFrags[operation], ); } this.states.filterShader = this.defaultFilterShaders[operation]; - } // use custom user-supplied shader else { @@ -1089,7 +1186,7 @@ class RendererGL extends Renderer { let texelSize = [ 1 / (target.width * target.pixelDensity()), - 1 / (target.height * target.pixelDensity()) + 1 / (target.height * target.pixelDensity()), ]; this.blendMode(constants.BLEND); @@ -1105,18 +1202,24 @@ class RendererGL extends Renderer { this.matchSize(tmp, target); // setup this.push(); - this.states.doStroke = false; + this.states.strokeColor = null; // draw main to temp buffer this.shader(this.states.filterShader); - this.states.filterShader.setUniform('texelSize', texelSize); - this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); - this.states.filterShader.setUniform('radius', Math.max(1, filterParameter)); + this.states.filterShader.setUniform("texelSize", texelSize); + this.states.filterShader.setUniform("canvasSize", [ + target.width, + target.height, + ]); + this.states.filterShader.setUniform( + "radius", + Math.max(1, filterParameter), + ); // Horiz pass: draw `target` to `tmp` tmp.draw(() => { - this.states.filterShader.setUniform('direction', [1, 0]); - this.states.filterShader.setUniform('tex0', target); + this.states.filterShader.setUniform("direction", [1, 0]); + this.states.filterShader.setUniform("tex0", target); this.clear(); this.shader(this.states.filterShader); this.noLights(); @@ -1125,8 +1228,8 @@ class RendererGL extends Renderer { // Vert pass: draw `tmp` to `fbo` fbo.draw(() => { - this.states.filterShader.setUniform('direction', [0, 1]); - this.states.filterShader.setUniform('tex0', tmp); + this.states.filterShader.setUniform("direction", [0, 1]); + this.states.filterShader.setUniform("tex0", tmp); this.clear(); this.shader(this.states.filterShader); this.noLights(); @@ -1138,22 +1241,24 @@ class RendererGL extends Renderer { // every other non-blur shader uses single pass else { fbo.draw(() => { - this.states.doStroke = false; + this.states.strokeColor = null; this.shader(this.states.filterShader); - this.states.filterShader.setUniform('tex0', target); - this.states.filterShader.setUniform('texelSize', texelSize); - this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); + this.states.filterShader.setUniform("tex0", target); + this.states.filterShader.setUniform("texelSize", texelSize); + this.states.filterShader.setUniform("canvasSize", [ + target.width, + target.height, + ]); // filterParameter uniform only used for POSTERIZE, and THRESHOLD // but shouldn't hurt to always set - this.states.filterShader.setUniform('filterParameter', filterParameter); + this.states.filterShader.setUniform("filterParameter", filterParameter); this.noLights(); this.plane(target.width, target.height); }); - } // draw fbo contents onto main renderer. this.push(); - this.states.doStroke = false; + this.states.strokeColor = null; this.clear(); this.push(); target.filterCamera._resize(); @@ -1162,10 +1267,14 @@ class RendererGL extends Renderer { this._drawingFilter = true; this.image( fbo, - 0, 0, - this.width, this.height, - -target.width / 2, -target.height / 2, - target.width, target.height + 0, + 0, + this.width, + this.height, + -target.width / 2, + -target.height / 2, + target.width, + target.height, ); this._drawingFilter = false; this.clearDepth(); @@ -1205,7 +1314,7 @@ class RendererGL extends Renderer { mode === constants.DODGE ) { console.warn( - 'BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode.' + "BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode.", ); } } @@ -1253,19 +1362,19 @@ class RendererGL extends Renderer { gl.stencilFunc( gl.ALWAYS, // the test 1, // reference value - 0xff // mask + 0xff, // mask ); gl.stencilOp( gl.KEEP, // what to do if the stencil test fails gl.KEEP, // what to do if the depth test fails - gl.REPLACE // what to do if both tests pass + gl.REPLACE, // what to do if both tests pass ); gl.disable(gl.DEPTH_TEST); this.push(); this.resetShader(); - if (this.states.doFill) this.fill(0, 0); - if (this.states.doStroke) this.stroke(0, 0); + if (this.states.fillColor) this.fill(0, 0); + if (this.states.strokeColor) this.stroke(0, 0); } endClip() { @@ -1275,12 +1384,12 @@ class RendererGL extends Renderer { gl.stencilOp( gl.KEEP, // what to do if the stencil test fails gl.KEEP, // what to do if the depth test fails - gl.KEEP // what to do if both tests pass + gl.KEEP, // what to do if both tests pass ); gl.stencilFunc( this._clipInvert ? gl.EQUAL : gl.NOTEQUAL, // the test 0, // reference value - 0xff // mask + 0xff, // mask ); gl.enable(gl.DEPTH_TEST); @@ -1300,51 +1409,6 @@ class RendererGL extends Renderer { this.drawTarget()._isClipApplied = false; } - /** - * Change weight of stroke - * @param {Number} stroke weight to be used for drawing - * @example - *
- * - * 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(); - * } - * - *
- * - * @alt - * black canvas with two purple rotating spheres with pink - * outlines the sphere on top has much heavier outlines, - */ - strokeWeight(w) { - if (this.curStrokeWeight !== w) { - this.pointSize = w; - this.curStrokeWeight = w; - } - } - // x,y are canvas-relative (pre-scaled by _pixelDensity) _getPixel(x, y) { const gl = this.GL; @@ -1355,7 +1419,7 @@ class RendererGL extends Renderer { y, gl.RGBA, gl.UNSIGNED_BYTE, - this._pInst.height * this._pInst.pixelDensity() + this._pInst.height * this._pInst.pixelDensity(), ); } @@ -1370,7 +1434,8 @@ class RendererGL extends Renderer { //@todo_FES if (this._pInst._glAttributes.preserveDrawingBuffer !== true) { console.log( - 'loadPixels only works in WebGL when preserveDrawingBuffer ' + 'is true.' + "loadPixels only works in WebGL when preserveDrawingBuffer " + + "is true.", ); return; } @@ -1378,19 +1443,18 @@ class RendererGL extends Renderer { const pd = this._pixelDensity; const gl = this.GL; - this.pixels = - readPixelsWebGL( - this.pixels, - gl, - null, - 0, - 0, - this.width * pd, - this.height * pd, - gl.RGBA, - gl.UNSIGNED_BYTE, - this.height * pd - ); + this.pixels = readPixelsWebGL( + this.pixels, + gl, + null, + 0, + 0, + this.width * pd, + this.height * pd, + gl.RGBA, + gl.UNSIGNED_BYTE, + this.height * pd, + ); } updatePixels() { @@ -1401,7 +1465,17 @@ class RendererGL extends Renderer { this.resetMatrix(); this.clear(); this.states.imageMode = constants.CORNER; - this.image(fbo, 0, 0, fbo.width, fbo.height, -fbo.width/2, -fbo.height/2, fbo.width, fbo.height); + this.image( + fbo, + 0, + 0, + fbo.width, + fbo.height, + -fbo.width / 2, + -fbo.height / 2, + fbo.width, + fbo.height, + ); this.pop(); this.GL.clearDepth(1); this.GL.clear(this.GL.DEPTH_BUFFER_BIT); @@ -1419,14 +1493,12 @@ class RendererGL extends Renderer { format: constants.UNSIGNED_BYTE, useDepth: this._pInst._glAttributes.depth, depthFormat: constants.UNSIGNED_INT, - antialias: this._pInst._glAttributes.antialias + antialias: this._pInst._glAttributes.antialias, }); } return this._tempFramebuffer; } - - ////////////////////////////////////////////// // HASH | for geometry ////////////////////////////////////////////// @@ -1453,7 +1525,7 @@ class RendererGL extends Renderer { const props = {}; for (const key in this.drawingContext) { const val = this.drawingContext[key]; - if (typeof val !== 'object' && typeof val !== 'function') { + if (typeof val !== "object" && typeof val !== "function") { props[key] = val; } } @@ -1471,21 +1543,17 @@ class RendererGL extends Renderer { this.canvas.style.height = `${h}px`; this._origViewport = { width: this.GL.drawingBufferWidth, - height: this.GL.drawingBufferHeight + height: this.GL.drawingBufferHeight, }; - this.viewport( - this._origViewport.width, - this._origViewport.height - ); + this.viewport(this._origViewport.width, this._origViewport.height); this.states.curCamera._resize(); //resize pixels buffer - if (typeof this.pixels !== 'undefined') { - this.pixels = - new Uint8Array( - this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4 - ); + if (typeof this.pixels !== "undefined") { + this.pixels = new Uint8Array( + this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4, + ); } for (const framebuffer of this.framebuffers) { @@ -1553,10 +1621,22 @@ class RendererGL extends Renderer { Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments); } else { this.states.uModelMatrix.apply([ - a, b, 0, 0, - c, d, 0, 0, - 0, 0, 1, 0, - e, f, 0, 1 + a, + b, + 0, + 0, + c, + d, + 0, + 0, + 0, + 0, + 1, + 0, + e, + f, + 0, + 1, ]); } } @@ -1594,7 +1674,7 @@ class RendererGL extends Renderer { } rotate(rad, axis) { - if (typeof axis === 'undefined') { + if (typeof axis === "undefined") { return this.rotateZ(rad); } Matrix.prototype.rotate.apply(this.states.uModelMatrix, arguments); @@ -1663,19 +1743,19 @@ class RendererGL extends Renderer { return this._getLineShader(); } - _getSphereMapping(img) { if (!this.sphereMapping) { - this.sphereMapping = this._pInst.createFilterShader( - sphereMapping - ); + this.sphereMapping = this._pInst.createFilterShader(sphereMapping); } this.states.uNMatrix.inverseTranspose(this.states.uViewMatrix); this.states.uNMatrix.invert3x3(this.states.uNMatrix); - this.sphereMapping.setUniform('uFovY', this.states.curCamera.cameraFOV); - this.sphereMapping.setUniform('uAspect', this.states.curCamera.aspectRatio); - this.sphereMapping.setUniform('uNewNormalMatrix', this.states.uNMatrix.mat3); - this.sphereMapping.setUniform('uSampler', img); + this.sphereMapping.setUniform("uFovY", this.states.curCamera.cameraFOV); + this.sphereMapping.setUniform("uAspect", this.states.curCamera.aspectRatio); + this.sphereMapping.setUniform( + "uNewNormalMatrix", + this.states.uNMatrix.mat3, + ); + this.sphereMapping.setUniform("uSampler", img); return this.sphereMapping; } @@ -1710,7 +1790,6 @@ class RendererGL extends Renderer { return this._getColorShader(); } - _getPointShader() { // select the point shader to use const point = this.states.userPointShader; @@ -1723,7 +1802,7 @@ class RendererGL extends Renderer { baseMaterialShader() { if (!this._pInst._glAttributes.perPixelLighting) { throw new Error( - 'The material shader does not support hooks without perPixelLighting. Try turning it back on.' + "The material shader does not support hooks without perPixelLighting. Try turning it back on.", ); } return this._getLightShader(); @@ -1734,25 +1813,25 @@ class RendererGL extends Renderer { if (this._pInst._glAttributes.perPixelLighting) { this._defaultLightShader = new Shader( this, - this._webGL2CompatibilityPrefix('vert', 'highp') + - defaultShaders.phongVert, - this._webGL2CompatibilityPrefix('frag', 'highp') + - defaultShaders.phongFrag, + this._webGL2CompatibilityPrefix("vert", "highp") + + defaultShaders.phongVert, + this._webGL2CompatibilityPrefix("frag", "highp") + + defaultShaders.phongFrag, { vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', - 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', - 'vec2 getUV': '(vec2 uv) { return uv; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' + "void beforeVertex": "() {}", + "vec3 getLocalPosition": "(vec3 position) { return position; }", + "vec3 getWorldPosition": "(vec3 position) { return position; }", + "vec3 getLocalNormal": "(vec3 normal) { return normal; }", + "vec3 getWorldNormal": "(vec3 normal) { return normal; }", + "vec2 getUV": "(vec2 uv) { return uv; }", + "vec4 getVertexColor": "(vec4 color) { return color; }", + "void afterVertex": "() {}", }, fragment: { - 'void beforeFragment': '() {}', - 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', - 'vec4 combineColors': `(ColorComponents components) { + "void beforeFragment": "() {}", + "Inputs getPixelInputs": "(Inputs inputs) { return inputs; }", + "vec4 combineColors": `(ColorComponents components) { vec4 color = vec4(0.); color.rgb += components.diffuse * components.baseColor; color.rgb += components.ambient * components.ambientColor; @@ -1761,18 +1840,18 @@ class RendererGL extends Renderer { color.a = components.opacity; return color; }`, - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'void afterFragment': '() {}' - } - } + "vec4 getFinalColor": "(vec4 color) { return color; }", + "void afterFragment": "() {}", + }, + }, ); } else { this._defaultLightShader = new Shader( this, - this._webGL2CompatibilityPrefix('vert', 'highp') + - defaultShaders.lightVert, - this._webGL2CompatibilityPrefix('frag', 'highp') + - defaultShaders.lightTextureFrag + this._webGL2CompatibilityPrefix("vert", "highp") + + defaultShaders.lightVert, + this._webGL2CompatibilityPrefix("frag", "highp") + + defaultShaders.lightTextureFrag, ); } } @@ -1788,27 +1867,27 @@ class RendererGL extends Renderer { if (!this._defaultNormalShader) { this._defaultNormalShader = new Shader( this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.normalVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.normalFrag, + this._webGL2CompatibilityPrefix("vert", "mediump") + + defaultShaders.normalVert, + this._webGL2CompatibilityPrefix("frag", "mediump") + + defaultShaders.normalFrag, { vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', - 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', - 'vec2 getUV': '(vec2 uv) { return uv; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' + "void beforeVertex": "() {}", + "vec3 getLocalPosition": "(vec3 position) { return position; }", + "vec3 getWorldPosition": "(vec3 position) { return position; }", + "vec3 getLocalNormal": "(vec3 normal) { return normal; }", + "vec3 getWorldNormal": "(vec3 normal) { return normal; }", + "vec2 getUV": "(vec2 uv) { return uv; }", + "vec4 getVertexColor": "(vec4 color) { return color; }", + "void afterVertex": "() {}", }, fragment: { - 'void beforeFragment': '() {}', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'void afterFragment': '() {}' - } - } + "void beforeFragment": "() {}", + "vec4 getFinalColor": "(vec4 color) { return color; }", + "void afterFragment": "() {}", + }, + }, ); } @@ -1823,27 +1902,27 @@ class RendererGL extends Renderer { if (!this._defaultColorShader) { this._defaultColorShader = new Shader( this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.normalVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.basicFrag, + this._webGL2CompatibilityPrefix("vert", "mediump") + + defaultShaders.normalVert, + this._webGL2CompatibilityPrefix("frag", "mediump") + + defaultShaders.basicFrag, { vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', - 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', - 'vec2 getUV': '(vec2 uv) { return uv; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' + "void beforeVertex": "() {}", + "vec3 getLocalPosition": "(vec3 position) { return position; }", + "vec3 getWorldPosition": "(vec3 position) { return position; }", + "vec3 getLocalNormal": "(vec3 normal) { return normal; }", + "vec3 getWorldNormal": "(vec3 normal) { return normal; }", + "vec2 getUV": "(vec2 uv) { return uv; }", + "vec4 getVertexColor": "(vec4 color) { return color; }", + "void afterVertex": "() {}", }, fragment: { - 'void beforeFragment': '() {}', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'void afterFragment': '() {}' - } - } + "void beforeFragment": "() {}", + "vec4 getFinalColor": "(vec4 color) { return color; }", + "void afterFragment": "() {}", + }, + }, ); } @@ -1882,25 +1961,25 @@ class RendererGL extends Renderer { if (!this._defaultPointShader) { this._defaultPointShader = new Shader( this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.pointVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.pointFrag, + this._webGL2CompatibilityPrefix("vert", "mediump") + + defaultShaders.pointVert, + this._webGL2CompatibilityPrefix("frag", "mediump") + + defaultShaders.pointFrag, { vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'float getPointSize': '(float size) { return size; }', - 'void afterVertex': '() {}' + "void beforeVertex": "() {}", + "vec3 getLocalPosition": "(vec3 position) { return position; }", + "vec3 getWorldPosition": "(vec3 position) { return position; }", + "float getPointSize": "(float size) { return size; }", + "void afterVertex": "() {}", }, fragment: { - 'void beforeFragment': '() {}', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'bool shouldDiscard': '(bool outside) { return outside; }', - 'void afterFragment': '() {}' - } - } + "void beforeFragment": "() {}", + "vec4 getFinalColor": "(vec4 color) { return color; }", + "bool shouldDiscard": "(bool outside) { return outside; }", + "void afterFragment": "() {}", + }, + }, ); } return this._defaultPointShader; @@ -1914,29 +1993,29 @@ class RendererGL extends Renderer { if (!this._defaultLineShader) { this._defaultLineShader = new Shader( this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.lineVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.lineFrag, + this._webGL2CompatibilityPrefix("vert", "mediump") + + defaultShaders.lineVert, + this._webGL2CompatibilityPrefix("frag", "mediump") + + defaultShaders.lineFrag, { vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'float getStrokeWeight': '(float weight) { return weight; }', - 'vec2 getLineCenter': '(vec2 center) { return center; }', - 'vec2 getLinePosition': '(vec2 position) { return position; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' + "void beforeVertex": "() {}", + "vec3 getLocalPosition": "(vec3 position) { return position; }", + "vec3 getWorldPosition": "(vec3 position) { return position; }", + "float getStrokeWeight": "(float weight) { return weight; }", + "vec2 getLineCenter": "(vec2 center) { return center; }", + "vec2 getLinePosition": "(vec2 position) { return position; }", + "vec4 getVertexColor": "(vec4 color) { return color; }", + "void afterVertex": "() {}", }, fragment: { - 'void beforeFragment': '() {}', - 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'bool shouldDiscard': '(bool outside) { return outside; }', - 'void afterFragment': '() {}' - } - } + "void beforeFragment": "() {}", + "Inputs getPixelInputs": "(Inputs inputs) { return inputs; }", + "vec4 getFinalColor": "(vec4 color) { return color; }", + "bool shouldDiscard": "(bool outside) { return outside; }", + "void afterFragment": "() {}", + }, + }, ); } @@ -1946,32 +2025,28 @@ class RendererGL extends Renderer { _getFontShader() { if (!this._defaultFontShader) { if (this.webglVersion === constants.WEBGL) { - this.GL.getExtension('OES_standard_derivatives'); + this.GL.getExtension("OES_standard_derivatives"); } this._defaultFontShader = new Shader( this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.fontVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.fontFrag + this._webGL2CompatibilityPrefix("vert", "mediump") + + defaultShaders.fontVert, + this._webGL2CompatibilityPrefix("frag", "mediump") + + defaultShaders.fontFrag, ); } return this._defaultFontShader; } - - _webGL2CompatibilityPrefix( - shaderType, - floatPrecision - ) { - let code = ''; + _webGL2CompatibilityPrefix(shaderType, floatPrecision) { + let code = ""; if (this.webglVersion === constants.WEBGL2) { - code += '#version 300 es\n#define WEBGL2\n'; + code += "#version 300 es\n#define WEBGL2\n"; } - if (shaderType === 'vert') { - code += '#define VERTEX_SHADER\n'; - } else if (shaderType === 'frag') { - code += '#define FRAGMENT_SHADER\n'; + if (shaderType === "vert") { + code += "#define VERTEX_SHADER\n"; + } else if (shaderType === "frag") { + code += "#define FRAGMENT_SHADER\n"; } if (floatPrecision) { code += `precision ${floatPrecision} float;\n`; @@ -2029,20 +2104,22 @@ class RendererGL extends Renderer { let width = smallWidth; let height = Math.floor(smallWidth * (input.height / input.width)); newFramebuffer = new Framebuffer(this, { - width, height, density: 1 - }) + width, + height, + density: 1, + }); // create framebuffer is like making a new sketch, all functions on main // sketch it would be available on framebuffer if (!this.states.diffusedShader) { this.states.diffusedShader = this._pInst.createShader( defaultShaders.imageLightVert, - defaultShaders.imageLightDiffusedFrag + defaultShaders.imageLightDiffusedFrag, ); } newFramebuffer.draw(() => { this.shader(this.states.diffusedShader); - this.states.diffusedShader.setUniform('environmentMap', input); - this.states.doStroke = false; + this.states.diffusedShader.setUniform("environmentMap", input); + this.states.strokeColor = null; this.noLights(); this.plane(width, height); }); @@ -2070,13 +2147,15 @@ class RendererGL extends Renderer { let tex; const levels = []; const framebuffer = new Framebuffer(this, { - width: size, height: size, density: 1 + width: size, + height: size, + density: 1, }); let count = Math.log(size) / Math.log(2); if (!this.states.specularShader) { this.states.specularShader = this._pInst.createShader( defaultShaders.imageLightVert, - defaultShaders.imageLightSpecularFrag + defaultShaders.imageLightSpecularFrag, ); } // currently only 8 levels @@ -2091,9 +2170,9 @@ class RendererGL extends Renderer { framebuffer.draw(() => { this.shader(this.states.specularShader); this.clear(); - this.states.specularShader.setUniform('environmentMap', input); - this.states.specularShader.setUniform('roughness', roughness); - this.states.doStroke = false; + this.states.specularShader.setUniform("environmentMap", input); + this.states.specularShader.setUniform("roughness", roughness); + this.states.strokeColor = null; this.noLights(); this.plane(w, w); }); @@ -2123,43 +2202,46 @@ class RendererGL extends Renderer { const modelMatrix = this.states.uModelMatrix; const viewMatrix = this.states.uViewMatrix; const projectionMatrix = this.states.uPMatrix; - const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix); + const modelViewMatrix = modelMatrix.copy().mult(viewMatrix); this.states.uMVMatrix = this.calculateCombinedMatrix(); const modelViewProjectionMatrix = modelViewMatrix.copy(); modelViewProjectionMatrix.mult(projectionMatrix); shader.setUniform( - 'uPerspective', - this.states.curCamera.useLinePerspective ? 1 : 0 + "uPerspective", + this.states.curCamera.useLinePerspective ? 1 : 0, ); - shader.setUniform('uViewMatrix', viewMatrix.mat4); - shader.setUniform('uProjectionMatrix', projectionMatrix.mat4); - shader.setUniform('uModelMatrix', modelMatrix.mat4); - shader.setUniform('uModelViewMatrix', modelViewMatrix.mat4); + shader.setUniform("uViewMatrix", viewMatrix.mat4); + shader.setUniform("uProjectionMatrix", projectionMatrix.mat4); + shader.setUniform("uModelMatrix", modelMatrix.mat4); + shader.setUniform("uModelViewMatrix", modelViewMatrix.mat4); shader.setUniform( - 'uModelViewProjectionMatrix', - modelViewProjectionMatrix.mat4 + "uModelViewProjectionMatrix", + modelViewProjectionMatrix.mat4, ); if (shader.uniforms.uNormalMatrix) { this.states.uNMatrix.inverseTranspose(this.states.uMVMatrix); - shader.setUniform('uNormalMatrix', this.states.uNMatrix.mat3); + shader.setUniform("uNormalMatrix", this.states.uNMatrix.mat3); } if (shader.uniforms.uCameraRotation) { this.states.curMatrix.inverseTranspose(this.states.uViewMatrix); - shader.setUniform('uCameraRotation', this.states.curMatrix.mat3); + shader.setUniform("uCameraRotation", this.states.curMatrix.mat3); } - shader.setUniform('uViewport', this._viewport); + shader.setUniform("uViewport", this._viewport); } _setStrokeUniforms(strokeShader) { // set the uniform values - strokeShader.setUniform('uSimpleLines', this._simpleLines); - strokeShader.setUniform('uUseLineColor', this._useLineColor); - strokeShader.setUniform('uMaterialColor', this.states.curStrokeColor); - strokeShader.setUniform('uStrokeWeight', this.curStrokeWeight); - strokeShader.setUniform('uStrokeCap', STROKE_CAP_ENUM[this.curStrokeCap]); - strokeShader.setUniform('uStrokeJoin', STROKE_JOIN_ENUM[this.curStrokeJoin]); + strokeShader.setUniform("uSimpleLines", this._simpleLines); + strokeShader.setUniform("uUseLineColor", this._useLineColor); + strokeShader.setUniform("uMaterialColor", this.states.curStrokeColor); + strokeShader.setUniform("uStrokeWeight", this.states.strokeWeight); + strokeShader.setUniform("uStrokeCap", STROKE_CAP_ENUM[this.curStrokeCap]); + strokeShader.setUniform( + "uStrokeJoin", + STROKE_JOIN_ENUM[this.curStrokeJoin], + ); } _setFillUniforms(fillShader) { @@ -2169,54 +2251,61 @@ class RendererGL extends Renderer { this.mixedSpecularColor = this.mixedSpecularColor.map( (mixedSpecularColor, index) => this.states.curFillColor[index] * this.states._useMetalness + - mixedSpecularColor * (1 - this.states._useMetalness) + mixedSpecularColor * (1 - this.states._useMetalness), ); } // TODO: optimize - fillShader.setUniform('uUseVertexColor', this._useVertexColor); - fillShader.setUniform('uMaterialColor', this.states.curFillColor); - fillShader.setUniform('isTexture', !!this.states._tex); + fillShader.setUniform("uUseVertexColor", this._useVertexColor); + fillShader.setUniform("uMaterialColor", this.states.curFillColor); + fillShader.setUniform("isTexture", !!this.states._tex); if (this.states._tex) { - fillShader.setUniform('uSampler', this.states._tex); + fillShader.setUniform("uSampler", this.states._tex); } - fillShader.setUniform('uTint', this.states.tint); + fillShader.setUniform("uTint", this.states.tint); - fillShader.setUniform('uHasSetAmbient', this.states._hasSetAmbient); - fillShader.setUniform('uAmbientMatColor', this.states.curAmbientColor); - fillShader.setUniform('uSpecularMatColor', this.mixedSpecularColor); - fillShader.setUniform('uEmissiveMatColor', this.states.curEmissiveColor); - fillShader.setUniform('uSpecular', this.states._useSpecularMaterial); - fillShader.setUniform('uEmissive', this.states._useEmissiveMaterial); - fillShader.setUniform('uShininess', this.states._useShininess); - fillShader.setUniform('uMetallic', this.states._useMetalness); + fillShader.setUniform("uHasSetAmbient", this.states._hasSetAmbient); + fillShader.setUniform("uAmbientMatColor", this.states.curAmbientColor); + fillShader.setUniform("uSpecularMatColor", this.mixedSpecularColor); + fillShader.setUniform("uEmissiveMatColor", this.states.curEmissiveColor); + fillShader.setUniform("uSpecular", this.states._useSpecularMaterial); + fillShader.setUniform("uEmissive", this.states._useEmissiveMaterial); + fillShader.setUniform("uShininess", this.states._useShininess); + fillShader.setUniform("uMetallic", this.states._useMetalness); this._setImageLightUniforms(fillShader); - fillShader.setUniform('uUseLighting', this.states.enableLighting); + fillShader.setUniform("uUseLighting", this.states.enableLighting); const pointLightCount = this.states.pointLightDiffuseColors.length / 3; - fillShader.setUniform('uPointLightCount', pointLightCount); - fillShader.setUniform('uPointLightLocation', this.states.pointLightPositions); + fillShader.setUniform("uPointLightCount", pointLightCount); fillShader.setUniform( - 'uPointLightDiffuseColors', - this.states.pointLightDiffuseColors + "uPointLightLocation", + this.states.pointLightPositions, ); fillShader.setUniform( - 'uPointLightSpecularColors', - this.states.pointLightSpecularColors + "uPointLightDiffuseColors", + this.states.pointLightDiffuseColors, + ); + fillShader.setUniform( + "uPointLightSpecularColors", + this.states.pointLightSpecularColors, ); - const directionalLightCount = this.states.directionalLightDiffuseColors.length / 3; - fillShader.setUniform('uDirectionalLightCount', directionalLightCount); - fillShader.setUniform('uLightingDirection', this.states.directionalLightDirections); + const directionalLightCount = + this.states.directionalLightDiffuseColors.length / 3; + fillShader.setUniform("uDirectionalLightCount", directionalLightCount); fillShader.setUniform( - 'uDirectionalDiffuseColors', - this.states.directionalLightDiffuseColors + "uLightingDirection", + this.states.directionalLightDirections, ); fillShader.setUniform( - 'uDirectionalSpecularColors', - this.states.directionalLightSpecularColors + "uDirectionalDiffuseColors", + this.states.directionalLightDiffuseColors, + ); + fillShader.setUniform( + "uDirectionalSpecularColors", + this.states.directionalLightSpecularColors, ); // TODO: sum these here... @@ -2224,55 +2313,67 @@ class RendererGL extends Renderer { this.mixedAmbientLight = [...this.states.ambientLightColors]; if (this.states._useMetalness > 0) { - this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors => { + this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors) => { let mixing = ambientColors - this.states._useMetalness; return Math.max(0, mixing); - })); + }); } - fillShader.setUniform('uAmbientLightCount', ambientLightCount); - fillShader.setUniform('uAmbientColor', this.mixedAmbientLight); + fillShader.setUniform("uAmbientLightCount", ambientLightCount); + fillShader.setUniform("uAmbientColor", this.mixedAmbientLight); const spotLightCount = this.states.spotLightDiffuseColors.length / 3; - fillShader.setUniform('uSpotLightCount', spotLightCount); - fillShader.setUniform('uSpotLightAngle', this.states.spotLightAngle); - fillShader.setUniform('uSpotLightConc', this.states.spotLightConc); - fillShader.setUniform('uSpotLightDiffuseColors', this.states.spotLightDiffuseColors); + fillShader.setUniform("uSpotLightCount", spotLightCount); + fillShader.setUniform("uSpotLightAngle", this.states.spotLightAngle); + fillShader.setUniform("uSpotLightConc", this.states.spotLightConc); fillShader.setUniform( - 'uSpotLightSpecularColors', - this.states.spotLightSpecularColors + "uSpotLightDiffuseColors", + this.states.spotLightDiffuseColors, + ); + fillShader.setUniform( + "uSpotLightSpecularColors", + this.states.spotLightSpecularColors, + ); + fillShader.setUniform("uSpotLightLocation", this.states.spotLightPositions); + fillShader.setUniform( + "uSpotLightDirection", + this.states.spotLightDirections, ); - fillShader.setUniform('uSpotLightLocation', this.states.spotLightPositions); - fillShader.setUniform('uSpotLightDirection', this.states.spotLightDirections); - fillShader.setUniform('uConstantAttenuation', this.states.constantAttenuation); - fillShader.setUniform('uLinearAttenuation', this.states.linearAttenuation); - fillShader.setUniform('uQuadraticAttenuation', this.states.quadraticAttenuation); + fillShader.setUniform( + "uConstantAttenuation", + this.states.constantAttenuation, + ); + fillShader.setUniform("uLinearAttenuation", this.states.linearAttenuation); + fillShader.setUniform( + "uQuadraticAttenuation", + this.states.quadraticAttenuation, + ); } // getting called from _setFillUniforms _setImageLightUniforms(shader) { //set uniform values - shader.setUniform('uUseImageLight', this.states.activeImageLight != null); + shader.setUniform("uUseImageLight", this.states.activeImageLight != null); // true if (this.states.activeImageLight) { // this.states.activeImageLight has image as a key // look up the texture from the diffusedTexture map let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight); - shader.setUniform('environmentMapDiffused', diffusedLight); + shader.setUniform("environmentMapDiffused", diffusedLight); let specularLight = this.getSpecularTexture(this.states.activeImageLight); - shader.setUniform('environmentMapSpecular', specularLight); + shader.setUniform("environmentMapSpecular", specularLight); } } _setPointUniforms(pointShader) { // set the uniform values - pointShader.setUniform('uMaterialColor', this.states.curStrokeColor); + pointShader.setUniform("uMaterialColor", this.states.curStrokeColor); // @todo is there an instance where this isn't stroke weight? // should be they be same var? pointShader.setUniform( - 'uPointSize', - this.pointSize * this._pixelDensity + "uPointSize", + this.states.strokeWeight * this._pixelDensity, ); } @@ -2280,13 +2381,7 @@ class RendererGL extends Renderer { * when passed more than two arguments it also updates or initializes * the data associated with the buffer */ - _bindBuffer( - buffer, - target, - values, - type, - usage - ) { + _bindBuffer(buffer, target, values, type, usage) { if (!target) target = this.GL.ARRAY_BUFFER; this.GL.bindBuffer(target, buffer); if (values !== undefined) { @@ -2315,8 +2410,8 @@ class RendererGL extends Renderer { Float64Array, Int16Array, Uint16Array, - Uint32Array - ].some(x => arr instanceof x); + Uint32Array, + ].some((x) => arr instanceof x); } /** @@ -2328,7 +2423,7 @@ class RendererGL extends Renderer { * [1, 2, 3, 4, 5, 6] */ _vToNArray(arr) { - return arr.flatMap(item => [item.x, item.y, item.z]); + return arr.flatMap((item) => [item.x, item.y, item.z]); } // function to calculate BezierVertex Coefficients @@ -2358,10 +2453,9 @@ class RendererGL extends Renderer { const p = [p1, p2, p3, p4]; return p; } +} -}; - -function rendererGL(p5, fn){ +function rendererGL(p5, fn) { p5.RendererGL = RendererGL; /** @@ -2519,15 +2613,15 @@ function rendererGL(p5, fn){ * @param {Object} obj object with key-value pairs */ fn.setAttributes = function (key, value) { - if (typeof this._glAttributes === 'undefined') { + if (typeof this._glAttributes === "undefined") { console.log( - 'You are trying to use setAttributes on a p5.Graphics object ' + - 'that does not use a WEBGL renderer.' + "You are trying to use setAttributes on a p5.Graphics object " + + "that does not use a WEBGL renderer.", ); return; } let unchanged = true; - if (typeof value !== 'undefined') { + if (typeof value !== "undefined") { //first time modifying the attributes if (this._glAttributes === null) { this._glAttributes = {}; @@ -2552,8 +2646,8 @@ function rendererGL(p5, fn){ if (!this._setupDone) { if (this._renderer.geometryBufferCache.numCached() > 0) { p5._friendlyError( - 'Sorry, Could not set the attributes, you need to call setAttributes() ' + - 'before calling the other drawing methods in setup()' + "Sorry, Could not set the attributes, you need to call setAttributes() " + + "before calling the other drawing methods in setup()", ); return; } @@ -2574,7 +2668,7 @@ function rendererGL(p5, fn){ fn._assert3d = function (name) { if (!this._renderer.isP3D) throw new Error( - `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see https://p5js.org/examples/form-3d-primitives.html for more information.` + `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see https://p5js.org/examples/form-3d-primitives.html for more information.`, ); }; @@ -2607,7 +2701,7 @@ export function readPixelsWebGL( height, format, type, - flipY + flipY, ) { // Record the currently bound framebuffer so we can go back to it after, and // bind the framebuffer we want to read from @@ -2625,12 +2719,12 @@ export function readPixelsWebGL( gl.readPixels( x, - flipY ? (flipY - y - height) : y, + flipY ? flipY - y - height : y, width, height, format, type, - pixels + pixels, ); // Re-bind whatever was previously bound @@ -2664,15 +2758,7 @@ export function readPixelsWebGL( * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about * @returns {Number[]} pixels The channel data for the pixel at that location */ -export function readPixelWebGL( - gl, - framebuffer, - x, - y, - format, - type, - flipY -) { +export function readPixelWebGL(gl, framebuffer, x, y, format, type, flipY) { // Record the currently bound framebuffer so we can go back to it after, and // bind the framebuffer we want to read from const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); @@ -2682,11 +2768,7 @@ export function readPixelWebGL( const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; const pixels = new TypedArrayClass(channels); - gl.readPixels( - x, flipY ? (flipY - y - 1) : y, 1, 1, - format, type, - pixels - ); + gl.readPixels(x, flipY ? flipY - y - 1 : y, 1, 1, format, type, pixels); // Re-bind whatever was previously bound gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); @@ -2697,6 +2779,6 @@ export function readPixelWebGL( export default rendererGL; export { RendererGL }; -if(typeof p5 !== 'undefined'){ +if (typeof p5 !== "undefined") { rendererGL(p5, p5.prototype); } diff --git a/src/webgl/shaders/font.vert b/src/webgl/shaders/font.vert index 4655bca0da..ce8b84ab18 100644 --- a/src/webgl/shaders/font.vert +++ b/src/webgl/shaders/font.vert @@ -24,7 +24,7 @@ void main() { 1. / length(newOrigin - newDX), 1. / length(newOrigin - newDY) ); - vec2 offset = pixelScale * normalize(aTexCoord - vec2(0.5, 0.5)) * vec2(1., -1.); + vec2 offset = pixelScale * normalize(aTexCoord - vec2(0.5, 0.5)); vec2 textureOffset = offset * (1. / vec2( uGlyphRect.z - uGlyphRect.x, uGlyphRect.w - uGlyphRect.y diff --git a/src/webgl/text.js b/src/webgl/text.js index ffef73167b..fbe377fe1d 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -7,14 +7,14 @@ import { Geometry } from './p5.Geometry'; function text(p5, fn){ // Text/Typography // @TODO: - RendererGL.prototype._applyTextProperties = function() { + //RendererGL.prototype._applyTextProperties = function() { //@TODO finish implementation //console.error('text commands not yet implemented in webgl'); - }; + //}; RendererGL.prototype.textWidth = function(s) { if (this._isOpenType()) { - return this._textFont._textWidth(s, this._textSize); + return this.states.textFont.font._textWidth(s, this.states.textSize); } return 0; // TODO: error @@ -165,23 +165,60 @@ function text(p5, fn){ * calculates rendering info for a glyph, including the curve information, * row & column stripes compiled into textures. */ - getGlyphInfo (glyph) { + getGlyphInfo(glyph) { // check the cache let gi = this.glyphInfos[glyph.index]; if (gi) return gi; - // get the bounding box of the glyph from opentype.js - const bb = glyph.getBoundingBox(); - const xMin = bb.x1; - const yMin = bb.y1; - const gWidth = bb.x2 - xMin; - const gHeight = bb.y2 - yMin; - const cmds = glyph.path.commands; + const { glyph: { path: { commands } } } = this.font._singleShapeToPath(glyph.shape); + let xMin = Infinity; + let xMax = -Infinity; + let yMin = Infinity; + let yMax = -Infinity; + + for (const cmd of commands) { + for (let i = 1; i < cmd.length; i += 2) { + xMin = Math.min(xMin, cmd[i]); + xMax = Math.max(xMax, cmd[i]); + yMin = Math.min(yMin, cmd[i + 1]); + yMax = Math.max(yMax, cmd[i + 1]); + } + } + // don't bother rendering invisible glyphs - if (gWidth === 0 || gHeight === 0 || !cmds.length) { + if (xMin >= xMax || yMin >= yMax || !commands.length) { return (this.glyphInfos[glyph.index] = {}); } + const gWidth = xMax - xMin; + const gHeight = yMax - yMin; + + // Convert arrays to named objects + const cmds = commands.map((command) => { + const type = command[0]; + switch (type) { + case 'Z': { + return { type }; + } + case 'M': + case 'L': { + const [, x, y] = command; + return { type, x, y }; + } + case 'Q': { + const [, x1, y1, x, y] = command; + return { type, x1, y1, x, y }; + } + case 'C': { + const [, x1, y1, x2, y2, x, y] = command; + return { type, x1, y1, x2, y2, x, y }; + } + default: { + throw new Error(`Unexpected path command: ${type}`); + } + } + }) + let i; const strokes = []; // the strokes in this glyph const rows = []; // the indices of strokes in each row @@ -624,7 +661,7 @@ function text(p5, fn){ // initialize the info for this glyph gi = this.glyphInfos[glyph.index] = { glyph, - uGlyphRect: [bb.x1, -bb.y1, bb.x2, -bb.y2], + uGlyphRect: [xMin, yMin, xMax, yMax], strokeImageInfo, strokes, colInfo: layout(cols, this.colDimImageInfos, this.colCellImageInfos), @@ -635,14 +672,14 @@ function text(p5, fn){ } } - RendererGL.prototype._renderText = function(p, line, x, y, maxY) { - if (!this._textFont || typeof this._textFont === 'string') { + RendererGL.prototype._renderText = function(line, x, y, maxY, minY) { + if (!this.states.textFont || typeof this.states.textFont === 'string') { console.log( 'WEBGL: you must load and set a font before drawing text. See `loadFont` and `textFont` for more details.' ); return; } - if (y >= maxY || !this.states.doFill) { + if (y >= maxY || !this.states.fillColor) { return; // don't render lines beyond our maxY position } @@ -653,26 +690,32 @@ function text(p5, fn){ return p; } - p.push(); // fix to #803 + this.push(); // fix to #803 // remember this state, so it can be restored later - const doStroke = this.states.doStroke; + const doStroke = this.states.strokeColor; const drawMode = this.states.drawMode; - this.states.doStroke = false; + this.states.strokeColor = null; this.states.drawMode = constants.TEXTURE; // get the cached FontInfo object - const font = this._textFont.font; - let fontInfo = this._textFont._fontInfo; + const { font } = this.states.textFont; + if (!font) { + throw new Error( + 'In WebGL mode, textFont() needs to be given the result of loadFont() instead of a font family name.' + ); + } + let fontInfo = this.states.textFont._fontInfo; if (!fontInfo) { - fontInfo = this._textFont._fontInfo = new FontInfo(font); + fontInfo = this.states.textFont._fontInfo = new FontInfo(font); } // calculate the alignment and move/scale the view accordingly - const pos = this._textFont._handleAlignment(this, line, x, y); - const fontSize = this._textSize; - const scale = fontSize / font.unitsPerEm; + // TODO: check this + const pos = { x, y } // this.states.textFont._handleAlignment(this, line, x, y); + const fontSize = this.states.textSize; + const scale = fontSize / (font.data?.head?.unitsPerEm || 1000); this.translate(pos.x, pos.y, 0); this.scale(scale, scale, 1); @@ -690,12 +733,13 @@ function text(p5, fn){ sh.setUniform('uStrokeImageSize', [strokeImageWidth, strokeImageHeight]); sh.setUniform('uGridSize', [charGridWidth, charGridHeight]); } + this._setGlobalUniforms(sh); this._applyColorBlend(this.states.curFillColor); - let g = this.retainedMode.geometry['glyph']; + let g = this.geometryBufferCache.getGeometryByID('glyph'); if (!g) { // create the geometry for rendering a quad - const geom = (this._textGeom = new Geometry(1, 1, function() { + g = (this._textGeom = new Geometry(1, 1, function() { for (let i = 0; i <= 1; i++) { for (let j = 0; j <= 1; j++) { this.vertices.push(new Vector(j, i, 0)); @@ -703,30 +747,26 @@ function text(p5, fn){ } } }, this) ); - geom.computeFaces().computeNormals(); - g = this.geometryBufferCache.ensureCached(geom); + g.gid = 'glyph'; + g.computeFaces().computeNormals(); + this.geometryBufferCache.ensureCached(g); } // bind the shader buffers - for (const buff of this.retainedMode.buffers.text) { + for (const buff of this.buffers.text) { buff._prepareBuffer(g, sh); } - this._bindBuffer(g.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); + this._bindBuffer(this.geometryBufferCache.cache.glyph.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); // this will have to do for now... sh.setUniform('uMaterialColor', this.states.curFillColor); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); try { - let dx = 0; // the x position in the line - let glyphPrev = null; // the previous glyph, used for kerning // fetch the glyphs in the line of text - const glyphs = font.stringToGlyphs(line); + const glyphs = font._positionGlyphs(line); for (const glyph of glyphs) { - // kern - if (glyphPrev) dx += font.getKerningValue(glyphPrev, glyph); - const gi = fontInfo.getGlyphInfo(glyph); if (gi.uGlyphRect) { const rowInfo = gi.rowInfo; @@ -738,28 +778,24 @@ function text(p5, fn){ sh.setUniform('uSamplerCols', colInfo.dimImageInfo.imageData); sh.setUniform('uGridOffset', gi.uGridOffset); sh.setUniform('uGlyphRect', gi.uGlyphRect); - sh.setUniform('uGlyphOffset', dx); + sh.setUniform('uGlyphOffset', glyph.x); sh.bindTextures(); // afterwards, only textures need updating // draw it gl.drawElements(gl.TRIANGLES, 6, this.GL.UNSIGNED_SHORT, 0); } - dx += glyph.advanceWidth; - glyphPrev = glyph; } } finally { // clean up sh.unbindShader(); - this.states.doStroke = doStroke; + this.states.strokeColor = doStroke; this.states.drawMode = drawMode; gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - p.pop(); + this.pop(); } - - return p; }; } diff --git a/test/js/mocks.js b/test/js/mocks.js index c70284553c..63d0cc7951 100644 --- a/test/js/mocks.js +++ b/test/js/mocks.js @@ -18,17 +18,25 @@ const httpMocks = [ export const httpMock = setupWorker(...httpMocks); // p5.js module mocks -export const mockP5 = { +export const mockP5 = vi.fn(); +Object.assign(mockP5, { _validateParameters: vi.fn(), _friendlyFileLoadError: vi.fn(), _friendlyError: vi.fn() -}; +}); const mockCanvas = document.createElement('canvas'); +mockCanvas.id = 'myCanvasID'; +document.getElementsByTagName("body")[0].appendChild(mockCanvas); + export const mockP5Prototype = { saveCanvas: vi.fn(), elt: mockCanvas, _curElement: { elt: mockCanvas - } + }, + canvas: { + id: 'myCanvasID' + }, + _elements: [] }; diff --git a/test/manual-test-examples/addons/p5.sound/autoCorrelation/sketch.js b/test/manual-test-examples/addons/p5.sound/autoCorrelation/sketch.js index 733963440b..f86b6e94a1 100644 --- a/test/manual-test-examples/addons/p5.sound/autoCorrelation/sketch.js +++ b/test/manual-test-examples/addons/p5.sound/autoCorrelation/sketch.js @@ -36,7 +36,7 @@ function draw() { for (var i = 0; i < corrBuff.length; i++) { var w = map(i, 0, corrBuff.length, 0, width); var h = map(corrBuff[i], -1, 1, height, 0); - curveVertex(w, h); + splineVertex(w, h); } endShape(); } diff --git a/test/manual-test-examples/p5.Font/Helvetica.ttf b/test/manual-test-examples/p5.Font/Helvetica.ttf deleted file mode 100644 index 3019c69fb3..0000000000 Binary files a/test/manual-test-examples/p5.Font/Helvetica.ttf and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/Merriweather-LightItalic.ttf b/test/manual-test-examples/p5.Font/Merriweather-LightItalic.ttf deleted file mode 100644 index dfa087caa4..0000000000 Binary files a/test/manual-test-examples/p5.Font/Merriweather-LightItalic.ttf and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/Montserrat-Regular.ttf b/test/manual-test-examples/p5.Font/Montserrat-Regular.ttf deleted file mode 100644 index b4368631ef..0000000000 Binary files a/test/manual-test-examples/p5.Font/Montserrat-Regular.ttf and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/OpenSans-Regular.ttf b/test/manual-test-examples/p5.Font/OpenSans-Regular.ttf deleted file mode 100644 index db433349b7..0000000000 Binary files a/test/manual-test-examples/p5.Font/OpenSans-Regular.ttf and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/SourceSansPro-Bold.ttf b/test/manual-test-examples/p5.Font/SourceSansPro-Bold.ttf deleted file mode 100755 index 50d81bdad5..0000000000 Binary files a/test/manual-test-examples/p5.Font/SourceSansPro-Bold.ttf and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/SourceSansPro-Italic.ttf b/test/manual-test-examples/p5.Font/SourceSansPro-Italic.ttf deleted file mode 100755 index e5a1a86e63..0000000000 Binary files a/test/manual-test-examples/p5.Font/SourceSansPro-Italic.ttf and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/SourceSansPro-Regular.otf b/test/manual-test-examples/p5.Font/SourceSansPro-Regular.otf deleted file mode 100644 index 38941ae72f..0000000000 Binary files a/test/manual-test-examples/p5.Font/SourceSansPro-Regular.otf and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/callback/index.html b/test/manual-test-examples/p5.Font/callback/index.html deleted file mode 100755 index f917cd3877..0000000000 --- a/test/manual-test-examples/p5.Font/callback/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/test/manual-test-examples/p5.Font/callback/sketch.js b/test/manual-test-examples/p5.Font/callback/sketch.js deleted file mode 100755 index 7294d1d7bc..0000000000 --- a/test/manual-test-examples/p5.Font/callback/sketch.js +++ /dev/null @@ -1,20 +0,0 @@ -function setup() { - createCanvas(240, 160); - textSize(18); - text('Default Text', 10, 30); - noStroke(); - fill(0, 102, 153); - text('Black No Stroke Text', 10, 60); - textSize(12); - fill(120); - loadFont('../SourceSansPro-Regular.otf', function(f) { - textFont(f); - text( - 'Simple long Text: Lorem Ipsum is simply dummy text of the printing and typesetting industry. ', - 10, - 90, - 220, - 60 - ); - }); -} diff --git a/test/manual-test-examples/p5.Font/custom/LEFT.BL.lead.png b/test/manual-test-examples/p5.Font/custom/LEFT.BL.lead.png deleted file mode 100644 index c7a64cee04..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/LEFT.BL.lead.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/LEFT.BOTTOM.lead.png b/test/manual-test-examples/p5.Font/custom/LEFT.BOTTOM.lead.png deleted file mode 100644 index 2be46d776d..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/LEFT.BOTTOM.lead.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/LEFT.CENTER.lead.png b/test/manual-test-examples/p5.Font/custom/LEFT.CENTER.lead.png deleted file mode 100644 index 50aacbea38..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/LEFT.CENTER.lead.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/LEFT.TOP.lead.png b/test/manual-test-examples/p5.Font/custom/LEFT.TOP.lead.png deleted file mode 100644 index 580cb1252f..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/LEFT.TOP.lead.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/index.html b/test/manual-test-examples/p5.Font/custom/index.html deleted file mode 100755 index 1f5dc86328..0000000000 --- a/test/manual-test-examples/p5.Font/custom/index.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - -
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - diff --git a/test/manual-test-examples/p5.Font/custom/sketch.js b/test/manual-test-examples/p5.Font/custom/sketch.js deleted file mode 100755 index b7243e0d64..0000000000 --- a/test/manual-test-examples/p5.Font/custom/sketch.js +++ /dev/null @@ -1,897 +0,0 @@ -var textSketch = function(p) { - var font, font2; - p.preload = function() { - //font = p.loadFont('../acmesa.ttf'); - font2 = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(240, 160); - //p.ellipse(20,20,50,70); - //p.textFont(font); - //p.text('Default Text', 10, 30); - p.textSize(18); - p.textFont(font2); - p.noStroke(); - p.fill(0, 102, 153); - p.text('Blue No Stroke Text', 10, 60); - //p.stroke(0, 200, 0); - //p.strokeWeight(0.5); - //p.text('Blue with Green Stroked Text', 10, 90); - p.noStroke(); - p.textSize(12); - p.fill(120); - p.text( - 'Simple long Text: Lorem Ipsum is simply dummy text of the printing and typesetting industry. ', - 10, - 90, - 220, - 60 - ); - }; -}; - -var textLineSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(240 * 4, 160); - p.textFont(font); - p.textSize(10); - p.stroke(0); - //1 - p.fill(255); - p.strokeWeight(1); - p.line(10, 10, 220, 10); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.TOP); - p.text('LEFT TOP is simply dummy text.', 10, 10); - //2 - p.fill(255); - p.strokeWeight(1); - p.line(10, 60, 220, 60); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.TOP); - p.text('CENTER TOP is simply dummy text.', 10, 60); - //3 - p.fill(255); - p.strokeWeight(1); - p.line(10, 110, 220, 110); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.TOP); - p.text('RIGHT TOP is simply dummy text.', 10, 110); - - //1 - p.fill(255); - p.strokeWeight(1); - p.line(250, 10, 470, 10); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.CENTER); - p.text('LEFT CENTER is simply dummy text.', 250, 10); - //2 - p.fill(255); - p.strokeWeight(1); - p.line(250, 60, 470, 60); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.CENTER); - p.text('CENTER CENTER is simply dummy text.', 250, 60); - //3 - p.fill(255); - p.strokeWeight(1); - p.line(250, 110, 470, 110); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.CENTER); - p.text('RIGHT CENTER is simply dummy text.', 250, 110); - - //1 - p.fill(255); - p.strokeWeight(1); - p.line(490, 10, 710, 10); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.BOTTOM); - p.text('LEFT BOTTOM is simply dummy text.', 490, 10); - //2 - p.fill(255); - p.strokeWeight(1); - p.line(490, 60, 710, 60); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.BOTTOM); - p.text('CENTER BOTTOM is simply dummy text.', 490, 60); - //3 - p.fill(255); - p.strokeWeight(1); - p.line(490, 110, 710, 110); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.BOTTOM); - p.text('RIGHT BOTTOM is simply dummy text.', 490, 110); - - //1 - p.fill(255); - p.strokeWeight(1); - p.line(730, 10, 950, 10); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.BASELINE); - p.text('LEFT BASELINE is simply dummy text.', 730, 10); - //2 - p.fill(255); - p.strokeWeight(1); - p.line(730, 60, 950, 60); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.BASELINE); - p.text('CENTER BASELINE is simply dummy text.', 730, 60); - //3 - p.fill(255); - p.strokeWeight(1); - p.line(730, 110, 950, 110); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.BASELINE); - p.text('RIGHT BASELINE is simply dummy text.', 730, 110); - }; -}; - -var textWrapSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(240 * 4, 160); - p.textFont(font); - p.textSize(10); - p.stroke(0); - //1 - p.fill(255); - p.strokeWeight(1); - p.rect(10, 10, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.TOP); - p.text( - 'LEFT TOP is simply dummy text of the printing and typesetting industry. ', - 10, - 10, - 220, - 40 - ); - //2 - p.fill(255); - p.strokeWeight(1); - p.rect(10, 60, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.TOP); - p.text( - 'CENTER TOP is simply dummy text of the printing and typesetting industry. ', - 10, - 60, - 220, - 40 - ); - //3 - p.fill(255); - p.strokeWeight(1); - p.rect(10, 110, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.TOP); - p.text( - 'RIGHT TOP is simply dummy text of the printing and typesetting industry. ', - 10, - 110, - 220, - 40 - ); - - //1 - p.fill(255); - p.strokeWeight(1); - p.rect(250, 10, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.CENTER); - p.text( - 'LEFT CENTER is simply dummy text of the printing and typesetting industry. ', - 250, - 10, - 220, - 40 - ); - //2 - p.fill(255); - p.strokeWeight(1); - p.rect(250, 60, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.CENTER); - p.text( - 'CENTER CENTER is simply dummy text of the printing and typesetting industry. ', - 250, - 60, - 220, - 40 - ); - //3 - p.fill(255); - p.strokeWeight(1); - p.rect(250, 110, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.CENTER); - p.text( - 'RIGHT CENTER is simply dummy text of the printing and typesetting industry. ', - 250, - 110, - 220, - 40 - ); - - //1 - p.fill(255); - p.strokeWeight(1); - p.rect(490, 10, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.BOTTOM); - p.text( - 'LEFT BOTTOM is simply dummy text of the printing and typesetting industry. ', - 490, - 10, - 220, - 40 - ); - //2 - p.fill(255); - p.strokeWeight(1); - p.rect(490, 60, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.BOTTOM); - p.text( - 'CENTER BOTTOM is simply dummy text of the printing and typesetting industry. ', - 490, - 60, - 220, - 40 - ); - //3 - p.fill(255); - p.strokeWeight(1); - p.rect(490, 110, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.BOTTOM); - p.text( - 'RIGHT BOTTOM is simply dummy text of the printing and typesetting industry. ', - 490, - 110, - 220, - 40 - ); - - //1 - p.fill(255); - p.strokeWeight(1); - p.rect(730, 10, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.BASELINE); - p.text( - 'LEFT BASELINE is simply dummy text of the printing and typesetting industry. ', - 730, - 10, - 220, - 40 - ); - //2 - p.fill(255); - p.strokeWeight(1); - p.rect(730, 60, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.BASELINE); - p.text( - 'CENTER BASELINE is simply dummy text of the printing and typesetting industry. ', - 730, - 60, - 220, - 40 - ); - //3 - p.fill(255); - p.strokeWeight(1); - p.rect(730, 110, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.BASELINE); - p.text( - 'RIGHT BASELINE is simply dummy text of the printing and typesetting industry. ', - 730, - 110, - 220, - 40 - ); - }; -}; - -var textAlignSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.fill(0); - p.strokeWeight(0); - p.textSize(12); - p.textAlign(p.RIGHT, p.TOP); - p.text('Top Right', 120, 30); - p.textAlign(p.CENTER, p.CENTER); - p.text('Center Center', 120, 60); - p.textAlign(p.LEFT, p.BOTTOM); - p.text('Left Bottom', 120, 90); - p.textAlign(p.RIGHT, p.BASELINE); - p.text('Right Baseline', 120, 90); - p.strokeWeight(1); - p.line(120, 0, 120, 160); - }; -}; - -var textLeadingSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(400, 200); - p.textFont(font); - p.fill(0); - p.textSize(12); - - p.line(0, 100, p.width, 100); - p.textAlign(p.LEFT, p.TOP); - p.strokeWeight(0); - - var s10 = 'LEFT/TOP@10px', - s20 = s10.replace('1', '2'), - s30 = s10.replace('1', '3'); - - p.textLeading(10); // Set leading to 10 - p.text(s10 + '\n' + s10 + '\n' + s10, 10, 100); - p.textLeading(20); // Set leading to 20 - p.text(s20 + '\n' + s20 + '\n' + s20, 140, 100); - p.textLeading(30); // Set leading to 30 - p.text(s30 + '\n' + s30 + '\n' + s30, 270, 100); - }; -}; - -var textLeadingSketch2 = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(400, 200); - p.textFont(font); - p.fill(0); - p.textSize(12); - - p.line(0, 100, p.width, 100); - p.textAlign(p.LEFT, p.CENTER); - p.strokeWeight(0); - - var s10 = 'LEFT/CENTER@10px', - s20 = s10.replace('1', '2'), - s30 = s10.replace('1', '3'); - - p.textLeading(10); // Set leading to 10 - p.text(s10 + '\n' + s10 + '\n' + s10, 10, 100); - p.textLeading(20); // Set leading to 20 - p.text(s20 + '\n' + s20 + '\n' + s20, 140, 100); - p.textLeading(30); // Set leading to 30 - p.text(s30 + '\n' + s30 + '\n' + s30, 270, 100); - }; -}; - -var textLeadingSketch3 = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(400, 200); - p.textFont(font); - p.fill(0); - p.textSize(12); - - p.line(0, 100, p.width, 100); - p.textAlign(p.LEFT, p.BASELINE); - p.strokeWeight(0); - - var s10 = 'LEFT/BASELINE@10px', - s20 = s10.replace('1', '2'), - s30 = s10.replace('1', '3'); - - p.textLeading(10); // Set leading to 10 - p.text(s10 + '\n' + s10 + '\n' + s10, 10, 100); - p.textLeading(20); // Set leading to 20 - p.text(s20 + '\n' + s20 + '\n' + s20, 140, 100); - p.textLeading(30); // Set leading to 30 - p.text(s30 + '\n' + s30 + '\n' + s30, 270, 100); - }; -}; - -var textLeadingSketch4 = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(400, 200); - p.textFont(font); - p.fill(0); - p.textSize(12); - - p.line(0, 100, p.width, 100); - p.textAlign(p.LEFT, p.BOTTOM); - p.strokeWeight(0); - - var s10 = 'LEFT/BOTTOM@10px', - s20 = s10.replace('1', '2'), - s30 = s10.replace('1', '3'); - - p.textLeading(10); // Set leading to 10 - p.text(s10 + '\n' + s10 + '\n' + s10, 10, 100); - p.textLeading(20); // Set leading to 20 - p.text(s20 + '\n' + s20 + '\n' + s20, 140, 100); - p.textLeading(30); // Set leading to 30 - p.text(s30 + '\n' + s30 + '\n' + s30, 270, 100); - }; -}; - -var textAlignmentSketch = function(p) { - var font1, font2, font3, font4; - var hAligns = [p.LEFT, p.CENTER, p.RIGHT]; - var vAligns = [p.TOP, p.CENTER, p.BASELINE, p.BOTTOM]; - var textString = 'Hello p5'; - var padding = 10; - p.preload = function() { - font1 = p.loadFont('../SourceSansPro-Regular.otf'); // different - font2 = p.loadFont('../FiraSans-Book.otf'); - font3 = p.loadFont('../Inconsolata-Bold.ttf'); - font4 = p.loadFont('../PlayfairDisplay-Regular.ttf'); - }; - var drawFontAlignments = function(font, xOff, yOff) { - p.textFont(font); - p.textSize(20); - for (var h = 0; h < hAligns.length; h += 1) { - for (var v = 0; v < vAligns.length; v += 1) { - // Distribute words across the screen - var x = xOff + p.map(h, 0, hAligns.length - 1, padding, 400 - padding); - var y = yOff + p.map(v, 0, vAligns.length - 1, padding, 200 - padding); - - p.stroke(200); - p.line(0, y, p.width, y); - p.line(x, 0, x, p.height); - - // Align the text & calculate the bounds - p.textAlign(hAligns[h], vAligns[v]); - - // Draw the text - p.fill(255, 0, 0); - p.noStroke(); - p.text(textString, x, y); - - // Draw the (x, y) coordinates - p.stroke(0); - p.fill('#FF8132'); - p.ellipse(x, y, 3, 3); - } - } - }; - p.setup = function() { - var renderer = p.createCanvas(400, 800); - renderer.elt.style.position = 'absolute'; - renderer.elt.style.top = '0'; - renderer.elt.style.left = '0'; - drawFontAlignments(font1, 0, 0); - drawFontAlignments(font2, 0, 200); - drawFontAlignments(font3, 0, 400); - drawFontAlignments(font4, 0, 600); - }; -}; - -var textVertAlignmentSketch = function(p) { - var fontNames = [ - 'acmesa.ttf', - 'FiraSans-Book.otf', - 'Lato-Black.ttf', - 'Inconsolata-Bold.ttf', - 'Merriweather-LightItalic.ttf', - 'Montserrat-Regular.ttf', - 'OpenSans-Regular.ttf', - 'SourceSansPro-Regular.otf' // different - ]; - var fonts = []; - var vAligns = [p.TOP, p.CENTER, p.BASELINE, p.BOTTOM]; - p.preload = function() { - for (var i = 0; i < fontNames.length; i += 1) { - fonts.push(p.loadFont('../' + fontNames[i])); - } - }; - var drawFontAlignments = function(font, xOff, yOff) { - p.textFont(font); - p.textSize(20); - for (var v = 0; v < vAligns.length; v += 1) { - // Distribute words across the screen - var x = xOff; - var y = yOff + p.map(v, 0, vAligns.length - 1, 10, p.height - 10); - - p.stroke(200); - p.line(0, y, p.width, y); - - // Align the text & calculate the bounds - p.textAlign(p.CENTER, vAligns[v]); - - // Draw the text - p.fill(255, 0, 0); - p.noStroke(); - p.text('Hello p5', x, y); - - // Draw the (x, y) coordinates - p.stroke(0); - p.fill('#FF8132'); - p.ellipse(x, y, 3, 3); - } - }; - p.setup = function() { - var renderer = p.createCanvas(1000, 200); - renderer.elt.style.position = 'absolute'; - renderer.elt.style.top = '0'; - renderer.elt.style.left = '0'; - for (var i = 0; i < fonts.length; i += 1) { - var x = p.map(i, 0, fonts.length - 1, 100, p.width - 100); - drawFontAlignments(fonts[i], x, 0); - } - }; -}; - -var textSizeSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.fill(0); - p.strokeWeight(0); - p.textSize(12); - p.text('Font Size 12', 10, 30); - p.textSize(14); - p.text('Font Size 14', 10, 60); - p.textSize(16); - p.text('Font Size 16', 10, 90); - }; -}; - -var textBoundsSketch = function(p) { - var font; - var text = 'Lorem ipsum dolor sit amet.'; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.strokeWeight(1); - p.textSize(16); - var bbox = font.textBounds(text, 30, 60, 16); - p.fill(255); - p.stroke(0); - p.rect(bbox.x, bbox.y, bbox.w, bbox.h); - p.fill(0); - p.strokeWeight(0); - p.text(text, 30, 60); - }; -}; - -var textStyleSketch = function(p) { - var font; - p.preload = function() { - fontRegular = p.loadFont('../SourceSansPro-Regular.otf'); - fontItalic = p.loadFont('../SourceSansPro-Italic.ttf'); - fontBold = p.loadFont('../SourceSansPro-Bold.ttf'); - }; - p.setup = function() { - p.createCanvas(240, 160); - p - .fill(0) - .strokeWeight(0) - .textSize(24); - p.textFont(fontRegular); - p.text('Font Style Normal', 30, 50); - p.textFont(fontItalic); - p.text('Font Style Italic', 30, 80); - p.textFont(fontBold); - p.text('Font Style Bold', 30, 110); - }; -}; - -var textWidthSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.fill(0); - p.strokeWeight(0); - p.textSize(12); - var s = "What's the width of this line?"; - var textWidth = p.textWidth(s); - p.text(s, 10, 30); - p.rect(10, 30, textWidth, 2); - p.text('width: ' + textWidth, 10, 60); - }; -}; - -var textOverlapSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../SourceSansPro-Regular.otf'); - }; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.fill(0); - p.strokeWeight(0); - p.textSize(72); - p.fill(0, 160); // Black with low opacity - p.text('O', 0, 100); - p.text('V', 30, 100); - p.text('E', 60, 100); - p.text('R', 90, 100); - p.text('L', 120, 100); - p.text('A', 150, 100); - p.text('P', 180, 100); - }; -}; - -var textFlySketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../acmesa.ttf'); - }; - var x1 = 100; - var x2 = 0; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.fill(0); - p.textSize(48); - }; - p.draw = function() { - p.background(204); - p.text('Left', x1, 50); - p.text('Right', x2, 150); - x2 += 2.0; - if (x2 > 240) { - x2 = -100; - } - x1 -= 1.0; - if (x1 < -100) { - x1 = 240; - } - }; -}; - -var textFlickerSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../acmesa.ttf'); - }; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.textSize(48); - p.noStroke(); - }; - p.draw = function() { - p.fill(204, 24); - p.rect(0, 0, p.width, p.height); - p.fill(0); - p.text('flicker Text', p.random(-100, 240), p.random(-20, 160)); - }; -}; - -var textFadeSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../acmesa.ttf'); - }; - var opacity = 0; - var direction = 1; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.textSize(72); - p.noStroke(); - }; - p.draw = function() { - p.background(204); - opacity += 4 * direction; - if (opacity < 0 || opacity > 255) { - direction = -direction; - } - p.fill(0, opacity); - p.text('fade', 50, 100); - }; -}; - -var textRotateSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../acmesa.ttf'); - }; - var angle = 0.0; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.textSize(24); - p.noStroke(); - p.fill(0); - }; - p.draw = function() { - p.background(204); - angle += 0.05; - p.push(); - p.translate(120, 80); - p.scale((p.cos(angle / 4.0) + 1.2) * 2.0); - p.rotate(angle); - p.text('Rotating', 0, 0); - p.pop(); - }; -}; - -var textGrowSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../acmesa.ttf'); - }; - var angle = 0.0; - var str = 'GROW'; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.textSize(24); - p.noStroke(); - p.fill(0, 0, 0, 120); - }; - p.draw = function() { - p.background(204); - angle += 0.1; - for (var i = 0; i < str.length; i++) { - var c = p.sin(angle + i / p.PI); - p.textSize((c + 1.0) * 40 + 10); - p.text(str.charAt(i), i * 40 + 20, 100); - } - }; -}; - -var textAvoidSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../acmesa.ttf'); - }; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.textSize(24); - p.noStroke(); - p.fill(0); - p.textAlign(p.CENTER); - }; - p.draw = function() { - p.background(204); - p.text('AVOID', p.width - p.mouseX, p.height - p.mouseY); - }; -}; - -var textBendSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../acmesa.ttf'); - }; - var str = 'Flexibility'; - p.setup = function() { - p.createCanvas(240, 160); - p.textFont(font); - p.textSize(30); - p.noStroke(); - p.fill(0); - }; - p.draw = function() { - p.background(204); - p.push(); - p.translate(0, 33); - for (var i = 0; i < str.length; i++) { - var angle = p.map(p.mouseX, 0, p.width, 0, p.PI / 8); - p.rotate(angle); - p.text(str[i], 20, 0); - p.translate(p.textWidth(str[i]) * 1.5, 0); - } - p.pop(); - }; -}; - -var typographyLetterSketch = function(p) { - var font; - p.preload = function() { - font = p.loadFont('../acmesa.ttf'); - }; - var margin = 10; - var gap = 46; - var counter = 35; - p.setup = function() { - p.createCanvas(720, 320); - p.textFont(font); - p.background(0); - p.textSize(24); - p.textStyle(p.BOLD); - p.textAlign(p.CENTER, p.CENTER); - p.translate(margin * 4, margin * 4); - for (var y = 0; y < p.height - gap; y += gap) { - for (var x = 0; x < p.width - gap; x += gap) { - var letter = p.char(counter); - if (letter === 'P' || letter === '5') { - p.fill(255, 204, 0); - } else if (letter === 'J' || letter === 'S') { - p.fill(204, 0, 255); - } else { - p.fill(255); - } - p.text(letter, x, y); - counter++; - } - } - }; -}; - -new p5(textSketch, 'textSketch'); -new p5(textLineSketch, 'textLineSketch'); -new p5(textWrapSketch, 'textWrapSketch'); -new p5(textAlignSketch, 'textAlignSketch'); -new p5(textLeadingSketch, 'textLeadingSketch'); -new p5(textLeadingSketch2, 'textLeadingSketch2'); -new p5(textLeadingSketch3, 'textLeadingSketch3'); -new p5(textLeadingSketch4, 'textLeadingSketch4'); -new p5(textAlignmentSketch, 'textAlignmentSketch'); -new p5(textVertAlignmentSketch, 'textVertAlignmentSketch'); -new p5(textSizeSketch, 'textSizeSketch'); -new p5(textBoundsSketch, 'textBoundsSketch'); -new p5(textStyleSketch, 'textStyleSketch'); -new p5(textWidthSketch, 'textWidthSketch'); -new p5(textOverlapSketch, 'textOverlapSketch'); -new p5(textFlySketch, 'textFlySketch'); -new p5(textFlickerSketch, 'textFlickerSketch'); -new p5(textFadeSketch, 'textFadeSketch'); -new p5(textRotateSketch, 'textRotateSketch'); -new p5(textGrowSketch, 'textGrowSketch'); -new p5(textAvoidSketch, 'textAvoidSketch'); -new p5(textBendSketch, 'textBendSketch'); -new p5(typographyLetterSketch, 'typographyLetterSketch'); diff --git a/test/manual-test-examples/p5.Font/custom/textAlignSketch.png b/test/manual-test-examples/p5.Font/custom/textAlignSketch.png deleted file mode 100644 index 571973b350..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/textAlignSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/textAlignmentSketch.png b/test/manual-test-examples/p5.Font/custom/textAlignmentSketch.png deleted file mode 100644 index 6d6628f03c..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/textAlignmentSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/textLineSketch.png b/test/manual-test-examples/p5.Font/custom/textLineSketch.png deleted file mode 100644 index 24d3b7fe00..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/textLineSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/textOverlapSketch.png b/test/manual-test-examples/p5.Font/custom/textOverlapSketch.png deleted file mode 100644 index 17343cce1b..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/textOverlapSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/textSizeSketch.png b/test/manual-test-examples/p5.Font/custom/textSizeSketch.png deleted file mode 100644 index 1813714359..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/textSizeSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/textSketch.png b/test/manual-test-examples/p5.Font/custom/textSketch.png deleted file mode 100644 index 965c569a96..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/textSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/textVertAlignmentSketch.png b/test/manual-test-examples/p5.Font/custom/textVertAlignmentSketch.png deleted file mode 100644 index 5ecce582b8..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/textVertAlignmentSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/textWidthSketch.png b/test/manual-test-examples/p5.Font/custom/textWidthSketch.png deleted file mode 100644 index 5b640ac39a..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/textWidthSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/custom/textWrapSketch.png b/test/manual-test-examples/p5.Font/custom/textWrapSketch.png deleted file mode 100644 index 622f09746f..0000000000 Binary files a/test/manual-test-examples/p5.Font/custom/textWrapSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/opentype/AvenirNextLTPro-Demi.otf b/test/manual-test-examples/p5.Font/opentype/AvenirNextLTPro-Demi.otf deleted file mode 100644 index 750b0c5994..0000000000 Binary files a/test/manual-test-examples/p5.Font/opentype/AvenirNextLTPro-Demi.otf and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/opentype/index.html b/test/manual-test-examples/p5.Font/opentype/index.html deleted file mode 100644 index 2a50b6a37e..0000000000 --- a/test/manual-test-examples/p5.Font/opentype/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/test/manual-test-examples/p5.Font/opentype/sketch.js b/test/manual-test-examples/p5.Font/opentype/sketch.js deleted file mode 100644 index f0cec39b59..0000000000 --- a/test/manual-test-examples/p5.Font/opentype/sketch.js +++ /dev/null @@ -1,51 +0,0 @@ -var font; -var snapDistance = 71; - -function preload() { - font = loadFont('AvenirNextLTPro-Demi.otf'); -} - -function setup() { - createCanvas(720, 480); - frameRate(24); - fill(255); - textSize(150); -} - -function draw() { - background(237, 34, 93); - - var path = font._getPath('p5*js', 170, 275); - doSnap(path, snapDistance); - font._renderPath(path); - - if (--snapDistance === -26) { - snapDistance = 67; - } -} - -function doSnap(path, dist) { - var i, - value = dist <= 0 ? 1 : dist; - - for (i = 0; i < path.commands.length; i++) { - var cmd = path.commands[i]; - if (cmd.type !== 'Z') { - cmd.x = snap(cmd.x, value); - cmd.y = snap(cmd.y, value); - } - if (cmd.type === 'Q' || cmd.type === 'C') { - cmd.x1 = snap(cmd.x1, value); - cmd.y1 = snap(cmd.y1, value); - } - if (cmd.type === 'C') { - cmd.x2 = snap(cmd.x2, value); - cmd.y2 = snap(cmd.y2, value); - } - } -} - -// Round a value to the nearest "step". -function snap(v, distance) { - return Math.round(v / distance) * distance; -} diff --git a/test/manual-test-examples/p5.Font/pathpoints/boids.js b/test/manual-test-examples/p5.Font/pathpoints/boids.js deleted file mode 100644 index c868aa9c33..0000000000 --- a/test/manual-test-examples/p5.Font/pathpoints/boids.js +++ /dev/null @@ -1,244 +0,0 @@ -// adapted from Shiffman's The Nature of Code -// http://natureofcode.com - -class Boid { - constructor(target) { - this.acceleration = createVector(0, 0); - this.velocity = createVector(random(-1, 1), random(-1, 1)); - this.position = createVector(width / 2, height / 2); - - this.r = 3.0; - this.maxspeed = 3; // Maximum speed - this.maxforce = 0.05; // Maximum steering force - - this.theta = - p5.Vector.fromAngle(radians(target.alpha)).heading() + radians(90); - this.target = createVector(target.x, target.y); - this.arrived = false; - this.hidden = true; - } - place (x, y) { - this.position = createVector(mouseX, mouseY); - this.velocity = p5.Vector.sub( - createVector(mouseX, mouseY), - createVector(pmouseX, pmouseY) - ); - this.hidden = false; - } - - run (boids) { - if (this.hidden) - return; - - if (flock.assemble) { - this.arrive(this.target); - } else { - this.flock(boids); - } - this.update(); - this.borders(); - this.render(); - } - - applyForce (force) { - // We could add mass here if we want A = F / M - this.acceleration.add(force); - } - - // We accumulate a new acceleration each time based on three rules - flock (boids) { - var sep = this.separate(boids); // Separation - var ali = this.align(boids); // Alignment - var coh = this.cohesion(boids); // Cohesion - - // Arbitrarily weight these forces - sep.mult(1.5); - ali.mult(1.0); - coh.mult(1.0); - // Add the force vectors to acceleration - this.applyForce(sep); - this.applyForce(ali); - this.applyForce(coh); - } - - // Method to update location - update () { - if (flock.assemble && - !this.arrived && - this.target.dist(this.position) < 1) { - this.arrived = true; - this.velocity = p5.Vector.fromAngle(this.theta + radians(90)); - } else { - this.velocity.add(this.acceleration); - this.velocity.limit(this.maxspeed); - this.position.add(this.velocity); - this.acceleration.mult(0); - } - } - - seek (target) { - var desired = p5.Vector.sub(target, this.position); - // Normalize desired and scale to maximum speed - desired.normalize(); - desired.mult(this.maxspeed); - // Steering = Desired minus Velocity - var steer = p5.Vector.sub(desired, this.velocity); - steer.limit(this.maxforce); // Limit to maximum steering force - return steer; - } - - render () { - // Draw a triangle rotated in the direction of velocity - var theta = this.velocity.heading() + radians(90); - fill(255); - noStroke(); - push(); - translate(this.position.x, this.position.y); - rotate(theta); - beginShape(); - vertex(0, -this.r * 2); - vertex(-this.r, this.r * 2); - vertex(this.r, this.r * 2); - endShape(CLOSE); - pop(); - } - - // Wraparound - borders () { - if (this.position.x < -this.r) - this.position.x = width + this.r; - if (this.position.y < -this.r) - this.position.y = height + this.r; - if (this.position.x > width + this.r) - this.position.x = -this.r; - if (this.position.y > height + this.r) - this.position.y = -this.r; - } - - // Separation - // Method checks for nearby boids and steers away - separate (boids) { - var desiredseparation = 25.0; - var steer = createVector(0, 0); - var count = 0; - // For every boid in the system, check if it's too close - for (var i = 0; i < boids.length; i++) { - var d = p5.Vector.dist(this.position, boids[i].position); - // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself) - if (d > 0 && d < desiredseparation) { - // Calculate vector pointing away from neighbor - var diff = p5.Vector.sub(this.position, boids[i].position); - diff.normalize(); - diff.div(d); // Weight by distance - steer.add(diff); - count++; // Keep track of how many - } - } - // Average -- divide by how many - if (count > 0) { - steer.div(count); - } - - // As long as the vector is greater than 0 - if (steer.mag() > 0) { - // Implement Reynolds: Steering = Desired - Velocity - steer.normalize(); - steer.mult(this.maxspeed); - steer.sub(this.velocity); - steer.limit(this.maxforce); - } - return steer; - } - - // Alignment - // For every nearby boid in the system, calculate the average velocity - align (boids) { - var neighbordist = 50; - var sum = createVector(0, 0); - var count = 0; - for (var i = 0; i < boids.length; i++) { - var d = p5.Vector.dist(this.position, boids[i].position); - if (d > 0 && d < neighbordist) { - sum.add(boids[i].velocity); - count++; - } - } - if (count > 0) { - sum.div(count); - sum.normalize(); - sum.mult(this.maxspeed); - var steer = p5.Vector.sub(sum, this.velocity); - steer.limit(this.maxforce); - return steer; - } else { - return createVector(0, 0); - } - } - - // Cohesion - // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location - cohesion (boids) { - var neighbordist = 50; - var sum = createVector(0, 0); // Start with empty vector to accumulate all locations - var num = 0; - for (var i = 0; i < boids.length; i++) { - var d = p5.Vector.dist(this.position, boids[i].position); - if (d > 0 && d < neighbordist) { - sum.add(boids[i].position); // Add location - num++; - } - } - if (num > 0) { - return this.seek(sum.div(num)); // Steer towards the location - } else { - return createVector(0, 0); - } - } - - arrive (target) { - // A vector pointing from the location to the target - var desired = p5.Vector.sub(target, this.position), d = desired.mag(); - - // Scale with arbitrary damping within 100 pixels - desired.setMag(d < 100 ? map(d, 0, 100, 0, this.maxspeed) : this.maxspeed); - - // Steering = Desired minus Velocity - var steer = p5.Vector.sub(desired, this.velocity); - steer.limit(this.maxforce); // Limit to maximum steering force - this.applyForce(steer); - } - -} - -function mouseOnScreen() { - return mouseX && mouseX <= width && mouseY && mouseY <= height; -} - -class Flock { - constructor() { - this.count = 0; - this.boids = []; - this.assemble = false; - } - arrived() { - var i; - if (arguments.length) { - for (i = 0; i < this.boids.length; i++) - this.boids[i].arrived = arguments[0]; - if (!arguments[0]) this.count = 0; - } else { - for (i = 0; i < this.boids.length; i++) - if (!this.boids[i].arrived) return false; - return true; - } - } - - run() { - this.assemble = this.count === flock.boids.length; - - if (!this.assemble && mouseOnScreen()) - this.boids[this.count++].place(mouseX, mouseY); - - for (var i = 0; i < this.boids.length; i++) this.boids[i].run(this.boids); - } -} diff --git a/test/manual-test-examples/p5.Font/pathpoints/index.html b/test/manual-test-examples/p5.Font/pathpoints/index.html deleted file mode 100644 index 8eb7c4bc0c..0000000000 --- a/test/manual-test-examples/p5.Font/pathpoints/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/test/manual-test-examples/p5.Font/pathpoints/sketch.js b/test/manual-test-examples/p5.Font/pathpoints/sketch.js deleted file mode 100644 index da989e3b79..0000000000 --- a/test/manual-test-examples/p5.Font/pathpoints/sketch.js +++ /dev/null @@ -1,27 +0,0 @@ -// Adapted from Dan Shiffman's 'The Nature -// of Code': http://natureofcode.com - -var flock; - -function setup() { - createCanvas(500, 300); - - flock = new Flock(); - - loadFont('../opentype/AvenirNextLTPro-Demi.otf', function(f) { - var points = f.textToPoints('p5.js', 80, 185, 150); - for (var k = 0; k < points.length; k++) { - flock.boids.push(new Boid(points[k])); - } - }); -} - -function draw() { - var c = flock.count / flock.boids.length; - background(c * 237, 34, 93); - flock.run(); -} - -function mouseReleased() { - if (flock.arrived()) flock.arrived(false); -} diff --git a/test/manual-test-examples/p5.Font/renderpath/index.html b/test/manual-test-examples/p5.Font/renderpath/index.html deleted file mode 100644 index 37545f805b..0000000000 --- a/test/manual-test-examples/p5.Font/renderpath/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/test/manual-test-examples/p5.Font/renderpath/sketch.js b/test/manual-test-examples/p5.Font/renderpath/sketch.js deleted file mode 100644 index 334c6c8da2..0000000000 --- a/test/manual-test-examples/p5.Font/renderpath/sketch.js +++ /dev/null @@ -1,35 +0,0 @@ -function setup() { - var txt = 'Default Text', - x = 190; - - createCanvas(200, 150); - - line(190, 0, 190, height); - - textAlign(RIGHT); - textSize(32); - text(txt, x, 30); - - loadFont('../SourceSansPro-Regular.otf', function(font) { - text(txt, x, 60); - - textSize(35); // not aligning correctly (ignore alignment or fix) - var path = font._getPath(txt, x, 90); - font._renderPath(path); - - textFont(font); - text(txt, x, 120); - - x = 20; - textSize(20); - textAlign(LEFT); - - var td = x + font._textWidth('space'); - var tw = font._textWidth(' '); - - text('space width: ' + tw.toFixed(2) + 'px', x, 140); - - line(td, 145, td, 145 - 22); - line(td + tw, 145, td + tw, 145 - 22); - }); -} diff --git a/test/manual-test-examples/p5.Font/simple/index.html b/test/manual-test-examples/p5.Font/simple/index.html deleted file mode 100644 index 184edd329e..0000000000 --- a/test/manual-test-examples/p5.Font/simple/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - -
- Note: (tight) bounds for a text string may start to the right of the its x-position (blue line)
-

 
- Issue #1958: -

 
- - Issue #5181: -

 
- - diff --git a/test/manual-test-examples/p5.Font/simple/sketch.js b/test/manual-test-examples/p5.Font/simple/sketch.js deleted file mode 100644 index 7adf603693..0000000000 --- a/test/manual-test-examples/p5.Font/simple/sketch.js +++ /dev/null @@ -1,198 +0,0 @@ -var _setup = function(p, font) { - var txt, - tb, - tw, - x = 20, - y = 50; - - p.createCanvas(240, 160); - p.textFont(font); - p.textSize(20); - - p.stroke('blue'); - p.line(x, 0, x, p.height); - - txt = ' leading space'; - tb = font.textBounds(txt, x, y); - tw = p.textWidth(txt); - p.stroke('black'); - p.rect(tb.x, tb.y, tb.w, tb.h); - p.noStroke(); - p.text(txt, x, y); - p.stroke('red'); - p.line(x, y + 6, x + tw, y + 6); - - y = 80; - txt = 'traction waste'; - tb = font.textBounds(txt, x, y); - tw = p.textWidth(txt); - p.stroke('black'); - p.rect(tb.x, tb.y, tb.w, tb.h); - p.noStroke(); - p.text(txt, x, y); - p.stroke('red'); - p.line(x, y + 6, x + tw, y + 6); - - y = 110; - txt = 'trailing space '; - tb = font.textBounds(txt, x, y); - tw = p.textWidth(txt); - p.stroke('black'); - p.rect(tb.x, tb.y, tb.w, tb.h); - p.noStroke(); - p.text(txt, x, y); - p.stroke('red'); - p.line(x, y + 6, x + tw, y + 6); - - y = 140; - txt = ' '; - tb = font.textBounds(txt, x, y); - tw = p.textWidth(txt); - p.stroke('black'); - p.rect(tb.x, tb.y, tb.w, p.max(tb.h, 3)); - p.noStroke(); - p.text(txt, x, y); - p.stroke('red'); - p.line(x, y + 6, x + tw, y + 6); -}; - -var textSketch = function(p) { - p.setup = function() { - p.loadFont('../acmesa.ttf', function(f) { - _setup(p, f); - }); - }; -}; - -var textSketchMono = function(p) { - p.setup = function() { - p.loadFont('../AndaleMono.ttf', function(f) { - _setup(p, f); - }); - }; -}; - -var textSketch1958 = function(p) { - // issue #1958 - var font, - lineW, - words = 'swimming back to the rock'; - - p.preload = function() { - font = p.loadFont('../OpenSans-Regular.ttf'); - }; - - p.setup = function() { - function textAsWords(words, x, y) { - var tw, - spaceW = p.textWidth(' '); - //console.log(spaceW); - for (var i = 0; i < words.length; i++) { - if (i !== 0) { - tw = p.textWidth(words[i - 1]); - x += tw + spaceW; - p.stroke(0); - p.noFill(); - p.rect(x - spaceW, y + 5, spaceW, -25); - } - p.fill(0); - p.noStroke(); - p.text(words[i], x, y); - } - } - - p.createCanvas(300, 200); - p.background(255); - - p.textSize(20); // Case 1: Default font - p.noStroke(); - p.text(words, 20, 50); - textAsWords(words.split(' '), 20, 80); - - p.stroke(255, 0, 0); - p.line(20, 0, 20, p.height); - - lineW = p.textWidth(words); - p.line(20 + lineW, 0, 20 + lineW, 90); - - p.textFont(font, 20); // Case 2: OpenSans - p.noStroke(); - p.text(words, 20, 120); - textAsWords(words.split(' '), 20, 150); - - p.stroke(255, 0, 0); - lineW = p.textWidth(words); - p.line(20 + lineW, 100, 20 + lineW, p.height - 20); - - p.stroke(0); - p.line(20, 160, 20 + p.textWidth(' '), 160); - }; -}; - -/*var textSketch1957 = function(p) { // issue #1957 - var font; - p.preload = function() { - font = p.loadFont("../AndaleMono.ttf"); - }; - p.setup = function() { - - p.createCanvas(300, 400); - p.textFont(font, 80); - - p.text("a", 0, 100); - p.text("b", 0, 200); - p.text("c", 0, 300); - - p.stroke(255,0,0); - p.line(p.textWidth("a"), 0, p.textWidth("a"), p.height); - p.line(p.textWidth("b"), 0, p.textWidth("b"), p.height); - p.line(p.textWidth("c"), 0, p.textWidth("c"), p.height); - p.noStroke(); - p.textSize(10); - p.text("a="+p.textWidth("a")+" b="+p.textWidth("b")+" c="+p.textWidth("c"), 10, 350); - console.log(font); - } -} -new p5(textSketch1957, "textSketch1957");*/ - -var textSketch5181 = function(p) { - // issue #5181 - var font, - txt = - "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged."; - - p.preload = function() { - font = p.loadFont('../OpenSans-Regular.ttf'); - }; - - p.setup = function() { - p.createCanvas(300, 700); - p.background(255); - - let bounds = [20, 10, 250, 130]; - p.textFont(font, 12); - p.rect(...bounds); - p.text('Default Size/Lead (12/15): ' + txt, ...bounds); - - bounds = [20, 150, 250, 230]; - p.textFont(font, 16); - p.rect(...bounds); - p.text('Default Size/Lead (16/20): ' + txt, ...bounds); - - bounds = [20, 390, 250, 105]; - p.textLeading(12); - p.textFont(font, 12); - p.rect(...bounds); - p.text('User-set Size/Lead (12/12): ' + txt, ...bounds); - - bounds = [20, 505, 250, 185]; - p.textFont(font, 20); - p.rect(...bounds); - p.text('Maintain Custom Leading (20/12): ' + txt, ...bounds); - }; -}; - -new p5(textSketch, 'textSketch'); -new p5(textSketchMono, 'textSketchMono'); -new p5(textSketch1958, 'textSketch1958'); -new p5(textSketch5181, 'textSketch5181'); diff --git a/test/manual-test-examples/p5.Font/style/index.html b/test/manual-test-examples/p5.Font/style/index.html deleted file mode 100644 index 35938afb91..0000000000 --- a/test/manual-test-examples/p5.Font/style/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/test/manual-test-examples/p5.Font/style/sketch.js b/test/manual-test-examples/p5.Font/style/sketch.js deleted file mode 100644 index adba674b10..0000000000 --- a/test/manual-test-examples/p5.Font/style/sketch.js +++ /dev/null @@ -1,10 +0,0 @@ -var font; - -function preload() { - font = loadFont('../acmesa.ttf'); -} - -function setup() { - var myDiv = createDiv('hello there'); - myDiv.style('font-family', 'acmesa'); -} diff --git a/test/manual-test-examples/p5.Font/svgpath/index.html b/test/manual-test-examples/p5.Font/svgpath/index.html deleted file mode 100644 index 02623e5b84..0000000000 --- a/test/manual-test-examples/p5.Font/svgpath/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/test/manual-test-examples/p5.Font/svgpath/sketch.js b/test/manual-test-examples/p5.Font/svgpath/sketch.js deleted file mode 100644 index c6e669ed7e..0000000000 --- a/test/manual-test-examples/p5.Font/svgpath/sketch.js +++ /dev/null @@ -1,40 +0,0 @@ -function setup() { - var canvas = createCanvas(200, 150).canvas; - - loadFont('../SourceSansPro-Regular.otf', function(font) { - // render text with opentype font - textAlign(RIGHT); - textFont(font, 32); - text('Text Path', 190, 60); - - // converted path to canvas Path2D then render - var pathStr = font._getPathData('Path Data', 190, 90); - var cPath = new Path2D(pathStr); - canvas.getContext('2d').fill(cPath); - - // not displayed, just print tag in console - var pathTag = font._getSVG('SVG', 190, 120, { - decimals: 4, - fill: 'red', - strokeWidth: 2, - stroke: 'green' - }); - //console.log(pathTag); - - // hit detection for canvas Path2D (cursor changes) - canvas.onmousemove = function(e) { - var context = e.target.getContext('2d'); - var coordX = e.offsetX; - var coordY = e.offsetY; - - // Test the square for clicks - if (context.isPointInPath(cPath, coordX, coordY)) { - e.target.style.cursor = 'pointer'; - return; - } - - // Reset the pointer to the default - e.target.style.cursor = 'default'; - }; - }); -} diff --git a/test/manual-test-examples/p5.Font/system/LEFT.BL.lead.png b/test/manual-test-examples/p5.Font/system/LEFT.BL.lead.png deleted file mode 100644 index 80ec65164d..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/LEFT.BL.lead.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/LEFT.BOTTOM.lead.png b/test/manual-test-examples/p5.Font/system/LEFT.BOTTOM.lead.png deleted file mode 100644 index e953cdbcc8..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/LEFT.BOTTOM.lead.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/LEFT.CENTER.lead.png b/test/manual-test-examples/p5.Font/system/LEFT.CENTER.lead.png deleted file mode 100644 index c970803170..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/LEFT.CENTER.lead.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/LEFT.TOP.lead.png b/test/manual-test-examples/p5.Font/system/LEFT.TOP.lead.png deleted file mode 100644 index 2479e78dca..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/LEFT.TOP.lead.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/index.html b/test/manual-test-examples/p5.Font/system/index.html deleted file mode 100755 index d6e618d600..0000000000 --- a/test/manual-test-examples/p5.Font/system/index.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - -
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - diff --git a/test/manual-test-examples/p5.Font/system/sketch.js b/test/manual-test-examples/p5.Font/system/sketch.js deleted file mode 100755 index 726abfc2bf..0000000000 --- a/test/manual-test-examples/p5.Font/system/sketch.js +++ /dev/null @@ -1,736 +0,0 @@ -var textSketch = function(p) { - p.setup = function() { - p.createCanvas(240, 160); - p.background(204); - p.textSize(19.5); - p.fill(255); - p.text('Default Text', 10, 30); - p.noStroke(); - p.fill(57, 112, 155); - p.text('Black No Stroke Text', 10, 60); - //p.textStyle(p.ITALIC); - //p.text('Blue No Stroke Text Italic', 10, 80); - //p.textStyle(p.BOLD); - //p.text('Blue No Stroke Text Bold', 10, 100); - p - .fill(120) - .textStyle(p.NORMAL) - .textSize(13.5) - .text( - 'Simple long Text: Lorem Ipsum is simply dummy text of the printing and typesetting industry. ', - 10, - 92, - 220, - 60 - ); - }; -}; - -var textLineSketch = function(p) { - p.setup = function() { - p.createCanvas(240 * 4, 160); - p.textSize(10); - p.stroke(0); - //1 - p.fill(255); - p.strokeWeight(1); - p.line(10, 10, 220, 10); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.TOP); - p.text('LEFT TOP is simply dummy text.', 10, 10); - //2 - p.fill(255); - p.strokeWeight(1); - p.line(10, 60, 220, 60); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.TOP); - p.text('CENTER TOP is simply dummy text.', 10, 60); - //3 - p.fill(255); - p.strokeWeight(1); - p.line(10, 110, 220, 110); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.TOP); - p.text('RIGHT TOP is simply dummy text.', 10, 110); - - //1 - p.fill(255); - p.strokeWeight(1); - p.line(250, 10, 470, 10); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.CENTER); - p.text('LEFT CENTER is simply dummy text.', 250, 10); - //2 - p.fill(255); - p.strokeWeight(1); - p.line(250, 60, 470, 60); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.CENTER); - p.text('CENTER CENTER is simply dummy text.', 250, 60); - //3 - p.fill(255); - p.strokeWeight(1); - p.line(250, 110, 470, 110); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.CENTER); - p.text('RIGHT CENTER is simply dummy text.', 250, 110); - - //1 - p.fill(255); - p.strokeWeight(1); - p.line(490, 10, 710, 10); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.BOTTOM); - p.text('LEFT BOTTOM is simply dummy text.', 490, 10); - //2 - p.fill(255); - p.strokeWeight(1); - p.line(490, 60, 710, 60); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.BOTTOM); - p.text('CENTER BOTTOM is simply dummy text.', 490, 60); - //3 - p.fill(255); - p.strokeWeight(1); - p.line(490, 110, 710, 110); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.BOTTOM); - p.text('RIGHT BOTTOM is simply dummy text.', 490, 110); - - //1 - p.fill(255); - p.strokeWeight(1); - p.line(730, 10, 950, 10); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.BASELINE); - p.text('LEFT BASELINE is simply dummy text.', 730, 10); - //2 - p.fill(255); - p.strokeWeight(1); - p.line(730, 60, 950, 60); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.BASELINE); - p.text('CENTER BASELINE is simply dummy text.', 730, 60); - //3 - p.fill(255); - p.strokeWeight(1); - p.line(730, 110, 950, 110); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.BASELINE); - p.text('RIGHT BASELINE is simply dummy text.', 730, 110); - }; -}; - -var textWrapSketch = function(p) { - p.setup = function() { - p.createCanvas(240 * 4, 160); - p.textSize(10); - p.stroke(0); - //1 - p.fill(255); - p.strokeWeight(1); - p.rect(10, 10, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.TOP); - p.text( - 'LEFT TOP is simply dummy text of the printing and typesetting industry. ', - 10, - 10, - 220, - 40 - ); - //2 - p.fill(255); - p.strokeWeight(1); - p.rect(10, 60, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.TOP); - p.text( - 'CENTER TOP is simply dummy text of the printing and typesetting industry. ', - 10, - 60, - 220, - 40 - ); - //3 - p.fill(255); - p.strokeWeight(1); - p.rect(10, 110, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.TOP); - p.text( - 'RIGHT TOP is simply dummy text of the printing and typesetting industry. ', - 10, - 110, - 220, - 40 - ); - - //1 - p.fill(255); - p.strokeWeight(1); - p.rect(250, 10, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.CENTER); - p.text( - 'LEFT CENTER is simply dummy text of the printing and typesetting industry. ', - 250, - 10, - 220, - 40 - ); - //2 - p.fill(255); - p.strokeWeight(1); - p.rect(250, 60, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.CENTER); - p.text( - 'CENTER CENTER is simply dummy text of the printing and typesetting industry. ', - 250, - 60, - 220, - 40 - ); - //3 - p.fill(255); - p.strokeWeight(1); - p.rect(250, 110, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.CENTER); - p.text( - 'RIGHT CENTER is simply dummy text of the printing and typesetting industry. ', - 250, - 110, - 220, - 40 - ); - - //1 - p.fill(255); - p.strokeWeight(1); - p.rect(490, 10, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.BOTTOM); - p.text( - 'LEFT BOTTOM is simply dummy text of the printing and typesetting industry. ', - 490, - 10, - 220, - 40 - ); - //2 - p.fill(255); - p.strokeWeight(1); - p.rect(490, 60, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.BOTTOM); - p.text( - 'CENTER BOTTOM is simply dummy text of the printing and typesetting industry. ', - 490, - 60, - 220, - 40 - ); - //3 - p.fill(255); - p.strokeWeight(1); - p.rect(490, 110, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.BOTTOM); - p.text( - 'RIGHT BOTTOM is simply dummy text of the printing and typesetting industry. ', - 490, - 110, - 220, - 40 - ); - - //1 - p.fill(255); - p.strokeWeight(1); - p.rect(730, 10, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.LEFT, p.BASELINE); - p.text( - 'LEFT BASELINE is simply dummy text of the printing and typesetting industry. ', - 730, - 10, - 220, - 40 - ); - //2 - p.fill(255); - p.strokeWeight(1); - p.rect(730, 60, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.CENTER, p.BASELINE); - p.text( - 'CENTER BASELINE is simply dummy text of the printing and typesetting industry. ', - 730, - 60, - 220, - 40 - ); - //3 - p.fill(255); - p.strokeWeight(1); - p.rect(730, 110, 220, 40); - p.strokeWeight(0); - p.fill(0); - p.textAlign(p.RIGHT, p.BASELINE); - p.text( - 'RIGHT BASELINE is simply dummy text of the printing and typesetting industry. ', - 730, - 110, - 220, - 40 - ); - }; -}; - -var textFontSketch = function(p) { - p.setup = function() { - p.createCanvas(240, 160); - p.textSize(20); - p.fill(0); - p.strokeWeight(0); - p.textFont('times'); - p.text('Times Font', 10, 30); - p.textFont('arial'); - p.text('Arial Font', 10, 60); - p.textFont('Courier'); - p.text('Courier Font', 10, 90); - }; -}; - -var textAlignSketch = function(p) { - p.setup = function() { - p.createCanvas(240, 160); - p.fill(0); - p.strokeWeight(0); - p.textSize(12); - p.textAlign(p.RIGHT, p.TOP); - p.text('Top Right', 120, 30); - p.textAlign(p.CENTER, p.CENTER); - p.text('Center Center', 120, 60); - p.textAlign(p.LEFT, p.BOTTOM); - p.text('Left Bottom', 120, 90); - p.textAlign(p.RIGHT, p.BASELINE); - p.text('Right Baseline', 120, 90); - p.strokeWeight(1); - p.line(120, 0, 120, 160); - }; -}; - -var textAllAlignmentsSketch = function(p) { - var systemFonts = ['Arial', 'Times New Roman', 'Consolas']; - var font1, font2, font3; - var hAligns = [p.LEFT, p.CENTER, p.RIGHT]; - var vAligns = [p.TOP, p.CENTER, p.BASELINE, p.BOTTOM]; - var padding = 10; - var drawFontAlignments = function(font, textString, xOff, yOff) { - p.textFont(font); - p.textSize(20); - for (var h = 0; h < hAligns.length; h += 1) { - for (var v = 0; v < vAligns.length; v += 1) { - // Distribute words across the screen - var x = xOff + p.map(h, 0, hAligns.length - 1, padding, 400 - padding); - var y = yOff + p.map(v, 0, vAligns.length - 1, padding, 200 - padding); - - p.stroke(200); - p.line(0, y, p.width, y); - p.line(x, 0, x, p.height); - - // Align the text & calculate the bounds - p.textAlign(hAligns[h], vAligns[v]); - - // Draw the text - p.fill(255, 0, 0); - p.noStroke(); - p.text(textString, x, y); - - // Draw the (x, y) coordinates - p.stroke(0); - p.fill('#FF8132'); - p.ellipse(x, y, 3, 3); - } - } - }; - p.setup = function() { - var renderer = p.createCanvas(400, 600); - renderer.elt.style.position = 'absolute'; - renderer.elt.style.top = '0'; - renderer.elt.style.left = '0'; - drawFontAlignments(systemFonts[0], 'Arial', 0, 0); - drawFontAlignments(systemFonts[1], 'Times', 0, 200); - drawFontAlignments(systemFonts[2], 'Consolas', 0, 400); - // These proprietary fonts aren't included in the repo! - // p.loadFont("../arial.ttf", function(font) {drawFontAlignments(font, "Arial", 0, 0)}); - // p.loadFont("../times.ttf", function(font) {drawFontAlignments(font, "Times", 0, 200)}); - // p.loadFont("../consola.ttf", function(font) {drawFontAlignments(font, "Consolas", 0, 400)}); - }; -}; - -var textLeadingSketch = function(p) { - p.setup = function() { - p.createCanvas(400, 200); - p.textFont('Arial'); - p.fill(0); - p.textSize(12); - - p.line(0, 100, p.width, 100); - p.textAlign(p.LEFT, p.TOP); - p.strokeWeight(0); - - var s10 = 'LEFT/TOP@10px', - s20 = s10.replace('1', '2'), - s30 = s10.replace('1', '3'); - - p.textLeading(10); // Set leading to 10 - p.text(s10 + '\n' + s10 + '\n' + s10, 10, 100); - p.textLeading(20); // Set leading to 20 - p.text(s20 + '\n' + s20 + '\n' + s20, 140, 100); - p.textLeading(30); // Set leading to 30 - p.text(s30 + '\n' + s30 + '\n' + s30, 270, 100); - }; -}; - -var textLeadingSketch2 = function(p) { - p.setup = function() { - p.createCanvas(400, 200); - p.textFont('Arial'); - p.fill(0); - p.textSize(12); - - p.line(0, 100, p.width, 100); - p.textAlign(p.LEFT, p.CENTER); - p.strokeWeight(0); - - var s10 = 'LEFT/CENTER@10px', - s20 = s10.replace('1', '2'), - s30 = s10.replace('1', '3'); - - p.textLeading(10); // Set leading to 10 - p.text(s10 + '\n' + s10 + '\n' + s10, 10, 100); - p.textLeading(20); // Set leading to 20 - p.text(s20 + '\n' + s20 + '\n' + s20, 140, 100); - p.textLeading(30); // Set leading to 30 - p.text(s30 + '\n' + s30 + '\n' + s30, 270, 100); - }; -}; - -var textLeadingSketch3 = function(p) { - p.setup = function() { - p.createCanvas(400, 200); - p.textFont('Arial'); - p.fill(0); - p.textSize(12); - - p.line(0, 100, p.width, 100); - p.textAlign(p.LEFT, p.BASELINE); - p.strokeWeight(0); - - var s10 = 'LEFT/BASELINE@10px', - s20 = s10.replace('1', '2'), - s30 = s10.replace('1', '3'); - - p.textLeading(10); // Set leading to 10 - p.text(s10 + '\n' + s10 + '\n' + s10, 10, 100); - p.textLeading(20); // Set leading to 20 - p.text(s20 + '\n' + s20 + '\n' + s20, 140, 100); - p.textLeading(30); // Set leading to 30 - p.text(s30 + '\n' + s30 + '\n' + s30, 270, 100); - }; -}; - -var textLeadingSketch4 = function(p) { - p.setup = function() { - p.createCanvas(400, 200); - p.textFont('Arial'); - p.fill(0); - p.textSize(12); - - p.line(0, 100, p.width, 100); - p.textAlign(p.LEFT, p.BOTTOM); - p.strokeWeight(0); - - var s10 = 'LEFT/BOTTOM@10px', - s20 = s10.replace('1', '2'), - s30 = s10.replace('1', '3'); - - p.textLeading(10); // Set leading to 10 - p.text(s10 + '\n' + s10 + '\n' + s10, 10, 100); - p.textLeading(20); // Set leading to 20 - p.text(s20 + '\n' + s20 + '\n' + s20, 140, 100); - p.textLeading(30); // Set leading to 30 - p.text(s30 + '\n' + s30 + '\n' + s30, 270, 100); - }; -}; - -var textSizeSketch = function(p) { - p.setup = function() { - p.createCanvas(240, 160); - p.fill(0); - p.strokeWeight(0); - p.textSize(12); - p.text('Font Size 12', 10, 30); - p.textSize(14); - p.text('Font Size 14', 10, 60); - p.textSize(16); - p.text('Font Size 16', 10, 90); - }; -}; - -var textStyleSketch = function(p) { - p.setup = function() { - p.createCanvas(240, 160); - p.fill(0); - p.strokeWeight(0); - p.textSize(12); - p.textStyle(p.NORMAL); - p.text('Font Style Normal', 10, 30); - p.textStyle(p.ITALIC); - p.text('Font Style Italic', 10, 60); - p.textStyle(p.BOLD); - p.text('Font Style Bold', 10, 90); - }; -}; - -var textWidthSketch = function(p) { - p.setup = function() { - p.createCanvas(240, 160); - p.fill(0); - p.strokeWeight(0); - p.textSize(12); - var s = "What's the width of this line?"; - var textWidth = p.textWidth(s); - p.text(s, 10, 30); - p.rect(10, 30, textWidth, 2); - p.text('width: ' + textWidth, 10, 60); - }; -}; - -var textOverlapSketch = function(p) { - p.setup = function() { - p.createCanvas(240, 160); - p.fill(0); - p.strokeWeight(0); - p.textSize(72); - p.fill(0, 160); // Black with low opacity - p.text('O', 0, 100); - p.text('V', 30, 100); - p.text('E', 60, 100); - p.text('R', 90, 100); - p.text('L', 120, 100); - p.text('A', 150, 100); - p.text('P', 180, 100); - }; -}; - -var textFlySketch = function(p) { - var x1 = 100; - var x2 = 0; - p.setup = function() { - p.createCanvas(240, 160); - p.fill(0); - p.textSize(48); - }; - p.draw = function() { - p.background(204); - p.text('Left', x1, 50); - p.text('Right', x2, 150); - x2 += 2.0; - if (x2 > 240) { - x2 = -100; - } - x1 -= 1.0; - if (x1 < -100) { - x1 = 240; - } - }; -}; - -var textFlickerSketch = function(p) { - p.setup = function() { - p.createCanvas(240, 160); - p.textSize(48); - p.noStroke(); - }; - p.draw = function() { - p.fill(204, 24); - p.rect(0, 0, p.width, p.height); - p.fill(0); - p.text('flicker Text', p.random(-100, 240), p.random(-20, 160)); - }; -}; - -var textFadeSketch = function(p) { - var opacity = 0; - var direction = 1; - p.setup = function() { - p.createCanvas(240, 160); - p.textSize(72); - p.noStroke(); - }; - p.draw = function() { - p.background(204); - opacity += 4 * direction; - if (opacity < 0 || opacity > 255) { - direction = -direction; - } - p.fill(0, opacity); - p.text('fade', 50, 100); - }; -}; - -var textRotateSketch = function(p) { - var angle = 0.0; - p.setup = function() { - p.createCanvas(240, 160); - p.textSize(24); - p.noStroke(); - p.fill(0); - }; - p.draw = function() { - p.background(204); - angle += 0.05; - p.push(); - p.translate(120, 80); - p.scale((p.cos(angle / 4.0) + 1.2) * 2.0); - p.rotate(angle); - p.text('Rotating', 0, 0); - p.pop(); - }; -}; - -var textGrowSketch = function(p) { - var angle = 0.0; - var str = 'GROW'; - p.setup = function() { - p.createCanvas(240, 160); - p.textSize(24); - p.noStroke(); - p.fill(0, 0, 0, 120); - }; - p.draw = function() { - p.background(204); - angle += 0.1; - for (var i = 0; i < str.length; i++) { - var c = p.sin(angle + i / p.PI); - p.textSize((c + 1.0) * 40 + 10); - p.text(str.charAt(i), i * 40 + 20, 100); - } - }; -}; - -var textAvoidSketch = function(p) { - p.setup = function() { - p.createCanvas(240, 160); - p.textSize(24); - p.noStroke(); - p.fill(0); - p.textAlign(p.CENTER); - }; - p.draw = function() { - p.background(204); - p.text('AVOID', p.width - p.mouseX, p.height - p.mouseY); - }; -}; - -var textBendSketch = function(p) { - var str = 'Flexibility'; - p.setup = function() { - p.createCanvas(240, 160); - p.textSize(30); - p.noStroke(); - p.fill(0); - }; - p.draw = function() { - p.background(204); - p.push(); - p.translate(0, 33); - for (var i = 0; i < str.length; i++) { - var angle = p.map(p.mouseX, 0, p.width, 0, p.PI / 8); - p.rotate(angle); - p.text(str[i], 20, 0); - p.translate(p.textWidth(str[i]) * 1.5, 0); - } - p.pop(); - }; -}; - -var typographyLetterSketch = function(p) { - var margin = 10; - var gap = 46; - var counter = 35; - p.setup = function() { - p.createCanvas(720, 320); - p.background(0); - p.textFont('Georgia'); - p.textSize(24); - p.textStyle(p.BOLD); - p.textAlign(p.CENTER, p.CENTER); - p.translate(margin * 4, margin * 4); - for (var y = 0; y < p.height - gap; y += gap) { - for (var x = 0; x < p.width - gap; x += gap) { - var letter = p.char(counter); - if (letter === 'P' || letter === '5') { - p.fill(255, 204, 0); - } else if (letter === 'J' || letter === 'S') { - p.fill(204, 0, 255); - } else { - p.fill(255); - } - p.text(letter, x, y); - counter++; - } - } - }; -}; - -new p5(textSketch, 'textSketch'); -new p5(textLineSketch, 'textLineSketch'); -new p5(textWrapSketch, 'textWrapSketch'); -new p5(textFontSketch, 'textFontSketch'); -new p5(textAlignSketch, 'textAlignSketch'); -new p5(textAllAlignmentsSketch, 'textAllAlignmentsSketch'); -new p5(textLeadingSketch, 'textLeadingSketch'); -new p5(textLeadingSketch2, 'textLeadingSketch2'); -new p5(textLeadingSketch3, 'textLeadingSketch3'); -new p5(textLeadingSketch4, 'textLeadingSketch4'); -new p5(textSizeSketch, 'textSizeSketch'); -new p5(textStyleSketch, 'textStyleSketch'); -new p5(textWidthSketch, 'textWidthSketch'); -new p5(textOverlapSketch, 'textOverlapSketch'); -new p5(textFlySketch, 'textFlySketch'); -new p5(textFlickerSketch, 'textFlickerSketch'); -new p5(textFadeSketch, 'textFadeSketch'); -new p5(textRotateSketch, 'textRotateSketch'); -new p5(textGrowSketch, 'textGrowSketch'); -new p5(textAvoidSketch, 'textAvoidSketch'); -new p5(textBendSketch, 'textBendSketch'); -new p5(typographyLetterSketch, 'typographyLetterSketch'); diff --git a/test/manual-test-examples/p5.Font/system/textAlignSketch.png b/test/manual-test-examples/p5.Font/system/textAlignSketch.png deleted file mode 100644 index 53f27cdaed..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/textAlignSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/textAllAlignmentsSketch.png b/test/manual-test-examples/p5.Font/system/textAllAlignmentsSketch.png deleted file mode 100644 index 5a3aa8558f..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/textAllAlignmentsSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/textLineSketch.png b/test/manual-test-examples/p5.Font/system/textLineSketch.png deleted file mode 100644 index 9189587cbf..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/textLineSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/textOverlapSketch.png b/test/manual-test-examples/p5.Font/system/textOverlapSketch.png deleted file mode 100644 index ca6a1ded2b..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/textOverlapSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/textSizeSketch.png b/test/manual-test-examples/p5.Font/system/textSizeSketch.png deleted file mode 100644 index 40a041e471..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/textSizeSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/textSketch.png b/test/manual-test-examples/p5.Font/system/textSketch.png deleted file mode 100644 index db3a943985..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/textSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/textWidthSketch.png b/test/manual-test-examples/p5.Font/system/textWidthSketch.png deleted file mode 100644 index fc1d0694c3..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/textWidthSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/system/textWrapSketch.png b/test/manual-test-examples/p5.Font/system/textWrapSketch.png deleted file mode 100644 index aaac7eb836..0000000000 Binary files a/test/manual-test-examples/p5.Font/system/textWrapSketch.png and /dev/null differ diff --git a/test/manual-test-examples/p5.Font/textAsWords/index.html b/test/manual-test-examples/p5.Font/textAsWords/index.html deleted file mode 100644 index 0ead77ae00..0000000000 --- a/test/manual-test-examples/p5.Font/textAsWords/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - 1. default
- 2. p5/loaded
- 3. opentype/loaded
-
- - - diff --git a/test/manual-test-examples/p5.Font/textAsWords/sketch.js b/test/manual-test-examples/p5.Font/textAsWords/sketch.js deleted file mode 100644 index 79875b85a0..0000000000 --- a/test/manual-test-examples/p5.Font/textAsWords/sketch.js +++ /dev/null @@ -1,85 +0,0 @@ -var font, - lineW, - words = 'swimming back to the rock'; - -function preload() { - font = loadFont('../Helvetica.ttf'); -} - -function setup() { - function textAsWords(words, x, y) { - var tw, - spaceW = textWidth(' '); - //console.log('space=' + spaceW); - for (var i = 0; i < words.length; i++) { - fill(0); - noStroke(); - text(words[i], x, y); - x += textWidth(words[i]); - //console.log(words[i] + '=' + x); - - if (i < words.length - 1) { - stroke(0); - noFill(); - rect(x, y + 5, spaceW, -25); - x += spaceW; - } - } - stroke(0, 0, 255); - line(x, y - 45, x, y + 5); - fill(0); - noStroke(); - } - - createCanvas(300, 280); - background(255); - - textSize(20); // Case 1: Default font - noStroke(); - //console.log('default'); - text(words, 20, 50); - textAsWords(words.split(' '), 20, 80); - - stroke(255, 0, 0); - line(20, 0, 20, height); - - textFont(font, 20); // Case 2: OpenSans - noStroke(); - //console.log('\np5/loaded'); - text(words, 20, 120); - - textAsWords(words.split(' '), 20, 150); - stroke(0); -} - -setTimeout(function() { - function _textAsWords(ctx, font, text, x, y, fontSize) { - var tw, - spaceW = font.getAdvanceWidth(' ', fontSize); - //console.log('space=' + spaceW); - - for (var i = 0; i < text.length; i++) { - var pth = font.getPath(text[i], x, y, fontSize); - pth.draw(ctx); - x += font.getAdvanceWidth(text[i], fontSize); - //console.log(text[i] + '=' + x); - if (i < text.length - 1) { - ctx.strokeRect(x, y + 5, spaceW, -25); - x += spaceW; - } - } - ctx.strokeStyle = '#00f'; - ctx.beginPath(); - ctx.moveTo(x, y - 45); - ctx.lineTo(x, y + 5); - ctx.stroke(); - } - - opentype.load('../Helvetica.ttf', function(err, font) { - if (err) throw 'Font could not be loaded: ' + err; - var ctx = document.getElementById('defaultCanvas0').getContext('2d'); - font.getPath(words, 20, 190, 20).draw(ctx); - //console.log('\nopentype/loaded'); - _textAsWords(ctx, font, words.split(' '), 20, 220, 20); - }); -}, 100); diff --git a/test/manual-test-examples/p5.Font/textInRect/index.html b/test/manual-test-examples/p5.Font/textInRect/index.html deleted file mode 100644 index 35938afb91..0000000000 --- a/test/manual-test-examples/p5.Font/textInRect/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/test/manual-test-examples/p5.Font/textInRect/sketch.js b/test/manual-test-examples/p5.Font/textInRect/sketch.js deleted file mode 100644 index d7d5fa0f49..0000000000 --- a/test/manual-test-examples/p5.Font/textInRect/sketch.js +++ /dev/null @@ -1,81 +0,0 @@ -let xpos = 50; -let ypos = 100; -let str = - 'One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve Thirteen Fourteen Fifteen Sixteen Seventeen Eighteen Nineteen Twenty Twenty-one Twenty-two Twenty-three Twenty-four Twenty-five Twenty-six Twenty-seven Twenty-eight Twenty-nine Thirty Thirty-one Thirty-two Thirty-three Thirty-four Thirty-five Thirty-six Thirty-seven Thirty-eight Thirty-nine Forty Forty-one Forty-two Forty-three Forty-four Forty-five Forty-six Forty-seven Forty-eight Forty-nine Fifty Fifty-one Fifty-two Fifty-three'; - -function setup() { - createCanvas(1050, 800); - background(245); - - let ta = textAscent(); - - textAlign(CENTER, TOP); - rect(xpos, ypos, 200, 200); - text(str, xpos, ypos, 200, 200); - xpos += 250; - - textAlign(CENTER, CENTER); - rect(xpos, ypos, 200, 200); - text(str, xpos, ypos, 200, 200); - xpos += 250; - - textAlign(CENTER, BOTTOM); - rect(xpos, ypos, 200, 200); - text(str, xpos, ypos, 200, 200); - xpos += 250; - - textAlign(CENTER, BASELINE); - rect(xpos, ypos, 200, 200); - text(str, xpos, ypos, 200, 200); - - textSize(18); - textAlign(CENTER, TOP); - text('TOP', 150, height / 2 - 40); - text('CENTER', 400, height / 2 - 40); - text('BOTTOM', 650, height / 2 - 40); - text('BASELINE', 900, height / 2 - 40); - textSize(12); - - xpos = 50; - ypos += 400; - - textAlign(CENTER, TOP); - rect(xpos, ypos, 200, 200); - text(str, xpos, ypos, 200); - xpos += 250; - - textAlign(CENTER, CENTER); - rect(xpos, ypos, 200, 200); - text(str, xpos, ypos, 200); - xpos += 250; - - textAlign(CENTER, BOTTOM); - rect(xpos, ypos, 200, 200); - text(str, xpos, ypos, 200); - xpos += 250; - - textAlign(CENTER, BASELINE); - rect(xpos, ypos, 200, 200); - text(str, xpos, ypos, 200); - - textSize(18); - textAlign(CENTER, TOP); - text('TOP', 150, height / 2 - 40); - text('CENTER', 400, height / 2 - 40); - text('BOTTOM', 650, height / 2 - 40); - text('BASELINE', 900, height / 2 - 40); - text('TOP', 150, ypos + 270); - text('CENTER', 400, ypos + 270); - text('BOTTOM', 650, ypos + 270); - text('BASELINE', 900, ypos + 270); - - fill(255); - noStroke(); - textSize(24); - - rect(0, height / 2, width, 15); - fill(0); - textAlign(LEFT, TOP); - text('text(s, x, y, w, h)', 20, 40); - text('text(s, x, y, w) [no height]', 20, height / 2 + 40); -} diff --git a/test/manual-test-examples/pixel/set-pixels.html b/test/manual-test-examples/pixel/set-pixels.html deleted file mode 100644 index ce9d5eb1f1..0000000000 --- a/test/manual-test-examples/pixel/set-pixels.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/test/manual-test-examples/pixel/set-pixels.js b/test/manual-test-examples/pixel/set-pixels.js deleted file mode 100644 index 5bb25845e4..0000000000 --- a/test/manual-test-examples/pixel/set-pixels.js +++ /dev/null @@ -1,23 +0,0 @@ -var img; -var radius = 60; -var smoothAmount; -var canvasImg; - -function preload() { - img = loadImage('unicorn.jpg'); // Load an image into the program -} - -function setup() { - createCanvas(256, 256); - loadPixels(); - set(width / 2, height / 2, img); -} - -function draw() { - for (var i = -5; i < 5; i++) { - set(mouseX + i, mouseY, [0, 0, 255, 100]); - } - set(mouseX, mouseY, [255, 0, 255, 255]); - set(mouseX + 10, mouseY + 10, 0); - updatePixels(); -} diff --git a/test/manual-test-examples/pixel/unicorn.jpg b/test/manual-test-examples/pixel/unicorn.jpg deleted file mode 100644 index 6c56624aca..0000000000 Binary files a/test/manual-test-examples/pixel/unicorn.jpg and /dev/null differ diff --git a/test/manual-test-examples/pixel/update-pixels.html b/test/manual-test-examples/pixel/update-pixels.html deleted file mode 100644 index 53736040b9..0000000000 --- a/test/manual-test-examples/pixel/update-pixels.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/test/manual-test-examples/pixel/update-pixels.js b/test/manual-test-examples/pixel/update-pixels.js deleted file mode 100644 index 8046250562..0000000000 --- a/test/manual-test-examples/pixel/update-pixels.js +++ /dev/null @@ -1,29 +0,0 @@ -var img; -var radius = 60; -var smoothAmount; -var canvasImg; - -function preload() { - img = loadImage('unicorn.jpg'); // Load an image into the program -} - -function setup() { - createCanvas(256, 256); - loadPixels(); -} - -function draw() { - for (var y = 0; y < height; y++) { - for (var x = 0; x < width; x++) { - if (pow(x - mouseX, 2) + pow(y - mouseY, 2) < pow(radius, 2)) { - var c = img.get(x, y); - set(x, y, c); - //pixels[4*(y*width+x)] = c[0]; - //pixels[4*(y*width+x)+1] = c[1]; - //pixels[4*(y*width+x)+2] = c[2]; - //pixels[4*(y*width+x)+3] = c[3]; - } - } - } - updatePixels(); -} diff --git a/test/manual-test-examples/p5.Font/acmesa.ttf b/test/manual-test-examples/type/font/Acmesa.ttf similarity index 100% rename from test/manual-test-examples/p5.Font/acmesa.ttf rename to test/manual-test-examples/type/font/Acmesa.ttf diff --git a/test/manual-test-examples/p5.Font/AndaleMono.ttf b/test/manual-test-examples/type/font/AndaleMono.ttf similarity index 100% rename from test/manual-test-examples/p5.Font/AndaleMono.ttf rename to test/manual-test-examples/type/font/AndaleMono.ttf diff --git a/test/manual-test-examples/type/font/BricolageGrotesque-Variable.ttf b/test/manual-test-examples/type/font/BricolageGrotesque-Variable.ttf new file mode 100644 index 0000000000..1c7c35e602 Binary files /dev/null and b/test/manual-test-examples/type/font/BricolageGrotesque-Variable.ttf differ diff --git a/test/manual-test-examples/p5.Font/FiraSans-Book.otf b/test/manual-test-examples/type/font/FiraSans-Book.otf similarity index 100% rename from test/manual-test-examples/p5.Font/FiraSans-Book.otf rename to test/manual-test-examples/type/font/FiraSans-Book.otf diff --git a/test/manual-test-examples/p5.Font/Lato-Black.ttf b/test/manual-test-examples/type/font/Lato-Black.ttf similarity index 100% rename from test/manual-test-examples/p5.Font/Lato-Black.ttf rename to test/manual-test-examples/type/font/Lato-Black.ttf diff --git a/test/manual-test-examples/type/font/LiberationSans-Bold.ttf b/test/manual-test-examples/type/font/LiberationSans-Bold.ttf new file mode 100644 index 0000000000..4581ebf3ee Binary files /dev/null and b/test/manual-test-examples/type/font/LiberationSans-Bold.ttf differ diff --git a/test/manual-test-examples/type/font/NotoNaskhArabic.woff2 b/test/manual-test-examples/type/font/NotoNaskhArabic.woff2 new file mode 100644 index 0000000000..5b28703a17 Binary files /dev/null and b/test/manual-test-examples/type/font/NotoNaskhArabic.woff2 differ diff --git a/test/manual-test-examples/p5.Font/PlayfairDisplay-Regular.ttf b/test/manual-test-examples/type/font/PlayfairDisplay.ttf similarity index 100% rename from test/manual-test-examples/p5.Font/PlayfairDisplay-Regular.ttf rename to test/manual-test-examples/type/font/PlayfairDisplay.ttf diff --git a/test/manual-test-examples/type/fontfaceobserver.standalone.js b/test/manual-test-examples/type/fontfaceobserver.standalone.js new file mode 100644 index 0000000000..3177ec6d94 --- /dev/null +++ b/test/manual-test-examples/type/fontfaceobserver.standalone.js @@ -0,0 +1,8 @@ +/* Font Face Observer v2.3.0 - © Bram Stein. License: BSD-3-Clause */(function(){function p(a,c){document.addEventListener?a.addEventListener("scroll",c,!1):a.attachEvent("scroll",c)}function u(a){document.body?a():document.addEventListener?document.addEventListener("DOMContentLoaded",function b(){document.removeEventListener("DOMContentLoaded",b);a()}):document.attachEvent("onreadystatechange",function g(){if("interactive"==document.readyState||"complete"==document.readyState)document.detachEvent("onreadystatechange",g),a()})};function w(a){this.g=document.createElement("div");this.g.setAttribute("aria-hidden","true");this.g.appendChild(document.createTextNode(a));this.h=document.createElement("span");this.i=document.createElement("span");this.m=document.createElement("span");this.j=document.createElement("span");this.l=-1;this.h.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.i.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;"; +this.j.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.m.style.cssText="display:inline-block;width:200%;height:200%;font-size:16px;max-width:none;";this.h.appendChild(this.m);this.i.appendChild(this.j);this.g.appendChild(this.h);this.g.appendChild(this.i)} +function x(a,c){a.g.style.cssText="max-width:none;min-width:20px;min-height:20px;display:inline-block;overflow:hidden;position:absolute;width:auto;margin:0;padding:0;top:-999px;white-space:nowrap;font-synthesis:none;font:"+c+";"}function B(a){var c=a.g.offsetWidth,b=c+100;a.j.style.width=b+"px";a.i.scrollLeft=b;a.h.scrollLeft=a.h.scrollWidth+100;return a.l!==c?(a.l=c,!0):!1}function C(a,c){function b(){var e=g;B(e)&&null!==e.g.parentNode&&c(e.l)}var g=a;p(a.h,b);p(a.i,b);B(a)};function D(a,c,b){c=c||{};b=b||window;this.family=a;this.style=c.style||"normal";this.weight=c.weight||"normal";this.stretch=c.stretch||"normal";this.context=b}var E=null,F=null,G=null,H=null;function I(a){null===F&&(M(a)&&/Apple/.test(window.navigator.vendor)?(a=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))(?:\.([0-9]+))/.exec(window.navigator.userAgent),F=!!a&&603>parseInt(a[1],10)):F=!1);return F}function M(a){null===H&&(H=!!a.document.fonts);return H} +function N(a,c){var b=a.style,g=a.weight;if(null===G){var e=document.createElement("div");try{e.style.font="condensed 100px sans-serif"}catch(q){}G=""!==e.style.font}return[b,g,G?a.stretch:"","100px",c].join(" ")} +D.prototype.load=function(a,c){var b=this,g=a||"BESbswy",e=0,q=c||3E3,J=(new Date).getTime();return new Promise(function(K,L){if(M(b.context)&&!I(b.context)){var O=new Promise(function(r,t){function h(){(new Date).getTime()-J>=q?t(Error(""+q+"ms timeout exceeded")):b.context.document.fonts.load(N(b,'"'+b.family+'"'),g).then(function(n){1<=n.length?r():setTimeout(h,25)},t)}h()}),P=new Promise(function(r,t){e=setTimeout(function(){t(Error(""+q+"ms timeout exceeded"))},q)});Promise.race([P,O]).then(function(){clearTimeout(e); +K(b)},L)}else u(function(){function r(){var d;if(d=-1!=k&&-1!=l||-1!=k&&-1!=m||-1!=l&&-1!=m)(d=k!=l&&k!=m&&l!=m)||(null===E&&(d=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent),E=!!d&&(536>parseInt(d[1],10)||536===parseInt(d[1],10)&&11>=parseInt(d[2],10))),d=E&&(k==y&&l==y&&m==y||k==z&&l==z&&m==z||k==A&&l==A&&m==A)),d=!d;d&&(null!==f.parentNode&&f.parentNode.removeChild(f),clearTimeout(e),K(b))}function t(){if((new Date).getTime()-J>=q)null!==f.parentNode&&f.parentNode.removeChild(f), +L(Error(""+q+"ms timeout exceeded"));else{var d=b.context.document.hidden;if(!0===d||void 0===d)k=h.g.offsetWidth,l=n.g.offsetWidth,m=v.g.offsetWidth,r();e=setTimeout(t,50)}}var h=new w(g),n=new w(g),v=new w(g),k=-1,l=-1,m=-1,y=-1,z=-1,A=-1,f=document.createElement("div");f.dir="ltr";x(h,N(b,"sans-serif"));x(n,N(b,"serif"));x(v,N(b,"monospace"));f.appendChild(h.g);f.appendChild(n.g);f.appendChild(v.g);b.context.document.body.appendChild(f);y=h.g.offsetWidth;z=n.g.offsetWidth;A=v.g.offsetWidth;t(); +C(h,function(d){k=d;r()});x(h,N(b,'"'+b.family+'",sans-serif'));C(n,function(d){l=d;r()});x(n,N(b,'"'+b.family+'",serif'));C(v,function(d){m=d;r()});x(v,N(b,'"'+b.family+'",monospace'))})})};"object"===typeof module?module.exports=D:(window.FontFaceObserver=D,window.FontFaceObserver.prototype.load=D.prototype.load);}()); diff --git a/test/manual-test-examples/type/img/LEFT.BL.lead.png b/test/manual-test-examples/type/img/LEFT.BL.lead.png new file mode 100644 index 0000000000..5169f1aa06 Binary files /dev/null and b/test/manual-test-examples/type/img/LEFT.BL.lead.png differ diff --git a/test/manual-test-examples/type/img/LEFT.BOTTOM.lead.png b/test/manual-test-examples/type/img/LEFT.BOTTOM.lead.png new file mode 100644 index 0000000000..f2f979214a Binary files /dev/null and b/test/manual-test-examples/type/img/LEFT.BOTTOM.lead.png differ diff --git a/test/manual-test-examples/type/img/LEFT.CENTER.lead.png b/test/manual-test-examples/type/img/LEFT.CENTER.lead.png new file mode 100644 index 0000000000..6e508183e5 Binary files /dev/null and b/test/manual-test-examples/type/img/LEFT.CENTER.lead.png differ diff --git a/test/manual-test-examples/type/img/LEFT.TOP.lead.png b/test/manual-test-examples/type/img/LEFT.TOP.lead.png new file mode 100644 index 0000000000..eb55c09604 Binary files /dev/null and b/test/manual-test-examples/type/img/LEFT.TOP.lead.png differ diff --git a/test/manual-test-examples/type/img/boundingBoxBreaks.p5.jpg b/test/manual-test-examples/type/img/boundingBoxBreaks.p5.jpg new file mode 100644 index 0000000000..27a1f3404f Binary files /dev/null and b/test/manual-test-examples/type/img/boundingBoxBreaks.p5.jpg differ diff --git a/test/manual-test-examples/type/img/boundingBoxBreaksLeft.p5.jpg b/test/manual-test-examples/type/img/boundingBoxBreaksLeft.p5.jpg new file mode 100644 index 0000000000..75a6ffd460 Binary files /dev/null and b/test/manual-test-examples/type/img/boundingBoxBreaksLeft.p5.jpg differ diff --git a/test/manual-test-examples/type/img/boundingBoxMulti.p5.jpg b/test/manual-test-examples/type/img/boundingBoxMulti.p5.jpg new file mode 100644 index 0000000000..81eb429477 Binary files /dev/null and b/test/manual-test-examples/type/img/boundingBoxMulti.p5.jpg differ diff --git a/test/manual-test-examples/type/img/loadedFontSketch.p5.png b/test/manual-test-examples/type/img/loadedFontSketch.p5.png new file mode 100644 index 0000000000..5beadcb1cf Binary files /dev/null and b/test/manual-test-examples/type/img/loadedFontSketch.p5.png differ diff --git a/test/manual-test-examples/type/img/multilineAlignChar.p5.png b/test/manual-test-examples/type/img/multilineAlignChar.p5.png new file mode 100644 index 0000000000..253ca8b5d7 Binary files /dev/null and b/test/manual-test-examples/type/img/multilineAlignChar.p5.png differ diff --git a/test/manual-test-examples/type/img/multilineAlignSketch.p5.png b/test/manual-test-examples/type/img/multilineAlignSketch.p5.png new file mode 100644 index 0000000000..1562e1cadf Binary files /dev/null and b/test/manual-test-examples/type/img/multilineAlignSketch.p5.png differ diff --git a/test/manual-test-examples/type/img/multilineAlignSketch.png b/test/manual-test-examples/type/img/multilineAlignSketch.png new file mode 100644 index 0000000000..589ec53b9e Binary files /dev/null and b/test/manual-test-examples/type/img/multilineAlignSketch.png differ diff --git a/test/manual-test-examples/type/img/p5.jpg b/test/manual-test-examples/type/img/p5.jpg new file mode 100644 index 0000000000..627ed49b3e Binary files /dev/null and b/test/manual-test-examples/type/img/p5.jpg differ diff --git a/test/manual-test-examples/type/img/p5v2.jpg b/test/manual-test-examples/type/img/p5v2.jpg new file mode 100644 index 0000000000..deb7b8c7d9 Binary files /dev/null and b/test/manual-test-examples/type/img/p5v2.jpg differ diff --git a/test/manual-test-examples/type/img/proc.jpg b/test/manual-test-examples/type/img/proc.jpg new file mode 100644 index 0000000000..ffcaca9ecd Binary files /dev/null and b/test/manual-test-examples/type/img/proc.jpg differ diff --git a/test/manual-test-examples/type/img/text-width-issue.png b/test/manual-test-examples/type/img/text-width-issue.png new file mode 100644 index 0000000000..fbf900718f Binary files /dev/null and b/test/manual-test-examples/type/img/text-width-issue.png differ diff --git a/test/manual-test-examples/type/img/textAlignSketch.p5.png b/test/manual-test-examples/type/img/textAlignSketch.p5.png new file mode 100644 index 0000000000..f852182e91 Binary files /dev/null and b/test/manual-test-examples/type/img/textAlignSketch.p5.png differ diff --git a/test/manual-test-examples/type/img/textAlignSketch.png b/test/manual-test-examples/type/img/textAlignSketch.png new file mode 100644 index 0000000000..909954aa14 Binary files /dev/null and b/test/manual-test-examples/type/img/textAlignSketch.png differ diff --git a/test/manual-test-examples/type/img/textAllAlignmentsSketch.p5.jpg b/test/manual-test-examples/type/img/textAllAlignmentsSketch.p5.jpg new file mode 100644 index 0000000000..e091e58a59 Binary files /dev/null and b/test/manual-test-examples/type/img/textAllAlignmentsSketch.p5.jpg differ diff --git a/test/manual-test-examples/type/img/textAllAlignmentsSketch.png b/test/manual-test-examples/type/img/textAllAlignmentsSketch.png new file mode 100644 index 0000000000..7d12fa834a Binary files /dev/null and b/test/manual-test-examples/type/img/textAllAlignmentsSketch.png differ diff --git a/test/manual-test-examples/type/img/textFontSketch.jpg b/test/manual-test-examples/type/img/textFontSketch.jpg new file mode 100644 index 0000000000..53b7f08456 Binary files /dev/null and b/test/manual-test-examples/type/img/textFontSketch.jpg differ diff --git a/test/manual-test-examples/type/img/textFontSketch.p5.jpg b/test/manual-test-examples/type/img/textFontSketch.p5.jpg new file mode 100644 index 0000000000..fb07179ccb Binary files /dev/null and b/test/manual-test-examples/type/img/textFontSketch.p5.jpg differ diff --git a/test/manual-test-examples/type/img/textLeadingBaseline.p5.jpg b/test/manual-test-examples/type/img/textLeadingBaseline.p5.jpg new file mode 100644 index 0000000000..b2fe9c75da Binary files /dev/null and b/test/manual-test-examples/type/img/textLeadingBaseline.p5.jpg differ diff --git a/test/manual-test-examples/type/img/textLeadingBottom.p5.jpg b/test/manual-test-examples/type/img/textLeadingBottom.p5.jpg new file mode 100644 index 0000000000..f9712d1e91 Binary files /dev/null and b/test/manual-test-examples/type/img/textLeadingBottom.p5.jpg differ diff --git a/test/manual-test-examples/type/img/textLeadingCenter.p5.jpg b/test/manual-test-examples/type/img/textLeadingCenter.p5.jpg new file mode 100644 index 0000000000..b52434b93c Binary files /dev/null and b/test/manual-test-examples/type/img/textLeadingCenter.p5.jpg differ diff --git a/test/manual-test-examples/type/img/textLeadingTop.p5.jpg b/test/manual-test-examples/type/img/textLeadingTop.p5.jpg new file mode 100644 index 0000000000..81eb429477 Binary files /dev/null and b/test/manual-test-examples/type/img/textLeadingTop.p5.jpg differ diff --git a/test/manual-test-examples/type/img/textOverlapSketch.p5.png b/test/manual-test-examples/type/img/textOverlapSketch.p5.png new file mode 100644 index 0000000000..c058ee6f8e Binary files /dev/null and b/test/manual-test-examples/type/img/textOverlapSketch.p5.png differ diff --git a/test/manual-test-examples/type/img/textOverlapSketch.png b/test/manual-test-examples/type/img/textOverlapSketch.png new file mode 100644 index 0000000000..397fe26d46 Binary files /dev/null and b/test/manual-test-examples/type/img/textOverlapSketch.png differ diff --git a/test/manual-test-examples/type/img/textSizeSketch.p5.png b/test/manual-test-examples/type/img/textSizeSketch.p5.png new file mode 100644 index 0000000000..0e4d2505f7 Binary files /dev/null and b/test/manual-test-examples/type/img/textSizeSketch.p5.png differ diff --git a/test/manual-test-examples/type/img/textSizeSketch.png b/test/manual-test-examples/type/img/textSizeSketch.png new file mode 100644 index 0000000000..c25f2673ae Binary files /dev/null and b/test/manual-test-examples/type/img/textSizeSketch.png differ diff --git a/test/manual-test-examples/type/img/textSketch.p5.png b/test/manual-test-examples/type/img/textSketch.p5.png new file mode 100644 index 0000000000..b7d50e717d Binary files /dev/null and b/test/manual-test-examples/type/img/textSketch.p5.png differ diff --git a/test/manual-test-examples/type/img/textSketch.png b/test/manual-test-examples/type/img/textSketch.png new file mode 100644 index 0000000000..3bbb8cbe62 Binary files /dev/null and b/test/manual-test-examples/type/img/textSketch.png differ diff --git a/test/manual-test-examples/type/img/textStyleSketch.p5.jpg b/test/manual-test-examples/type/img/textStyleSketch.p5.jpg new file mode 100644 index 0000000000..a8bfe6bf58 Binary files /dev/null and b/test/manual-test-examples/type/img/textStyleSketch.p5.jpg differ diff --git a/test/manual-test-examples/type/img/textVerticalAlign.p5.png b/test/manual-test-examples/type/img/textVerticalAlign.p5.png new file mode 100644 index 0000000000..ad6782e882 Binary files /dev/null and b/test/manual-test-examples/type/img/textVerticalAlign.p5.png differ diff --git a/test/manual-test-examples/type/img/textVerticalAlign.png b/test/manual-test-examples/type/img/textVerticalAlign.png new file mode 100644 index 0000000000..4e1e7e0ecf Binary files /dev/null and b/test/manual-test-examples/type/img/textVerticalAlign.png differ diff --git a/test/manual-test-examples/type/img/textWidthSketch.p5.png b/test/manual-test-examples/type/img/textWidthSketch.p5.png new file mode 100644 index 0000000000..3942aad917 Binary files /dev/null and b/test/manual-test-examples/type/img/textWidthSketch.p5.png differ diff --git a/test/manual-test-examples/type/img/textWidthSketch.png b/test/manual-test-examples/type/img/textWidthSketch.png new file mode 100644 index 0000000000..ee10f578c2 Binary files /dev/null and b/test/manual-test-examples/type/img/textWidthSketch.png differ diff --git a/test/manual-test-examples/type/img/typographyLetterSketch.p5.png b/test/manual-test-examples/type/img/typographyLetterSketch.p5.png new file mode 100644 index 0000000000..03826a12fc Binary files /dev/null and b/test/manual-test-examples/type/img/typographyLetterSketch.p5.png differ diff --git a/test/manual-test-examples/type/img/typographyLetterSketch.png b/test/manual-test-examples/type/img/typographyLetterSketch.png new file mode 100644 index 0000000000..08465e118c Binary files /dev/null and b/test/manual-test-examples/type/img/typographyLetterSketch.png differ diff --git a/test/manual-test-examples/type/index.html b/test/manual-test-examples/type/index.html new file mode 100755 index 0000000000..e2f605bbe2 --- /dev/null +++ b/test/manual-test-examples/type/index.html @@ -0,0 +1,2724 @@ + + + + + + + + + + + + + +
+

textFont

+ + +
+ +
+

loadFont

+ +
+ +
+

right-to-leftBounds

+
+ +
+

textAttributes

+ + +
+ +
+

textToPoints

+
+ +
+

textToPaths

+
+ +
+

fontStretch (via textFont)

+
+ +
+

fontStretch (via textProperty)

+
+ +
+

directFontSet

+
+ + + +
+

letterSpacing

+
+ +
+

wordSpacing

+
+ +
+

fontKerning

+
+ +
+

fontVariantCaps (differs between FF/Chrome)

+
+ +
+

textRendering property

+
+ +
+

singleLineBounds

+
+ +
+

loadedBoundsSingle

+
+ +
+

systemBoundsMulti (RectMode.DEFAULT)

+
+
+

systemBoundsMulti (RectMode.CENTER)

+
+
+

systemBoundsMulti (RectMode.CORNERS)

+
+
+

systemBoundsMulti (RectMode.RADIUS)

+
+ +
+

textToPointsBounds (RectMode.DEFAULT)

+
+
+

textToPointsBounds (RectMode.CENTER)

+
+
+

textToPointsBounds (RectMode.CORNERS)

+
+
+

textToPointsBounds (RectMode.RADIUS)

+
+ + +
+

systemBoundsMulti.Overflow

+
+ +
+

singleWordBounds

+
+ +
+

manualLineBreaksLeft

+ +
+ +
+

manualLineBreaks

+ +
+ +
+

textVerticalAlign

+ + +
+ +
+

multilineAlign.Word

+ + +
+ +
+

multilineAlign.Char

+ +
+ +
+

textAlign

+ + +
+ +
+ +

Overlay (Processing)

+ +
+
+ +

Overlay (p5.js v1)

+
+ +
+

textLeading.Top

+ + +
+ +
+

textLeading.Center

+ + +
+ +
+

textLeading.Baseline

+ + +
+ +
+

textLeading.Bottom

+ + +
+ +
+

textSizeSketch

+ + +
+ +
+

textStyleSketch

+ +
+ +
+

fontWidth/textWidth

+ + +
+ +
+

typographyGrid

+ + +
+ +
+

singleWideLine

+
+ +
+

textAlphaSketch

+ + +
+ + + + \ No newline at end of file diff --git a/test/manual-test-examples/type/issues/fontSizeAdjustIssue.html b/test/manual-test-examples/type/issues/fontSizeAdjustIssue.html new file mode 100644 index 0000000000..8373e11ad1 --- /dev/null +++ b/test/manual-test-examples/type/issues/fontSizeAdjustIssue.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/type/issues/fontStretchIssue.html b/test/manual-test-examples/type/issues/fontStretchIssue.html new file mode 100644 index 0000000000..ba7ab56db3 --- /dev/null +++ b/test/manual-test-examples/type/issues/fontStretchIssue.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/type/text-to-paths.html b/test/manual-test-examples/type/text-to-paths.html new file mode 100644 index 0000000000..97b464b8e7 --- /dev/null +++ b/test/manual-test-examples/type/text-to-paths.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/type/text-to-points.html b/test/manual-test-examples/type/text-to-points.html new file mode 100644 index 0000000000..233d966d24 --- /dev/null +++ b/test/manual-test-examples/type/text-to-points.html @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/type/variable-font-local.html b/test/manual-test-examples/type/variable-font-local.html new file mode 100644 index 0000000000..f652fe2e8f --- /dev/null +++ b/test/manual-test-examples/type/variable-font-local.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/type/variable-font.html b/test/manual-test-examples/type/variable-font.html new file mode 100644 index 0000000000..3465f07584 --- /dev/null +++ b/test/manual-test-examples/type/variable-font.html @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/type/vite.config.mjs b/test/manual-test-examples/type/vite.config.mjs new file mode 100644 index 0000000000..301192ed7f --- /dev/null +++ b/test/manual-test-examples/type/vite.config.mjs @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; +import vitePluginString from 'vite-plugin-string'; + +export default defineConfig({ + root: './', + plugins: [ + vitePluginString({ + include: [ + 'src/webgl/shaders/**/*' + ] + }) + ] +}); diff --git a/test/manual-test-examples/webgl/curves/sketch.js b/test/manual-test-examples/webgl/curves/sketch.js index 435ad59dd3..bdde1699e0 100644 --- a/test/manual-test-examples/webgl/curves/sketch.js +++ b/test/manual-test-examples/webgl/curves/sketch.js @@ -39,16 +39,16 @@ function draw() { fill(0, 77, 64); beginShape(); - curveVertex(10, 150, -4); - curveVertex(10, 150, -4); - curveVertex(60, 80, -4); - curveVertex(140, 100, -4); - curveVertex(200, 100, -4); - curveVertex(200, 110, -4); - curveVertex(160, 140, -4); - curveVertex(80, 160, -4); - curveVertex(10, 150, -4); - curveVertex(10, 150, -4); + splineVertex(10, 150, -4); + splineVertex(10, 150, -4); + splineVertex(60, 80, -4); + splineVertex(140, 100, -4); + splineVertex(200, 100, -4); + splineVertex(200, 110, -4); + splineVertex(160, 140, -4); + splineVertex(80, 160, -4); + splineVertex(10, 150, -4); + splineVertex(10, 150, -4); endShape(); angle += 0.01; diff --git a/test/manual-test-examples/webgl/geometryImmediate/sketch.js b/test/manual-test-examples/webgl/geometryImmediate/sketch.js index 27ce74a562..8ec44bc3fd 100644 --- a/test/manual-test-examples/webgl/geometryImmediate/sketch.js +++ b/test/manual-test-examples/webgl/geometryImmediate/sketch.js @@ -82,7 +82,7 @@ function drawStrip(mode) { } function ngon(n, x, y, d) { - beginShape(TESS); + beginShape(PATH); for (let i = 0; i < n + 1; i++) { angle = TWO_PI / n * i; px = x + sin(angle) * d / 2; diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js index f16a426d54..31676179ff 100644 --- a/test/unit/accessibility/describe.js +++ b/test/unit/accessibility/describe.js @@ -1,72 +1,72 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import describe from '../../../src/accessibility/describe'; suite('describe', function() { - let myp5; - let myID = 'myCanvasID'; + const myID = 'myCanvasID'; beforeAll(function() { - new p5(function(p) { - p.setup = function() { - let cnv = p.createCanvas(100, 100); - cnv.id(myID); - myp5 = p; - }; - }); - }); + describe(mockP5, mockP5Prototype); - afterAll(function() { - myp5.remove(); + mockP5Prototype.LABEL = 'label'; + mockP5Prototype.FALLBACK = 'fallback'; }); suite('p5.prototype.describe', function() { test('should be a function', function() { - assert.ok(myp5.describe); - assert.typeOf(myp5.describe, 'function'); + assert.ok(mockP5Prototype.describe); + assert.typeOf(mockP5Prototype.describe, 'function'); }); + test('err when LABEL at param #0', function() { assert.throws( function() { - myp5.describe(myp5.LABEL); + mockP5Prototype.describe(mockP5Prototype.LABEL); }, Error, 'description should not be LABEL or FALLBACK' ); }); + test('should create description as fallback', function() { - myp5.describe('a'); + mockP5Prototype.describe('a'); let actual = document.getElementById(myID + '_fallbackDesc'); assert.deepEqual(actual.innerHTML, 'a.'); }); + test('should not add extra period if string ends in "."', function() { - myp5.describe('A.'); + mockP5Prototype.describe('A.'); let actual = document.getElementById(myID + '_fallbackDesc'); assert.deepEqual(actual.innerHTML, 'A.'); }); + test('should not add period if string ends in "!" or "?', function() { - myp5.describe('A!'); + mockP5Prototype.describe('A!'); let actual = document.getElementById(myID + '_fallbackDesc'); if (actual.innerHTML === 'A!') { - myp5.describe('A?'); + mockP5Prototype.describe('A?'); actual = document.getElementById(myID + '_fallbackDesc'); assert.deepEqual(actual.innerHTML, 'A?'); } }); + test('should create description when called after describeElement()', function() { - myp5.describeElement('b', 'c'); - myp5.describe('a'); + mockP5Prototype.describeElement('b', 'c'); + mockP5Prototype.describe('a'); let actual = document.getElementById(myID + '_fallbackDesc'); assert.deepEqual(actual.innerHTML, 'a.'); }); + test('should create Label adjacent to canvas', function() { - myp5.describe('a', myp5.LABEL); + mockP5Prototype.describe('a', mockP5Prototype.LABEL); let actual = document.getElementById(myID + '_labelDesc'); assert.deepEqual(actual.innerHTML, 'a.'); }); + test('should create Label adjacent to canvas when label of element already exists', function() { - myp5.describeElement('ba', 'c', myp5.LABEL); - myp5.describe('a', myp5.LABEL); + mockP5Prototype.describeElement('ba', 'c', mockP5Prototype.LABEL); + mockP5Prototype.describe('a', mockP5Prototype.LABEL); let actual = document.getElementById(myID + '_labelDesc'); assert.deepEqual(actual.innerHTML, 'a.'); }); @@ -74,69 +74,77 @@ suite('describe', function() { suite('p5.prototype.describeElement', function() { test('should be a function', function() { - assert.ok(myp5.describeElement); - assert.typeOf(myp5.describeElement, 'function'); + assert.ok(mockP5Prototype.describeElement); + assert.typeOf(mockP5Prototype.describeElement, 'function'); }); + test('err when LABEL at param #0', function() { assert.throws( function() { - myp5.describeElement(myp5.LABEL, 'b'); + mockP5Prototype.describeElement(mockP5Prototype.LABEL, 'b'); }, Error, 'element name should not be LABEL or FALLBACK' ); }); + test('err when LABEL at param #1', function() { assert.throws( function() { - myp5.describeElement('a', myp5.LABEL); + mockP5Prototype.describeElement('a', mockP5Prototype.LABEL); }, Error, 'description should not be LABEL or FALLBACK' ); }); + test('should create element description as fallback', function() { - myp5.describeElement('az', 'b'); + mockP5Prototype.describeElement('az', 'b'); let actual = document.getElementById(myID + '_fte_az').innerHTML; assert.deepEqual(actual, 'az:b.'); }); + test('should not add extra ":" if element name ends in colon', function() { - myp5.describeElement('ab:', 'b.'); + mockP5Prototype.describeElement('ab:', 'b.'); let actual = document.getElementById(myID + '_fte_ab').innerHTML; assert.deepEqual(actual, 'ab:b.'); }); + test('should replace ";", ",", "." for ":" in element name', function() { let actual; - myp5.describeElement('ac;', 'b.'); + mockP5Prototype.describeElement('ac;', 'b.'); if ( document.getElementById(myID + '_fte_ac').innerHTML === 'ac:b.' ) { - myp5.describeElement('ad,', 'b.'); + mockP5Prototype.describeElement('ad,', 'b.'); if ( document.getElementById(myID + '_fte_ad').innerHTML === 'ad:b.' ) { - myp5.describeElement('ae.', 'b.'); + mockP5Prototype.describeElement('ae.', 'b.'); actual = document.getElementById(myID + '_fte_ae').innerHTML; assert.deepEqual(actual, 'ae:b.'); } } }); + test('should create element description when called after describe()', function() { - myp5.describe('c'); - myp5.describeElement('af', 'b'); + mockP5Prototype.describe('c'); + mockP5Prototype.describeElement('af', 'b'); let actual = document.getElementById(myID + '_fte_af').innerHTML; assert.deepEqual(actual, 'af:b.'); }); + test('should create element label adjacent to canvas', function() { - myp5.describeElement('ag', 'b', myp5.LABEL); + mockP5Prototype.describeElement('ag', 'b', mockP5Prototype.LABEL); const actual = document.getElementById(myID + '_lte_ag').innerHTML; assert.deepEqual(actual, 'ag:b.'); }); + test('should create element label adjacent to canvas when called after describe()', function() { - myp5.describe('c', myp5.LABEL); - myp5.describeElement('ah:', 'b', myp5.LABEL); + mockP5Prototype.describe('c', mockP5Prototype.LABEL); + mockP5Prototype.describeElement('ah:', 'b', mockP5Prototype.LABEL); const actual = document.getElementById(myID + '_lte_ah').innerHTML; assert.deepEqual(actual, 'ah:b.'); }); diff --git a/test/unit/accessibility/outputs.js b/test/unit/accessibility/outputs.js index c94662931d..56d0c8cae0 100644 --- a/test/unit/accessibility/outputs.js +++ b/test/unit/accessibility/outputs.js @@ -1,32 +1,26 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import outputs from '../../../src/accessibility/outputs'; +import textOutput from '../../../src/accessibility/textOutput'; +// TODO: Is it possible to test this without a runtime? suite('outputs', function() { - let myp5; let myID = 'myCanvasID'; beforeAll(function() { - new p5(function(p) { - p.setup = function() { - let cnv = p.createCanvas(100, 100); - cnv.id(myID); - myp5 = p; - }; - }); - }); - - afterAll(function() { - myp5.remove(); + outputs(mockP5, mockP5Prototype); + textOutput(mockP5, mockP5Prototype); }); suite('p5.prototype.textOutput', function() { test('should be a function', function() { - assert.ok(myp5.textOutput); - assert.typeOf(myp5.textOutput, 'function'); + assert.ok(mockP5Prototype.textOutput); + assert.typeOf(mockP5Prototype.textOutput, 'function'); }); let expected = 'Your output is a, 100 by 100 pixels, white canvas containing the following shape:'; - test('should create output as fallback', function() { + + test.todo('should create output as fallback', function() { return new Promise(function(resolve, reject) { let actual = ''; new p5(function(p) { @@ -51,7 +45,8 @@ suite('outputs', function() { }); }); }); - test('should create output as label', function() { + + test.todo('should create output as label', function() { return new Promise(function(resolve, reject) { let label = ''; let fallback = ''; @@ -80,6 +75,7 @@ suite('outputs', function() { }); }); }); + test.todo('should create text output for arc()', function() { return new Promise(function(resolve, reject) { expected = @@ -107,6 +103,7 @@ suite('outputs', function() { }); }); }); + test.todo('should create text output for ellipse()', function() { return new Promise(function(resolve, reject) { expected = @@ -134,6 +131,7 @@ suite('outputs', function() { }); }); }); + test.todo('should create text output for triangle()', function() { return new Promise(function(resolve, reject) { expected = @@ -165,13 +163,14 @@ suite('outputs', function() { suite('p5.prototype.gridOutput', function() { test('should be a function', function() { - assert.ok(myp5.gridOutput); - assert.typeOf(myp5.gridOutput, 'function'); + assert.ok(mockP5Prototype.gridOutput); + assert.typeOf(mockP5Prototype.gridOutput, 'function'); }); let expected = 'white canvas, 100 by 100 pixels, contains 1 shape: 1 square'; - test('should create output as fallback', function() { + + test.todo('should create output as fallback', function() { return new Promise(function(resolve, reject) { let actual = ''; new p5(function(p) { @@ -196,7 +195,8 @@ suite('outputs', function() { }); }); }); - test('should create output as label', function() { + + test.todo('should create output as label', function() { return new Promise(function(resolve, reject) { let label = ''; let fallback = ''; @@ -225,6 +225,7 @@ suite('outputs', function() { }); }); }); + test.todo('should create text output for quad()', function() { return new Promise(function(resolve, reject) { expected = 'red quadrilateral, location = top left, area = 45 %'; @@ -251,6 +252,7 @@ suite('outputs', function() { }); }); }); + test.todo('should create text output for point()', function() { return new Promise(function(resolve, reject) { expected = 'dark fuchsia point, location = bottom right'; @@ -277,6 +279,7 @@ suite('outputs', function() { }); }); }); + test.todo('should create text output for triangle()', function() { return new Promise(function(resolve, reject) { expected = 'green triangle, location = top left, area = 13 %'; diff --git a/test/manual-test-examples/p5.Font/Inconsolata-Bold.ttf b/test/unit/assets/Inconsolata-Bold.ttf similarity index 100% rename from test/manual-test-examples/p5.Font/Inconsolata-Bold.ttf rename to test/unit/assets/Inconsolata-Bold.ttf diff --git a/test/unit/core/2d_primitives.js b/test/unit/core/2d_primitives.js index 523e549900..a85e07ae87 100644 --- a/test/unit/core/2d_primitives.js +++ b/test/unit/core/2d_primitives.js @@ -1,72 +1,63 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import primitives from '../../../src/shape/2d_primitives'; suite('2D Primitives', function() { - var myp5; - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - - afterAll(async function() { - await myp5.remove(); + primitives(mockP5, mockP5Prototype); }); suite('p5.prototype.arc', function() { test('should be a function', function() { - assert.ok(myp5.arc); - assert.typeOf(myp5.arc, 'function'); + assert.ok(mockP5Prototype.arc); + assert.typeOf(mockP5Prototype.arc, 'function'); }); }); suite('p5.prototype.ellipse', function() { test('should be a function', function() { - assert.ok(myp5.ellipse); - assert.typeOf(myp5.ellipse, 'function'); + assert.ok(mockP5Prototype.ellipse); + assert.typeOf(mockP5Prototype.ellipse, 'function'); }); }); suite('p5.prototype.line', function() { test('should be a function', function() { - assert.ok(myp5.line); - assert.typeOf(myp5.line, 'function'); + assert.ok(mockP5Prototype.line); + assert.typeOf(mockP5Prototype.line, 'function'); }); }); suite('p5.prototype.point', function() { test('should be a function', function() { - assert.ok(myp5.point); - assert.typeOf(myp5.point, 'function'); + assert.ok(mockP5Prototype.point); + assert.typeOf(mockP5Prototype.point, 'function'); }); }); suite('p5.prototype.quad', function() { test('should be a function', function() { - assert.ok(myp5.quad); - assert.typeOf(myp5.quad, 'function'); + assert.ok(mockP5Prototype.quad); + assert.typeOf(mockP5Prototype.quad, 'function'); }); }); suite('p5.prototype.rect', function() { test('should be a function', function() { - assert.ok(myp5.rect); - assert.typeOf(myp5.rect, 'function'); + assert.ok(mockP5Prototype.rect); + assert.typeOf(mockP5Prototype.rect, 'function'); }); }); suite('p5.prototype.triangle', function() { test('should be a function', function() { - assert.ok(myp5.triangle); - assert.typeOf(myp5.triangle, 'function'); + assert.ok(mockP5Prototype.triangle); + assert.typeOf(mockP5Prototype.triangle, 'function'); }); }); suite('p5.prototype.square', function() { test('should be a function', function() { - assert.ok(myp5.square); - assert.typeOf(myp5.square, 'function'); + assert.ok(mockP5Prototype.square); + assert.typeOf(mockP5Prototype.square, 'function'); }); }); @@ -75,7 +66,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i, 2 * Math.PI * j, 500, @@ -92,7 +83,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i + 1, 2 * Math.PI * j + 1, 500, @@ -109,7 +100,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i - 0.000001, 2 * Math.PI * j + 0.000001, 500, @@ -126,7 +117,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i + 0.000001, 2 * Math.PI * j - 0.000001, 500, @@ -143,7 +134,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i + 0.999999, 2 * Math.PI * j + 1.000001, 500, @@ -160,7 +151,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i + 1.000001, 2 * Math.PI * j + 0.999999, 500, @@ -177,7 +168,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i - 0.1, 2 * Math.PI * j + 0.1, 500, @@ -194,7 +185,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i + 0.1, 2 * Math.PI * j - 0.1, 500, @@ -211,7 +202,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i + 0.9, 2 * Math.PI * j + 1.1, 500, @@ -228,7 +219,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * i + 1.1, 2 * Math.PI * j + 0.9, 500, @@ -245,7 +236,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * (i + 40 / 360), 2 * Math.PI * (j + 230 / 360), 500, @@ -262,7 +253,7 @@ suite('2D Primitives', function() { var i, j, angles; for (i = -2; i <= 2; i++) { for (j = -2; j <= 2; j++) { - angles = myp5._normalizeArcAngles( + angles = mockP5Prototype._normalizeArcAngles( 2 * Math.PI * (i + 320 / 360), 2 * Math.PI * (j + 130 / 360), 500, diff --git a/test/unit/core/attributes.js b/test/unit/core/attributes.js index 28c658a482..6cd0f416e2 100644 --- a/test/unit/core/attributes.js +++ b/test/unit/core/attributes.js @@ -1,66 +1,57 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import attributes from '../../../src/shape/attributes'; suite('Attributes', function() { - var myp5; - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); - }); - - afterAll(function() { - myp5.remove(); + attributes(mockP5, mockP5Prototype); }); suite('p5.prototype.ellipseMode', function() { test('should be a function', function() { - assert.ok(myp5.ellipseMode); - assert.typeOf(myp5.ellipseMode, 'function'); + assert.ok(mockP5Prototype.ellipseMode); + assert.typeOf(mockP5Prototype.ellipseMode, 'function'); }); }); suite('p5.prototype.rectMode', function() { test('should be a function', function() { - assert.ok(myp5.rectMode); - assert.typeOf(myp5.rectMode, 'function'); + assert.ok(mockP5Prototype.rectMode); + assert.typeOf(mockP5Prototype.rectMode, 'function'); }); }); suite('p5.prototype.noSmooth', function() { test('should be a function', function() { - assert.ok(myp5.noSmooth); - assert.typeOf(myp5.noSmooth, 'function'); + assert.ok(mockP5Prototype.noSmooth); + assert.typeOf(mockP5Prototype.noSmooth, 'function'); }); }); suite('p5.prototype.smooth', function() { test('should be a function', function() { - assert.ok(myp5.smooth); - assert.typeOf(myp5.smooth, 'function'); + assert.ok(mockP5Prototype.smooth); + assert.typeOf(mockP5Prototype.smooth, 'function'); }); }); suite('p5.prototype.strokeCap', function() { test('should be a function', function() { - assert.ok(myp5.strokeCap); - assert.typeOf(myp5.strokeCap, 'function'); + assert.ok(mockP5Prototype.strokeCap); + assert.typeOf(mockP5Prototype.strokeCap, 'function'); }); }); suite('p5.prototype.strokeJoin', function() { test('should be a function', function() { - assert.ok(myp5.strokeJoin); - assert.typeOf(myp5.strokeJoin, 'function'); + assert.ok(mockP5Prototype.strokeJoin); + assert.typeOf(mockP5Prototype.strokeJoin, 'function'); }); }); suite('p5.prototype.strokeWeight', function() { test('should be a function', function() { - assert.ok(myp5.strokeWeight); - assert.typeOf(myp5.strokeWeight, 'function'); + assert.ok(mockP5Prototype.strokeWeight); + assert.typeOf(mockP5Prototype.strokeWeight, 'function'); }); }); }); diff --git a/test/unit/core/curves.js b/test/unit/core/curves.js index c2fb72a36f..d389e0fb9d 100644 --- a/test/unit/core/curves.js +++ b/test/unit/core/curves.js @@ -1,35 +1,33 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import curves from '../../../src/shape/curves'; suite('Curves', function() { - var myp5; - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }); + mockP5Prototype._renderer = { + _curveTightness: 0 + }; + curves(mockP5, mockP5Prototype); }); - afterAll(function() { - myp5.remove(); + afterAll(() => { + delete mockP5Prototype._renderer; }); suite('p5.prototype.bezier', function() { test('should be a function', function() { - assert.ok(myp5.bezier); - assert.typeOf(myp5.bezier, 'function'); + assert.ok(mockP5Prototype.bezier); + assert.typeOf(mockP5Prototype.bezier, 'function'); }); }); suite('p5.prototype.bezierPoint', function() { var result; test('should be a function', function() { - assert.ok(myp5.bezierPoint); - assert.typeOf(myp5.bezierPoint, 'function'); + assert.ok(mockP5Prototype.bezierPoint); + assert.typeOf(mockP5Prototype.bezierPoint, 'function'); }); test('should return the correct point on a Bezier Curve', function() { - result = myp5.bezierPoint(85, 10, 90, 15, 0.5); + result = mockP5Prototype.bezierPoint(85, 10, 90, 15, 0.5); assert.equal(result, 50); assert.notEqual(result, -1); }); @@ -38,30 +36,30 @@ suite('Curves', function() { suite('p5.prototype.bezierTangent', function() { var result; test('should be a function', function() { - assert.ok(myp5.bezierTangent); - assert.typeOf(myp5.bezierTangent, 'function'); + assert.ok(mockP5Prototype.bezierTangent); + assert.typeOf(mockP5Prototype.bezierTangent, 'function'); }); test('should return the correct point on a Bezier Curve', function() { - result = myp5.bezierTangent(95, 73, 73, 15, 0.5); + result = mockP5Prototype.bezierTangent(95, 73, 73, 15, 0.5); assert.equal(result, -60); }); }); suite('p5.prototype.curve', function() { test('should be a function', function() { - assert.ok(myp5.curve); - assert.typeOf(myp5.curve, 'function'); + assert.ok(mockP5Prototype.curve); + assert.typeOf(mockP5Prototype.curve, 'function'); }); }); suite('p5.prototype.curvePoint', function() { var result; test('should be a function', function() { - assert.ok(myp5.curvePoint); - assert.typeOf(myp5.curvePoint, 'function'); + assert.ok(mockP5Prototype.curvePoint); + assert.typeOf(mockP5Prototype.curvePoint, 'function'); }); test('should return the correct point on a Catmull-Rom Curve', function() { - result = myp5.curvePoint(5, 5, 73, 73, 0.5); + result = mockP5Prototype.curvePoint(5, 5, 73, 73, 0.5); assert.equal(result, 39); assert.notEqual(result, -1); }); @@ -70,11 +68,11 @@ suite('Curves', function() { suite('p5.prototype.curveTangent', function() { var result; test('should be a function', function() { - assert.ok(myp5.curveTangent); - assert.typeOf(myp5.curveTangent, 'function'); + assert.ok(mockP5Prototype.curveTangent); + assert.typeOf(mockP5Prototype.curveTangent, 'function'); }); test('should return the correct point on a Catmull-Rom Curve', function() { - result = myp5.curveTangent(95, 73, 73, 15, 0.5); + result = mockP5Prototype.curveTangent(95, 73, 73, 15, 0.5); assert.equal(result, 10); assert.notEqual(result, -1); }); diff --git a/test/unit/core/structure.js b/test/unit/core/structure.js index cf972f5395..1dd708db78 100644 --- a/test/unit/core/structure.js +++ b/test/unit/core/structure.js @@ -58,18 +58,7 @@ suite('Structure', function() { suite('p5.prototype.push and p5.prototype.pop', function() { function getRenderState() { - var state = {}; - for (var key in myp5._renderer) { - var value = myp5._renderer[key]; - if ( - typeof value !== 'function' && - key !== '_cachedFillStyle' && - key !== '_cachedStrokeStyle' - ) { - state[key] = value; - } - } - return state; + return { ...myp5._renderer.states }; } function assertCanPreserveRenderState(work) { diff --git a/test/unit/core/transform.js b/test/unit/core/transform.js index 1e37634760..c4b0edbb8e 100644 --- a/test/unit/core/transform.js +++ b/test/unit/core/transform.js @@ -1,96 +1,79 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import transform from '../../../src/core/transform'; suite('Transform', function() { - var sketch1; // sketch without WEBGL Mode - var sketch2; // skecth with WEBGL mode - beforeAll(function() { - new p5(function(p) { - p.setup = function() { - sketch1 = p; - }; - }); - new p5(function(p) { - p.setup = function() { - p.createCanvas(100, 100, p.WEBGL); - sketch2 = p; - }; - }); - }); - - afterAll(function() { - sketch1.remove(); - sketch2.remove(); + transform(mockP5, mockP5Prototype); }); suite('p5.prototype.rotate', function() { test('should be a function', function() { - assert.ok(sketch1.rotate); - assert.typeOf(sketch1.rotate, 'function'); + assert.ok(mockP5Prototype.rotate); + assert.typeOf(mockP5Prototype.rotate, 'function'); }); }); suite('p5.prototype.rotateX', function() { test('should be a function', function() { - assert.ok(sketch1.rotateX); - assert.typeOf(sketch1.rotateX, 'function'); + assert.ok(mockP5Prototype.rotateX); + assert.typeOf(mockP5Prototype.rotateX, 'function'); }); test('throws error. should be used in WEBGL mode', function() { assert.throws(function() { - sketch1.rotateX(100); + mockP5Prototype.rotateX(100); }, Error); }); }); suite('p5.prototype.rotateY', function() { test('should be a function', function() { - assert.ok(sketch1.rotateY); - assert.typeOf(sketch1.rotateY, 'function'); + assert.ok(mockP5Prototype.rotateY); + assert.typeOf(mockP5Prototype.rotateY, 'function'); }); test('throws error. should be used in WEBGL mode', function() { assert.throws(function() { - sketch1.rotateY(100); + mockP5Prototype.rotateY(100); }, Error); }); }); suite('p5.prototype.rotateZ', function() { test('should be a function', function() { - assert.ok(sketch1.rotateZ); - assert.typeOf(sketch1.rotateZ, 'function'); + assert.ok(mockP5Prototype.rotateZ); + assert.typeOf(mockP5Prototype.rotateZ, 'function'); }); test('throws error. should be used in WEBGL mode', function() { assert.throws(function() { - sketch1.rotateZ(100); + mockP5Prototype.rotateZ(100); }, Error); }); }); suite('p5.prototype.scale', function() { test('should be a function', function() { - assert.ok(sketch1.scale); - assert.typeOf(sketch1.scale, 'function'); + assert.ok(mockP5Prototype.scale); + assert.typeOf(mockP5Prototype.scale, 'function'); }); }); suite('p5.prototype.shearX', function() { test('should be a function', function() { - assert.ok(sketch1.shearX); - assert.typeOf(sketch1.shearX, 'function'); + assert.ok(mockP5Prototype.shearX); + assert.typeOf(mockP5Prototype.shearX, 'function'); }); }); suite('p5.prototype.shearY', function() { test('should be a function', function() { - assert.ok(sketch1.shearY); - assert.typeOf(sketch1.shearY, 'function'); + assert.ok(mockP5Prototype.shearY); + assert.typeOf(mockP5Prototype.shearY, 'function'); }); }); suite('p5.prototype.translate', function() { test('should be a function', function() { - assert.ok(sketch1.translate); - assert.typeOf(sketch1.translate, 'function'); + assert.ok(mockP5Prototype.translate); + assert.typeOf(mockP5Prototype.translate, 'function'); }); }); }); diff --git a/test/unit/core/vertex.js b/test/unit/core/vertex.js index eed414a2e1..98077e03b1 100644 --- a/test/unit/core/vertex.js +++ b/test/unit/core/vertex.js @@ -31,10 +31,6 @@ suite('Vertex', function() { assert.ok(myp5.quadraticVertex); assert.typeOf(myp5.quadraticVertex, 'function'); }); - test('_friendlyError is called. vertex() should be used once before quadraticVertex()', function() { - myp5.quadraticVertex(80, 20, 50, 50, 10, 20); - expect(_friendlyErrorSpy).toHaveBeenCalledTimes(1); - }); }); suite('p5.prototype.bezierVertex', function() { @@ -42,10 +38,6 @@ suite('Vertex', function() { assert.ok(myp5.bezierVertex); assert.typeOf(myp5.bezierVertex, 'function'); }); - test('_friendlyError is called. vertex() should be used once before bezierVertex()', function() { - myp5.bezierVertex(25, 30, 25, -30, -25, 30); - expect(_friendlyErrorSpy).toHaveBeenCalledTimes(1); - }); }); suite('p5.prototype.curveVertex', function() { diff --git a/test/unit/data/local_storage.js b/test/unit/data/local_storage.js index 2ebcd24946..75a9f94da0 100644 --- a/test/unit/data/local_storage.js +++ b/test/unit/data/local_storage.js @@ -1,84 +1,86 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import storage from '../../../src/data/local_storage'; +import p5Color from '../../../src/color/p5.Color'; +import creatingReading from '../../../src/color/creating_reading'; +import p5Vector from '../../../src/math/p5.Vector'; +import math from '../../../src/math/math'; suite('local storage', function() { - var myp5; - var myBoolean = false; - var myObject = { one: 1, two: { nested: true } }; - var myNumber = 46; - var myString = 'coolio'; - var myColor; - var myVector; + const myBoolean = false; + const myObject = { one: 1, two: { nested: true } }; + const myNumber = 46; + const myString = 'coolio'; + let myColor; + let myVector; - var hardCodedTypeID = 'p5TypeID'; + const hardCodedTypeID = 'p5TypeID'; beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - myColor = myp5.color(40, 100, 70); - myVector = myp5.createVector(10, 20, 30); - myp5.storeItem('myBoolean', myBoolean); - myp5.storeItem('myObject', myObject); - myp5.storeItem('myNumber', myNumber); - myp5.storeItem('myString', myString); - myp5.storeItem('myColor', myColor); - myp5.storeItem('myVector', myVector); - }; - }); - }); + storage(mockP5, mockP5Prototype); + p5Color(mockP5, mockP5Prototype); + creatingReading(mockP5, mockP5Prototype); + p5Vector(mockP5, mockP5Prototype); + math(mockP5, mockP5Prototype); + + mockP5Prototype.storeItem('myBoolean', myBoolean); + mockP5Prototype.storeItem('myObject', myObject); + mockP5Prototype.storeItem('myNumber', myNumber); + mockP5Prototype.storeItem('myString', myString); - afterAll(function() { - myp5.remove(); + myColor = mockP5Prototype.color(40, 100, 70); + myVector = mockP5Prototype.createVector(10, 20, 30); + mockP5Prototype.storeItem('myColor', myColor); + mockP5Prototype.storeItem('myVector', myVector); }); suite('all keys and type keys should exist in local storage', function() { test('boolean storage retrieval should work', function() { - assert.isTrue(myp5.getItem('myBoolean') === false); + assert.equal(mockP5Prototype.getItem('myBoolean'), false); }); test('boolean storage should store the correct type ID', function() { - assert.isTrue( - localStorage.getItem('myBoolean' + hardCodedTypeID) === 'boolean' + assert.equal( + localStorage.getItem('myBoolean' + hardCodedTypeID), 'boolean' ); }); test('object storage should work', function() { - assert.deepEqual(myp5.getItem('myObject'), { + assert.deepEqual(mockP5Prototype.getItem('myObject'), { one: 1, two: { nested: true } }); }); test('object storage retrieval should store the correct type ID', function() { - assert.isTrue( - localStorage.getItem('myObject' + hardCodedTypeID) === 'object' + assert.equal( + localStorage.getItem('myObject' + hardCodedTypeID), 'object' ); }); test('number storage retrieval should work', function() { - assert.isTrue(myp5.getItem('myNumber') === 46); + assert.equal(mockP5Prototype.getItem('myNumber'), 46); }); test('number storage should store the correct type ID', function() { - assert.isTrue( - localStorage.getItem('myNumber' + hardCodedTypeID) === 'number' + assert.equal( + localStorage.getItem('myNumber' + hardCodedTypeID), 'number' ); }); test('string storage retrieval should work', function() { - assert.isTrue(myp5.getItem('myString') === 'coolio'); + assert.equal(mockP5Prototype.getItem('myString'), 'coolio'); }); test('string storage should store the correct type ID', function() { - assert.isTrue( - localStorage.getItem('myString' + hardCodedTypeID) === 'string' + assert.equal( + localStorage.getItem('myString' + hardCodedTypeID), 'string' ); }); test('p5 Color should retrieve as p5 Color', function() { - assert.isTrue(myp5.getItem('myColor') instanceof p5.Color); + assert.instanceOf(mockP5Prototype.getItem('myColor'), mockP5.Color); }); test('p5 Vector should retrieve as p5 Vector', function() { - assert.isTrue(myp5.getItem('myVector') instanceof p5.Vector); + assert.instanceOf(mockP5Prototype.getItem('myVector'), mockP5.Vector); }); }); var checkRemoval = function(key) { - myp5.removeItem(key); - assert.deepEqual(myp5.getItem(key), null); - assert.deepEqual(myp5.getItem(key + hardCodedTypeID), null); + mockP5Prototype.removeItem(key); + assert.deepEqual(mockP5Prototype.getItem(key), null); + assert.deepEqual(mockP5Prototype.getItem(key + hardCodedTypeID), null); }; suite('should be able to remove all items', function() { @@ -110,14 +112,14 @@ suite('local storage', function() { suite('should be able to clear all items at once', function () { test('should remove all items set by storeItem()', function () { localStorage.setItem('extra', 'stuff'); - myp5.clearStorage(); - assert.deepEqual(myp5.getItem('myBoolean'), null); - assert.deepEqual(myp5.getItem('myNumber'), null); - assert.deepEqual(myp5.getItem('myObject'), null); - assert.deepEqual(myp5.getItem('myString'), null); - assert.deepEqual(myp5.getItem('myColor'), null); - assert.deepEqual(myp5.getItem('myVector'), null); - assert.deepEqual(myp5.getItem('extra'), 'stuff'); + mockP5Prototype.clearStorage(); + assert.deepEqual(mockP5Prototype.getItem('myBoolean'), null); + assert.deepEqual(mockP5Prototype.getItem('myNumber'), null); + assert.deepEqual(mockP5Prototype.getItem('myObject'), null); + assert.deepEqual(mockP5Prototype.getItem('myString'), null); + assert.deepEqual(mockP5Prototype.getItem('myColor'), null); + assert.deepEqual(mockP5Prototype.getItem('myVector'), null); + assert.deepEqual(mockP5Prototype.getItem('extra'), 'stuff'); }); }); }); diff --git a/test/unit/data/p5.TypedDict.js b/test/unit/data/p5.TypedDict.js index dda3179547..331031a57e 100644 --- a/test/unit/data/p5.TypedDict.js +++ b/test/unit/data/p5.TypedDict.js @@ -1,39 +1,31 @@ -import p5 from '../../../src/app.js'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import typeDict from '../../../src/data/p5.TypedDict'; suite('Dictionary Objects', function() { - var myp5; - var stringDict; - var numberDict; + let stringDict; + let numberDict; beforeAll(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; - stringDict = myp5.createStringDict('happy', 'coding'); - numberDict = myp5.createNumberDict(1, 2); - }; - }); - }); - - afterAll(function() { - myp5.remove(); + typeDict(mockP5, mockP5Prototype); + stringDict = mockP5Prototype.createStringDict('happy', 'coding'); + numberDict = mockP5Prototype.createNumberDict(1, 2); }); suite('p5.prototype.stringDict', function() { test('should be created', function() { - assert.isTrue(stringDict instanceof p5.StringDict); + assert.instanceOf(stringDict, mockP5.StringDict); }); test('has correct structure', function() { assert.deepEqual( - JSON.stringify(stringDict), - JSON.stringify({ data: { happy: 'coding' } }) + stringDict, + { data: { happy: 'coding' } } ); }); test('should have correct size', function() { var amt = stringDict.size(); - assert.isTrue(amt === Object.keys(stringDict.data).length); + assert.lengthOf(Object.keys(stringDict.data), amt); }); test('should add new key-value pairs', function() { @@ -59,19 +51,19 @@ suite('Dictionary Objects', function() { suite('p5.prototype.numberDict', function() { test('should be created', function() { - assert.isTrue(numberDict instanceof p5.NumberDict); + assert.instanceOf(numberDict, mockP5.NumberDict); }); test('has correct structure', function() { assert.deepEqual( - JSON.stringify(numberDict), - JSON.stringify({ data: { 1: 2 } }) + numberDict, + { data: { 1: 2 } } ); }); test('should have correct size', function() { var amt = numberDict.size(); - assert.isTrue(amt === Object.keys(numberDict.data).length); + assert.lengthOf(Object.keys(numberDict.data), amt); }); test('should add new key-value pairs', function() { diff --git a/test/unit/dom/dom.js b/test/unit/dom/dom.js index 3dad137e98..96c1a7ec81 100644 --- a/test/unit/dom/dom.js +++ b/test/unit/dom/dom.js @@ -1,58 +1,66 @@ -import p5 from '../../../src/app.js'; import { testSketchWithPromise } from '../../js/p5_helpers'; +import { mockP5, mockP5Prototype } from '../../js/mocks'; +import dom from '../../../src/dom/dom'; +import { Element } from '../../../src/dom/p5.Element'; +import creatingReading from '../../../src/color/creating_reading'; +import p5Color from '../../../src/color/p5.Color'; + suite('DOM', function() { + beforeAll(() => { + dom(mockP5, mockP5Prototype); + creatingReading(mockP5, mockP5Prototype); + p5Color(mockP5, mockP5Prototype); + }); + + // Selectors suite('p5.prototype.select', function() { - /** - * Uses p5 in instance-mode inside a custom container. - * All elements are attached inside the container for testing - * And cleaned up on teardown. - */ - let myp5; let myp5Container; + const generateButton = (name, className = null) => { + const button = mockP5Prototype.createButton(name); + if (className) { + button.class(className); + } + return button; + }; + + const generateDiv = (id = null, className = null) => { + const div = mockP5Prototype.createDiv(); + if (id) { + div.id(id); + } + if (className) { + div.class(className); + } + return div; + }; + beforeEach(function() { myp5Container = document.createElement('div'); document.body.appendChild(myp5Container); - new p5(function(p) { - p.setup = function() { - myp5 = p; - }; - }, myp5Container); }); afterEach(function() { - myp5.remove(); - if (myp5Container && myp5Container.parentNode) { - myp5Container.parentNode.removeChild(myp5Container); - } - myp5Container = null; + document.body.innerHTML = ""; }); test('should return only one p5.element if match is found', function() { // Adding 2 buttons to container - myp5.createCheckbox('Text 1'); - myp5.createCheckbox('Text 2'); - const result = myp5.select('input'); + mockP5Prototype.createCheckbox('Text 1'); + mockP5Prototype.createCheckbox('Text 2'); + const result = mockP5Prototype.select('input'); - assert.instanceOf(result, p5.Element); + assert.instanceOf(result, Element); }); - const generateButton = (name, className = null) => { - const button = myp5.createButton(name); - if (className) { - button.class(className); - } - return button; - }; - test('should find element by class name', function() { // Creates 2 buttons with same class and test if it selects only one. const testClassName = 'test-button'; const testButton = generateButton('Button 1', testClassName); generateButton('Button 2', testClassName); - const result = myp5.select(`.${testClassName}`); + const result = mockP5Prototype.select(`.${testClassName}`); assert.deepEqual(result.elt, testButton.elt); }); @@ -61,10 +69,10 @@ suite('DOM', function() { const testClassName = 'test-button'; generateButton('Button 1', testClassName); const testButton = generateButton('Button 2', testClassName); - const testContainer = myp5.createDiv(); + const testContainer = mockP5Prototype.createDiv(); testButton.parent(testContainer); - const result = myp5.select(`.${testClassName}`, testContainer); + const result = mockP5Prototype.select(`.${testClassName}`, testContainer); assert.deepEqual(testButton.elt, result.elt); }); @@ -72,7 +80,7 @@ suite('DOM', function() { // Gives unused className and tests if it returns null const testClassName = 'test-button'; generateButton('Test Button', testClassName); - const result = myp5.select('.classNameWithTypo'); + const result = mockP5Prototype.select('.classNameWithTypo'); assert.isNull(result); }); @@ -80,7 +88,7 @@ suite('DOM', function() { // Creates 2 similar elements and tests if it selects only one. const testButton = generateButton('Button 1'); generateButton('Button 2'); - const result = myp5.select('button'); + const result = mockP5Prototype.select('button'); assert.deepEqual(result.elt, testButton.elt); }); @@ -89,30 +97,19 @@ suite('DOM', function() { // selects inside the container generateButton('Button 1'); const testButton = generateButton('Button 2'); - const testDiv = myp5.createDiv(); + const testDiv = mockP5Prototype.createDiv(); testButton.parent(testDiv); - const result = myp5.select('button', testDiv); + const result = mockP5Prototype.select('button', testDiv); assert.deepEqual(result.elt, testButton.elt); }); test('should return null when no matches are found by tag name', function() { generateButton('Test Button'); - const result = myp5.select('video', myp5Container); + const result = mockP5Prototype.select('video', myp5Container); assert.isNull(result); }); - const generateDiv = (id = null, className = null) => { - const div = myp5.createDiv(); - if (id) { - div.id(id); - } - if (className) { - div.class(className); - } - return div; - }; - test('should select element in container using CSS selector with ID', function() { const divID = 'divId'; const testDiv = generateDiv(divID); @@ -120,7 +117,7 @@ suite('DOM', function() { generateButton('Button 2'); testButton.parent(testDiv); - const result = myp5.select(`#${divID} button`); + const result = mockP5Prototype.select(`#${divID} button`); assert.deepEqual(result.elt, testButton.elt); }); @@ -131,51 +128,37 @@ suite('DOM', function() { generateButton('Button 2'); testButton.parent(testDiv); - const result = myp5.select(`.${divClass} button`); + const result = mockP5Prototype.select(`.${divClass} button`); assert.deepEqual(result.elt, testButton.elt); }); }); suite('p5.prototype.selectAll', function() { - let myp5; - let myp5Container; - beforeEach(function() { - return new Promise(resolve => { - myp5Container = document.createElement('div'); - document.body.appendChild(myp5Container); - new p5(function(p) { - p.setup = function() { - myp5 = p; - let mydiv = document.createElement('div'); - mydiv.setAttribute('id', 'main'); - let childbutton = document.createElement('button'); - childbutton.setAttribute('class', 'p5button'); - mydiv.append(childbutton); - let otherbutton = document.createElement('button'); - otherbutton.setAttribute('class', 'p5button'); - myp5Container.append(mydiv, otherbutton); - resolve(); - }; - }, myp5Container); - }); + const mydiv = document.createElement('div'); + mydiv.setAttribute('id', 'main'); + + const childbutton = document.createElement('button'); + childbutton.setAttribute('class', 'p5button'); + mydiv.append(childbutton); + + const otherbutton = document.createElement('button'); + otherbutton.setAttribute('class', 'p5button'); + + document.body.append(mydiv, otherbutton); }); afterEach(function() { - myp5.remove(); - if (myp5Container && myp5Container.parentNode) { - myp5Container.parentNode.removeChild(myp5Container); - } - myp5Container = null; + document.body.innerHTML = ""; }); test('should return an array', function() { - const elements = myp5.selectAll('button'); + const elements = mockP5Prototype.selectAll('button'); assert.isArray(elements); }); test('should return empty array when no matching classes are found', function() { - const elements = myp5.selectAll('.randomElements'); + const elements = mockP5Prototype.selectAll('.randomElements'); assert.isArray(elements); assert.lengthOf(elements, 0); }); @@ -189,15 +172,15 @@ suite('DOM', function() { test('should find all elements with matching class name', function() { const testClassName = 'p5button'; - const p5Results = myp5.selectAll(`.${testClassName}`); - const domResults = myp5Container.getElementsByClassName(testClassName); + const p5Results = mockP5Prototype.selectAll(`.${testClassName}`); + const domResults = document.getElementsByClassName(testClassName); matchResults(p5Results, domResults); }); test('should find all elements with matching class name in given container', function() { const testClassName = 'p5button'; const parentContainerId = 'main'; - const p5Results = myp5.selectAll( + const p5Results = mockP5Prototype.selectAll( `.${testClassName}`, `#${parentContainerId}` ); @@ -208,15 +191,15 @@ suite('DOM', function() { test('should find all elements with matching tag name', function() { const testTagName = 'button'; - const p5Results = myp5.selectAll(testTagName); - const domResults = myp5Container.getElementsByTagName(testTagName); + const p5Results = mockP5Prototype.selectAll(testTagName); + const domResults = document.getElementsByTagName(testTagName); matchResults(p5Results, domResults); }); test('should find all elements with matching tag name in given container', function() { const testTagName = 'button'; const parentContainerId = 'main'; - const p5Results = myp5.selectAll(testTagName, `#${parentContainerId}`); + const p5Results = mockP5Prototype.selectAll(testTagName, `#${parentContainerId}`); const containerElement = document.getElementById(parentContainerId); const domResults = containerElement.getElementsByTagName(testTagName); matchResults(p5Results, domResults); @@ -225,56 +208,15 @@ suite('DOM', function() { test('should find all elements in container using CSS selector with id', function() { const testTagName = 'button'; const parentContainerId = 'main'; - const p5Results = myp5.selectAll(`#${parentContainerId} ${testTagName}`); + const p5Results = mockP5Prototype.selectAll(`#${parentContainerId} ${testTagName}`); const containerElement = document.getElementById(parentContainerId); const domResults = containerElement.getElementsByTagName(testTagName); matchResults(p5Results, domResults); }); }); - suite('p5.prototype.removeElements', function() { - let myp5; - let myp5Container; - - beforeEach(function() { - myp5Container = document.createElement('div'); - document.body.appendChild(myp5Container); - new p5(function(p) { - p.setup = function() { - // configure p5 to not add a canvas by default. - p.noCanvas(); - myp5 = p; - }; - }, myp5Container); - }); - - afterEach(function() { - myp5.remove(); - if (myp5Container && myp5Container.parentNode) { - myp5Container.parentNode.removeChild(myp5Container); - } - myp5Container = null; - }); - - 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) { - myp5.createElement(tag); - } - // Check if all elements are created. - assert.deepEqual(myp5Container.childElementCount, tags.length); - - // Call removeElements and check if only canvas is remaining - myp5.removeElements(); - assert.deepEqual(myp5Container.childElementCount, 1); - const remainingElement = myp5Container.children[0]; - assert.instanceOf(remainingElement, HTMLCanvasElement); - }); - }); - - suite('p5.Element.prototype.changed', function() { + // Events + suite.todo('p5.Element.prototype.changed', function() { testSketchWithPromise( 'should trigger callback when element changes', function(sketch, resolve, reject) { @@ -301,7 +243,7 @@ suite('DOM', function() { ); }); - suite('p5.Element.prototype.input', function() { + suite.todo('p5.Element.prototype.input', function() { testSketchWithPromise( 'should trigger callback when input is provided', function(sketch, resolve, reject) { @@ -328,206 +270,169 @@ suite('DOM', function() { ); }); - suite('p5.prototype.createDiv', function() { - let myp5; - let testElement; + suite.todo('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'); - beforeEach(function() { - new p5(function(p) { - p.setup = function() { - myp5 = p; + // 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); + }; }); + }); + // Add/remove elements + suite('p5.prototype.createDiv', function() { 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.createDiv); + assert.isFunction(mockP5Prototype.createDiv); }); test('should return a p5.Element of div type', function() { - testElement = myp5.createDiv(); - assert.instanceOf(testElement, p5.Element); + const testElement = mockP5Prototype.createDiv(); + assert.instanceOf(testElement, Element); assert.instanceOf(testElement.elt, HTMLDivElement); }); test('should set given param as innerHTML of div', function() { const testHTML = '

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); }