Write your own image effects
An effect is a plain JavaScript file. No compiler, installation or registration is needed. Custom effects, like the built-in ones, require Pulpo SVG Pro.
Install and try an effect
- Download the code template or copy the code below.
Save it as UTF-8 text named
my-duotone.js, notmy-duotone.js.txt. - Put it directly in
effects/at the root of the folder mounted as your project. Opening a single SVG does not give the app access to its neighbouring folders. Subfolders ofeffects/are not scanned. - Mount the project folder in Pulpo SVG, or press Reindex in the project panel if it is already mounted. Repeat this after editing the JavaScript file.
- Select one raster image (such as PNG or JPEG), then open Tools → Image → Effect. Your effect appears with a From project badge. Linked SVGs are not supported.
- Choose Duotone, change Shadows and Highlights, and move Amount. At 0 the image is unchanged; at 100 it uses the two colours. Transparency is preserved. Cancel to discard the preview, or Apply to keep the result. Save the SVG afterwards.
my-project/
├── artwork/
│ └── drawing.svg
└── effects/
└── my-duotone.js
The location is relative to the mounted project, not to drawing.svg.
Complete code template
Change id and name, then edit apply.
The comments explain what each part does. The script works with either app language.
/*
* Pulpo SVG effect template.
* Save this file in your project's effects/ folder. Change id and name,
* then edit apply. Use a plain-text editor; no compilation is needed.
* Full instructions: https://collideworks.com/pulpo-svg/docs/effects.html
*/
effect({
// Unique id. Reusing a built-in effect id replaces that effect in this project.
id: 'duotone',
// Names and labels appear as written. Use the language of your audience.
name: 'Duotone',
description: 'Maps brightness onto two colours.',
// Controls appear automatically. Values arrive in p under each control id.
// range -> number { min, max, step, value }
// select -> string { options: [{ value, label }], value }
// toggle -> boolean { value }
// color -> '#rrggbb' { value }
// palette -> ['#rrggbb'] (no value: the user chooses a palette)
controls: [
{ id: 'oscuro', type: 'color', label: 'Shadows', value: '#1a1c2c' },
{ id: 'claro', type: 'color', label: 'Highlights', value: '#ffcd75' },
{ id: 'mezcla', type: 'range', label: 'Amount', min: 0, max: 100, step: 1, value: 100 },
],
/*
* img.data: RGBA channels, four numbers per pixel, from 0 to 255.
* Modify the array elements in place; do not replace it or return a result.
* img.width / img.height: pixel dimensions; do not change them.
* p: current control values, keyed by their ids.
* apply must be synchronous: no async functions or promises.
* Each pass starts from the original image, not the previous preview.
*/
apply: function (img, p) {
var k = p.mezcla / 100;
var a = hex(p.oscuro);
var b = hex(p.claro);
var d = img.data;
for (var i = 0; i < d.length; i += 4) {
// Weighted brightness: green contributes more than blue to perceived lightness.
var y = (0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]) / 255;
d[i] += (a[0] + (b[0] - a[0]) * y - d[i]) * k;
d[i + 1] += (a[1] + (b[1] - a[1]) * y - d[i + 1]) * k;
d[i + 2] += (a[2] + (b[2] - a[2]) * y - d[i + 2]) * k;
// Leave alpha (d[i + 3]) unchanged to preserve transparency.
}
function hex(s) {
var n = parseInt(s.slice(1), 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
},
});
File format and pixels
Call effect({...}) once per file. Supply a unique non-empty id,
a non-empty name and an apply function.
description and controls are optional.
Do not use import or export.
The file name does not identify the effect: id does. A project effect with the
same id as a built-in one replaces it. Give independent effects different ids;
if two project files share an id, the later file in name order takes precedence.
apply(img, params) must finish synchronously, without async, promises
or timers. Modify the elements of img.data in place and return nothing.
Do not replace the array or change img.width or img.height.
img.data is a Uint8ClampedArray: four channels per pixel in RGBA order,
each from 0 to 255. Writes are automatically clamped to that range.
The pixel at column x, row y starts at (y * img.width + x) * 4.
Leave the fourth channel (alpha) unchanged to preserve transparency.
params contains each control's current value under its id.
Every preview starts from a fresh copy of the session's original image.
Avoid keeping state between calls. Previews use at most 512 pixels on the longest side;
Apply processes at full resolution up to 4096 pixels on the longest side; larger images
are reduced proportionally to limit memory use. Use the dimensions passed to each call.
Declare your controls
The app builds the controls from data. Use a distinct id for each control. Names, descriptions and labels are displayed as written, without automatic translation; choose the language of your users.
controls: [
{ id: 'amount', type: 'range', label: 'Amount', min: 0, max: 100, step: 1, value: 50 },
{ id: 'mode', type: 'select', label: 'Mode',
options: [{ value: 'soft', label: 'Soft' }, { value: 'hard', label: 'Hard' }], value: 'soft' },
{ id: 'enabled', type: 'toggle', label: 'Enabled', value: true },
{ id: 'tint', type: 'color', label: 'Tint', value: '#ff0000' },
{ id: 'palette', type: 'palette', label: 'Palette' },
]
| Type | Value received in params | Declaration |
|---|---|---|
| range | Number | min, max (greater than min), value; step defaults to 1. |
| select | Option value string | At least one option, plus the initial value. |
| toggle | Boolean | Initial value: true or false. |
| color | #rrggbb string | Initial colour in the same format. |
| palette | Array of #rrggbb strings | No value field; the app resolves the chosen palette. |
For example, read params.amount or params.tint in apply.
A palette control uses the palettes already available in the app, including project
palettes/ files. Handle an empty colour array. Invalid controls are omitted;
the rest of the effect can still load. Starting values resets the controls.
Keep or discard the result
Closing the panel or cancelling discards the preview. Apply offers two destinations:
- Embed in document: stores a PNG data URI inside the SVG. This may make the SVG larger; the source image file is unchanged.
- Save as file and link: writes PNG or JPEG beside the SVG and updates its image link. This option requires a saved SVG and a writable project. PNG preserves transparency; JPEG replaces transparent areas with white. Choose a new name to preserve the original. Replacing an existing file requires confirmation.
The SVG change can be undone. Undo does not delete or restore a written image file. Save the SVG to keep its new image or link.
Execution and performance
Only install scripts you wrote yourself or obtained from someone you trust. Scripts run when the project is loaded or reindexed, before you press Apply.
Your effect should only process the pixels it receives. Do not use it to open files, connect to the internet or change the app's interface. Standard JavaScript tools such as Math and typed arrays are available for your calculations.
Keep calculations that do not change from pixel to pixel outside the loop, and avoid sorting or creating new arrays for every pixel. Test on a small image first. If a script stops responding, Pulpo may stop it after a minute; reopen the effect and check your loops. Some devices cannot interrupt a stuck script, so avoid loops without a clear end.
Troubleshooting
- Missing effect: check the mounted root, the effects folder name, the .js extension and Reindex. Loading errors appear below the effect list with the file name.
- Missing controls: check control types, ids, range bounds and select options.
- No visible change: read the same parameter ids you declared and keep apply synchronous.
- Wrong colours: the pixel stride is four, not three; preserve alpha.
- Slow or stuck: check loop bounds and try a small image first.
Built-in effects
You can also choose palette mapping (palette-map), brightness, contrast
and gamma (adjust), grayscale (grayscale), inverted colours
(invert) or fewer tones per channel (posterize) from the effect list.
Give your own script a different id to keep both effects available. If you deliberately
reuse one of these ids, your project script takes its place in this project.