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 8390b6e72f..702811727d 100644 --- a/preview/index.html +++ b/preview/index.html @@ -20,30 +20,30 @@ import p5 from '../src/app.js'; const sketch = function (p) { - let f; - const testWebgl = true - - p.setup = async function () { - // TODO: make this work without a name - f = await p.loadFont('font/Lato-Black.ttf', 'Lato') - p.createCanvas(200, 200, testWebgl ? p.WEBGL : undefined); + p.setup = function () { + p.createCanvas(100, 100, p.WEBGL); }; p.draw = function () { - p.background(0, 50, 50); - if (testWebgl) p.translate(-p.width/2, -p.height/2); - - p.fill('white'); - p.textSize(60); - p.textAlign(p.RIGHT, p.CENTER) - p.textFont(f) - p.text('hello, world!', 0, p.height/2, p.width); + 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(); }; }; new p5(sketch); -

hello, world!

\ No newline at end of file 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 37aae7c13f..b7b935dbb0 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -414,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 4f7813d4b0..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,14 +28,15 @@ 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, + strokeWeight: 1, textFont: { family: 'sans-serif' }, textLeading: 15, @@ -40,6 +44,9 @@ class Renderer { textSize: 12, textAlign: constants.LEFT, textBaseline: constants.BASELINE, + bezierOrder: 3, + splineEnds: constants.INCLUDE, + textWrap: constants.WORD, // added v2.0 @@ -57,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() { @@ -99,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 = {}) { @@ -161,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) { @@ -262,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; } diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 0c1e3c08fe..6213b08228 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -5,6 +5,7 @@ import { Graphics } from './p5.Graphics'; import { Image } from '../image/p5.Image'; import { Element } from '../dom/p5.Element'; import { MediaElement } from '../dom/p5.MediaElement'; +import { PrimitiveToPath2DConverter } from '../shape/custom_shapes'; const styleEmpty = 'rgba(0,0,0,0)'; // const alphaThreshold = 0.00125; // minimum visible @@ -68,6 +69,8 @@ class Renderer2D extends Renderer { // Set and return p5.Element this.wrappedElt = new Element(this.elt, this._pInst); + + this.clipPath = null; } remove(){ @@ -201,7 +204,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 @@ -212,7 +215,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 @@ -252,6 +255,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); @@ -270,36 +288,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(); @@ -654,7 +673,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. @@ -672,7 +691,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) { @@ -692,7 +711,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) { @@ -716,9 +735,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]), @@ -749,8 +768,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; @@ -763,8 +782,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; @@ -784,9 +803,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; @@ -820,9 +839,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; @@ -891,10 +910,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; @@ -902,9 +921,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], @@ -933,270 +952,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 ////////////////////////////////////////////// @@ -1224,6 +979,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; @@ -1274,30 +1030,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 ////////////////////////////////////////////// @@ -1352,11 +1092,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); 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/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/text2d.js b/src/type/text2d.js index 23e9c49d89..446bf496b4 100644 --- a/src/type/text2d.js +++ b/src/type/text2d.js @@ -1096,11 +1096,11 @@ function text2d(p5, fn) { this.push(); // no stroke unless specified by user - if (states.doStroke && states.strokeSet) { + if (states.strokeColor && states.strokeSet) { this.textDrawingContext().strokeText(text, x, y); } - if (!this._clipping && states.doFill) { + if (!this._clipping && states.fillColor) { // if fill hasn't been set by user, use default text fill if (!states.fillSet) { 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 288d83f822..4a42d117fe 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 b695e0d967..8f407a5c2f 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()), ]; // apply blur shader with multiple passes. @@ -1100,19 +1197,25 @@ class RendererGL extends Renderer { this.matchSize(tmp, target); // setup this.push(); - this.states.doStroke = false; + this.states.strokeColor = null; this.blendMode(constants.BLEND); // 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(); @@ -1121,8 +1224,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(); @@ -1134,23 +1237,25 @@ class RendererGL extends Renderer { // every other non-blur shader uses single pass else { fbo.draw(() => { - this.states.doStroke = false; + this.states.strokeColor = null; this.blendMode(constants.BLEND); 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(); this.states.imageMode = constants.CORNER; @@ -1161,10 +1266,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(); @@ -1204,7 +1313,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.", ); } } @@ -1252,19 +1361,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() { @@ -1274,12 +1383,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); @@ -1299,51 +1408,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; @@ -1354,7 +1418,7 @@ class RendererGL extends Renderer { y, gl.RGBA, gl.UNSIGNED_BYTE, - this._pInst.height * this._pInst.pixelDensity() + this._pInst.height * this._pInst.pixelDensity(), ); } @@ -1369,7 +1433,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; } @@ -1377,19 +1442,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() { @@ -1400,7 +1464,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); @@ -1418,14 +1492,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 ////////////////////////////////////////////// @@ -1452,7 +1524,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; } } @@ -1470,21 +1542,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) { @@ -1552,10 +1620,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, ]); } } @@ -1593,7 +1673,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); @@ -1662,19 +1742,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; } @@ -1709,7 +1789,6 @@ class RendererGL extends Renderer { return this._getColorShader(); } - _getPointShader() { // select the point shader to use const point = this.states.userPointShader; @@ -1722,7 +1801,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(); @@ -1733,25 +1812,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; @@ -1760,18 +1839,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, ); } } @@ -1787,27 +1866,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": "() {}", + }, + }, ); } @@ -1822,27 +1901,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": "() {}", + }, + }, ); } @@ -1881,25 +1960,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; @@ -1913,29 +1992,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": "() {}", + }, + }, ); } @@ -1945,32 +2024,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`; @@ -2028,20 +2103,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); }); @@ -2069,13 +2146,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 @@ -2090,9 +2169,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); }); @@ -2122,43 +2201,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) { @@ -2168,54 +2250,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... @@ -2223,55 +2312,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, ); } @@ -2279,13 +2380,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) { @@ -2314,8 +2409,8 @@ class RendererGL extends Renderer { Float64Array, Int16Array, Uint16Array, - Uint32Array - ].some(x => arr instanceof x); + Uint32Array, + ].some((x) => arr instanceof x); } /** @@ -2327,7 +2422,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 @@ -2357,10 +2452,9 @@ class RendererGL extends Renderer { const p = [p1, p2, p3, p4]; return p; } +} -}; - -function rendererGL(p5, fn){ +function rendererGL(p5, fn) { p5.RendererGL = RendererGL; /** @@ -2518,15 +2612,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 = {}; @@ -2551,8 +2645,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; } @@ -2573,7 +2667,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.`, ); }; @@ -2606,7 +2700,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 @@ -2624,12 +2718,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 @@ -2663,15 +2757,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); @@ -2681,11 +2767,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); @@ -2696,6 +2778,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/text.js b/src/webgl/text.js index 9fa77b6b45..fbe377fe1d 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -679,7 +679,7 @@ function text(p5, fn){ ); return; } - if (y > maxY || y < minY || !this.states.doFill) { + if (y >= maxY || !this.states.fillColor) { return; // don't render lines beyond our maxY position } @@ -693,10 +693,10 @@ function text(p5, fn){ 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 @@ -790,7 +790,7 @@ function text(p5, fn){ // clean up sh.unbindShader(); - this.states.doStroke = doStroke; + this.states.strokeColor = doStroke; this.states.drawMode = drawMode; gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); 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/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/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/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/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/webgl.js b/test/unit/visual/cases/webgl.js index 8b77b6cc46..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); 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/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 76ef1d98a2..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/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index bf657540ea..9c67afa893 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()' ); }); @@ -638,10 +638,22 @@ 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() { @@ -1465,33 +1477,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], @@ -1591,33 +1603,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); @@ -1630,39 +1642,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( @@ -1744,55 +1756,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: @@ -1806,20 +1822,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( @@ -1892,16 +1908,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( @@ -1939,91 +1955,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); @@ -2580,13 +2524,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({ @@ -2594,23 +2543,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', @@ -2624,28 +2556,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', @@ -2675,15 +2606,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 { @@ -2694,15 +2625,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); }