Make one with ChatGPT, Claude or another AI
Copy a prompt below, replace the DESCRIPTION line with your idea, and paste it into the chat.
The AI returns the files; save them into the folders shown, run python3 pack.py <folder> and
import with + in the Installed tab.
Tip: if the AI can read links, also give it https://engine.oprimo.dev/docs/llms-full.txt,
which has the whole spec in one file.
Prompt: Metal shader (free, lightest on battery)
You are creating an animated wallpaper for PrimoEngine (macOS), type "metal".
DESCRIPTION: <describe it, e.g. "a soft aurora in greens and purples over a starry sky">
Return EXACTLY two code blocks, nothing else:
1) manifest.json (saved as <folder>/manifest.json)
2) shader.metal (saved as <folder>/content/shader.metal)
manifest.json rules:
- {"schemaVersion": 1, "id": "com.user.<slug>", "version": "1.0.0", "title": "<short title>",
"type": "metal", "entry": "content/shader.metal", "minMacOS": "26.0",
"config": [
{"key":"speed","type":"float","min":0.1,"max":3.0,"default":1.0,"label":"Speed"},
{"key":"tint","type":"color","default":"#FFFFFF","label":"Tint"}],
"capabilities": {"network": []}}
- Do not include "checksum" (pack.py computes it). Add no other config entries (the shader only receives speed and tint).
shader.metal rules (Metal Shading Language, compiled at runtime on macOS 26):
- Start with: #include <metal_stdlib> and using namespace metal;
- Declare exactly:
struct Uniforms { float2 resolution; float time; float speed; float4 tint; };
struct VOut { float4 position [[position]]; };
vertex VOut v_main(uint vid [[vertex_id]]) {
float2 p = float2((vid << 1) & 2, vid & 2);
VOut o; o.position = float4(p * 2.0 - 1.0, 0.0, 1.0); return o; }
- Fragment: fragment float4 f_main(float4 fragCoord [[position]], constant Uniforms& u [[buffer(0)]])
- fragCoord.xy is in pixels, origin top-left (y grows downward).
uv = fragCoord.xy / u.resolution. Animate with u.time * u.speed. Multiply the final color by u.tint.rgb.
Return alpha 1.0.
- FORBIDDEN anywhere in the code (including inside names): "kernel ", "[[kernel", "device ",
"threadgroup ", "atomic_", "#include \"". No textures, no samplers, no buffers other than Uniforms,
no iMouse.
- This is MSL, not GLSL: use float2/float3/float4, fmod instead of mod, fract, mix, smoothstep;
float literals with a decimal point (1.0); helper functions defined before f_main; no recursion.
- It must look good full-screen, animate smoothly and continuously, and be cheap on the GPU
(at most ~64 loop iterations per pixel).
Prompt: web / Canvas / WebGL (free)
You are creating an animated wallpaper for PrimoEngine (macOS), type "web".
DESCRIPTION: <describe it>
Return EXACTLY two code blocks:
1) manifest.json (saved as <folder>/manifest.json)
2) index.html (saved as <folder>/content/web/index.html), a single file, CSS and JS inline.
manifest.json:
- {"schemaVersion": 1, "id": "com.user.<slug>", "version": "1.0.0", "title": "<title>",
"type": "web", "entry": "content/web/index.html", "minMacOS": "26.0",
"config": [ ...2 to 5 useful settings... ], "capabilities": {"network": []}}
- Allowed setting types: "float" and "int" (with min, max, default), "bool" (default true/false),
"color" (default "#RRGGBB"). Each has "key" and "label". No "enum". No "checksum".
index.html:
- Runs in a full-screen WKWebView (Safari/WebKit) behind the desktop icons. NO INTERNET:
no CDNs, fetch, external fonts or remote images. No mouse/keyboard. No localStorage.
- Read settings like this, with defaults matching the manifest:
const cfg = { /* defaults */ };
window.LiveWallpaper = window.LiveWallpaper || {};
function readConfig(c) { Object.assign(cfg, c || {}); /* re-apply */ }
window.LiveWallpaper.onConfig = readConfig;
readConfig(window.LiveWallpaper.config);
"int" settings arrive as decimals: use Math.round. "color" arrives as "#RRGGBB".
- Animate with requestAnimationFrame; handle resize and devicePixelRatio; html/body with no margin,
overflow hidden, dark background.
- Plain Canvas 2D or WebGL (no external libraries). Light on battery.
If something fails
Paste the error from the app (or from pack.py) back into the chat together with the
error table and ask for a fix.