Skip to content

Transformation Matrix for p5.Strands - #8984

Draft
perminder-17 wants to merge 8 commits into
processing:mainfrom
perminder-17:transform-strands
Draft

Transformation Matrix for p5.Strands#8984
perminder-17 wants to merge 8 commits into
processing:mainfrom
perminder-17:transform-strands

Conversation

@perminder-17

@perminder-17 perminder-17 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Resolves #8953

Changes:

Screenshots of the change:

PR Checklist

Comment thread src/strands/strands_api.js Outdated
Comment on lines +885 to +951

const originalScale = fn.scale;
augmentFn(fn, p5, 'scale', function (...args) {
const t = args[0];
if (!isStrandsTransform(t)) return originalScale.apply(this, args);
const scales = args.slice(1);
const uniform = scales.length === 1; // scale(t, s) scales every axis by s
const x = scales[0] ?? 1;
const y = scales[1] ?? (uniform ? x : 1);
const z = scales[2] ?? (uniform ? x : 1);
return transformStep(t,
[x, 0, 0, 0, y, 0, 0, 0, 1],
[x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1]);
});

const originalRotate = fn.rotate;
augmentFn(fn, p5, 'rotate', function (...args) {
const t = args[0];
if (!isStrandsTransform(t)) return originalRotate.apply(this, args);
const angle = args[1]; // 2D: rotate in-plane; 3D: rotate about the Z axis
const c = this.cos(angle);
const s = this.sin(angle);
const ns = s.mult(-1); // -sin
return transformStep(t,
[c, s, 0, ns, c, 0, 0, 0, 1],
[c, s, 0, 0, ns, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
});

augmentFn(fn, p5, 'skewX', function (...args) {
if (!strandsContext.active) {
p5._friendlyError(`It looks like you've called skewX outside of a shader's modify() function.`);
return;
}
const [t, angle] = args;
const k = this.tan(angle); // x' = x + tan(angle) * y
return transformStep(t,
[1, 0, 0, k, 1, 0, 0, 0, 1],
[1, 0, 0, 0, k, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
});

augmentFn(fn, p5, 'skewY', function (...args) {
if (!strandsContext.active) {
p5._friendlyError(`It looks like you've called skewY outside of a shader's modify() function.`);
return;
}
const [t, angle] = args;
const k = this.tan(angle); // y' = y + tan(angle) * x
return transformStep(t,
[1, k, 0, 0, 1, 0, 0, 0, 1],
[1, k, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
});

augmentFn(fn, p5, 'rotateAxisAngle', function (...args) {
if (!strandsContext.active) {
p5._friendlyError(`It looks like you've called rotateAxisAngle outside of a shader's modify() function.`);
return;
}
const [t, axis, angle] = args;
if (!t?.isStrandsNode || t.typeInfo().baseType !== BaseType.MAT || t.dimension !== 4) {
FES.userError('type error',
'rotateAxisAngle() only works on a 3D transform created with transform3D().');
}
const n = this.normalize(axis);
const x = n.x, y = n.y, z = n.z;
const c = this.cos(angle);
const s = this.sin(angle);
const omc = p5.strandsNode(1).sub(c); // 1 - cos

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @davepagurek ,

Just thinking out loud. In our non-strands modules, each area of functionality lives in its own file, for example, transform.js keeps the JSDoc, examples, and implementation together in one place.

I was thinking of following that same convention for the strands transforms: moving the matrix and transform helpers out of strands_api.js into a dedicated strands_transform.js (something better name for the file), with each function's docs kept alongside its implementation.

One thing to check: right now the strands functions are documented centrally in p5.strands.js rather than inline. Would you prefer keeping that, or colocating the transform docs with the code in the new file?

@ksen0

ksen0 commented Aug 10, 2026

Copy link
Copy Markdown
Member

Just starte looking at this one, really exciting! I will work on some more complex sketches to test it out, but I appreciate the docs you've included out of the box.

Would it be a good idea to add some bench and/or unit and/or visual tests here? I think visual and unit at minimum, given the scope of the PR, would be good...

@perminder-17

Copy link
Copy Markdown
Collaborator Author

Just starte looking at this one, really exciting! I will work on some more complex sketches to test it out, but I appreciate the docs you've included out of the box.

Ah, correct. I'm also getting feedback and testing it with more complex examples myself. For now, I'm trying to find ways to break the feature and catch any issues.

Once I'm confident with the code, I'll add tests for them.

*
* function slide() {
* getWorldInputs((inputs) => {
* // translate() returns a new transform, so assign the result back.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add an inline URL here? Or add a note at the end of the transform() docs to mention strands + link to the transformPoint docs as the spot with more in depth explanation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or interleave some writing before the strands example saying the same thing

* }
*
* function wobble() {
* getWorldInputs((inputs) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use worldInputs.begin() and worldInputs.end() in the examples for consistency?

@davepagurek

Copy link
Copy Markdown
Contributor

Some feedback:

  • We could make visual tests out of examples
  • We should make sure that in the website reference, we don't accidentally recategorize the transform methods into the strands category. Maybe we need to add e.g. @module tags onto individual methods?
  • We also want the strands parts to come last, it may be that adding our docs to the existing docs rather than making new ones and relying on the docs builder to stitch them together is the most reliable way of doing that
  • Once main is merged in, could we add examples using instances(n) and using transforms per instance?
  • Not blocking for this PR but thoughts for the future: for sketches like https://openprocessing.org/@davepagurek/2985673 when transforms vary based on position, the current transformNormal logic is not quite correct. That sketch has some code to figure out what the new normal should be by sampling a few nearby points, but it relies on a slightly different structure (your transform is a function based on an input position, not just a single matrix.) Any thoughts on what it could look like in the future if we wanted to support that? Just to confirm that it wouldn't clash with our current API?
  • Just noticed that the current p5 transforms for skewing are called shearX and shearY but it looks like the ones we've added here use skew as a prefix instead, based on the DOMMatrix API. I hadn't realized we use different conventions there. Do you or @ksen0 have thoughts on whether it's better to just use what p5 already has for naming there?

@davepagurek

Copy link
Copy Markdown
Contributor

oh and one more thing that I forgot: add on to the logging system for experimental features in #9109. It sounds like we don't want a whole new contributor doc, but we just want to add a SECTION to the strands doc, and then link to that section via a #sectionname suffix (I think we do that in tables of contents in other contributor docs.) We may need to update the system a little to make a category that links to a section of a doc rather than to a whole doc of its own, + we may need to use the manual checker rather than the decorator to catch uses of these functions since we don't want it logging in regular non-strands use

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support transformation matrices in p5.strands

3 participants