[2.x] The `cross()` method of `p5.Vector` is incorrect in the 2.x context
The core problem: A 1.x feature became a 2.x bug
I'd like to propose a fix for p5.Vector.cross(). I know we've discussed this method before, and at the time, a change was deferred because it was considered a breaking change rather than a fix.
However, as I've been organizing the 2.0 stabilization work for p5.Vector, I've realized the context has fundamentally changed. The move from 1.x's 3D-padded vectors to 2.x's true n-dimensional vectors means the old 1.x behavior is no longer just confusing—it is now demonstrably incorrect and actively hides bugs. This new context promotes the issue from a feature request to a critical bug fix.
Here's a breakdown of the problem:
-
In 1.x (correct): All vectors were 3D, and
createVector(1, 0)was just a shortcut forcreateVector(1, 0, 0). Thecross()method always operated on two 3D vectors and returned a 3D vector. This was 100% correct. -
In 2.x (bugged): Now,
createVector(1, 0)creates a true 2D vector. The currentcross()implementation "fixes" this by silently padding this 2D vector back to 3D to perform the calculation.
This silent, implicit padding is the bug. It is incorrect for four reasons:
- It hides bugs that break sketches: This is the biggest issue. In 2.0, mixing 2D and 3D vectors is almost certainly a programming error. The current behavior hides this bug instead of throwing a friendly error that helps users find it.
- It's mathematically and programmatically non-standard: The vector cross product is only traditionally defined for two 3D vectors. No other major library (three.js, Godot, Blender) behaves as p5 does currently. They all correctly recognize that the 2D "cross product" is a different operation that returns a scalar. Rather than opening doors, p5-specific rules create obstacles.
-
It's internally inconsistent: This "pad-by-zero" logic contradicts the simple rules already in p5.js. For example,
mult(2)uses broadcasting (repeating the value2), not padding. This "pad-by-zero" rule forcross()introduces a second, conflicting system for handling dimension mismatches that no longer applies in 2.x, which is a classic source of user confusion and a violation of the Principle of Least Surprise. - It creates a documentation and learning hurdle: This behavior is nearly impossible to document clearly. Users would need to be told the method computes the result as if the inputs are padded with zeros, but in contrast with 1.x, the inputs are not actually padded (mutated) with zeros.
Proposed solution: A simple, clear, correct API
We can fix this by aligning with both mathematical and programming best practices.
-
Create a simple
scalarCross()method (2D input only):- Input: This method will only accept two 2D vectors.
-
Output: It will return a scalar (a
Number) representing a signed 2D area. -
Benefits: This creates a clear, readable, and discoverable API for a common 2D operation. It's far more intuitive than the 1.x pattern of
a.cross(b).z, which hid the operation's true purpose: it's a special 2D operation for things like determining winding order. It also aligns with precedents like three.js (Vector2.cross) and Godot (Vector2.cross).
-
Make
cross()simple and standard (3D input only):- Input: This method will only accept two 3D vectors.
- Output: It will return a 3D vector.
-
Benefits: This makes the
cross()method simple and predictable. Its contract is unambiguous (3D in, 3D out), which removes the 1.x baggage of handling other dimensions. Throwing a friendly error for non-3D inputs then helps users find bugs instead of hiding them.
Disruption assessment: This is a fix, not a breaking change
The primary concern with this change is backward compatibility. I believe the data shows that fixing this bug will cause minimal disruption, while not fixing it will cause ongoing confusion for all 2.0 users.
I performed a Google search (site:https://editor.p5js.org/ "createVector" "cross") and a similar search on OpenProcessing. I found only 19 sketches that used cross().
- $\approx 50\verb|%|$ (9 sketches) already use two 3D vectors and will not be affected at all.
-
$\approx 50\verb|%|$ (10 sketches) will be affected. The fix for them is simple and leads to more correct, explicit code (e.g., replacing
a.cross(b).zwitha.scalarCross(b), or explicitly padding a vector with0).
The high, permanent cost of keeping a bug that hides errors for all users outweighs the low, one-time cost of fixing it for a handful of existing sketches. This proposal provides a simple, clear, correct API that aligns with mathematical practice, our own API patterns for 2.x, and the best practices of all major creative-coding libraries.
[Edit] Blocking issues (foundational issues for all of p5.Vector)
I’ve already started working on a fix for this. Could you please assign me the issue?
my implementation according to purposed solution by you:
scalarCross() method (2D only):
- Returns a scalar representing signed 2D area
- Throws helpful error for 3D vectors
- Includes both instance and static versions
scalarCross(v) {
// Ensure both vectors are 2D (z component should be 0 or undefined)
if ((this.z !== 0 && this.z !== undefined) || (v.z !== 0 && v.z !== undefined)) {
throw new Error('scalarCross() is only defined for 2D vectors. Use cross() for 3D vectors.');
}
return this.x * v.y - this.y * v.x;
}
static scalarCross(v1, v2) {
return v1.scalarCross(v2);
}
cross() method (3D only):
- Now requires 3D vectors (throws error for 2D)
- Clean, unambiguous 3D in → 3D out contract
angleBetween() method:
- Uses
scalarCross()for 2D vectors - Uses traditional
cross()for 3D vectors
@MRKrinetic: Thanks for offering to help! I can assign this to you once the community agrees with the proposal. It's good to have more people take a look before releasing the changes to the full p5 community. For now, I'll just provide a few clarifications regarding your implementation. I really appreciate your effort, since it has helped me to clarify which other issues are blocking the current issue.
Blocking issue #8153: $n$-dimensional vectors in 2.x are truly $n$-dimensional Vectors in 2.x are truly $n$-dimensional. For example, a 2D vector has only two components. If it has three components, then even if the $z$ coordinate is zero, it is 3D, not 2D. However, this assumes the proposal in #8153 is accepted. So that's a blocker for this issue and likely others.
Blocking issue #8155: API for checking dimensions is currently unstable
Another related issue is #8155, which aims to provide a user-facing API that provides a standard way to check the dimension of a vector. That API is still being developed and would provide the most stable feature for you to use in your implementation (checking the $z$ component is not enough, as explained further below). There are ways to check the dimension now using internal features, but these are currently unstable and may be changed. So, we'd ideally settle #8155 first to determine the public API for checking dimension, and then use the public API to check dimensions in the scalarCross() and cross() implementations, and elsewhere. If we rely on unstable, internal features to check the dimension, then refactoring those dimension-checking features could break the new scalarCross() and cross() implementations.
p5.js 2.x has vectors of dimension $n \geq 1$, not just 2D and 3D I should also note that in 2.x, we have 1D, 2D, 3D, 4D, 5D, 6D, ... vectors. We have vectors of every dimension $n \geq 1$. So, for example, if we want a feature to only support 3D vectors, then it wouldn't only throw an error for 2D vectors. It would throw an error for 1D, 2D, 4D, 5D, 6D, ... If a feature supports only 3D vectors, say, than it can use logic like "if vector has dimension 3, proceed; else, throw error." Note that checking if $z$ is defined isn't sufficient to check the dimension of a vector: if $z$ is defined, the dimension could be 3D or higher, and if it were undefined, it could be 1D or 2D.
angleBetween() needs to be refactored so that it doesn't use cross products at all (separate issue)
I also want to thank you for being so thorough. I can see that you identified an internal use of the cross product that needs to be looked at, in angleBetween(). I'm going to post a separate issue for that, since angleBetween() should actually work for vectors of all dimensions, by using the dot product instead of the cross product. This not only prevents us from having to use separate implementations for 2D and 3D vectors, but also allows us to using a single implementation for all dimensions $n \geq1$.
Suggested next steps for you
Thanks again for your help. If you're looking for next steps, then you can comment on #8153 to indicate whether you support that proposal, and you can comment on #8155 to comment on whether you support the currently proposed .shape API for checking dimensions (the most recent comments on that issue outline the latest proposals). I'm still in the process of reviewing overall getter/setter API patterns in p5 to confirm that's the right choice, but that API is probably the leading candidate currently.
Hey @GregStanton , I’ve been following the discussion around createVector() with no arguments, and I understand the current plan is to have it return [0, 0, 0] with a warning. That makes sense to me—it gives us a consistent default while nudging users toward explicit dimensionality.
Once that’s settled, I believe it unlocks the rest of the dimension-aware vector work we’ve been discussing:
- With
createVector()defaulting to 3D, we can reliably detect whether a vector is 2D or 3D using the rules from #8153. - Then, using the shape API improvements from #8155, we can make dimension checks easier and safer across the board.
- This sets the stage for a clean fix to
p5.Vector.cross()—restricting it to 3D inputs only, and throwing a friendly error otherwise. - Since
createVector()with no arguments already returns[0, 0, 0], that also solves the edge case where users pass nothing tocross()—we’ll know it’s 3D. - I’m also proposing a
scalarCross()method for 2D vectors, which returns a signed scalar area. It’s clearer and more discoverable than the olda.cross(b).zpattern. - For
angleBetween(), I have an idea to refactor it using the dot product instead of cross product, so it works for all dimensions ≥ 1. I’ll wait for the forum consensus before sharing that in detail.
Once the forum thread wraps up, could you please assign me this? I’d love to take the lead on implementing scalarCross(), fixing cross(), and helping with the angleBetween() refactor if needed.
Hi @Ayaan005-sudo, thanks for digging into this and the createVector() discussion!
There's a critical point of confusion I need to clarify: The "warning" approach that the maintainers are currently pursuing for createVector() creates a problem for cross() rather than solving it.
The problem is that users can call createVector() without arguments when they intend to create a 2D vector or a 3D vector, but the current "warning" approach will only produce a 3D vector. This means it will be impossible for a 2D-only scalarCross() or a 3D-only cross() to correctly determine the intended dimension.
So for now, my recommendation is to please continue holding off on implementation. We are still blocked until the core policies (like the createVector() behavior, the x/y/z behavior, and the dimension-checking API) are finalized. Once we're ready to go, @MRKrinetic will be first in line for this cross() issue.
Thanks for your patience!
P.S. Great minds think alike! The scalarCross() API is the exact solution I proposed in the issue description. It's great to see you landing on the same solution!
@GregStanton Thanks for the clarification! That makes perfect sense — I hadn’t considered how the default createVector() behavior could block dimension-specific methods like cross() and scalarCross().
I’ll continue holding off on implementation until the core policies are finalized. Looking forward to seeing how the dimension-checking API evolves, and happy to support @MRKrinetic when the time comes!
Appreciate the heads-up — and glad to hear we landed on the same scalarCross() idea 😄