← Back to Documentation

Writing Custom Effects

Ghost Arcade ships with 134 built-in post-processing effects, and you can drop in your own. A custom effect is a single .dmfx.json file containing the shader source, the parameter definitions, and a bit of metadata. Import it from the Effect Picker and it appears in the “Custom” category alongside the built-ins.

Developer Skill

custom-effects.md

Hand this file to Claude or ChatGPT and it will author effects that fit our runtime — catalog entry, param defs, shader, and the 10 defensive-programming rules from the 134-effect audit.

Download skill ↓
Starter

my-custom-effect.dmfx.json

A working template with two parameters and a tint shader you can modify. Also available inside the app via Add Effect → Template.

Download template ↓

What's in the file

A .dmfx.json is a single JSON envelope with five required sections. Here's the template you get from the Template button:

{
  "version": 1,
  "type": "my-custom-effect",
  "label": "My Custom Effect",
  "category": "Stylize",
  "description": "What this effect does, in one sentence.",
  "author": "Your Name",
  "previewCSS": "linear-gradient(45deg, #f0f, #0ff)",
  "params": [
    { "param": "uAmount", "name": "Intensity",
      "type": "slider", "min": 0, "max": 1, "default": 0.5, "step": 0.01 },
    { "param": "uColor",  "name": "Tint",
      "type": "color",  "default": [1, 0.5, 0.2, 1] }
  ],
  "shader": "precision highp float;\nuniform sampler2D uTexture;\n..."
}

The shader skeleton

Effects are post-processing passes. Your shader receives the layer's current texture as uTexture and writes a modified version. The runtime also gives you uResolution (output size) and uTime (seconds since start), plus any custom uniforms you declare in params.

precision highp float;

uniform sampler2D uTexture;
uniform vec2 uResolution;
uniform float uTime;
uniform float uAmount;
uniform vec3 uColor;
varying vec2 vUv;

void main() {
  vec4 src = texture2D(uTexture, vUv);
  vec3 tinted = mix(src.rgb, src.rgb * uColor, uAmount);
  gl_FragColor = vec4(clamp(tinted, 0.0, 1.0), src.a);
}

The rules every shipping effect follows

We audited every built-in effect. These are the defensive-programming patterns we ship and expect from imports:

Importing into the app

Once your .dmfx.json file is ready:

  1. On any layer, click + Add Effect.
  2. In the top-right of the Effect Picker, click + Import Custom.
  3. Select your file. The app validates the JSON shape and the shader's required uniforms before accepting it.
  4. The effect appears in the Custom category with a purple badge. Double-click to apply to the current layer.
  5. To remove an imported effect, hover the row in the picker and click the × in the corner.

Imported effects persist across app restarts (stored in your browser's localStorage, not cloud-synced). If you want to share an effect, just send the .dmfx.json file.

Performance: effects chain

A layer can have many effects, and they run as a chain of full-screen passes. Five chained effects at 1080p = five times the per-pixel work per frame. Keep each effect cheap: