Skip to content

[2.x] The cross() method of p5.Vector is incorrect in the 2.x context #8210

@GregStanton

Description

@GregStanton

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 for createVector(1, 0, 0). The cross() 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 current cross() 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:

  1. 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.
  2. 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.
  3. 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 value 2), not padding. This "pad-by-zero" rule for cross() 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.
  4. 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.

  1. 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).
  2. 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).z with a.scalarCross(b), or explicitly padding a vector with 0).

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)

  • #8153: Consensus would clarify the meaning of 2D, in the context of 2.x.
  • #8155: Consensus on a user-facing API for checking dimension size would enable a more stable implementation of scalarCross() and cross() for 2.x.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status

    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions