From e8e2c288da57396546d9645acd1267c48fef9502 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 16 Jan 2025 16:44:02 -0500 Subject: [PATCH] Implement splineProperty --- src/core/constants.js | 6 +- src/core/p5.Renderer.js | 26 +++++--- src/shape/curves.js | 59 +----------------- src/shape/custom_shapes.js | 53 ++++++++-------- src/webgl/p5.RendererGL.js | 30 --------- test/unit/core/curves.js | 6 +- test/unit/visual/cases/shapes.js | 6 +- test/unit/visual/cases/typography.js | 2 +- .../000.png | Bin 839 -> 825 bytes .../000.png | Bin 771 -> 709 bytes test/unit/visual/visualTest.js | 20 +++--- 11 files changed, 73 insertions(+), 135 deletions(-) diff --git a/src/core/constants.js b/src/core/constants.js index 0885ab3e48..6c36301c70 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -1347,7 +1347,7 @@ export const FLOAT = 'float'; export const HALF_FLOAT = 'half-float'; /** - * The `splineEnds` mode where splines curve through + * The `splineProperty('ends')` mode where splines curve through * their first and last points. * @typedef {unique symbol} INCLUDE * @property {INCLUDE} INCLUDE @@ -1356,7 +1356,7 @@ export const HALF_FLOAT = 'half-float'; export const INCLUDE = Symbol('include'); /** - * The `splineEnds` mode where the first and last points in a spline + * The `splineProperty('ends')` 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 @@ -1365,7 +1365,7 @@ export const INCLUDE = Symbol('include'); export const EXCLUDE = Symbol('exclude'); /** - * The `splineEnds` mode where the spline loops back to its first point. + * The `splineProperty('ends')` mode where the spline loops back to its first point. * Only used internally. * @typedef {unique symbol} JOIN * @property {JOIN} JOIN diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index bcc69ed086..e5ba194d63 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -10,6 +10,18 @@ import { Image } from '../image/p5.Image'; import { Vector } from '../math/p5.Vector'; import { Shape } from '../shape/custom_shapes'; +class ClonableObject { + constructor(obj = {}) { + for (const key in obj) { + this[key] = obj[key]; + } + } + + clone() { + return new ClonableObject(this); + } +}; + class Renderer { static states = { strokeColor: null, @@ -30,7 +42,7 @@ class Renderer { textAlign: constants.LEFT, textBaseline: constants.BASELINE, bezierOrder: 3, - splineEnds: constants.INCLUDE, + splineProperties: new ClonableObject({ ends: constants.INCLUDE, tightness: 0 }), textWrap: constants.WORD, // added v2.0 @@ -77,7 +89,6 @@ class Renderer { this._clipping = false; this._clipInvert = false; - this._curveTightness = 0; this._currentShape = undefined; // Lazily generate current shape } @@ -150,11 +161,11 @@ class Renderer { this.currentShape.bezierVertex(position, textureCoordinates); } - splineEnds(mode) { - if (mode === undefined) { - return this.states.splineEnds; + splineProperty(key, value) { + if (value === undefined) { + return this.states.splineProperties[key]; } else { - this.states.splineEnds = mode; + this.states.splineProperties[key] = value; } this.updateShapeProperties(); } @@ -305,7 +316,8 @@ class Renderer { updateShapeProperties() { this.currentShape.bezierOrder(this.states.bezierOrder); - this.currentShape.splineEnds(this.states.splineEnds); + this.currentShape.splineProperty('ends', this.states.splineProperties.ends); + this.currentShape.splineProperty('tightness', this.states.splineProperties.tightness); } updateShapeVertexProperties() { diff --git a/src/shape/curves.js b/src/shape/curves.js index 93a19c1d38..370011152e 100644 --- a/src/shape/curves.js +++ b/src/shape/curves.js @@ -765,61 +765,6 @@ function curves(p5, fn){ return this; }; - /** - * Adjusts the way curve() and - * 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. - * - * The parameter, `tightness`, determines how the curve fits to the vertex - * points. By default, `tightness` is set to 0. Setting tightness to 1, - * as in `curveTightness(1)`, connects the curve's points using straight - * lines. Values in the range from –5 to 5 deform curves while leaving them - * recognizable. - * - * @method curveTightness - * @param {Number} amount amount of tightness. - * @chainable - * - * @example - *
- * - * // Move the mouse left and right to see the curve change. - * - * function setup() { - * createCanvas(100, 100); - * - * describe('A black curve forms a sideways U shape. The curve deforms as the user moves the mouse from left to right'); - * } - * - * function draw() { - * background(200); - * - * // Set the curve's tightness using the mouse. - * let t = map(mouseX, 0, 100, -5, 5, true); - * curveTightness(t); - * - * // Draw the curve. - * noFill(); - * beginShape(); - * splineVertex(10, 26); - * splineVertex(10, 26); - * splineVertex(83, 24); - * splineVertex(83, 61); - * splineVertex(25, 65); - * splineVertex(25, 65); - * endShape(); - * } - * - *
- */ - fn.curveTightness = function(t) { - // p5._validateParameters('curveTightness', arguments); - this._renderer._curveTightness = t; - return this; - }; - /** * Calculates coordinates along a spline curve using interpolation. * @@ -934,7 +879,7 @@ function curves(p5, fn){ */ fn.curvePoint = function(a, b, c, d, t) { // p5._validateParameters('curvePoint', arguments); - const s = this._renderer._curveTightness, + const s = this._renderer.states.splineProperties.tightness, t3 = t * t * t, t2 = t * t, f1 = (s - 1) / 2 * t3 + (1 - s) * t2 + (s - 1) / 2 * t, @@ -1051,7 +996,7 @@ function curves(p5, fn){ fn.curveTangent = function(a, b, c, d, t) { // p5._validateParameters('curveTangent', arguments); - const s = this._renderer._curveTightness, + const s = this._renderer.states.splineProperties.tightness, tt3 = t * t * 3, t2 = t * 2, f1 = (s - 1) / 2 * tt3 + (1 - s) * t2 + (s - 1) / 2, diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 84724486c4..58465a2298 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -280,8 +280,10 @@ to interpolated endpoints (a breaking change) */ class SplineSegment extends Segment { #vertexCapacity = Infinity; - _splineEnds = constants.INCLUDE; - _splineTightness = 0; + _splineProperties = { + ends: constants.INCLUDE, + tightness: 0 + }; get vertexCapacity() { return this.#vertexCapacity; @@ -296,7 +298,7 @@ class SplineSegment extends Segment { } get canOverrideAnchor() { - return this._splineEnds === constants.EXCLUDE; + return this._splineProperties.ends === constants.EXCLUDE; } // assuming for now that the first interpolated vertex is always @@ -304,7 +306,7 @@ class SplineSegment extends Segment { // if this spline segment doesn't follow another segment, // the first vertex is in an anchor get _firstInterpolatedVertex() { - if (this._splineEnds === constants.EXCLUDE) { + if (this._splineProperties.ends === constants.EXCLUDE) { return this._comesAfterSegment ? this.vertices[1] : this.vertices[0]; @@ -328,10 +330,10 @@ class SplineSegment extends Segment { // doesn't line up with end of last segment addToShape(shape) { const added = super.addToShape(shape); - this._splineEnds = shape._splineEnds; - this._splineTightness = shape._splineTightness; + this._splineProperties.ends = shape._splineProperties.ends; + this._splineProperties.tightness = shape._splineProperties.tightness; - if (this._splineEnds !== constants.EXCLUDE) return added; + if (this._splineProperties.ends !== constants.EXCLUDE) return added; let verticesPushed = !this._belongsToShape; let lastPrimitive = shape.at(-1, -1); @@ -367,9 +369,9 @@ class SplineSegment extends Segment { // override method on base class getEndVertex() { - if (this._splineEnds === constants.INCLUDE) { + if (this._splineProperties.ends === constants.INCLUDE) { return super.getEndVertex(); - } else if (this._splineEnds === constants.EXCLUDE) { + } else if (this._splineProperties.ends === constants.EXCLUDE) { return this.vertices.at(-2); } else { return this.getStartVertex(); @@ -389,10 +391,10 @@ class SplineSegment extends Segment { } const prevVertex = this.getStartVertex(); - if (this._splineEnds === constants.INCLUDE) { + if (this._splineProperties.ends === constants.INCLUDE) { points.unshift(prevVertex); points.push(this.vertices.at(-1)); - } else if (this._splineEnds === constants.JOIN) { + } else if (this._splineProperties.ends === constants.JOIN) { points.unshift(this.vertices.at(-1), prevVertex); points.push(prevVertex, this.vertices.at(0)); } @@ -410,7 +412,7 @@ class SplineSegment extends Segment { } close() { - this._splineEnds = constants.JOIN; + this._splineProperties.ends = constants.JOIN; } } @@ -581,10 +583,12 @@ class Shape { #initialVertexProperties; #primitiveShapeCreators; #bezierOrder = 3; - _splineTightness = 0; kind = null; contours = []; - _splineEnds = constants.INCLUDE; + _splineProperties = { + tightness: 0, + ends: constants.INCLUDE + }; userVertexProperties = null; constructor( @@ -828,12 +832,8 @@ class Shape { this.#bezierOrder = order; } - splineEnds(mode) { - this._splineEnds = mode; - } - - splineTightness(tightness) { - this._splineTightness = tightness; + splineProperty(key, value) { + this._splineProperties[key] = value; } /* @@ -1076,7 +1076,7 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { const shape = splineSegment._shape; if ( - splineSegment._splineEnds === constants.EXCLUDE && + splineSegment._splineProperties.ends === constants.EXCLUDE && !splineSegment._comesAfterSegment ) { let startVertex = splineSegment._firstInterpolatedVertex; @@ -1088,7 +1088,7 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { ); let bezierArrays = shape.catmullRomToBezier( arrayVertices, - splineSegment._splineTightness + splineSegment._splineProperties.tightness ).map(arr => arr.map(vertArr => shape.arrayToVertex(vertArr))); for (const array of bezierArrays) { const points = array.flatMap(vert => [vert.position.x, vert.position.y]); @@ -1217,7 +1217,7 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { ); let bezierArrays = shape.catmullRomToBezier( arrayVertices, - splineSegment._splineTightness + splineSegment._splineProperties.tightness ); let startVertex = shape.vertexToArray(splineSegment._firstInterpolatedVertex); for (const array of bezierArrays) { @@ -1596,10 +1596,11 @@ function customShapes(p5, fn) { /** * TODO: documentation - * @param {SHOW|HIDE} mode + * @param {String} key + * @param value */ - fn.splineEnds = function(mode) { - return this._renderer.splineEnds(mode); + fn.splineProperty = function(key, value) { + return this._renderer.splineProperty(key, value); }; /** diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b6b90984af..b6ab38c9c3 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -407,8 +407,6 @@ class RendererGL extends Renderer { this.filterLayerTemp = undefined; this.defaultFilterShaders = {}; - this._curveTightness = 6; - this.fontInfos = {}; this._curShader = undefined; @@ -2409,34 +2407,6 @@ class RendererGL extends Renderer { _vToNArray(arr) { return arr.flatMap((item) => [item.x, item.y, item.z]); } - - // function to calculate BezierVertex Coefficients - _bezierCoefficients(t) { - const t2 = t * t; - const t3 = t2 * t; - const mt = 1 - t; - const mt2 = mt * mt; - const mt3 = mt2 * mt; - return [mt3, 3 * mt2 * t, 3 * mt * t2, t3]; - } - - // function to calculate QuadraticVertex Coefficients - _quadraticCoefficients(t) { - const t2 = t * t; - const mt = 1 - t; - const mt2 = mt * mt; - return [mt2, 2 * mt * t, t2]; - } - - // function to convert Bezier coordinates to Catmull Rom Splines - _bezierToCatmull(w) { - const p1 = w[1]; - const p2 = w[1] + (w[2] - w[0]) / this._curveTightness; - const p3 = w[2] - (w[3] - w[1]) / this._curveTightness; - const p4 = w[2]; - const p = [p1, p2, p3, p4]; - return p; - } } function rendererGL(p5, fn) { diff --git a/test/unit/core/curves.js b/test/unit/core/curves.js index d389e0fb9d..4840e273b1 100644 --- a/test/unit/core/curves.js +++ b/test/unit/core/curves.js @@ -4,7 +4,11 @@ import curves from '../../../src/shape/curves'; suite('Curves', function() { beforeAll(function() { mockP5Prototype._renderer = { - _curveTightness: 0 + states: { + splineProperties: { + tightness: 0 + } + } }; curves(mockP5, mockP5Prototype); }); diff --git a/test/unit/visual/cases/shapes.js b/test/unit/visual/cases/shapes.js index 61f8c0d3d9..8e7835a604 100644 --- a/test/unit/visual/cases/shapes.js +++ b/test/unit/visual/cases/shapes.js @@ -128,7 +128,7 @@ visualSuite('Shape drawing', function() { visualTest('Drawing with curves with hidden ends', function(p5, screenshot) { setup(p5); p5.beginShape(); - p5.splineEnds(p5.EXCLUDE); + p5.splineProperty('ends', p5.EXCLUDE); p5.splineVertex(10, 10); p5.splineVertex(15, 40); p5.splineVertex(40, 35); @@ -152,7 +152,7 @@ visualSuite('Shape drawing', function() { visualTest('Drawing with curves with tightness', function(p5, screenshot) { setup(p5); - p5.curveTightness(0.5); + p5.splineProperty('tightness', -1); p5.beginShape(); p5.splineVertex(10, 10); p5.splineVertex(15, 40); @@ -166,7 +166,7 @@ visualSuite('Shape drawing', function() { visualTest('Drawing closed curve loops', function(p5, screenshot) { setup(p5); p5.beginShape(); - p5.splineEnds(p5.EXCLUDE); + p5.splineProperty('ends', p5.EXCLUDE); p5.splineVertex(10, 10); p5.splineVertex(15, 40); p5.splineVertex(40, 35); diff --git a/test/unit/visual/cases/typography.js b/test/unit/visual/cases/typography.js index 3bf1332344..df0450f0b2 100644 --- a/test/unit/visual/cases/typography.js +++ b/test/unit/visual/cases/typography.js @@ -543,4 +543,4 @@ visualSuite("Typography", function () { screenshot(); }); }); -}); +}, { shiftThreshold: 3 }); diff --git a/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves with tightness/000.png b/test/unit/visual/screenshots/Shape drawing/2D mode/Drawing with curves with tightness/000.png index 593867d9c8a3e3a4c4a35431219eef9e038d8616..36ee10117a1850bf4af80c7d3a182571a3224412 100644 GIT binary patch delta 790 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 { + let lastShiftThreshold let lastPrefix; let lastDeviceRatio = window.devicePixelRatio; beforeAll(() => { lastPrefix = namePrefix; namePrefix += escapeName(name) + '/'; + lastShiftThreshold = shiftThreshold; + if (newShiftThreshold !== undefined) { + shiftThreshold = newShiftThreshold + } // Force everything to be 1x window.devicePixelRatio = 1; @@ -76,6 +81,7 @@ export function visualSuite( afterAll(() => { namePrefix = lastPrefix; window.devicePixelRatio = lastDeviceRatio; + shiftThreshold = lastShiftThreshold; }); }); } @@ -109,7 +115,7 @@ export async function checkMatch(actual, expected, p5) { cnv.image(actual, 0, 0); cnv.blendMode(DIFFERENCE); cnv.image(expectedWithBg, 0, 0); - for (let i = 0; i < SHIFT_THRESHOLD; i++) { + for (let i = 0; i < shiftThreshold; i++) { cnv.filter(ERODE, false); } const diff = cnv.get();