Skip to content

Commit b650adc

Browse files
authored
Merge branch 'main' into fix/strands-hook-lifecycle-errors
2 parents 5a55f16 + 3998034 commit b650adc

301 files changed

Lines changed: 12194 additions & 147 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.all-contributorsrc‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7621,6 +7621,15 @@
76217621
"test",
76227622
"bug"
76237623
]
7624+
},
7625+
{
7626+
"login": "shakeelmohamed",
7627+
"name": "Shakeel Mohamed",
7628+
"avatar_url": "https://avatars.githubusercontent.com/u/1480063?v=4",
7629+
"profile": "https://github.com/shakeelmohamed",
7630+
"contributions": [
7631+
"test"
7632+
]
76247633
}
76257634
],
76267635
"repoType": "github",

‎CONTRIBUTORS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,7 @@
11371137
<td align="center" valign="top" width="16.66%"><a href="https://github.com/okra-sf"><img src="https://avatars.githubusercontent.com/u/110788460?v=4?s=64" width="64px;" alt="okra-sf"/><br /><sub><b>okra-sf</b></sub></a><br /><a href="https://github.com/processing/p5.js/commits?author=okra-sf" title="Code">💻</a></td>
11381138
<td align="center" valign="top" width="16.66%"><a href="https://github.com/Danyccsf"><img src="https://avatars.githubusercontent.com/u/186502464?v=4?s=64" width="64px;" alt="Dany Valverde Caldas"/><br /><sub><b>Dany Valverde Caldas</b></sub></a><br /><a href="https://github.com/processing/p5.js/commits?author=Danyccsf" title="Tests">⚠️</a></td>
11391139
<td align="center" valign="top" width="16.66%"><a href="https://github.com/Pcmhacker-piro"><img src="https://avatars.githubusercontent.com/u/181658297?v=4?s=64" width="64px;" alt="Prakash Meena"/><br /><sub><b>Prakash Meena</b></sub></a><br /><a href="https://github.com/processing/p5.js/commits?author=Pcmhacker-piro" title="Code">💻</a> <a href="https://github.com/processing/p5.js/commits?author=Pcmhacker-piro" title="Tests">⚠️</a> <a href="https://github.com/processing/p5.js/issues?q=author%3APcmhacker-piro" title="Bug reports">🐛</a></td>
1140+
<td align="center" valign="top" width="16.66%"><a href="https://github.com/shakeelmohamed"><img src="https://avatars.githubusercontent.com/u/1480063?v=4?s=64" width="64px;" alt="Shakeel Mohamed"/><br /><sub><b>Shakeel Mohamed</b></sub></a><br /><a href="https://github.com/processing/p5.js/commits?author=shakeelmohamed" title="Tests">⚠️</a></td>
11401141
</tr>
11411142
</tbody>
11421143
</table>

‎contributor_docs/p5.strands.md‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,60 @@ At this point, the general technical framework has been set up (see the technica
2323

2424
Finally, since the system is still so new and is still changing, it lacks extensive tutorials and examples. If you want to write or make videos to help people understand how to use p5.strands, or show examples illustrating the kinds of things you can make with it, reach out!
2525

26+
## Transforms and matrices
27+
28+
p5.strands includes a set of functions for building and applying transformation matrices inside a shader. They are newer than the rest of p5.strands, so using them logs a console warning that points here: they are the part of the API most likely to still change, and we want to hear how they work out for you.
29+
30+
The functions look like the transforms you already use on the canvas:
31+
32+
- `transform2D()` creates a 3x3 matrix that can move, turn, scale, and slant 2D points, such as the texture coordinates in a filter shader. `transform3D()` creates a 4x4 matrix that does the same for 3D positions. Both start out as the identity transform, which leaves points exactly where they are. They are the same functions as `mat3()` and `mat4()`, and `mat2()` exists for plain 2x2 math, but a 2x2 matrix has no room to store a move, so there is no transform alias for it.
33+
- `translate()`, `rotate()`, `scale()`, `shearX()`, and `shearY()` add a step to a transform, and `rotateX()`, `rotateY()`, `rotateZ()`, and `rotateAxisAngle()` do the same for 3D transforms. Unlike their canvas counterparts, they don't change anything in place: each returns a new transform, so the result has to be assigned back, as in `t = translate(t, 20, 0, 0)`. Steps combine the way they do on the canvas, so translating and then rotating turns a shape in place before moving it.
34+
- `transformPoint()` applies a transform to a position, and `transformNormal()` applies it to a surface direction, which has to ignore the move part of the transform to stay correct.
35+
36+
`translate()`, `rotate()`, `scale()`, `shearX()`, `shearY()`, and `rotateX()`/`rotateY()`/`rotateZ()` share their names with the regular p5 functions, so p5.strands only takes over when their first argument is a transform created inside a shader callback, and `translate(20, 0)` on the canvas works the same as it always has. The rest of these functions only exist inside a shader callback. Angles inside a shader are always in radians, since `angleMode()` doesn't reach inside the shader.
37+
38+
A typical use is placing many copies of a shape at once, giving each instance its own transform based on `instanceIndex`:
39+
40+
```js
41+
let myShader;
42+
let count = 10;
43+
44+
function setup() {
45+
createCanvas(200, 200, WEBGL);
46+
myShader = buildMaterialShader(placeOnRing);
47+
}
48+
49+
function placeOnRing() {
50+
worldInputs.begin();
51+
let angle = instanceIndex * TWO_PI / count;
52+
let t = transform3D();
53+
t = rotateY(t, angle);
54+
t = translate(t, 65, 0, 0);
55+
worldInputs.position = transformPoint(t, worldInputs.position);
56+
worldInputs.normal = transformNormal(t, worldInputs.normal);
57+
worldInputs.end();
58+
}
59+
60+
function draw() {
61+
background(220);
62+
lights();
63+
noStroke();
64+
fill('red');
65+
shader(myShader);
66+
instances(count).box(18);
67+
}
68+
```
69+
70+
The reference pages for [`transform2D()`](https://p5js.org/reference/p5/transform2D/) and [`transform3D()`](https://p5js.org/reference/p5/transform3D/) have more examples.
71+
72+
### What we want feedback on
73+
74+
The most useful thing you can do is to try the transforms out in a real sketch and let us know how it went: what you were trying to make, what took longest to figure out, and where the names or the way the functions fit together tripped you up. These functions are the newest part of p5.strands, so now is the best time to tell us what feels awkward, before the API settles.
75+
76+
Bugs are just as welcome. If something renders differently from what the same transforms would do on the canvas, or an error message leaves you stuck, please report it (and help fix it if you are up for it!).
77+
78+
Finally, since these functions are so new, there are very few examples of them in use. Sketches, write-ups, or videos showing what you have made with them help others get started, and help us see which parts need more explanation. For any of this, file an issue on GitHub or talk to us on Discord.
79+
2680
## Technical overview
2781

2882
At its core, p5.strands works in four steps:

‎contributor_docs/p5.svg.md‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<!-- An overview of the goals of p5's native SVG export, import, and vector shape recording system. -->
2+
3+
# p5.svg Overview
4+
5+
`p5.svg` is an experimental native vector graphics system provided in p5.js starting from version 2.4. It allows you to create, import, and export vector graphics directly in p5.js without needing external addons. With `p5.svg`, artists, designers, and educators can generate graphics that scale smoothly for high-DPI displays, print, pen plotters, CNC routers, laser cutters, and embroidery machines using familiar p5.js drawing functions.
6+
7+
The specifics of these APIs are currently experimental and subject to evolution based on community feedback. A valuable contribution to the project is testing these APIs, reporting edge cases, and sharing feedback on usability and performance!
8+
9+
## Project Goals
10+
11+
`p5.svg` addresses several key goals:
12+
13+
- **Vector Graphics**: `p5.svg` allows p5.js drawings to be saved as vector graphics that can scale to different sizes without losing sharpness.
14+
- **Familiar p5 Drawing Workflow**: To create SVG output, the only thing you need to do differently is use `createShape()` and `buildShape()`. Inside these functions, use the standard drawing API (`rect`, `circle`, `path`, `fill`, `stroke`, `translate`, `rotate`, etc.) as you usually would.
15+
- **Import and Export SVG Files**: `p5.svg` allows you to load existing SVG files with `loadSVG()` and save p5.js drawings as SVG files using `saveSVG()` or `getSVG()`.
16+
17+
## What Needs Feedback
18+
19+
The main ways you can help develop `p5.svg` are:
20+
21+
- **Shape API**: Test vector shape functions like `createSVG()`, `loadSVG()`, `buildShape()`, `createShape()`, `shape()`, `getSVG()`, and `saveSVG()`. We want feedback on whether the API feels familiar to someone learning p5.js, whether the functions are easy to understand for beginners, and whether they are easy to use when teaching p5.js. Let us know if any choices feel confusing or tricky for students, or if anything feels inconsistent with the rest of p5.js.
22+
- **SVG Path Parsing**: Test importing SVGs generated by vector design tools (Adobe Illustrator, Inkscape, Figma). Report any unsupported path commands (`M`, `L`, `H`, `V`, `C`, `S`, `Q`, `T`, `A`, `Z`) or malformed elements.
23+
- **Styling and CSS Properties**: Help verify that stroke weights, colors, fill rules, opacities, gradients, and transform stacks behave consistently across export and import pipelines.
24+
- **Performance & Memory**: Test large or complex SVG scenes to help identify bottlenecks in DOM parsing, AST construction, or XML string generation.
25+
26+
## Technical Overview
27+
28+
Behind the scenes, `p5.svg` operates in three main layers:
29+
30+
1. **Shape Recording (`src/shape/svg/svg_recorder.js`)**:
31+
- Intercepts 2D drawing calls (`rect`, `ellipse`, `line`, `beginShape`/`endShape`, `fill`, `stroke`, `push`, `pop`, `translate`, `rotate`, `scale`, `applyMatrix`).
32+
- Builds an Abstract Syntax Tree (AST) composed of node instances (`ScopeNode`, `ShapeNode`, `BackgroundNode`, `ClearNode`, `ImageNode`).
33+
- Tracks coordinate transformations using an internal `TransformStack`.
34+
35+
2. **SVG Export & Visitor (`src/shape/svg/svg_export.js`)**:
36+
- Implements `SVGExportAddon` and `SVGVisitor` (extending `p5.PrimitiveVisitor`).
37+
- Traverses the shape AST to output standard SVG 2.0 XML markup string via `getSVG()` or triggers browser file downloads via `saveSVG()`.
38+
39+
3. **SVG Import & Parsing (`src/shape/svg/svg_import.js`)**:
40+
- Implements `SVGImportAddon` to parse external SVG DOM trees.
41+
- Tokenizes path data commands (`PATH_COMMANDS`) and converts SVG elements (`<path>`, `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polyline>`, `<polygon>`, `<g>`) into internal p5 `ShapeCollection` structures ready for playback via `shape()`.
42+
43+
## Usage Example
44+
45+
### Exporting an SVG
46+
47+
```js
48+
function setup() {
49+
createCanvas(400, 400);
50+
51+
// Record drawing commands into a vector shape
52+
const record = buildShape(() => {
53+
background(245);
54+
fill(99, 102, 241);
55+
stroke(0);
56+
strokeWeight(2);
57+
circle(200, 200, 150);
58+
});
59+
60+
// Save as vector file
61+
saveSVG(record, 'my-vector.svg');
62+
}
63+
```
64+
65+
### Importing and Replaying an SVG
66+
67+
```js
68+
let botLogo;
69+
70+
async function setup() {
71+
createCanvas(500, 500);
72+
73+
try {
74+
// loadSVG returns a promise; await the resolved ShapeCollection
75+
botLogo = await loadSVG('assets/robot.svg');
76+
console.log('SVG Loaded successfully!');
77+
} catch (err) {
78+
console.error('Failed to load SVG:', err);
79+
}
80+
}
81+
82+
function draw() {
83+
background(255);
84+
85+
// Render the SVG once it is fully loaded
86+
if (botLogo) {
87+
shape(botLogo, 100, 100);
88+
} else {
89+
fill(100);
90+
text('Loading SVG...', 20, 30);
91+
}
92+
}
93+
```
94+
95+
## Contributing
96+
97+
We welcome contributions to `p5.svg`! You can get involved by:
98+
99+
* **Testing existing SVG workflows** and reporting bugs or unexpected behavior on GitHub.
100+
* **Proposing and implementing new SVG features**, such as expanded SVG element support, clipping paths, gradients, filters, and custom SVG attributes.
101+
* **Improving SVG import and parsing**, including support for additional path commands and CSS/SVG attributes.
102+
* **Adding tests and examples** to validate new functionality and demonstrate real-world SVG workflows.
103+
* **Creating tutorials and creative coding examples** that showcase how `p5.svg` can be used in p5.js projects.
104+
* **Reviewing and providing feedback on experimental APIs** to help improve their usability, consistency, and performance.
105+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!-- Guide to writing performance tests for p5.js source code. -->
2+
3+
# Performance Testing
4+
5+
Performance testing is an essential part of measuring how fast isolated code paths take to execute under a known range of conditions.
6+
7+
Implementation notes:
8+
9+
- All pre-existing benchmarks included the full p5 instance setup which adds ~50ms to each test. The typography performance tests show how to isolate performance testing away from setup overhead.
10+
- The `textToPoints() single word, with render` test draws the calculated points, others just run `textToPoints()`.
11+
12+
## Typography Tests
13+
14+
To isolate typography tests, run the following:
15+
16+
```shell
17+
npx vitest bench test/bench/typography.bench.js --reporter=verbose
18+
```
19+
20+
### Manual tests on v1.x
21+
22+
These manual tests are useful to loosely check performance against v2.x as a baseline. The test harness is compatible with the one in `test/bench/typography.bench.js`.
23+
24+
To manually run typography tests against p5.js v1.x, open the following files in a browser:
25+
26+
- `textToPoints()`: `test/bench/v1_manual/text_to_points.html`
27+
28+
Note: you will need to use a [local server](https://github.com/processing/p5.js/wiki/Local-server) to avoid CORS issues when loading fonts. For example, from the root of the repo:
29+
30+
```shell
31+
npx http-server -c-1
32+
```
33+
34+
Then tests can be accessed at `http://localhost:8080/test/bench/v1_manual/text_to_points.html`.

‎src/app.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ import shader from './webgl/p5.Shader';
6161
p5.registerAddon(shader);
6262
import strands from './strands/p5.strands';
6363
p5.registerAddon(strands);
64+
import svg from './shape/svg/p5.svg';
65+
p5.registerAddon(svg);
6466

6567
import { waitForDocumentReady, _globalInit } from './core/init';
6668
waitForDocumentReady().then(_globalInit);

‎src/color/p5.Color.js‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ class Color {
319319
return this._array;
320320
}
321321

322-
lerp(color, amt, mode){
322+
lerp(color, amt, mode) {
323323
let lerpMode = mode;
324324
if (typeof amt === 'object') {
325325
// Passing in options object
@@ -726,13 +726,7 @@ class Color {
726726
}
727727

728728
_getRGBA(maxes = [1, 1, 1, 1]) {
729-
// Get colorjs maxes
730729
const colorjsMaxes = Color.#colorjsMaxes[RGB];
731-
732-
// Normalize everything to 0,1 or the provided range (map)
733-
let coords = structuredClone(to(this._color, 'srgb').coords);
734-
coords.push(this._color.alpha);
735-
736730
const rangeMaxes = maxes.map(v => {
737731
if (!Array.isArray(v)) {
738732
return [0, v];
@@ -741,7 +735,15 @@ class Color {
741735
}
742736
});
743737

744-
coords = coords.map((coord, i) => {
738+
let coords;
739+
if (this.mode === RGB) {
740+
coords = [...this._color.coords, this._color.alpha];
741+
} else {
742+
coords = structuredClone(to(this._color, 'srgb').coords);
743+
coords.push(this._color.alpha);
744+
}
745+
746+
return coords.map((coord, i) => {
745747
return map(
746748
coord,
747749
colorjsMaxes[i][0],
@@ -750,8 +752,6 @@ class Color {
750752
rangeMaxes[i][1]
751753
);
752754
});
753-
754-
return coords;
755755
}
756756

757757
_getMode() {

‎src/core/experimental.js‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { FES } from '../friendly_errors/fes';
88
* - Create a name for the subject area, e.g. 'webgpu'
99
* - Write a document in the contributor_docs folder for the subject area
1010
* describing its goals and what we want feedback on. The file should match
11-
* the subject area name, plus the .md suffix.
11+
* the subject area name, plus the .md suffix. If the subject area is a part of
12+
* a larger experimental area, e.g. 'p5.strands.transforms' within 'p5.strands',
13+
* it doesn't need a whole doc of its own: add a section to the larger area's doc
14+
* instead, and point to it in experimentalDocSections below.
1215
* - Write a message below that will show up in the console when functionality
1316
* from that subject area. A link to the doc will be automatically appended. Index
1417
* the message by the same subject area name.
@@ -29,13 +32,33 @@ import { FES } from '../friendly_errors/fes';
2932
*
3033
* ...or, if you need to conditionally warn about experimental functionality, you
3134
* can directly call warnExperimental(p5, pInst, subjectArea) inside a function.
35+
* This is also the way to go when a function is only experimental some of the
36+
* time, e.g. translate() is a regular p5 function unless it's given a p5.strands
37+
* transform, and regular use shouldn't log a warning. Note that FES is switched
38+
* off while a p5.strands callback runs, so functions used inside one add their
39+
* subject area to strandsContext.experimentalFeaturesUsed instead, and modify()
40+
* calls warnExperimental for each once the callback is done.
3241
*/
3342

3443
const experimentalMessages = {
3544
webgpu: 'WEBGPU mode is experimental, so its functions and constants may change in future versions. You can get involved by giving feedback to help direct its development!',
3645
'p5.strands': 'p5.strands shaders are experimental, so functions for building shaders and the hooks available within them may change in future versions. You can get involved by giving feedback to help direct its development!',
46+
'p5.strands.transforms': 'p5.strands transforms are experimental, so transform2D(), transform3D(), the matrix constructors, and the functions that build on them may change in future versions. You can get involved by giving feedback to help direct their development!',
47+
'p5.svg': 'SVG features are experimental, so SVG export, import, and shape recording functions may change in future versions. You can get involved by giving feedback to help direct its development!'
3748
};
3849

50+
// By default, a subject area links to the contributor doc with the same name. A
51+
// subject area that is a part of a larger one can instead point to a section of
52+
// that area's doc, so that it doesn't need a whole doc of its own.
53+
const experimentalDocSections = {
54+
'p5.strands.transforms': { doc: 'p5.strands', section: 'transforms-and-matrices' },
55+
};
56+
57+
function experimentalDocURL(subjectArea) {
58+
const { doc = subjectArea, section } = experimentalDocSections[subjectArea] || {};
59+
return `https://p5js.org/contribute/${doc}/${section ? `#${section}` : ''}`;
60+
}
61+
3962
// Just in case it's not possible to get access to the p5 instance from something,
4063
// we still don't want to make logs super noisy from repeated warnings, so we'll
4164
// fall back on this global cache. It means if you create a second p5 instance, it
@@ -49,7 +72,9 @@ export function warnExperimental(p5, pInst, subjectArea) {
4972
target.warnedExperimental = target.warnedExperimental || {};
5073
target.warnedExperimental[subjectArea] = true;
5174

52-
FES.log`${experimentalMessages[subjectArea]} For more info, see https://p5js.org/contribute/${subjectArea}/`();
75+
const message = experimentalMessages[subjectArea];
76+
const url = experimentalDocURL(subjectArea);
77+
FES.log`${message} For more info, see ${url}`();
5378
}
5479
}
5580

‎src/core/main.js‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -394,11 +394,7 @@ class p5 {
394394
this._removeAbortController.abort();
395395

396396
// remove DOM elements created by p5
397-
for (const e of this._elements) {
398-
if (e.elt && e.elt.parentNode) {
399-
e.elt.parentNode.removeChild(e.elt);
400-
}
401-
}
397+
this._elements.slice().forEach(e => e.remove());
402398

403399
// Run `remove` hooks
404400
await this._runLifecycleHook('remove');

0 commit comments

Comments
 (0)