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 () {