← Files pen.devARCHIVED FILE
skills/pen-dev/scripts-and-shaders.md
6.22 KB · Oct 8, 2026 · 06:38 UTC
# Using scripts and shaders on the pen.dev canvas
## Scripting
Use `script` node types to generate content with JavaScript. Scripts are `.js` files on disk, referenced via relative uri path in `scriptUri`.
- Every script must start with `/** @schema 2.11 */` (current version). Missing this tag is an error.
- Scripts receive a `pencil` object: `pencil.width`, `pencil.height`, `pencil.input.<name>`.
- Scripts must return an array of node objects following the `.pen` schema.
- Declare inputs as `@input name: type [= default]`. Available types: `number`, `string`, `boolean`, `color`, `ref`, `enum`.
- Math.random() is deterministic in scripts and can be safely used for procedural generation.
```js
/**
* @schema 2.11
* @input rows: number = 3
* @input gap: number = 4
* @input color: color = #3B82F6
* @input label: string = "Hello"
* @input filled: boolean = true
* @input layout: enum("grid", "stack", "scatter") = "grid"
* @input target: ref
*/
const rows = Math.max(1, Math.floor(pencil.input.rows));
const cellH = (pencil.height - pencil.input.gap * (rows - 1)) / rows;
const nodes = [];
for (let r = 0; r < rows; r++) {
nodes.push({
type: "rectangle",
name: "Bar " + (r + 1),
x: 0,
y: r * (cellH + pencil.input.gap),
width: pencil.width,
height: cellH * Math.random(),
fill: pencil.input.color,
});
}
return nodes;
```
## Shaders
The `shader` fill type can be used to create complex graphics effects using WebGL shaders. The `shader` effect type runs the same kind of shader over the rendered node instead (see Shader effects below).
- Shaders are WebGL 1.0 fragment shaders (#version 100), with one addition: textureSize(sampler, lod) is available for aspect-correct texturing.
- Supported uniform types:
- float, int: as numbers
- vec2/3/4, ivec2/3/4: as arrays of numbers or "#RRGGBB" strings for colors
- sampler2D: as image URL strings
- Uniforms can be annotated with special comments:
- @color: marks vec3 or vec4 uniforms to use color picker controls.
- @default: sets the default value for the uniform.
- @resolution: set to the resolution of the output. e.g. can be used to normalize gl_FragCoord.
- @mouse: set to the mouse position in the same space as gl_FragCoord. For interactive effects.
- @time: set to the elapsed time in seconds. For animations.
- @sdf: a sampler2D set to an SDF texture of the node's shape (in a shader effect: of the rendered content). The r channel holds the signed distance in @resolution units (positive = inside). The gb channels hold the gradient of the distance field (direction of increasing distance), in texel space. Use gb instead of numerically differentiating the r channel!
- @content: a sampler2D set to the node's own rendering below the shader, over transparent: in a fill the fills under it (plus the node's effects drawn before fills, like shadows), in a stroke additionally all fills, in an effect every fill, stroke and earlier effect. Texels are premultiplied by alpha, like gl_FragColor; outside the node it is transparent.
- @backdrop: a sampler2D set to the content rendered behind the node, with nothing of the node itself. Outside the node it mirrors. Offset/distort the sampling coordinate for refraction-like effects. Ideal for glass/frosted/refraction/magnifier effects. Prefer @backdrop over faking the background.
- @mipmap: the texture will be mipmapped. Off by default. Turn it on when the sample coordinate varies smoothly (glass, refraction, blur); keep it off when it jumps between neighboring pixels (pixelation, mosaics, high-frequency turbulence).
- @min/@max: set the range of a uniform for better UI controls. Only applies to number uniforms.
- @range <min>, <max>: shorthand for @min and @max on the same line. Shows a slider in the UI.
- @label <text>: sets the uniform's display name in the UI. Always set the label.
```glsl
/** @resolution */
uniform vec2 u_resolution;
/**
* @label Size
* @default 32
*/
uniform float u_size;
/**
* @label Primary Color
* @color
* @default #ffffff
*/
uniform vec3 u_color1;
/**
* @label Secondary Color
* @color
* @default #000000
*/
uniform vec3 u_color2;
void main() {
vec2 cell = floor(gl_FragCoord.xy / u_size);
float check = mod(cell.x + cell.y, 2.0);
vec3 color = mix(u_color1, u_color2, check);
gl_FragColor = vec4(color, 1.0);
}
```
### Shader effects
A `shader` effect (`effect: { type: "shader", url, uniforms }`) post-processes the node: the node, its children and its other effects are rendered into the `@content` texture, and the shader's output is drawn in place of the node. Use it for distortion, pixelation, color grading, glitch and similar effects on existing content. Use a shader fill when the shader generates the image itself. `@content` and `@backdrop` mean the same in fills and effects, so a shader written for one works as the other; a `@backdrop` shader (glass, ripple, ...) refracts what is behind the node either way.
- `@resolution` and `gl_FragCoord` are the same as for a fill: the node's size and the node's own coordinates. The shader is run over the node's visual bounds (strokes, shadows, blur, unclipped children included), so there `gl_FragCoord` goes outside `0..@resolution`, and `@content`, `@sdf` and `@backdrop` cover those bounds too: uv outside `0..1` samples them.
- Multiple shader effects are chained in list order: each one's `@content` is the output of the previous one.
- `@sdf` is the SDF of the content's alpha channel (edge at 50% alpha), so shadows, blurs and earlier shader effects on the node move the edge.
- `@content` is rendered over transparent, so blend modes inside the node only blend with the node's own content below them. A blend mode that reaches the bottom of the node (nothing opaque under it) blends with the scene when drawn normally, but not inside `@content`.
- Combine `@content` and `@backdrop`: distort the shape with `@content` and refract `@backdrop` through the result for a glass effect on a distorted node.
```glsl
/** @resolution */
uniform vec2 u_resolution;
/** @content */
uniform sampler2D u_content;
/**
* @label Pixel Size
* @default 8
* @range 1, 64
*/
uniform float u_pixelSize;
void main() {
vec2 cell = (floor(gl_FragCoord.xy / u_pixelSize) + 0.5) * u_pixelSize;
gl_FragColor = texture2D(u_content, cell / u_resolution);
}
```
SHA-256: 6e87c2ef2dc73f79a3fcec3b6756e081db3aaad077d17d03de4ff56ab9dbd8ed