Repository: processing/p5.js-website
Pull request #777: p5.strands first tutorial
Author: login: lukeplowden
Created: 2025-04-07T21:38:04Z | Merged: 2025-04-09T21:05:23Z

I have added a p5.strands tutorial for the 2.0 release. It may be a little on the long side but can be approached in multiple sections. Let me know what you think!

@davepagurek 


Diff:
diff --git a/package-lock.json b/package-lock.json
index d4a116c578..5d98bd40ee 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,6 +42,7 @@
         "@typescript-eslint/parser": "^7.0.2",
         "@uiw/codemirror-extensions-basic-setup": "^4.21.22",
         "@uiw/react-codemirror": "^4.21.22",
+        "cross-env": "^7.0.3",
         "documentation": "^14.0.3",
         "eslint": "^8.56.0",
         "eslint-config-preact": "^1.3.0",
@@ -6865,6 +6866,25 @@
       "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
       "dev": true
     },
+    "node_modules/cross-env": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+      "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cross-spawn": "^7.0.1"
+      },
+      "bin": {
+        "cross-env": "src/bin/cross-env.js",
+        "cross-env-shell": "src/bin/cross-env-shell.js"
+      },
+      "engines": {
+        "node": ">=10.14",
+        "npm": ">=6",
+        "yarn": ">=1"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
diff --git a/package.json b/package.json
index d0ea9b404d..e550d4c03d 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,7 @@
     "@typescript-eslint/parser": "^7.0.2",
     "@uiw/codemirror-extensions-basic-setup": "^4.21.22",
     "@uiw/react-codemirror": "^4.21.22",
+    "cross-env": "^7.0.3",
     "documentation": "^14.0.3",
     "eslint": "^8.56.0",
     "eslint-config-preact": "^1.3.0",
diff --git a/src/content/tutorials/config.ts b/src/content/tutorials/config.ts
index d2b82c673c..e65892bc2b 100644
--- a/src/content/tutorials/config.ts
+++ b/src/content/tutorials/config.ts
@@ -7,6 +7,7 @@ export const categories = [
   "web-design",
   "accessibility",
   "criticalAI",
+  "p5-strands",
   "webgl",
   "advanced",
 ] as const;
diff --git a/src/content/tutorials/en/intro-to-p5-strands.mdx b/src/content/tutorials/en/intro-to-p5-strands.mdx
new file mode 100644
index 0000000000..f5bf122625
--- /dev/null
+++ b/src/content/tutorials/en/intro-to-p5-strands.mdx
@@ -0,0 +1,755 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.png
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader
+    - en/p5/basematerialshader
+    - en/p5/basestrokeshader
+    - en/p5/basenormalshader
+    - en/p5/basefiltershader
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Like any medium, shaders also offer their own creative possibilities to explore.
+
+Shaders are programs that run in parallel on the GPU to create visual effects. The GPU is very efficient at running millions of similar operations in parallel, whereas the CPU (which JavaScript uses) is very efficient at running sequential tasks and managing program logic. 
+
+As it turns out, drawing to the screen (rendering) can take advantage of parallel operations. With shaders, we can make visuals which would otherwise be slow or difficult like realistic lighting simulations, post processing effects, and rendering complex geometries. Learning shaders is valuable for anyone interested in graphics programming. This could be for game development, VFX for films, or for any kind of digital arts. It's also a fun and unique way to think using computers.
+
+To learn how p5.strands works, we'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Fragment shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex shader and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  // shader(baseColorShader());
+  lightingSetup();
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting `shader(baseColorShader())` produces a different result, where the blue tint of the lighting does not affect the visible color of the sphere.
+
+<Callout title="Tip">
+Try uncommenting the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead, shaders apply a set of instructions across all vertices or pixels simultaneously. Each vertex or pixel independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
+
+The "strands" also refer to access points into the shader pipeline that let you modify specific aspects of rendering without building entire shaders from scratch. These access points allow you to modify specific aspects of the rendering process without needing to construct the entire shader from scratch.
+
+To summarise, p5.strands is a JavaScript-based shading language which sits on top of GLSL. It allows you to write shaders in a regular `.js` file without writing GLSL in string literals. It also removes some setup code in the process, and integrates with the rest of p5.js, to bring learning and using shaders closer to the core of p5.js.
+
+## Getting started with p5.strands
+### The setup
+Let's build a minimal shader example to introduce p5.strands' key concepts. With p5.strands, you can modify sections (known as "strands") of ready-made shaders using JavaScript.
+
+<EditableSketch code={`
+let firstShader;
+
+// Our callback function using p5.strands, 
+// which determines how to modify the baseColorShader()
+
+function firstShaderCallback() {
+	getFinalColor((color) => {
+		color = [1, 1, 0, 1]; // Yellow in RGBA (0-1 scale)
+		return color;
+	});
+} 
+
+async function setup() {
+	createCanvas(200, 200, WEBGL);
+	// p5.strands are made using the modify function on a shader 
+	firstShader = baseColorShader().modify(firstShaderCallback);
+}
+
+function draw() {
+  background(0);
+	shader(firstShader);
+	sphere(80);
+}
+`}/>
+
+In this sketch, you should see a yellow sphere.
+
+### What's happening here?
+1. Shaders: p5.js already uses shaders behind the scenes in WebGL mode. p5.strands exposes parts of these shaders so you can modify them.
+2. Strands: A "strand" is a specific part of a shader you can modify. In our example, getFinalColor is a strand that lets you change an object's final color.
+3. Modifying shaders: The pattern works like this:
+- Start with a base shader (`baseColorShader()`)
+- Call its `modify()` method, passing your callback function
+- Inside your callback, use strand functions (like `getFinalColor`) to make changes
+4. Vectors: Shaders work with vectors—collections of numbers that represent colors, positions, etc. In p5.strands, you can create vectors using array syntax `[x, y, z, w]` or the `vec4()` function.
+
+### Available shaders
+p5.strands provides the following modifiable shaders. Click the links below for their reference which tells us what functions are available for overriding.
+- [`baseColorShader`](/reference/p5/baseColorShader): The default shader in WebGL mode.
+- [`baseMaterialShader`](/reference/p5/baseMaterialShader): Automatically applied if you have any lights in the scene.
+- [`baseNormalShader`](/reference/p5/baseNormalShader): Applied by calling `normalMaterial()`.
+- [`baseStrokeShader`](/reference/p5/baseStrokeShader): Applied unless `noStroke()` is called, draws stroke on objects.
+- [`baseFilterShader`](/reference/p5/baseFilterShader): A template for making post-processing filters such as those provided by p5.js, like `filter(BLUR)`.
+
+Now that we've built our first p5.strands modification, let's create a more complex scene! We'll use `baseColorShader` and `baseStrokeShader` to create 3D objects, then apply post-processing with `baseFilterShader` to enhance the final result.
+
+## Building a scene
+### Instancing particles
+Because the GPU excels at parallel computation, it can draw thousands or millions of particles simultaneously. We can do this with a technique called **GPU Instancing**.
+
+In GPU instancing, we ask the GPU to draw multiple copies of the same object, each with a unique ID (from 0 to n-1). We can then position each instance based on its ID. For example, placing objects at coordinates `[ID, 0, 0]` would create a line along the x-axis.
+
+In p5.js, instancing is available via optional parameters for `endShape` and `model`. It does also require a custom shader to work. In our case, let's use `model` and build a sphere shape.
+
+```js
+particleModel = buildGeometry(() => sphere(10, 2, 4));
+```
+
+Let's start by offsetting instances along the x-axis using `baseColorShader()`:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	getWorldInputs((inputs) => {
+		// Offset each instance by its ID
+		inputs.position.x += instanceID() * 20;
+    // Move the particles left on the screen
+    inputs.position.x -= 150;
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(300, 300, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 10, 2));
+	instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+	background(0);
+	orbitControl();
+	shader(instancingShader)
+  strokeShader(instancingStrokeShader)
+	model(particleModel, 16);
+}
+`} />
+
+The getWorldInputs() callback receives an inputs struct containing data about the current vertex: `position`, `normal`, `texCoord`, and `color`. The "world" part indicates that this runs after JavaScript transformations like translate() or scale() have been applied.
+
+<Callout title='World Space vs. Object Space'>
+Moving in world space is like moving relative to the entire scene, while object transformations are relative to the object's center.
+</ Callout>
+
+Now let's distribute our particles in a more interesting pattern - placing them randomly on a sphere:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+  // a pseudorandom function from:
+  // https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
+  function rand(seed) {
+  return fract(sin(dot(seed, [12.9898, 78.233])) * 43758.5453123);
+}
+
+function sphere() {
+  let id = instanceID();
+  let theta = rand([id, 0.1234])  * TWO_PI;
+  let phi = rand([id, 3.321]) * PI;
+
+  let r = 180;
+  let x = r * sin(phi) * cos(theta);
+  let y = r * cos(phi);
+  let z = r * sin(phi) * sin(theta);
+  return [x, y, z];
+}
+
+getWorldInputs((inputs) => {
+  inputs.position += sphere();
+  return inputs;
+  });
+}
+
+async function setup() {
+  createCanvas(400, 400, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 4, 2));
+  instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+  background(0);
+  orbitControl();
+  shader(instancingShader)
+  fill(255, 100, 150)
+  strokeShader(instancingStrokeShader)
+  model(particleModel, 1000);
+}
+`} />
+
+The `rand()` helper function uses three GLSL functions ported to p5.strands:
+- `fract()`: Takes the fractional part of a number (e.g., `3.42` → `0.42`)
+- `sin()`: Returns the sine of an angle
+- `dot()`: Returns the dot product of two vectors, which measures their similarity.
+While these functions have their conventional mathematical meanings, here they're combined to generate pseudorandom values based on the instance ID.
+
+#### Adding movement to the particles
+For animating the particles, we will introduce the concept of uniform variables. 
+
+Uniforms are values passed from JavaScript to your shader. Think of them like global variables. When you change their values, all instances of the shader will see the same value immediately, without having to refresh the sketch. This makes uniforms ideal for interactive parts of your shader. 
+
+```js
+const time = uniformFloat(() => millis());
+```
+
+In p5.strands, we declare uniforms with `uniformFloat`, `uniformVector3` and so on. They should be declared inside of the callback, but not inside one of the strands itself. 
+
+Optionally, we can provide a default value for the uniform. If we set the default value as a function, it will be run every frame and the uniform value set. Alternatively, it is possible to set uniforms in the usual way for shaders in p5.js, by calling `myShader.setUniform('time', millis())`. 
+
+For our case, let's set a callback which does the same thing, for our uniform variable `time`.
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+  // Add the uniform declaration
+  const time = uniformFloat(() => millis());
+
+  // a pseudorandom function from:
+  // https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
+  function rand(seed) {
+    return fract(sin(dot(seed, [12.9898, 78.233])) * 43758.5453123);
+  }
+
+function sphere() {
+  let id = instanceID();
+  let theta = rand([id, 0.1234])  * TWO_PI;
+  // change the particle's position over time:
+  let phi = rand([id, 3.321]) * PI + time / 10000;
+
+  // Change the radius to create a more interesting shape:
+  let r = 200 * sin(phi);
+  let x = r * sin(phi) * cos(theta);
+  // Stretch the y dimensions a little
+  let y = r * 1.5 * cos(phi);
+  let z = r * sin(phi) * sin(theta);
+  return [x, y, z];
+}
+
+getWorldInputs((inputs) => {
+  inputs.position += sphere();
+  return inputs;
+});
+}
+
+async function setup() {
+  createCanvas(400, 400, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 4, 2));
+  instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+  background(0);
+  orbitControl();
+  shader(instancingShader)
+  fill(255, 100, 150)
+  strokeShader(instancingStrokeShader)
+  model(particleModel, 1000);
+}
+`} />
+
+By adding `time / 10000` to the phi angle and modifying the radius with `sin()`, we create animated particles that move around a pulsing, stretched sphere. Play around and see what other shapes you can create.
+
+<Callout title='Experiment!'>
+Try replacing `sin()` with `tan()`, `acosh()`, or combinations of functions. Small changes in shader code often create dramatically different visual effects, so experimentation goes a long way.
+</ Callout>
+
+### Fresnel effect
+If you've ever seen a material in a 3D render which appears to glow at the edges, or noticed how the light reflections appear to change on virtual water as you move your viewpoint, you were seeing the Fresnel effect. This effect changes how materials look when viewed at an angle.
+
+The Fresnel effect will be checking which parts of the shape are pointing away from the camera. For this reason, it is helpful for us to work in camera space. In camera space, also known as view space:
+- The camera is position at the origin `(0, 0, 0)`
+- The camera looks along the negative Z-axis
+- All 3D positions are relative to the camera's perspective
+
+<Callout title='Camera Space'>
+This is another relative view of the virtual world, where the camera is positioned at `(0, 0)`, so everything else is position relative to it.
+</ Callout>
+
+This perspective makes it easier to determine how surfaces appear to the viewer:
+
+```js
+function fresnelCallback() {
+	getCameraInputs((inputs) => {
+		// The normal vector on the sphere's surface
+		let normalVector = normalize(inputs.normal);
+		// This creates a vector pointing from the surface to the camera
+		let viewVector = normalize(-inputs.position);
+		// ...
+		return inputs;
+	})
+}
+```
+
+The line let viewVector = normalize(-inputs.position) might seem counterintuitive, so let's break it down:
+
+1. In camera space, the camera is at `(0, 0, 0)`
+2. inputs.position gives us the position of the current vertex being processed
+3. When we negate this (`-inputs.position`), we get a vector pointing from the vertex toward the camera
+4. `normalize()` converts this to a unit vector (where every component is between 0 - 1), making it useful for direction calculations regardless of distance
+
+#### Calculating the Fresnel factor
+
+The core of the Fresnel effect is comparing two directions:
+1. The surface normal (where the surface is "pointing")
+2. The view direction (where the camera is relative to the surface)
+
+```js
+let base = 1 - dot(normalVector, viewVector);
+let fresnel = pow(base, 2);
+```
+The dot product between these directions measures how directly the surface at any point is facing the camera. Technically, it returns the cosine of the angle between them. What this really tells us is that, when the dot product of two vectors is:
+- **Equal to 1**, the vectors point in the same direction
+- **Equal to 0**, the vectors are perpendicular
+- **Equal to -1**, the vectors are pointing in opposite directions.
+
+For points where the surface faces directly at the camera, the normal and view vector will be nearly parallel, giving a dot product close to 1. At grazing angles, for example at the top of a sphere, the dot product would be closer to 0. 
+
+By doing `1 - dot(normalVector, viewVector)`, we are inverting this relationship so that faces at the edge are closer to 1. This will help us make highlights around the edge of the object. 
+
+We raise this value to a power with `pow(base, 2)` to make the transition more dramatic - darkening the center more rapidly and creating a sharper glow at the edges. 
+
+#### Applying the effect to the color
+```js
+let col = mix([0, 0, 0], [1, 0.5, 0.7], fresnel);
+inputs.color = [col, 1];
+```
+
+The `mix()` function in GLSL is similar to p5.js's lerp function. The color should now interpolate between `[0, 0, 0]` (black) when the object faces the camera, and the value of `fresnel` is close to 0, to a pinkish color `[1, 0.5, 0.7]` at its edges when `fresnel` is closer to one.
+
+As a note, `getCameraInputs` is inside of the **vertex shader**, which is usually where we will do calculations which rely on the position of vertices. Therefore, we are setting the **vertex color** at this point. The fragment shader will interpolate between vertices to make sure that the pixels between them look continuous.
+
+Notice the returned value above: `[col, 1]`. This is one of the ways in which vectors are prioritised as the main data type in shader languages, including p5.strands. We are able to construct a 4-component vector (`RGBA`) from a 3-component vector(`RGB`) plus a float (alpha). This is a hallmark of shader languages.
+
+<EditableSketch code={`
+let fresnelShader
+
+function fresnelCallback() {
+  const fresnelPower = uniformFloat(2);
+  const fresnelBias = uniformFloat(-0.1);
+  const fresnelScale = uniformFloat(4);
+
+  getCameraInputs((inputs) => {
+    let n = normalize(inputs.normal);
+    let v = normalize(-inputs.position);
+    let base = 1.0 - dot(n, v);
+    let fresnel = fresnelScale * pow(base, fresnelPower) + fresnelBias;
+    let col = mix([0, 0, 0], [1, .5, .7], fresnel);
+    inputs.color = [col, 1];
+    return inputs;
+  });
+}
+
+async function setup() {
+  createCanvas(400, 400, WEBGL);
+  fresnelShader = baseColorShader().modify(fresnelCallback);
+}
+
+function draw() {
+  background(0);
+  noStroke();
+  orbitControl();
+  shader(fresnelShader)
+  sphere(80);
+} 
+`} />
+
+
+<Callout title="Tip">
+Try sending `const mousePos = uniformVector2(() => [mouseX, mouseY])` to this shader, and using those values for an interactive, color changing effect instead of the hardcoded pink.
+</ Callout>
+
+#### Fine-tuning
+
+The uniforms in effect give you some control over the end result.
+- `fresnelPower`: Higher values create a sharper transition from center to edge
+- `fresnelBias`: Shifts the center of the effect
+- `fresnelScale`: Amplifies the strength of the effect overall
+
+Uniforms can be changed without reloading the sketch if you use `setUniform()` or set the default value to a function which returns the desired value. For this demo, we'll just hard code some values.
+
+## Post-processing
+Filter shaders are built in much the same way as any other shader in p5.strands. The only difference is that we are only concerned with the fragment shader in filters, the one which decides the color of what's on the screen. They work by taking a snapshot of the sketch every frame, and sending through a fragment shader to manipulate the color. There are effects which are only possible through post-processing in this way. 
+
+For the `baseFilterShader()` there is only one hook available, `getColor(inputs, canvasContent)`. Inputs contains `texCoord`, `canvasSize`, and `texelSize`. The other parameter, `canvasContent` is the snapshot of the sketch mentioned above.
+
+### Pixelating effect
+We can achieve a pixelation effect by sampling the color of the scene at fewer points than there are real pixels. If you are not familiar with texture coordinates, also known as UV coordinates, try returning `[inputs.texCoord, 0, 1]` at the end of the `getColor` function.
+
+<EditableSketch code={`
+let pixelateShader;
+
+function pixelateCallback() {
+  getColor((inputs, canvasContent) => {
+    let coord = inputs.texCoord;
+    return [coord, 0, 1];
+  });
+}
+
+async function setup() {
+  createCanvas(200, 200);
+  pixelateShader = baseFilterShader().modify(pixelateCallback)
+}
+
+function draw() {
+  filter(pixelateShader);
+}
+`}/>
+
+The top left corner of the screen is now black, as it has a value of `(0, 0)` in texture coordinates. Down in the bottom left, it's green, showing that the texture coordinates are `(0, 1)`, and there is red in the top right with a value of `(1, 0)`.
+
+With that in mind, let's manipulate the number of texture coordinates, so that more pixels will sample their color from the same place on the original texture. We do this proportionally to `inputs.canvasSize` to get square pixels.
+
+<EditableSketch code={`
+let pixelateShader;
+
+function pixelateCallback() {
+  const pixelCountX = uniformFloat(() => 80);
+
+  getColor((inputs, canvasContent) => {
+    const aspectRatio = inputs.canvasSize.x / inputs.canvasSize.y;
+    const pixelSize = [pixelCountX, pixelCountX / aspectRatio];
+
+    // Sample the canvasContent texture with the modified texCoord
+    const coord = inputs.texCoord;
+    const modifiedCoord = floor(coord * pixelSize) / pixelSize;
+
+    const col = getTexture(canvasContent, modifiedCoord);
+    return col;
+  });
+}
+
+async function setup() {
+  createCanvas(200, 200, WEBGL);
+  pixelateShader = baseFilterShader().modify(pixelateCallback)
+}
+
+function draw() {
+  background(0);
+  rotateY(millis()/2000)
+  sphere(90);
+  filter(pixelateShader);
+}
+`}/>
+This is the entire code for our pixelating shader. Put this filter at the bottom of our draw function to pixelate the entire scene by calling `filter(pixelShader)`. We also need to use `baseFilterShader().modify(pixelateCallback)` to construct the shader as before.
+
+### Bloom
+In postprocessing, bloom is an effect which makes the brightest parts of an image bleed out and cause the surrounding pixels to light up. This produces the sense that light is emitting from parts of the scene, but without any expensive lighting calculations. 
+
+Bloom works by taking a blurred version of the image and setting some threshold values. Anything above that threshold (i.e. the brightest parts of an image) will be added to the original, non blurred version of the image. This creates a glow around the original objects.
+
+We could write the shader to produce the blurred image, but p5.js already has provided us with a `filter(BLUR)` which we can use. We will have to approach this effect slightly differently. Firstly, we will need to create a `p5.Framebuffer` object to capture the contents of the canvas before we blur it. 
+
+```js
+let originalImage;
+
+async function setup() {
+  // previous code...
+  originalImage = createFramebuffer();
+}
+
+function draw() {
+  // Draw the previous code to a framebuffer, so that we can store it before our  blur is applied.
+  originalImage.begin();
+  // previous code...
+  originalImage.end(); 
+
+  imageMode(CENTER);
+  image(originalImage, 0, 0);
+
+  // changing this value affects the spread of the bloom
+  filter(BLUR, 20);
+}
+```
+
+ At this point, the sketch should look identical. However, we can now use this framebuffer in our bloom shader. We do so by creating another uniform, but this time a `uniformTexture`. We still want to define the default parameter as a function in order for it to be retrieved after we make the Framebuffer in `setup`.
+
+```js
+function bloomCallback() {
+  const ogImage = uniformTexture(() => originalImage)
+
+  getColor((input, canvasContent) => {
+    // Get our textures converted into vector values.
+    const blurred = getTexture(canvasContent, input.texCoord);
+    const original = getTexture(ogImage, input.texCoord);
+
+    const intensity = max(original, 0.3) * 1.5;
+    // Overlay the blurred image
+    const bloom = original + blurred * intensity;
+    return [bloom.rgb, 1]
+  });
+}
+```
+
+<EditableSketch code={`
+let bloomShader;
+let originalImage;
+
+function bloomCallback() {
+  const preBlur = uniformTexture(() => originalImage);
+
+  getColor((input, canvasContent) => {
+    const blurred = getTexture(canvasContent, input.texCoord);
+    const original = getTexture(preBlur, input.texCoord);
+
+    const intensity = max(original, 0.2) * 8;
+    
+    const bloom = original + blurred * intensity;
+    return [bloom.rgb, 1];
+  });
+}
+
+
+async function setup() {
+  createCanvas(200, 200, WEBGL);
+  originalImage = createFramebuffer();
+  bloomShader = baseFilterShader().modify(bloomCallback);
+}
+
+function draw() {
+  // Draw the previous code to a framebuffer
+  // so that we can store it before the blur
+  originalImage.begin();
+  background(0);
+  rotateY(millis()/1000);
+  fill(40, 120, 255);
+  box(80);
+  originalImage.end();
+
+  imageMode(CENTER);
+  image(originalImage, 0, 0);
+
+  // changing this value affects the spread of the bloom
+  filter(BLUR, 20);
+  filter(bloomShader);
+}
+`} />
+
+In the variable `brightPass`, we have two magic numbers which could be uniforms instead, or feel free to adjust the values to find something which looks good. The `0.2` inside of `max` acts as *a kind* of threshold. Areas which are full black in the blurred image will not be affected by bloom, as they will ultimately be multiplying by 0. The `8` multiplies the overall strength of the effect.
+
+We only select the `.rgb` components of the bloom, as otherwise our alpha will be higher than 1 which gives unexpected results. Since we have selected only some components, we have an opportunity to try 'swizzling'. Swizzling is a feature of GLSL and other shader languages which lets us select whichever components of a vector to construct a new one.
+
+Any combination of `.rgba`, `.xyzw`, or `.stpq` (for texture coordinates) can be accessed, or set like this. Each of these sets are an alias for the others, they ultimately just select value `[0, 1, 2, 3]` in array terms. In other words writing `col.xyzw` is the same as writing `col.rgba`.
+
+Try changing it to `.grg` or any other combination, for some last minute color changes. This will construct a new vector `[col.g, col.g, col.r, 1]`. For example `[col.yyy, 1]` produces a greyscale output, as the `rgb` values are all the same.
+
+<Callout title="Hint">
+Try 'swizzling' the return value and writing something like 
+`inputs.color = [col.ggr, 1]`. 
+</ Callout>
+
+## Review
+We have written 4 shaders and learned to manipulate vertex and fragment shaders in p5.strands.
+
+- **Basic Color Shader** - We started with a simple modification of the base color shader, learning how to access and modify the final color of an object. This took place in the *fragment shader*.
+- **Instanced Particles** - We tried our hand at GPU instancing and rendered hundreds of particles. We moved objects in world space in the *vertex shader*.
+- **Fresnel Edge Highlighting** - We made a more advanced effect to make a glowing edge on 3D objects. We set the color of each vertex based on its position, so we did this in the *vertex shader*, in *camera space*.
+- **Post-processing** - We used two filter shaders to tie the scene together. These only required us to modify a *fragment shader*.
+
+The finished code for this project is posted below, with the example sketch.
+
+### What's next?
+Many GLSL examples can be ported to p5.strands, as a large amount of the language features are supported. We have functions to construct GLSL types by doing `vec4(1.0)`, so some helper functions can be copied, such as the random function we used earlier. Find some example effects you wish to create, and write the code strand by strand.
+
+For more resources on shaders, try:
+- [p5.js](/tutorials/intro-to-shaders), for a similar p5.js tutorial using GLSL
+- [p5.js shaders](https://itp-xstory.github.io/p5js-shaders/#/), a shader guide by Casey Conchinha and Louise Lessél.
+- [Shadertoy](https://www.shadertoy.com/), a massive online collection of shaders that are written in a browser editor.
+- [The Book of Shaders](http://www.thebookofshaders.com/), a shader guide by Patricio Gonzalez Vivo and Jen Lowe.
+
+## Final Code
+<EditableSketch code={`
+let instancedShader;
+let instancedStrokeShader;
+let stars;
+let originalImage;
+let pixelateShader;
+let fresnelShader;
+let bloomShader;
+
+function fresnelShaderCallback() {
+  const fresnelPower = uniformFloat(2);
+  const fresnelBias = uniformFloat(-0.1);
+  const fresnelScale = uniformFloat(2);
+
+  getCameraInputs((inputs) => {
+    let n = normalize(inputs.normal);
+    let v = normalize(-inputs.position);
+    let base = 1.0 - dot(n, v);
+    let fresnel = fresnelScale * pow(base, fresnelPower) + fresnelBias;
+    let col = mix([0, 0, 0], [1, .5, .7], fresnel);
+    inputs.color = [col, 1];
+    return inputs;
+  });
+}
+
+function starShaderCallback() {
+  const time = uniformFloat(() => millis());
+  const skyRadius = uniformFloat(250);
+  
+  function rand2(st) {
+    return fract(sin(dot(st, [12.9898, 78.233])) * 43758.5453123);
+  }
+
+  function semiSphere() {
+    let id = instanceID();
+    let theta = rand2([id, 0.1234])  * TWO_PI + time / 100000;
+    let phi = rand2([id, 3.321]) * PI + time / 50000;
+  
+    let r = skyRadius;
+    r *= sin(phi);
+    let x = r * sin(phi) * cos(theta);
+    let y = r * 1.5 * cos(phi);
+    let z = r * sin(phi) * sin(theta);
+    return [x, y, z];
+  }
+
+  getWorldInputs((inputs) => {
+    inputs.position += semiSphere();
+    return inputs;
+  });
+
+  getObjectInputs((inputs) => {
+    let size = 1 + 0.5 * sin(time * 0.002 + instanceID());
+    inputs.position *= size;
+    return inputs;
+  });
+}
+
+function pixelateShaderCallback() {
+  const pixelCountX = uniformFloat(()=> 280);
+
+  getColor((inputs, canvasContent) => {
+    const aspectRatio = inputs.canvasSize.x / inputs.canvasSize.y;
+    const pixelSize = [pixelCountX, pixelCountX / aspectRatio];
+
+    let coord = inputs.texCoord;
+    coord = floor(coord * pixelSize) / pixelSize;
+
+    let col = getTexture(canvasContent, coord);
+    return col//[coord, 0, 1];
+  });
+}
+
+function bloomShaderCallback() {
+  const preBlur = uniformTexture(() => originalImage);
+
+  getColor((input, canvasContent) => {
+    const blurredCol = getTexture(canvasContent, input.texCoord);
+    const originalCol = getTexture(preBlur, input.texCoord);
+
+    const intensity = max(originalCol, 0.1) * 12.2;
+    
+    const bloom = originalCol + blurredCol * intensity;
+    return [bloom.rgb, 1];
+  });
+}
+
+async function setup(){
+  createCanvas(800, 600, WEBGL);
+  stars = buildGeometry(() => sphere(8, 4, 2))
+  originalImage = createFramebuffer();
+
+  starShader = baseMaterialShader().modify(starShaderCallback); 
+  starStrokeShader = baseStrokeShader().modify(starShaderCallback)
+  fresnelShader = baseColorShader().modify(fresnelShaderCallback);
+  bloomShader = baseFilterShader().modify(bloomShaderCallback);
+  pixelateShader = baseFilterShader().modify(pixelateShaderCallback);
+}
+
+function draw(){
+  originalImage.begin();
+  background(0);
+  orbitControl();
+
+  push()
+  strokeWeight(2)
+  stroke(255,0,0)
+  rotateX(PI/2 + millis() * 0.0005);
+  fill(255,100, 150)
+  strokeShader(starStrokeShader)
+  shader(starShader);
+  model(stars, 1000);
+  pop()
+
+  push()
+  shader(fresnelShader)
+  noStroke()
+  sphere(90);
+  filter(pixelateShader);
+  pop()
+
+  originalImage.end();
+  
+  imageMode(CENTER)
+  image(originalImage, 0, 0)
+  
+  filter(BLUR, 25)
+  filter(bloomShader);
+}
+`} />
\ No newline at end of file
diff --git a/src/content/tutorials/images/featured/intro-to-p5-strands.png b/src/content/tutorials/images/featured/intro-to-p5-strands.png
new file mode 100644
index 0000000000..0187970ba8
Binary files /dev/null and b/src/content/tutorials/images/featured/intro-to-p5-strands.png differ
diff --git a/src/content/ui/en.yaml b/src/content/ui/en.yaml
index 75cbeb5519..b856167f39 100644
--- a/src/content/ui/en.yaml
+++ b/src/content/ui/en.yaml
@@ -100,6 +100,8 @@ tutorialCategories:
   accessibility: "Accessibility"
   webgl: "WebGL"
   "advanced": "Advanced Topics"
+  "p5-strands": p5.strands
+  criticalAI: "Critical AI"
 tutorialsPage:
   education-resources: Education Resources
   education-resources-snippet: "Every teaching experience has unique goals, messages, conditions, and environments. By documenting and sharing p5.js educational resources, such as workshops and classes, we aim to better connect the p5.js learner and educator communities worldwide. "
diff --git a/src/scripts/branchTest.ts b/src/scripts/branchTest.ts
index 601d11f76c..1290c9092f 100644
--- a/src/scripts/branchTest.ts
+++ b/src/scripts/branchTest.ts
@@ -35,7 +35,7 @@ if (existsSync(parsedP5Path)) {
 }
 
 // Build the reference using the specified environment
-execSync(`${env} npm run build:reference`, { stdio: "inherit" });
+execSync(`cross-env ${env} npm run build:reference`, { stdio: "inherit" });
 
 // Run a dev server
-execSync(`${env} npm run dev`, { stdio: "inherit" });
+execSync(`cross-env ${env} npm run dev`, { stdio: "inherit" });


Reviews:
user: login: ksen0
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T10:41:25Z
user: login: ksen0
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T10:43:29Z
user: login: ksen0
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T10:43:57Z
user: login: lukeplowden
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T10:45:41Z
user: login: ksen0
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T10:59:05Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T12:59:52Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:02:16Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:11:42Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:13:49Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:15:20Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:19:19Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:19:44Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:24:34Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:28:34Z
user: login: davepagurek
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T13:32:54Z
user: login: ksen0
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T20:52:23Z

Review comments:
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: ksen0
body: Minor suggestion:

"Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering." -> "Shaders are programs that run in parallel on the GPU to create visual effects that would be difficult to achieve with traditional rendering, which run in sequence on the CPU."

Maybe it can be part of the following paragraph? I think it's important to at least partly state in the introduction _how_ they are more efficient. And for similar reason mentioning CPU: "run on GPU" - as opposed to what?
created_at: 2025-04-08T10:41:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: ksen0
body: Minor:

I really like making clear who is this for and what skills it develops outside of p5!

"Learning shaders is valuable for anyone interested in graphics programming" - VFX? game development, lighting, textures? VR? Real-time processing of video data? I don't really know what makes sense to include in this list but 1 or 2 concrete examples to make this more real could be good?
created_at: 2025-04-08T10:43:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: ksen0
body: Fragment shaders (typo fix)
created_at: 2025-04-08T10:43:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: lukeplowden
body: Thanks for reading it over! Keep them coming and I'll make changes later on 
created_at: 2025-04-08T10:45:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: ksen0
body: Minor, phrasing: "Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders." -> "Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own."
created_at: 2025-04-08T10:49:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: ksen0
body: This section is great!
created_at: 2025-04-08T10:50:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead of working sequentially, shaders apply a set of instructions across all pixels simultaneously. Each pixel or vertex independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: ksen0
body: Minor: "across all pixels or vertices"

or (minor, style) maybe always keep the same order throughout the tutorial? First vertex then pixel/fragment. So the repetition helps to orient.
created_at: 2025-04-08T10:51:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead of working sequentially, shaders apply a set of instructions across all pixels simultaneously. Each pixel or vertex independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: ksen0
body: +1 on "Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."" That's wonderful, could even be highlighted somewhere else in the tutorial. If a reader gets 1 thing out of this, I think that would be a good candidate for that 1 takeaway.
created_at: 2025-04-08T10:52:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead of working sequentially, shaders apply a set of instructions across all pixels simultaneously. Each pixel or vertex independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
+
+The "strands" also refer to access points into the shader pipeline that let you modify specific aspects of rendering without building entire shaders from scratch. These access points allow you to modify specific aspects of the rendering process without needing to construct the entire shader from scratch.
+
+## Getting started with p5.strands
+### The setup
+Let's build a minimal shader example to introduce p5.strands' key concepts. P5.strands allows you to modify parts of p5.js shaders (called "strands") using JavaScript.
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: ksen0
body: Minor, style: "P5" always lowercase "p5"

If it's too awkward, could rephrase: "With p5.strands, you can modify parts of p5.js shaders (called "strands") using JavaScript." What do you think?
created_at: 2025-04-08T10:54:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead of working sequentially, shaders apply a set of instructions across all pixels simultaneously. Each pixel or vertex independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
+
+The "strands" also refer to access points into the shader pipeline that let you modify specific aspects of rendering without building entire shaders from scratch. These access points allow you to modify specific aspects of the rendering process without needing to construct the entire shader from scratch.
+
+## Getting started with p5.strands
+### The setup
+Let's build a minimal shader example to introduce p5.strands' key concepts. P5.strands allows you to modify parts of p5.js shaders (called "strands") using JavaScript.
+
+<EditableSketch code={`
+let firstShader;
+
+// Our callback function using p5.strands, which determines how to modify the baseColorShader()
+
+function firstShaderCallback() {
+	getFinalColor((color) => {
+		color = [1, 1, 0, 1]; // Yellow in RGBA (0-1 scale)
+		return color;
+	});
+} 
+
+async function setup() {
+	createCanvas(200, 200, WEBGL);
+	// p5.strands are made using the modify function on a shader 
+	firstShader = baseColorShader().modify(firstShaderCallback);
+}
+
+function draw() {
+  background(0);
+	shader(firstShader);
+	sphere(80);
+}
+`}/>
+
+Run this sketch now—you should see a yellow sphere.
+
+### What's happening here?
+1. Shaders: P5.js already uses shaders behind the scenes in WebGL mode. p5.strands exposes parts of these shaders so you can modify them.
+2. Strands: A "strand" is a specific part of a shader you can modify. In our example, getFinalColor is a strand that lets you change an object's final color.
+3. Modifying shaders: The pattern works like this:
+- Start with a base shader (`baseColorShader()`)
+- Call its `modify()` method, passing your callback function
+- Inside your callback, use strand functions (like `getFinalColor`) to make changes
+4. Vectors: Shaders work with vectors—collections of numbers that represent colors, positions, etc. In p5.strands, you can create vectors using array syntax `[x, y, z, w]` or the `vec4()` function.
+
+### Available shaders
+p5.strands provides the following modifiable shaders. Click the links below for their reference which tells us what functions are available for overriding.
+- [`baseColorShader`](/reference/p5/baseColorShader): The default shader in WebGL mode.
+- [`baseMaterialShader`](/reference/p5/baseMaterialShader): Automatically applied if you have any lights in the scene.
+- [`baseNormalShader`](/reference/p5/baseNormalShader): Applied by calling `normalMaterial()`.
+- [`baseStrokeShader`](/reference/p5/baseStrokeShader): Applied unless `noStroke()` is called, draws stroke on objects.
+- [`baseFilterShader`](/reference/p5/baseFilterShader): A template for making post-processing filters such as those provided by p5.js, like `filter(BLUR)`.
+
+Now that we've built our first p5.strands modification, let's create a more complex scene! We'll use `baseColorShader` and `baseStrokeShader` to create 3D objects, then apply post-processing with `baseFilterShader` to enhance the final result.
+
+## Building a scene
+### Instancing particles
+Because the GPU excels at parallel computation, it can draw thousands or millions of particles simultaneously. We can do this with a technique called **GPU Instancing**.
+
+In GPU instancing, we ask the GPU to draw multiple copies of the same object, each with a unique ID (from 0 to n-1). We can then position each instance based on its ID. For example, placing objects at coordinates `[ID, 0, 0]` would create a line along the x-axis.
+
+In p5.js, instancing is available via optional parameters for `endShape` and `model`. It does also require a custom shader to work. In our case, let's use `model` and build a sphere shape.
+
+```js
+particleModel = buildGeometry(() => sphere(10, 2, 4));
+```
+
+Let's start by offsetting instances along the x-axis using `baseColorShader()`:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	getWorldInputs((inputs) => {
+		// Offset each instance by its ID
+		inputs.position.x += instanceID() * 20;
+    // Move the particles left on the screen
+    inputs.position.x -= 150;
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(300, 300, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 10, 2));
+	instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+	background(0);
+	orbitControl();
+	shader(instancingShader)
+  strokeShader(instancingStrokeShader)
+	model(particleModel, 16);
+}
+`} />
+
+The getWorldInputs() callback receives an inputs struct containing data about the current vertex: `position`, `normal`, `texCoord`, and `color`. The "world" part indicates that this runs after JavaScript transformations like translate() or scale() have been applied.
+
+<Callout title='World Space vs. Object Space'>
+Moving in world space is like moving relative to the entire scene, while object transformations are relative to the object's center.
+</ Callout>
+
+Now let's distribute our particles in a more interesting pattern - placing them randomly on a sphere:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	// a pseudorandom function from:
+	// https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
+	function rand(seed) {
+		return fract(sin(dot(seed, [12.9898, 78.233])) * 43758.5453123);
+	}
+
+	function sphere() {
+    let id = instanceID();
+    let theta = rand([id, 0.1234])  * TWO_PI;
+    let phi = rand([id, 3.321]) * PI;
+  
+    let r = 180;
+    let x = r * sin(phi) * cos(theta);
+    let y = r * cos(phi);
+    let z = r * sin(phi) * sin(theta);
+    return [x, y, z];
+	}
+	
+	getWorldInputs((inputs) => {
+		inputs.position += sphere();
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(400, 400, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 4, 2));
+	instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+	background(0);
+	orbitControl();
+	shader(instancingShader)
+  fill(255, 100, 150)
+  strokeShader(instancingStrokeShader)
+	model(particleModel, 1000);
+}
+`} />
+
+The `rand()` helper function uses three GLSL functions ported to p5.strands:
+- `fract()`: Takes the fractional part of a number (e.g., `3.42` → `0.42`)
+- `sin()`: Returns the sine of an angle
+- `dot()`: Returns the dot roduct of two vectors, which measures their similarity.
+While these functions have their conventional mathematical meanings, here they're combined to generate pseudorandom values based on the instance ID.
+
+#### Adding movement to the particles
+For animating the particles, we will introduce the concept of uniform variables. 
+
+Uniforms are values passed from JavaScript to your shader. Think of them like global variables. When you change their values, all instances of the shader will see the same value immediately, without having to refresh the sketch. This makes uniforms ideal for interactive parts of your shader. 
+
+```js
+const time = uniformFloat(() => millis());
+```
+
+In p5.strands, we declare uniforms with `uniformFloat`, `uniformVector3` and so on. They should be declared inside of the callback, but not inside one of the strands itself. 
+
+Optionally, we can provide a default value for the uniform. If we set the default value as a function, it will be run every frame and the uniform value set. Alternatively, it is possible to set uniforms in the usual way for shaders in p5.js, by calling `myShader.setUniform('time', millis())`. 
+
+For our case, let's set a callback which does the same thing, for our uniform variable `time`.
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	// Add the uniform declaration
+  const time = uniformFloat(() => millis());
+
+	// a pseudorandom function from:
+	// https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
+	function rand(seed) {
+		return fract(sin(dot(seed, [12.9898, 78.233])) * 43758.5453123);
+	}
+
+	function sphere() {
+    let id = instanceID();
+    let theta = rand([id, 0.1234])  * TWO_PI;
+    // change the particle's position over time:
+    let phi = rand([id, 3.321]) * PI + time / 10000;
+  
+    // Change the radius to create a more interesting shape:
+    let r = 200 * sin(phi);
+    let x = r * sin(phi) * cos(theta);
+    // Stretch the y dimensions a little
+    let y = r * 1.5 * cos(phi);
+    let z = r * sin(phi) * sin(theta);
+    return [x, y, z];
+	}
+	
+	getWorldInputs((inputs) => {
+		inputs.position += sphere();
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(400, 400, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 4, 2));
+	instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+	background(0);
+	orbitControl();
+	shader(instancingShader)
+  fill(255, 100, 150)
+  strokeShader(instancingStrokeShader)
+	model(particleModel, 1000);
+}
+`} />
+
+By adding `time / 10000` to the phi angle and modifying the radius with `sin()`, we create animated particles that move around a pulsing, stretched sphere. Play around and see what other shapes you can create.
+
+<Callout title='Experiment!'>
+Try replacing `sin()` with `tan()`, `acosh()`, or combinations of functions. Small changes in shader code often create dramatically different visual effects, so experimentation goes a long way.
+</ Callout>
+
+### Fresnel effect
+If you've ever seen a material in a 3D render which appears to glow at the edges, or noticed how the light reflections appear to change on virtual water as you move your viewpoint, you were seeing the Fresnel effect. This effect changes how materials look when viewed at an angle.
+
+The Fresnel effect will be checking which parts of the shape are pointing away from the camera. For this reason, it is helpful for us to work in camera space. In camera space, also known as view space:
+- The camera is position at the origin `(0, 0, 0)`
+- The camera looks along the negative Z-axis
+- All 3D positions are relative to the camera's perspective
+
+<Callout title='Camera Space'>
+This is another relative view of the virtual world, where the camera is positioned at `(0, 0)`, so everything else is position relative to it.
+</ Callout>
+
+This perspective makes it easier to determine how surfaces appear to the viewer:
+
+```js
+function fresnelCallback() {
+	getCameraInputs((inputs) => {
+		// The normal vector on the sphere's surface
+		let normalVector = normalize(inputs.normal);
+		// This creates a vector pointing from the surface to the camera
+		let viewVector = normalize(-inputs.position);
+		// ...
+		return inputs;
+	})
+}
+```
+
+The line let viewVector = normalize(-inputs.position) might seem counterintuitive, so let's break it down:
+
+1. In camera space, the camera is at `(0, 0, 0)`
+2. inputs.position gives us the position of the current vertex being processed
+3. When we negate this (`-inputs.position`), we get a vector pointing from the vertex toward the camera
+4. `normalize()` converts this to a unit vector (where every component is between 0 - 1), making it useful for direction calculations regardless of distance
+
+#### Calculating the Fresnel factor
+
+The core of the Fresnel effect is comparing two directions:
+1. The surface normal (where the surface is "pointing")
+2. The view direction (where the camera is relative to the surface)
+
+```js
+let base = 1 - dot(normalVector, viewVector);
+let fresnel = pow(base, 2);
+```
+The dot product between these directions measures how 'edge-like' each point on our sphere is. It returns the cosine of the angle between them. The values range from 1 (when they're parallel) to 0 when they're perpendicular, and -1 when they're opposite. 
+
+For points where the surface faces directly at the camera, the normal and view vector will be nearly parallel, giving a dot product close to 1. At grazing angles, for example at the top of a sphere, the dot product would be closer to 0. 
+
+By doing `1 - dot(normalVector, viewVector)`, we are inverting this relationship so that faces at the edge are closer to 1. This will help us make highlights around the edge of the object. 
+
+We raise this value to a power with `pow(base, 2)` to make the transition more dramatic - darkening the center more rapidly and creating a sharper glow at the edges. 
+
+#### Applying the effect to the color
+```js
+let col = mix([0, 0, 0], [1, 0.5, 0.7], fresnel);
+inputs.color = [col, 1];
+```
+
+The `mix()` function in GLSL is similar to p5.js's lerp function. The colour should now interpolate between `[0, 0, 0]` (black) when the object faces the camera, and the value of `fresnel` is close to 0, to a pinkish color `[1, 0.5, 0.7]` at its edges when `fresnel` is closer to one.
+
+Notice the returned value here, `[col, 1]`. This is one of the ways in which vectors are prioritised as the main data type in shader languages, including p5.strands. We are able to construct a 4-component vector (`RGBA`) from a 3-component vector(`RGB`) plus a float (alpha). This is a hallmark of shader languages.
+
+<EditableSketch code={`
+let fresnelShader
+
+function fresnelCallback() {
+  const fresnelPower = uniformFloat(2);
+  const fresnelBias = uniformFloat(-0.1);
+  const fresnelScale = uniformFloat(4);
+
+  getCameraInputs((inputs) => {
+    let n = normalize(inputs.normal);
+    let v = normalize(-inputs.position);
+    let base = 1.0 - dot(n, v);
+    let fresnel = fresnelScale * pow(base, fresnelPower) + fresnelBias;
+    let col = mix([0, 0, 0], [1, .5, .7], fresnel);
+    inputs.color = [col, 1];
+    return inputs;
+  });
+}
+
+async function setup() {
+  createCanvas(400, 400, WEBGL);
+  fresnelShader = baseColorShader().modify(fresnelCallback);
+}
+
+function draw() {
+  background(0);
+  noStroke();
+  orbitControl();
+  shader(fresnelShader)
+  sphere(80);
+} 
+`} />
+
+
+<Callout title="Tip">
+Try sending `const mousePos = uniformVector2(() => [mouseX, mouseY])` to this shader, and using those values for an interactive, colour changing effect instead of the hardcoded pink.
+</ Callout>
+
+#### Fine-tuning
+
+The uniforms in effect give you some control over the end result.
+- `fresnelPower`: Higher values create a sharper transition from center to edge
+- `fresnelBias`: Shifts the center of the effect
+- `fresnelScale`: Amplifies the strength of the effect overall
+
+Uniforms can be changed without reloading the sketch if you use `setUniform()` or set the default value to a function which returns the desired value. For this demo, we'll just hard code some values.
+
+## Post-processing
+Filter shaders are built in much the same way as any other shader in p5.strands. The only difference is that we are only concerned with the fragment shader in filters, the one which decides the color of what's on the screen. They work by taking a snapshot of the sketch every frame, and sending through a fragment shader to manipulate the colour. There are effects which are only possible through post-processing in this way. 
+
+For the `baseFilterShader()` there is only one hook available, `getColor(inputs, canvasContent)`. Inputs contains `texCoord`, `canvasSize`, and `texelSize`. The other parameter, `canvasContent` is the snapshot of the sketch mentioned above.
+
+### Pixelating effect
+We can achieve a pixelation effect by sampling the color of the scene at fewer points than there are real pixels. If you are not familiar with texture coordinates, also known as UV coordinates, try returning `[inputs.texCoord, 0, 1]` at the end of the `getColor` function.
+
+<EditableSketch code={`
+let pixelateShader;
+
+function pixelateCallback() {
+	getColor((inputs, canvasContent) => {
+		let coord = inputs.texCoord;
+		return [coord, 0, 1];
+	});
+}
+
+async function setup() {
+  createCanvas(200, 200);
+  pixelateShader = baseFilterShader().modify(pixelateCallback)
+}
+
+function draw() {
+  filter(pixelateShader);
+}
+`}/>
+
+The top left corner of the screen is now black, as it has a value of `(0, 0)` in texture coordinates. Down in the bottom left, it's green, showing that the texture coordinates are `(0, 1)`, and there is red in the top right with a value of `(1, 0)`.
+
+With that in mind, let's manipulate the number of texture coordinates, so that more pixels will sample their colour from the same place on the original texture. We do this proportionally to `inputs.canvasSize` to get square pixels.
+
+<EditableSketch code={`
+let pixelateShader;
+
+function pixelateCallback() {
+	const pixelCountX = uniformFloat(() => 80);
+
+	getColor((inputs, canvasContent) => {
+		const aspectRatio = inputs.canvasSize.x / inputs.canvasSize.y;
+		const pixelSize = [pixelCountX, pixelCountX / aspectRatio];
+		
+		// Sample the canvasContent texture with the modified texCoord
+		const coord = inputs.texCoord;
+		const modifiedCoord = floor(coord * pixelSize) / pixelSize;
+	
+		const col = getTexture(canvasContent, modifiedCoord);
+		return col;
+	});
+}
+
+async function setup() {
+  createCanvas(200, 200, WEBGL);
+  pixelateShader = baseFilterShader().modify(pixelateCallback)
+}
+
+function draw() {
+  background(0);
+  rotateY(millis()/2000)
+  sphere(90);
+  filter(pixelateShader);
+}
+`}/>
+This is the entire code for our pixelating shader. Put this filter at the bottom of our draw function to pixelate the entire scene by calling `filter(pixelShader)`. We also need to use `baseFilterShader().modify(pixelateCallback)` to construct the shader as before.
+
+### Bloom
+In postprocessing, bloom is an effect which makes the brightest parts of an image bleed out and cause the surrounding pixels to light up. This produces the sense that light is emitting from parts of the scene, but without any expensive lighting calculations. 
+
+Bloom works by taking a blurred version of the image and setting some threshold values. Anything above that threshold (i.e. the brightest parts of an image) will be added to the original, non blurred version of the image. This creates a glow around the original objects.
+
+We could write the shader to produce the blurred image, but p5.js already has provided us with a `filter(BLUR)` which we can use. We will have to approach this effect slightly differently. Firstly, we will need to create a `p5.Framebuffer` object to capture the contents of the canvas before we blur it. 
+
+```js
+let originalImage;
+
+async function setup() {
+	// previous code...
+	originalImage = createFramebuffer();
+}
+
+function draw() {
+	// Draw the previous code to a framebuffer, so that we can store it before our blur is applied.
+	originalImage.begin();
+	// previous code...
+	originalImage.end();
+
+	imageMode(CENTER);
+	image(originalImage, 0, 0);
+	
+	// changing this value affects the spread of the bloom
+	filter(BLUR, 20);
+}
+```
+
+ At this point, the sketch should look identical. However, we can now use this framebuffer in our bloom shader. We do so by creating another uniform, but this time a `uniformTexture`. We still want to define the default parameter as a function in order for it to be retrieved after we make the Framebuffer in `setup`.
+
+```js
+function bloomCallback() {
+	const ogImage = uniformTexture(() => originalImage)
+	
+	getColor((input, canvasContent) => {
+		// Get our textures converted into vector values.
+		const blurred = getTexture(canvasContent, input.texCoord);
+		const original = getTexture(ogImage, input.texCoord);
+
+		const intensity = max(original, 0.3) * 1.5;
+		// Overlay the blurred image
+		const bloom = original + blurred * intensity;
+		return [bloom.rgb, 1]
+	});
+}
+```
+
+<EditableSketch code={`
+let bloomShader;
+let originalImage;
+
+function bloomCallback() {
+  const preBlur = uniformTexture(() => originalImage);
+
+  getColor((input, canvasContent) => {
+    const blurred = getTexture(canvasContent, input.texCoord);
+    const original = getTexture(preBlur, input.texCoord);
+
+    const intensity = max(original, 0.2) * 8;
+    
+    const bloom = original + blurred * intensity;
+    return [bloom.rgb, 1];
+  });
+}
+
+
+async function setup() {
+	createCanvas(200, 200, WEBGL);
+	originalImage = createFramebuffer();
+  bloomShader = baseFilterShader().modify(bloomCallback);
+}
+
+function draw() {
+	// Draw the previous code to a framebuffer, so that we can store it before our blur is applied.
+	originalImage.begin();
+  background(0);
+  rotateY(millis()/1000);
+  fill(40, 120, 255);
+  box(80);
+	originalImage.end();
+
+	imageMode(CENTER);
+	image(originalImage, 0, 0);
+	
+	// changing this value affects the spread of the bloom
+	filter(BLUR, 20);
+  filter(bloomShader);
+}
+`} />
+
+In the variable `brightPass`, we have two magic numbers which could be uniforms instead, or feel free to adjust the values to find something which looks good. The `0.2` inside of `max` acts as *a kind* of threshold. Areas which are full black in the blurred image will not be affected by bloom, as they will ultimately be multiplying by 0. The `8` multiplies the overall strength of the effect.
+
+We only select the `.rgb` components of the bloom, as otherwise our alpha will be higher than 1 which gives unexpected results. Since we have selected only some components, we have an opportunity to try 'swizzling'. Swizzling is a feature of GLSL and other shader languages which lets us select whichever components of a vector to construct a new one.
+
+Any combination of `.rgba`, `.xyzw`, or `.stpq` (for texture coordinates) can be accessed, or set like this. Each of these sets are an alias for the others, they ultimately just select value `[0, 1, 2, 3]` in array terms. In other words writing `col.xyzw` is the same as writing `col.rgba`.
+
+Try changing it to `.grg` or any other combination, for some last minute colour changes. This will construct a new vector `[col.g, col.g, col.r, 1]`. For example `[col.yyy, 1]` produces a greyscale output, as the `rgb` values are all the same.
+
+<Callout title="Hint">
+Try 'swizzling' the return value and writing something like 
+`inputs.color = [col.ggr, 1]`. 
+</ Callout>
+
+## Review
+We have written 4 shaders and learned to manipulate vertex and fragment shaders in p5.strands.
+
+- **Basic Color Shader** - We started with a simple modification of the base color shader, learning how to access and modify the final color of an object.
+- **Instanced Particles** - We tried our hand at GPU instancing and rendered hundreds of particles.
+- **Fresnel Edge Highlighting** - We made a more advanced effect to make a glowing edge on 3D objects.
+- **Post-processing** - We used two filter shaders to tie the scene together.
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: ksen0
body: Since the readers might have _just now_ learned fragment vs vertex shaders, I think it would be used to indicate for each of these for which are fragment shaders, and which is the vertex shader
created_at: 2025-04-08T10:56:51Z
author_association: MEMBER
diff_hunk: @@ -35,7 +35,7 @@ if (existsSync(parsedP5Path)) {
 }
 
 // Build the reference using the specified environment
-execSync(`${env} npm run build:reference`, { stdio: "inherit" });
+execSync(`cross-env ${env} npm run build:reference`, { stdio: "inherit" });
path: src/scripts/branchTest.ts
user: login: davepagurek
body: Thanks for adding this!
created_at: 2025-04-08T12:59:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: davepagurek
body: I think the trailing slashes on these are causing the build errors, do all the checks pass if you take them out, e.g. `en/p5/basecolorshader`?
created_at: 2025-04-08T13:02:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: davepagurek
body: Minor thing: currently the `baseMaterialShader` line is on the third line of `draw` 🙂 
created_at: 2025-04-08T13:11:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: davepagurek
body: Also it might be worth writing "change `baseMaterialShader` to `baseColorShader`" to make the change clearer?
created_at: 2025-04-08T13:13:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead of working sequentially, shaders apply a set of instructions across all pixels simultaneously. Each pixel or vertex independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
+
+The "strands" also refer to access points into the shader pipeline that let you modify specific aspects of rendering without building entire shaders from scratch. These access points allow you to modify specific aspects of the rendering process without needing to construct the entire shader from scratch.
+
+## Getting started with p5.strands
+### The setup
+Let's build a minimal shader example to introduce p5.strands' key concepts. P5.strands allows you to modify parts of p5.js shaders (called "strands") using JavaScript.
+
+<EditableSketch code={`
+let firstShader;
+
+// Our callback function using p5.strands, which determines how to modify the baseColorShader()
+
+function firstShaderCallback() {
+	getFinalColor((color) => {
+		color = [1, 1, 0, 1]; // Yellow in RGBA (0-1 scale)
+		return color;
+	});
+} 
+
+async function setup() {
+	createCanvas(200, 200, WEBGL);
+	// p5.strands are made using the modify function on a shader 
+	firstShader = baseColorShader().modify(firstShaderCallback);
+}
+
+function draw() {
+  background(0);
+	shader(firstShader);
+	sphere(80);
+}
+`}/>
+
+Run this sketch now—you should see a yellow sphere.
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: davepagurek
body: Since these auto-run, maybe we could rephrase this like, "In this sketch, you should see a yellow sphere."
created_at: 2025-04-08T13:15:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead of working sequentially, shaders apply a set of instructions across all pixels simultaneously. Each pixel or vertex independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
+
+The "strands" also refer to access points into the shader pipeline that let you modify specific aspects of rendering without building entire shaders from scratch. These access points allow you to modify specific aspects of the rendering process without needing to construct the entire shader from scratch.
+
+## Getting started with p5.strands
+### The setup
+Let's build a minimal shader example to introduce p5.strands' key concepts. P5.strands allows you to modify parts of p5.js shaders (called "strands") using JavaScript.
+
+<EditableSketch code={`
+let firstShader;
+
+// Our callback function using p5.strands, which determines how to modify the baseColorShader()
+
+function firstShaderCallback() {
+	getFinalColor((color) => {
+		color = [1, 1, 0, 1]; // Yellow in RGBA (0-1 scale)
+		return color;
+	});
+} 
+
+async function setup() {
+	createCanvas(200, 200, WEBGL);
+	// p5.strands are made using the modify function on a shader 
+	firstShader = baseColorShader().modify(firstShaderCallback);
+}
+
+function draw() {
+  background(0);
+	shader(firstShader);
+	sphere(80);
+}
+`}/>
+
+Run this sketch now—you should see a yellow sphere.
+
+### What's happening here?
+1. Shaders: P5.js already uses shaders behind the scenes in WebGL mode. p5.strands exposes parts of these shaders so you can modify them.
+2. Strands: A "strand" is a specific part of a shader you can modify. In our example, getFinalColor is a strand that lets you change an object's final color.
+3. Modifying shaders: The pattern works like this:
+- Start with a base shader (`baseColorShader()`)
+- Call its `modify()` method, passing your callback function
+- Inside your callback, use strand functions (like `getFinalColor`) to make changes
+4. Vectors: Shaders work with vectors—collections of numbers that represent colors, positions, etc. In p5.strands, you can create vectors using array syntax `[x, y, z, w]` or the `vec4()` function.
+
+### Available shaders
+p5.strands provides the following modifiable shaders. Click the links below for their reference which tells us what functions are available for overriding.
+- [`baseColorShader`](/reference/p5/baseColorShader): The default shader in WebGL mode.
+- [`baseMaterialShader`](/reference/p5/baseMaterialShader): Automatically applied if you have any lights in the scene.
+- [`baseNormalShader`](/reference/p5/baseNormalShader): Applied by calling `normalMaterial()`.
+- [`baseStrokeShader`](/reference/p5/baseStrokeShader): Applied unless `noStroke()` is called, draws stroke on objects.
+- [`baseFilterShader`](/reference/p5/baseFilterShader): A template for making post-processing filters such as those provided by p5.js, like `filter(BLUR)`.
+
+Now that we've built our first p5.strands modification, let's create a more complex scene! We'll use `baseColorShader` and `baseStrokeShader` to create 3D objects, then apply post-processing with `baseFilterShader` to enhance the final result.
+
+## Building a scene
+### Instancing particles
+Because the GPU excels at parallel computation, it can draw thousands or millions of particles simultaneously. We can do this with a technique called **GPU Instancing**.
+
+In GPU instancing, we ask the GPU to draw multiple copies of the same object, each with a unique ID (from 0 to n-1). We can then position each instance based on its ID. For example, placing objects at coordinates `[ID, 0, 0]` would create a line along the x-axis.
+
+In p5.js, instancing is available via optional parameters for `endShape` and `model`. It does also require a custom shader to work. In our case, let's use `model` and build a sphere shape.
+
+```js
+particleModel = buildGeometry(() => sphere(10, 2, 4));
+```
+
+Let's start by offsetting instances along the x-axis using `baseColorShader()`:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	getWorldInputs((inputs) => {
+		// Offset each instance by its ID
+		inputs.position.x += instanceID() * 20;
+    // Move the particles left on the screen
+    inputs.position.x -= 150;
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(300, 300, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 10, 2));
+	instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+	background(0);
+	orbitControl();
+	shader(instancingShader)
+  strokeShader(instancingStrokeShader)
+	model(particleModel, 16);
+}
+`} />
+
+The getWorldInputs() callback receives an inputs struct containing data about the current vertex: `position`, `normal`, `texCoord`, and `color`. The "world" part indicates that this runs after JavaScript transformations like translate() or scale() have been applied.
+
+<Callout title='World Space vs. Object Space'>
+Moving in world space is like moving relative to the entire scene, while object transformations are relative to the object's center.
+</ Callout>
+
+Now let's distribute our particles in a more interesting pattern - placing them randomly on a sphere:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	// a pseudorandom function from:
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: davepagurek
body: Looks like we've got some mixed tabs + spaces here, could we change the tabs to spaces for consistency?
created_at: 2025-04-08T13:19:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead of working sequentially, shaders apply a set of instructions across all pixels simultaneously. Each pixel or vertex independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
+
+The "strands" also refer to access points into the shader pipeline that let you modify specific aspects of rendering without building entire shaders from scratch. These access points allow you to modify specific aspects of the rendering process without needing to construct the entire shader from scratch.
+
+## Getting started with p5.strands
+### The setup
+Let's build a minimal shader example to introduce p5.strands' key concepts. P5.strands allows you to modify parts of p5.js shaders (called "strands") using JavaScript.
+
+<EditableSketch code={`
+let firstShader;
+
+// Our callback function using p5.strands, which determines how to modify the baseColorShader()
+
+function firstShaderCallback() {
+	getFinalColor((color) => {
+		color = [1, 1, 0, 1]; // Yellow in RGBA (0-1 scale)
+		return color;
+	});
+} 
+
+async function setup() {
+	createCanvas(200, 200, WEBGL);
+	// p5.strands are made using the modify function on a shader 
+	firstShader = baseColorShader().modify(firstShaderCallback);
+}
+
+function draw() {
+  background(0);
+	shader(firstShader);
+	sphere(80);
+}
+`}/>
+
+Run this sketch now—you should see a yellow sphere.
+
+### What's happening here?
+1. Shaders: P5.js already uses shaders behind the scenes in WebGL mode. p5.strands exposes parts of these shaders so you can modify them.
+2. Strands: A "strand" is a specific part of a shader you can modify. In our example, getFinalColor is a strand that lets you change an object's final color.
+3. Modifying shaders: The pattern works like this:
+- Start with a base shader (`baseColorShader()`)
+- Call its `modify()` method, passing your callback function
+- Inside your callback, use strand functions (like `getFinalColor`) to make changes
+4. Vectors: Shaders work with vectors—collections of numbers that represent colors, positions, etc. In p5.strands, you can create vectors using array syntax `[x, y, z, w]` or the `vec4()` function.
+
+### Available shaders
+p5.strands provides the following modifiable shaders. Click the links below for their reference which tells us what functions are available for overriding.
+- [`baseColorShader`](/reference/p5/baseColorShader): The default shader in WebGL mode.
+- [`baseMaterialShader`](/reference/p5/baseMaterialShader): Automatically applied if you have any lights in the scene.
+- [`baseNormalShader`](/reference/p5/baseNormalShader): Applied by calling `normalMaterial()`.
+- [`baseStrokeShader`](/reference/p5/baseStrokeShader): Applied unless `noStroke()` is called, draws stroke on objects.
+- [`baseFilterShader`](/reference/p5/baseFilterShader): A template for making post-processing filters such as those provided by p5.js, like `filter(BLUR)`.
+
+Now that we've built our first p5.strands modification, let's create a more complex scene! We'll use `baseColorShader` and `baseStrokeShader` to create 3D objects, then apply post-processing with `baseFilterShader` to enhance the final result.
+
+## Building a scene
+### Instancing particles
+Because the GPU excels at parallel computation, it can draw thousands or millions of particles simultaneously. We can do this with a technique called **GPU Instancing**.
+
+In GPU instancing, we ask the GPU to draw multiple copies of the same object, each with a unique ID (from 0 to n-1). We can then position each instance based on its ID. For example, placing objects at coordinates `[ID, 0, 0]` would create a line along the x-axis.
+
+In p5.js, instancing is available via optional parameters for `endShape` and `model`. It does also require a custom shader to work. In our case, let's use `model` and build a sphere shape.
+
+```js
+particleModel = buildGeometry(() => sphere(10, 2, 4));
+```
+
+Let's start by offsetting instances along the x-axis using `baseColorShader()`:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	getWorldInputs((inputs) => {
+		// Offset each instance by its ID
+		inputs.position.x += instanceID() * 20;
+    // Move the particles left on the screen
+    inputs.position.x -= 150;
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(300, 300, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 10, 2));
+	instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+	background(0);
+	orbitControl();
+	shader(instancingShader)
+  strokeShader(instancingStrokeShader)
+	model(particleModel, 16);
+}
+`} />
+
+The getWorldInputs() callback receives an inputs struct containing data about the current vertex: `position`, `normal`, `texCoord`, and `color`. The "world" part indicates that this runs after JavaScript transformations like translate() or scale() have been applied.
+
+<Callout title='World Space vs. Object Space'>
+Moving in world space is like moving relative to the entire scene, while object transformations are relative to the object's center.
+</ Callout>
+
+Now let's distribute our particles in a more interesting pattern - placing them randomly on a sphere:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	// a pseudorandom function from:
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: davepagurek
body: There are some in other parts too, might be worth doing a find-and-replace in the document
created_at: 2025-04-08T13:19:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead of working sequentially, shaders apply a set of instructions across all pixels simultaneously. Each pixel or vertex independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
+
+The "strands" also refer to access points into the shader pipeline that let you modify specific aspects of rendering without building entire shaders from scratch. These access points allow you to modify specific aspects of the rendering process without needing to construct the entire shader from scratch.
+
+## Getting started with p5.strands
+### The setup
+Let's build a minimal shader example to introduce p5.strands' key concepts. P5.strands allows you to modify parts of p5.js shaders (called "strands") using JavaScript.
+
+<EditableSketch code={`
+let firstShader;
+
+// Our callback function using p5.strands, which determines how to modify the baseColorShader()
+
+function firstShaderCallback() {
+	getFinalColor((color) => {
+		color = [1, 1, 0, 1]; // Yellow in RGBA (0-1 scale)
+		return color;
+	});
+} 
+
+async function setup() {
+	createCanvas(200, 200, WEBGL);
+	// p5.strands are made using the modify function on a shader 
+	firstShader = baseColorShader().modify(firstShaderCallback);
+}
+
+function draw() {
+  background(0);
+	shader(firstShader);
+	sphere(80);
+}
+`}/>
+
+Run this sketch now—you should see a yellow sphere.
+
+### What's happening here?
+1. Shaders: P5.js already uses shaders behind the scenes in WebGL mode. p5.strands exposes parts of these shaders so you can modify them.
+2. Strands: A "strand" is a specific part of a shader you can modify. In our example, getFinalColor is a strand that lets you change an object's final color.
+3. Modifying shaders: The pattern works like this:
+- Start with a base shader (`baseColorShader()`)
+- Call its `modify()` method, passing your callback function
+- Inside your callback, use strand functions (like `getFinalColor`) to make changes
+4. Vectors: Shaders work with vectors—collections of numbers that represent colors, positions, etc. In p5.strands, you can create vectors using array syntax `[x, y, z, w]` or the `vec4()` function.
+
+### Available shaders
+p5.strands provides the following modifiable shaders. Click the links below for their reference which tells us what functions are available for overriding.
+- [`baseColorShader`](/reference/p5/baseColorShader): The default shader in WebGL mode.
+- [`baseMaterialShader`](/reference/p5/baseMaterialShader): Automatically applied if you have any lights in the scene.
+- [`baseNormalShader`](/reference/p5/baseNormalShader): Applied by calling `normalMaterial()`.
+- [`baseStrokeShader`](/reference/p5/baseStrokeShader): Applied unless `noStroke()` is called, draws stroke on objects.
+- [`baseFilterShader`](/reference/p5/baseFilterShader): A template for making post-processing filters such as those provided by p5.js, like `filter(BLUR)`.
+
+Now that we've built our first p5.strands modification, let's create a more complex scene! We'll use `baseColorShader` and `baseStrokeShader` to create 3D objects, then apply post-processing with `baseFilterShader` to enhance the final result.
+
+## Building a scene
+### Instancing particles
+Because the GPU excels at parallel computation, it can draw thousands or millions of particles simultaneously. We can do this with a technique called **GPU Instancing**.
+
+In GPU instancing, we ask the GPU to draw multiple copies of the same object, each with a unique ID (from 0 to n-1). We can then position each instance based on its ID. For example, placing objects at coordinates `[ID, 0, 0]` would create a line along the x-axis.
+
+In p5.js, instancing is available via optional parameters for `endShape` and `model`. It does also require a custom shader to work. In our case, let's use `model` and build a sphere shape.
+
+```js
+particleModel = buildGeometry(() => sphere(10, 2, 4));
+```
+
+Let's start by offsetting instances along the x-axis using `baseColorShader()`:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	getWorldInputs((inputs) => {
+		// Offset each instance by its ID
+		inputs.position.x += instanceID() * 20;
+    // Move the particles left on the screen
+    inputs.position.x -= 150;
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(300, 300, WEBGL);
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: davepagurek
body: This is pretty minor, but due to some sketchy layout algorithms, sketches >200px wide put the code below instead of next to it, and limit the code editor width to the width of the body text column:

<img width="901" alt="image" src="https://github.com/user-attachments/assets/e65578f4-4f8d-4593-83b5-8c777888f8a3" />

Meanwhile the 200px wide ones go side-by-side as long as there's room:
<img width="1338" alt="image" src="https://github.com/user-attachments/assets/91ad7ddd-3fa3-4e06-b3f3-684ea5208d24" />

I bring this up only because currently some lines wrap a little awkwardly:
<img width="776" alt="image" src="https://github.com/user-attachments/assets/568682ff-fc15-481c-8174-d44fdd126947" />

So one possible quick update could be to use a smaller canvas to use the different layout, but another (maybe better?) change could just be to try to break up longer lines a bit, e.g.:
```js
particleModel = buildGeometry(() => {
  sphere(10, 10, 2)
});
```
created_at: 2025-04-08T13:24:33Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
+    - en/p5/basematerialshader/
+    - en/p5/basestrokeshader/
+    - en/p5/basenormalshader/
+    - en/p5/basefiltershader/
+authors:
+  - Luke Plowden
+---
+
+import EditableSketch from "../../../components/EditableSketch/index.astro";
+import Callout from "../../../components/Callout/index.astro";
+
+## Introduction
+
+In this tutorial, we'll explore **p5.strands**, a new way of writing shaders using JavaScript in p5.js. Shaders are programs that run on the GPU to create visual effects that would be difficult to achieve with traditional rendering.
+
+While many shader effects *could* be created with the p5.js 2D renderer, shaders are more efficient for complex effects and can be consistently applied across multiple objects. Learning shaders is valuable for anyone interested in graphics programming.
+
+We'll create a 3D sketch using 4 different shaders, each illustrating different concepts. The code will be built incrementally throughout the tutorial, with the complete code available at the end.
+
+## What is a Shader?
+
+Shaders are GPU programs written in specialized languages like GLSL for WebGL. There are two main types:
+- **Vertex shaders:** Transform the vertices of shapes (defining where to draw something)
+- **Vertex shaders:** Determine the color of each pixel (defining how to draw something)
+
+<Callout title="Info">
+  A fragment shader is sometimes referred to as a pixel shader.
+</Callout>
+
+A vertex and a fragment shader are both required to render anything in WebGL mode. Fortunately, p5.js provides some default shaders that you can use, so you don't need to write your own shaders. Consider this sketch:
+
+<EditableSketch code={`
+let myShader;
+
+function lightingSetup() {
+  ambientLight(80, 80, 140);
+  const pointX = cos(millis() / 750) * 100;
+  const pointZ = sin(millis() / 750) * 100;
+  pointLight(80, 80, 255, pointX, -60, pointZ);
+}
+
+function setup() {
+  createCanvas(200, 200, WEBGL);
+}
+
+function draw() {
+  background(100);
+  lightingSetup();
+  // shader(baseMaterialShader());
+  sphere(80);
+}
+`} />
+
+Even without explicitly setting it, a shader calculates the lighting and shading of the fill, and another does the stroke. Uncommenting shader(baseMaterialShader()) produces the same result because p5.js uses this shader by default.
+
+<Callout title="Tip">
+Try changing the 2nd line in `draw` to use `baseColorShader()` instead. The blue tint of the light in `lightingSetup` is taken into account by the material shader.
+</ Callout>
+
+Alternatively, we can use the `baseColorShader` instead and the fill is now a solid white color, not affected by lighting. The filters available when you call `filter()` are also shaders which p5.js provides for you.
+
+Part of the fun of WebGL is that you can write your own shaders, and it unlocks a lot of possibilities for creating which would be difficult to with the p2D renderer. They can also run a lot faster. In the next section, we will start the main part of the tutorial and learn how to use the new p5.js shading language.
+
+## What is p5.strands?
+
+p5.strands takes its name from the way shaders process information in parallel, rather than sequentially.
+
+In the rest of p5.js, we're accustomed to writing instructions that run sequentially, one after another. We might call `circle(10, 10)` and tell the canvas, "Draw a white circle at point `(10, 10)`." We then continue giving instructions, layer by layer, drawing each shape on top of the previous ones.
+
+Instead of working sequentially, shaders apply a set of instructions across all pixels simultaneously. Each pixel or vertex independently follows the same rules, but produces a unique result based on its position. Rather than explicitly drawing the circle, we instead ask each pixel individually, "Are you within a circle centered at `(10, 10)`? If so, change your color."
+
+The "strands" also refer to access points into the shader pipeline that let you modify specific aspects of rendering without building entire shaders from scratch. These access points allow you to modify specific aspects of the rendering process without needing to construct the entire shader from scratch.
+
+## Getting started with p5.strands
+### The setup
+Let's build a minimal shader example to introduce p5.strands' key concepts. P5.strands allows you to modify parts of p5.js shaders (called "strands") using JavaScript.
+
+<EditableSketch code={`
+let firstShader;
+
+// Our callback function using p5.strands, which determines how to modify the baseColorShader()
+
+function firstShaderCallback() {
+	getFinalColor((color) => {
+		color = [1, 1, 0, 1]; // Yellow in RGBA (0-1 scale)
+		return color;
+	});
+} 
+
+async function setup() {
+	createCanvas(200, 200, WEBGL);
+	// p5.strands are made using the modify function on a shader 
+	firstShader = baseColorShader().modify(firstShaderCallback);
+}
+
+function draw() {
+  background(0);
+	shader(firstShader);
+	sphere(80);
+}
+`}/>
+
+Run this sketch now—you should see a yellow sphere.
+
+### What's happening here?
+1. Shaders: P5.js already uses shaders behind the scenes in WebGL mode. p5.strands exposes parts of these shaders so you can modify them.
+2. Strands: A "strand" is a specific part of a shader you can modify. In our example, getFinalColor is a strand that lets you change an object's final color.
+3. Modifying shaders: The pattern works like this:
+- Start with a base shader (`baseColorShader()`)
+- Call its `modify()` method, passing your callback function
+- Inside your callback, use strand functions (like `getFinalColor`) to make changes
+4. Vectors: Shaders work with vectors—collections of numbers that represent colors, positions, etc. In p5.strands, you can create vectors using array syntax `[x, y, z, w]` or the `vec4()` function.
+
+### Available shaders
+p5.strands provides the following modifiable shaders. Click the links below for their reference which tells us what functions are available for overriding.
+- [`baseColorShader`](/reference/p5/baseColorShader): The default shader in WebGL mode.
+- [`baseMaterialShader`](/reference/p5/baseMaterialShader): Automatically applied if you have any lights in the scene.
+- [`baseNormalShader`](/reference/p5/baseNormalShader): Applied by calling `normalMaterial()`.
+- [`baseStrokeShader`](/reference/p5/baseStrokeShader): Applied unless `noStroke()` is called, draws stroke on objects.
+- [`baseFilterShader`](/reference/p5/baseFilterShader): A template for making post-processing filters such as those provided by p5.js, like `filter(BLUR)`.
+
+Now that we've built our first p5.strands modification, let's create a more complex scene! We'll use `baseColorShader` and `baseStrokeShader` to create 3D objects, then apply post-processing with `baseFilterShader` to enhance the final result.
+
+## Building a scene
+### Instancing particles
+Because the GPU excels at parallel computation, it can draw thousands or millions of particles simultaneously. We can do this with a technique called **GPU Instancing**.
+
+In GPU instancing, we ask the GPU to draw multiple copies of the same object, each with a unique ID (from 0 to n-1). We can then position each instance based on its ID. For example, placing objects at coordinates `[ID, 0, 0]` would create a line along the x-axis.
+
+In p5.js, instancing is available via optional parameters for `endShape` and `model`. It does also require a custom shader to work. In our case, let's use `model` and build a sphere shape.
+
+```js
+particleModel = buildGeometry(() => sphere(10, 2, 4));
+```
+
+Let's start by offsetting instances along the x-axis using `baseColorShader()`:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	getWorldInputs((inputs) => {
+		// Offset each instance by its ID
+		inputs.position.x += instanceID() * 20;
+    // Move the particles left on the screen
+    inputs.position.x -= 150;
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(300, 300, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 10, 2));
+	instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+	background(0);
+	orbitControl();
+	shader(instancingShader)
+  strokeShader(instancingStrokeShader)
+	model(particleModel, 16);
+}
+`} />
+
+The getWorldInputs() callback receives an inputs struct containing data about the current vertex: `position`, `normal`, `texCoord`, and `color`. The "world" part indicates that this runs after JavaScript transformations like translate() or scale() have been applied.
+
+<Callout title='World Space vs. Object Space'>
+Moving in world space is like moving relative to the entire scene, while object transformations are relative to the object's center.
+</ Callout>
+
+Now let's distribute our particles in a more interesting pattern - placing them randomly on a sphere:
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	// a pseudorandom function from:
+	// https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
+	function rand(seed) {
+		return fract(sin(dot(seed, [12.9898, 78.233])) * 43758.5453123);
+	}
+
+	function sphere() {
+    let id = instanceID();
+    let theta = rand([id, 0.1234])  * TWO_PI;
+    let phi = rand([id, 3.321]) * PI;
+  
+    let r = 180;
+    let x = r * sin(phi) * cos(theta);
+    let y = r * cos(phi);
+    let z = r * sin(phi) * sin(theta);
+    return [x, y, z];
+	}
+	
+	getWorldInputs((inputs) => {
+		inputs.position += sphere();
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(400, 400, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 4, 2));
+	instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+	background(0);
+	orbitControl();
+	shader(instancingShader)
+  fill(255, 100, 150)
+  strokeShader(instancingStrokeShader)
+	model(particleModel, 1000);
+}
+`} />
+
+The `rand()` helper function uses three GLSL functions ported to p5.strands:
+- `fract()`: Takes the fractional part of a number (e.g., `3.42` → `0.42`)
+- `sin()`: Returns the sine of an angle
+- `dot()`: Returns the dot roduct of two vectors, which measures their similarity.
+While these functions have their conventional mathematical meanings, here they're combined to generate pseudorandom values based on the instance ID.
+
+#### Adding movement to the particles
+For animating the particles, we will introduce the concept of uniform variables. 
+
+Uniforms are values passed from JavaScript to your shader. Think of them like global variables. When you change their values, all instances of the shader will see the same value immediately, without having to refresh the sketch. This makes uniforms ideal for interactive parts of your shader. 
+
+```js
+const time = uniformFloat(() => millis());
+```
+
+In p5.strands, we declare uniforms with `uniformFloat`, `uniformVector3` and so on. They should be declared inside of the callback, but not inside one of the strands itself. 
+
+Optionally, we can provide a default value for the uniform. If we set the default value as a function, it will be run every frame and the uniform value set. Alternatively, it is possible to set uniforms in the usual way for shaders in p5.js, by calling `myShader.setUniform('time', millis())`. 
+
+For our case, let's set a callback which does the same thing, for our uniform variable `time`.
+
+<EditableSketch code={`
+let instancingShader;
+let instancingStrokeShader;
+let particleModel;
+
+function instancingCallback() {
+	// Add the uniform declaration
+  const time = uniformFloat(() => millis());
+
+	// a pseudorandom function from:
+	// https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
+	function rand(seed) {
+		return fract(sin(dot(seed, [12.9898, 78.233])) * 43758.5453123);
+	}
+
+	function sphere() {
+    let id = instanceID();
+    let theta = rand([id, 0.1234])  * TWO_PI;
+    // change the particle's position over time:
+    let phi = rand([id, 3.321]) * PI + time / 10000;
+  
+    // Change the radius to create a more interesting shape:
+    let r = 200 * sin(phi);
+    let x = r * sin(phi) * cos(theta);
+    // Stretch the y dimensions a little
+    let y = r * 1.5 * cos(phi);
+    let z = r * sin(phi) * sin(theta);
+    return [x, y, z];
+	}
+	
+	getWorldInputs((inputs) => {
+		inputs.position += sphere();
+		return inputs;
+	});
+}
+
+async function setup() {
+	createCanvas(400, 400, WEBGL);
+  particleModel = buildGeometry(() => sphere(10, 4, 2));
+	instancingShader = baseColorShader().modify(instancingCallback);
+  instancingStrokeShader = baseStrokeShader().modify(instancingCallback);
+}
+
+function draw() {
+	background(0);
+	orbitControl();
+	shader(instancingShader)
+  fill(255, 100, 150)
+  strokeShader(instancingStrokeShader)
+	model(particleModel, 1000);
+}
+`} />
+
+By adding `time / 10000` to the phi angle and modifying the radius with `sin()`, we create animated particles that move around a pulsing, stretched sphere. Play around and see what other shapes you can create.
+
+<Callout title='Experiment!'>
+Try replacing `sin()` with `tan()`, `acosh()`, or combinations of functions. Small changes in shader code often create dramatically different visual effects, so experimentation goes a long way.
+</ Callout>
+
+### Fresnel effect
+If you've ever seen a material in a 3D render which appears to glow at the edges, or noticed how the light reflections appear to change on virtual water as you move your viewpoint, you were seeing the Fresnel effect. This effect changes how materials look when viewed at an angle.
+
+The Fresnel effect will be checking which parts of the shape are pointing away from the camera. For this reason, it is helpful for us to work in camera space. In camera space, also known as view space:
+- The camera is position at the origin `(0, 0, 0)`
+- The camera looks along the negative Z-axis
+- All 3D positions are relative to the camera's perspective
+
+<Callout title='Camera Space'>
+This is another relative view of the virtual world, where the camera is positioned at `(0, 0)`, so everything else is position relative to it.
+</ Callout>
+
+This perspective makes it easier to determine how surfaces appear to the viewer:
+
+```js
+function fresnelCallback() {
+	getCameraInputs((inputs) => {
+		// The normal vector on the sphere's surface
+		let normalVector = normalize(inputs.normal);
+		// This creates a vector pointing from the surface to the camera
+		let viewVector = normalize(-inputs.position);
+		// ...
+		return inputs;
+	})
+}
+```
+
+The line let viewVector = normalize(-inputs.position) might seem counterintuitive, so let's break it down:
+
+1. In camera space, the camera is at `(0, 0, 0)`
+2. inputs.position gives us the position of the current vertex being processed
+3. When we negate this (`-inputs.position`), we get a vector pointing from the vertex toward the camera
+4. `normalize()` converts this to a unit vector (where every component is between 0 - 1), making it useful for direction calculations regardless of distance
+
+#### Calculating the Fresnel factor
+
+The core of the Fresnel effect is comparing two directions:
+1. The surface normal (where the surface is "pointing")
+2. The view direction (where the camera is relative to the surface)
+
+```js
+let base = 1 - dot(normalVector, viewVector);
+let fresnel = pow(base, 2);
+```
+The dot product between these directions measures how 'edge-like' each point on our sphere is. It returns the cosine of the angle between them. The values range from 1 (when they're parallel) to 0 when they're perpendicular, and -1 when they're opposite. 
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: davepagurek
body: Might be worth reiterating what you said before about the dot product telling you how similar two vectors are, returning a value between -1 (opposite) and 1 (the same).
created_at: 2025-04-08T13:28:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,744 @@
+---
+title: "p5.strands: Introduction to Shaders"
+description: An introduction to shaders using p5.strands
+category: p5-strands
+categoryIndex: 0
+featuredImage: ../images/featured/intro-to-p5-strands.jpg
+featuredImageAlt: An abstract, cosmic scene with particles
+relatedContent:
+  references:
+    - en/p5/p5shader
+    - en/p5shader/modify
+    - en/p5shader/inspecthooks
+    - en/p5/basecolorshader/
path: src/content/tutorials/en/intro-to-p5-strands.mdx
user: login: davepagurek
body: Just tried it out, looks like it did, so I'm going to push the changes
created_at: 2025-04-08T13:32:54Z
author_association: COLLABORATOR
diff_hunk: @@ -100,6 +100,8 @@ tutorialCategories:
   accessibility: "Accessibility"
   webgl: "WebGL"
   "advanced": "Advanced Topics"
+  "p5-strands": p5.strands
+  criticalAI: "Critical AI"
path: src/content/ui/en.yaml
user: login: ksen0
body: Nice catch
created_at: 2025-04-09T20:52:23Z
author_association: MEMBER

PR comments:
user: login: davepagurek
created_at: 2025-04-08T13:10:32Z
body: @ksen0 @lukeplowden how do you feel about a separate category for p5.strands? Do you think it's too weird with just the one entry for now? We can always move this into webgl until we have more

<img width="1722" alt="image" src="https://github.com/user-attachments/assets/0fd0e4b6-003f-458e-aa78-7195bad5886b" />

author_association: COLLABORATOR
user: login: davepagurek
created_at: 2025-04-08T13:31:14Z
body: This is looking really good, great work @lukeplowden! 
author_association: COLLABORATOR
user: login: ksen0
created_at: 2025-04-08T14:55:09Z
body: > @ksen0 @lukeplowden how do you feel about a separate category for p5.strands? Do you think it's too weird with just the one entry for now? We can always move this into webgl until we have more

I strongly support the new category! We could open an issue to propose tutorials on p5.strands? Or a clear candidate for another tutorial would be about how to contribute to p5.strands maintenance, as it is an experimental feature. So, for everyone already familiar with GLSL use in p5.js and interested in getting involved.
author_association: MEMBER
user: login: davepagurek
created_at: 2025-04-08T17:29:20Z
body: One other candidate tutorial could be something going over the functions available and why you might use them? or like some "recipes" you can follow, like ways to use `mix` and `smoothstep`, or how to do randomness
author_association: COLLABORATOR
user: login: lukeplowden
created_at: 2025-04-09T15:56:52Z
body: @ksen0 & @davepagurek 
I've just made some adjustments to the wording & grammar etc based on your feedback. Thank you for the help. 
Dave, I've not looked at the line wrapping yet, but will do later on
author_association: MEMBER
user: login: ksen0
created_at: 2025-04-09T21:05:15Z
body: @lukeplowden please feel free to make a PR with line wrapping changes - I'll merge this as it is because it looks good overall and it would be great to include it in overall beta.p5js.org review of tutorials!
author_association: MEMBER