diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index e7a90d4218..efa7a02b62 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -2314,27 +2314,40 @@ class Vector { } /** - * Calculates new `x`, `y`, and `z` components that are proportionally the + * Calculates new vector components that are proportionally the * same distance between two vectors. * + * `lerp()` can use separate numbers, as in `v.lerp(3, 3, 0.5)`, another + * p5.Vector object, as in `v.lerp(v2, 0.5)`, or an + * array of numbers, as in `v.lerp([3, 3], 0.5)`. When using numbers, the last + * argument is always the interpolation amount. + * * The `amt` parameter is the amount to interpolate between the old vector and * the new vector. 0.0 keeps all components equal to the old vector's, 0.5 is * halfway between, and 1.0 sets all components equal to the new vector's. + * Values of `amt` outside the range 0 to 1 are allowed and extrapolate beyond + * the target. + * + * This method supports N-dimensional vectors. You should interpolate vectors + * only when they are the same size. When two vectors of different sizes are + * used, the smaller dimension will be used, and any additional values of the + * longer vector will be ignored. * * The static version of `lerp()`, as in `p5.Vector.lerp(v0, v1, 0.5)`, * returns a new p5.Vector object and doesn't change - * the original. + * the originals. * - * @param {Number} x x component. - * @param {Number} y y component. - * @param {Number} z z component. - * @param {Number} amt amount of interpolation between 0.0 (old vector) - * and 1.0 (new vector). 0.5 is halfway between. + * @param {...Number} values target vector components + * @param {Number} amt amount of interpolation between 0.0 (old vector) + * and 1.0 (new vector). 0.5 is halfway between. * @chainable * * @example - * // META:norender * function setup() { + * createCanvas(100, 100); + * + * background(200); + * * // Create a p5.Vector object. * let v0 = createVector(1, 1, 1); * let v1 = createVector(3, 3, 3); @@ -2342,26 +2355,76 @@ class Vector { * // Interpolate. * v0.lerp(v1, 0.5); * - * // Prints "p5.Vector Object : [2, 2, 2]" to the console. - * print(v0.toString()); + * // Display the result. + * textAlign(CENTER, CENTER); + * text(v0.toString(), 0, 0, width, height); + * + * describe('The text "p5.Vector Object : [2, 2, 2]" written on a gray square.'); + * } + * + * @example + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Vector object. + * let v = createVector(1, 1); + * + * // Interpolate with numbers. The last argument is the amount. + * v.lerp(3, 3, 0.5); + * + * // Display the result. + * textAlign(CENTER, CENTER); + * text(v.toString(), 0, 0, width, height); + * + * describe('The text "p5.Vector Object : [2, 2]" written on a gray square.'); + * } + * + * @example + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Vector object. + * let v = createVector(1, 1, 1); + * + * // Interpolate with an array. + * v.lerp([3, 3, 3], 0.5); + * + * // Display the result. + * textAlign(CENTER, CENTER); + * text(v.toString(), 0, 0, width, height); + * + * describe('The text "p5.Vector Object : [2, 2, 2]" written on a gray square.'); * } * * @example - * // META:norender * function setup() { + * createCanvas(100, 100); + * + * background(200); + * * // Create a p5.Vector object. * let v = createVector(1, 1, 1); * * // Interpolate. * v.lerp(3, 3, 3, 0.5); * - * // Prints "p5.Vector Object : [2, 2, 2]" to the console. - * print(v.toString()); + * // Display the result. + * textAlign(CENTER, CENTER); + * text(v.toString(), 0, 0, width, height); + * + * describe('The text "p5.Vector Object : [2, 2, 2]" written on a gray square.'); * } * * @example - * // META:norender * function setup() { + * createCanvas(100, 100); + * + * background(200); + * * // Create p5.Vector objects. * let v0 = createVector(1, 1, 1); * let v1 = createVector(3, 3, 3); @@ -2369,8 +2432,31 @@ class Vector { * // Interpolate. * let v2 = p5.Vector.lerp(v0, v1, 0.5); * - * // Prints "p5.Vector Object : [2, 2, 2]" to the console. - * print(v2.toString()); + * // Display the result. + * textAlign(CENTER, CENTER); + * text(v2.toString(), 0, 0, width, height); + * + * describe('The text "p5.Vector Object : [2, 2, 2]" written on a gray square.'); + * } + * + * @example + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create p5.Vector objects. + * let v0 = createVector(0, 1, 0, 1); + * let v1 = createVector(1, 0, 1, 0); + * + * // Interpolate. + * v0.lerp(v1, 0.5); + * + * // Display the result. + * textAlign(CENTER, CENTER); + * text(v0.toString(), 0, 0, width, height); + * + * describe('The text "p5.Vector Object : [0.5, 0.5, 0.5, 0.5]" written on a gray square.'); * } * * @example @@ -2415,18 +2501,23 @@ class Vector { * pop(); * } */ + /** + * @param {Number[]} arr array to lerp towards. + * @param {Number} amt + * @chainable + */ /** * @param {p5.Vector} v p5.Vector to lerp toward. * @param {Number} amt * @chainable */ - lerp(x, y, z, amt) { - if (x instanceof Vector) { - return this.lerp(x.x, x.y, x.z, y); + lerp(values, amt) { + const minDimension = prioritizeSmallerDimension(this.dimensions, values); + shrinkToDimension(this.values, minDimension); + + for (let i = 0; i < this.values.length; i++) { + this.values[i] += (values[i] - this.values[i]) * amt; } - this.x += (x - this.x) * amt || 0; - this.y += (y - this.y) * amt || 0; - this.z += (z - this.z) * amt || 0; return this; } diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index d2121101f5..1bf7348c41 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -1,5 +1,13 @@ import { Vector } from './p5.Vector.js'; +function _vectorError(instance, message = 'Requires valid arguments') { + if (!instance.friendlyErrorsDisabled()) { + instance._friendlyError(message, 'p5.Vector'); + } + + return instance; +} + /** * @private * @internal @@ -25,17 +33,69 @@ export function _defaultEmptyVector(target){ /** * @private * @internal + * @param {Boolean} expectsSoloNumberArgument whether a single number scales + * every component (mult/div/rem) + * @param {Number} [trailingArgCount=0] number of trailing args to peel off + * before treating the rest as vector components (e.g. 1 for lerp's amt) + * @param {Number[]} [trailingDefaults] if provided, used when only component + * values are passed (no trailing args), e.g. `[0]` so `v.lerp(other)` uses amt 0 */ -export function _validatedVectorOperation(expectsSoloNumberArgument){ +export function _validatedVectorOperation( + expectsSoloNumberArgument, + trailingArgCount = 0, + trailingDefaults +){ return function(target){ return function (...args) { - if (args.length === 0) { + const trailing = []; + + if (trailingArgCount > 0) { + const onlyValues = + args.length === 1 && + (args[0] instanceof Vector || Array.isArray(args[0])); + + if (onlyValues && trailingDefaults) { + for (let i = 0; i < trailingDefaults.length; i++) { + trailing.push(trailingDefaults[i]); + } + } else if (args.length < trailingArgCount) { + if (trailingDefaults && args.length === 0) { + // No arguments? No action (same as other vector ops) + return this; + } + return _vectorError(this); + } else { + for (let i = 0; i < trailingArgCount; i++) { + trailing.unshift(args.pop()); + } + + for (let i = 0; i < trailing.length; i++) { + const t = trailing[i]; + if (typeof t !== 'number' || !Number.isFinite(t)) { + return _vectorError(this, 'Arguments contain non-finite numbers'); + } + } + + if (args.length === 0) { + return _vectorError(this); + } + } + } else if (args.length === 0) { // No arguments? No action return this; - } else if (args[0] instanceof Vector) { + } + + if (args[0] instanceof Vector) { + // Do not allow extra args after a vector when trailing args are used + if (trailingArgCount > 0 && args.length > 1) { + return _vectorError(this); + } // First argument is a vector? Make it an array args = args[0].values; } else if (Array.isArray(args[0])) { + if (trailingArgCount > 0 && args.length > 1) { + return _vectorError(this); + } // First argument is an array? Great, keep it! args = args[0]; } else if (args.length === 1){ @@ -49,28 +109,16 @@ export function _validatedVectorOperation(expectsSoloNumberArgument){ for (let i = 0; i < args.length; i++) { const v = args[i]; if (typeof v !== 'number' || !Number.isFinite(v)) { - if (!this.friendlyErrorsDisabled()) { - this._friendlyError( - 'Arguments contain non-finite numbers', - 'p5.Vector' - ); - } - return this; + return _vectorError(this, 'Arguments contain non-finite numbers'); } } } else { if (typeof args !== 'number' || !Number.isFinite(args)) { - if (!this.friendlyErrorsDisabled()) { - this._friendlyError( - 'Arguments contain non-finite numbers', - 'p5.Vector' - ); - } - return this; + return _vectorError(this, 'Arguments contain non-finite numbers'); } } - return target.call(this, args); + return target.call(this, args, ...trailing); }; }; } @@ -90,5 +138,6 @@ export default function vectorValidation(p5, fn, lifecycles){ p5.registerDecorator('p5.Vector.prototype.div', _validatedVectorOperation(true)); p5.registerDecorator('p5.Vector.prototype.add', _validatedVectorOperation(false)); p5.registerDecorator('p5.Vector.prototype.sub', _validatedVectorOperation(false)); + p5.registerDecorator('p5.Vector.prototype.lerp', _validatedVectorOperation(false, 1, [0])); } diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index df694d71a6..61425fbbc6 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -1,7 +1,6 @@ import { default as vector, Vector } from '../../../src/math/p5.Vector.js'; import { default as math } from '../../../src/math/math.js'; import { _defaultEmptyVector, _validatedVectorOperation } from '../../../src/math/patch-vector.js'; -import { vi } from 'vitest'; suite('p5.Vector', function () { @@ -35,6 +34,10 @@ suite('p5.Vector', function () { Vector.prototype.mult = _validatedVectorOperation(true)(Vector.prototype.mult, options); Vector.prototype.rem = _validatedVectorOperation(true)(Vector.prototype.rem, options); Vector.prototype.div = _validatedVectorOperation(true)(Vector.prototype.div, options); + Vector.prototype.lerp = _validatedVectorOperation(false, 1, [0])( + Vector.prototype.lerp, + options + ); }); afterEach(function () {}); @@ -1066,6 +1069,14 @@ suite('p5.Vector', function () { assert.deepEqual(v3.rem(v2).values, [0, 2]); expect(v3.rem(v2).dimensions).to.eql(2); }); + + test('should be prioritized in lerp()', function () { + assert.deepEqual(v1.lerp(v2, 0.5).values, [1.5]); + expect(v1.dimensions).to.eql(1); + + assert.deepEqual(v3.lerp(v2, 0.5).values, [3, 4]); + expect(v3.dimensions).to.eql(2); + }); }); suite('dot', function () { @@ -1483,23 +1494,24 @@ suite('p5.Vector', function () { }); suite('lerp', function () { + beforeEach(function () { + v = new Vector(0, 0, 0); + }); + test('should return the same object', function () { expect(v.lerp()).to.eql(v); }); suite('with p5.Vector', function() { - test('should call lerp with 4 arguments', function() { - vi.spyOn(v, 'lerp'); - v.lerp(new Vector(1,2,3), 1); - expect(v.lerp).toHaveBeenCalledWith(1, 2, 3, 1); + test('should lerp toward the vector by amt', function() { + const v2 = new Vector(2, 2, 2); + v.lerp(v2, 0.5); + expect(v.values).to.eql([1, 1, 1]); }); }); suite('with x, y, z, amt', function () { beforeEach(function () { - v.x = 0; - v.y = 0; - v.z = 0; v.lerp(2, 2, 2, 0.5); }); @@ -1516,15 +1528,93 @@ suite('p5.Vector', function () { }); }); - suite('with no amt', function () { - test('should assume 0 amt', function () { - v.x = 0; - v.y = 0; - v.z = 0; - v.lerp(2, 2, 2); - expect(v.x).to.eql(0); - expect(v.y).to.eql(0); - expect(v.z).to.eql(0); + suite('with x, y, z, w, amt', function () { + beforeEach(function () { + v = new Vector(0, 0, 0, 0); + v.lerp(2, 2, 2, 2, 0.5); + }); + + test('should lerp x by amt', function () { + expect(v.x).to.eql(1); + }); + + test('should lerp y by amt', function () { + expect(v.y).to.eql(1); + }); + + test('should lerp z by amt', function () { + expect(v.z).to.eql(1); + }); + + test('should lerp w by amt', function () { + expect(v.values[3]).to.eql(1); + }); + }); + + suite('with array', function () { + test('should lerp toward array components by amt', function () { + v.lerp([2, 2, 2], 0.5); + expect(v.values).to.eql([1, 1, 1]); + }); + }); + + suite('with 2D numeric components', function () { + test('should treat the last argument as amt', function () { + v = new Vector(1, 1); + v.lerp(3, 3, 0.5); + expect(v.values).to.eql([2, 2]); + }); + }); + + suite('with amt outside 0 to 1', function () { + test('should extrapolate when amt is greater than 1', function () { + v.lerp(2, 2, 2, 2); + expect(v.values).to.eql([4, 4, 4]); + }); + + test('should extrapolate when amt is less than 0', function () { + v.lerp(2, 2, 2, -1); + expect(v.values).to.eql([-2, -2, -2]); + }); + }); + + suite('with invalid arguments', function () { + test('should assume amt of 0 when only a vector is passed', function () { + const target = new Vector(2, 2, 2); + FESCalled = false; + v.set(0, 0, 0); + v.lerp(target); + expect(FESCalled).to.eql(false); + expect(v.values).to.eql([0, 0, 0]); + }); + + test('should assume amt of 0 when only an array is passed', function () { + FESCalled = false; + v.lerp([2, 2, 2]); + expect(FESCalled).to.eql(false); + expect(v.values).to.eql([0, 0, 0]); + }); + + test('should not change vector when amt is non-finite', function () { + FESCalled = false; + v.lerp(2, 2, 2, NaN); + expect(FESCalled).to.eql(true); + expect(v.values).to.eql([0, 0, 0]); + }); + + test('should not change vector when components are non-finite', function () { + FESCalled = false; + v.lerp(2, NaN, 2, 0.5); + expect(FESCalled).to.eql(true); + expect(v.values).to.eql([0, 0, 0]); + }); + + test('should not change vector with extra args after a vector', function () { + const target = new Vector(2, 2, 2); + FESCalled = false; + v.lerp(target, 1, 0.5); + expect(FESCalled).to.eql(true); + expect(v.values).to.eql([0, 0, 0]); }); }); }); @@ -1546,8 +1636,13 @@ suite('p5.Vector', function () { }); test('should return neither v1 nor v2', function () { - expect(res).to.not.eql(v1); - expect(res).to.not.eql(v2); + expect(res).to.not.equal(v1); + expect(res).to.not.equal(v2); + }); + + test('should not mutate v1 or v2', function () { + expect(v1.values).to.eql([0, 0, 0]); + expect(v2.values).to.eql([2, 2, 2]); }); test('should res to be [1, 1, 1]', function () { @@ -1557,6 +1652,50 @@ suite('p5.Vector', function () { }); }); + suite('v.lerp(v2, amt) on 4 dimensions', function () { + var res, v1, v2; + beforeEach(function () { + v1 = new Vector(0, 1, 0, 1); + v2 = new Vector(1, 0, 1, 0); + res = v1.lerp(v2, 0.5); + }); + + test('should return this', function () { + expect(res).to.equal(v1); + }); + + test('should not mutate the argument vector', function () { + expect(v2.values).to.eql([1, 0, 1, 0]); + }); + + test('should lerp all components', function () { + expect(res.values).to.eql([0.5, 0.5, 0.5, 0.5]); + }); + }); + + suite('p5.Vector.lerp(v1, v2, amt) on 4 dimensions', function () { + var res, v1, v2; + beforeEach(function () { + v1 = new Vector(0, 1, 0, 1); + v2 = new Vector(1, 0, 1, 0); + res = Vector.lerp(v1, v2, 0.5); + }); + + test('should return neither v1 nor v2', function () { + expect(res).to.not.equal(v1); + expect(res).to.not.equal(v2); + }); + + test('should not mutate v1 or v2', function () { + expect(v1.values).to.eql([0, 1, 0, 1]); + expect(v2.values).to.eql([1, 0, 1, 0]); + }); + + test('should lerp all components', function () { + expect(res.values).to.eql([0.5, 0.5, 0.5, 0.5]); + }); + }); + suite('v.slerp(w, amt)', function () { var w; beforeEach(function () {