Crie seu wallpaper: guia prático
Um wallpaper do PrimoEngine é um arquivo .livewallpaper: um ZIP com um manifest.json e uma
pasta content/. Existem três tipos:
| Tipo | O que é | Plano |
|---|---|---|
metal | Um shader de fragmento em Metal (MSL). O mais leve na bateria. | Grátis |
web | Uma página HTML/Canvas/WebGL rodando isolada, offline. | Grátis |
video | Um vídeo .mp4/.mov/.m4v em loop. | Premium |
Você precisa de: um Mac com PrimoEngine, um editor de texto e Python 3 (já vem com as
Command Line Tools do macOS; confira com python3 --version no Terminal). Baixe o
pack.py: ele valida seu wallpaper, calcula o checksum e gera o arquivo.
Todos os campos e erros estão na Referência.
1. Seu primeiro shader em 10 minutos
1. Crie esta estrutura:
hello-shader/
manifest.json
content/
shader.metal
2. hello-shader/manifest.json:
{
"schemaVersion": 1,
"id": "com.seunome.hello-shader",
"version": "1.0.0",
"title": "Hello Shader",
"author": { "handle": "@seunome" },
"type": "metal",
"entry": "content/shader.metal",
"minMacOS": "26.0",
"config": [
{ "key": "speed", "type": "float", "min": 0.1, "max": 3.0, "default": 1.0, "label": "Velocidade" },
{ "key": "tint", "type": "color", "default": "#FFFFFF", "label": "Cor" }
],
"capabilities": { "network": [] }
}
3. hello-shader/content/shader.metal:
#include <metal_stdlib>
using namespace metal;
// Contrato com o PrimoEngine: mantenha esta struct exatamente assim (32 bytes, buffer 0).
struct Uniforms {
float2 resolution; // tamanho em pixels
float time; // segundos desde que o wallpaper começou
float speed; // ajuste "speed" (padrão 1.0)
float4 tint; // ajuste "tint" em RGB 0..1 (a = 1)
};
struct VOut { float4 position [[position]]; };
// Obrigatório: vértice de tela cheia, com o nome exato v_main.
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;
}
// Obrigatório: fragmento, com o nome exato f_main.
fragment float4 f_main(float4 fragCoord [[position]], constant Uniforms& u [[buffer(0)]]) {
float2 uv = fragCoord.xy / u.resolution; // 0..1, origem no canto superior esquerdo
float t = u.time * u.speed;
float v = sin(uv.x * 8.0 + t) + sin(uv.y * 6.0 - t * 0.7)
+ sin(length(uv - 0.5) * 14.0 - t * 1.3);
float3 col = 0.5 + 0.5 * cos(6.28318 * (v * 0.2 + float3(0.0, 0.33, 0.67)));
return float4(col * u.tint.rgb, 1.0);
}
4. Empacote:
python3 pack.py hello-shader
Sai hello-shader.livewallpaper. 5. No PrimoEngine, aba Instalados → + (Importar) e
escolha o arquivo. Pronto, ele já vai para a área de trabalho.
Vindo do Shadertoy? iResolution.xy → u.resolution, iTime → u.time,
fragCoord é igual (mas o y cresce para baixo; use uv.y = 1.0 - uv.y se precisar),
vec2/vec3 → float2/float3, mix/fract/mod → mix/fract/fmod. Não há iMouse nem texturas
(iChannel).
Regras do shader (o app recusa na importação se violar):
- Precisa ter
f_main; e precisa terv_main(sem ele o shader fica preto). - Proibido:
kernel, buffersdevice, memóriathreadgroup,atomic_*,#include "arquivo". A checagem é textual e sensível a maiúsculas: evite essas palavras seguidas de espaço até em nomes de variáveis (my_kernel,device). Comentários são ignorados. - Só
speedetintchegam ao shader. Outros ajustes aparecem no painel, mas não têm efeito.
2. Wallpaper web (Canvas / WebGL)
Estrutura:
hello-web/
manifest.json
content/
web/
index.html ← e quaisquer assets (js, css, png, jpg, webp, svg, woff2, wasm, mp4…)
hello-web/manifest.json:
{
"schemaVersion": 1,
"id": "com.seunome.hello-web",
"version": "1.0.0",
"title": "Hello Web",
"author": { "handle": "@seunome" },
"type": "web",
"entry": "content/web/index.html",
"minMacOS": "26.0",
"config": [
{ "key": "count", "type": "int", "min": 20, "max": 400, "default": 150, "label": "Partículas" },
{ "key": "speed", "type": "float", "min": 0.1, "max": 3.0, "default": 1.0, "label": "Velocidade" },
{ "key": "trails", "type": "bool", "default": true, "label": "Rastro" },
{ "key": "color", "type": "color", "default": "#7FD1FF", "label": "Cor" }
],
"capabilities": { "network": [] }
}
hello-web/content/web/index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #0b0e14; }
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
// O app injeta window.LiveWallpaper.config antes deste script e chama
// window.LiveWallpaper.onConfig(cfg) quando o usuário muda um ajuste.
// Os padrões abaixo fazem a página funcionar também num navegador comum.
const cfg = { count: 150, speed: 1, trails: true, color: "#7FD1FF" };
window.LiveWallpaper = window.LiveWallpaper || {};
function readConfig(c) { Object.assign(cfg, c || {}); reset(); }
window.LiveWallpaper.onConfig = readConfig;
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
let W, H, dots = [];
function resize() {
const dpr = window.devicePixelRatio || 1;
W = canvas.width = innerWidth * dpr;
H = canvas.height = innerHeight * dpr;
}
function reset() {
const n = Math.round(cfg.count); // ajustes "int" chegam como número: arredonde
dots = Array.from({ length: n }, () => ({
x: Math.random() * (W || 1), y: Math.random() * (H || 1),
a: Math.random() * Math.PI * 2, r: 1 + Math.random() * 3,
}));
}
function frame(t) {
ctx.fillStyle = cfg.trails ? "rgba(11,14,20,0.12)" : "#0b0e14";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = cfg.color;
for (const d of dots) {
d.a += 0.01 * cfg.speed;
d.x = (d.x + Math.cos(d.a) * cfg.speed * 1.5 + W) % W;
d.y = (d.y + Math.sin(d.a * 0.7) * cfg.speed * 1.5 + H) % H;
ctx.beginPath(); ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2); ctx.fill();
}
requestAnimationFrame(frame);
}
addEventListener("resize", () => { resize(); reset(); });
resize();
readConfig(window.LiveWallpaper.config);
requestAnimationFrame(frame);
</script>
</body>
</html>
Empacote com python3 pack.py hello-web e importe.
Como a página roda (sandbox):
- Sem internet. Toda requisição de rede é bloqueada. Inclua tudo (scripts, fontes, imagens,
Three.js etc.) dentro de
content/web/. Nada de CDN. - Arquivos são servidos por
lwp://wallpaper/…a partir da pasta doentry. Use caminhos relativos (./img/foto.jpg); nada fora dessa pasta é acessível. - Sem
file://, sem navegar para outra página, sem câmera/microfone/localização. localStorage/cookies não persistem entre execuções.- O wallpaper fica atrás dos ícones e não recebe cliques nem mouse, então não dependa de interação.
- Teste abrindo o
index.htmlno Safari (mesmo motor, WebKit). UserequestAnimationFrame: o app pausa a página quando a área de trabalho não está visível.
3. Wallpaper de vídeo (Premium)
O jeito mais simples: Instalados → + (Importar) e escolha direto um .mp4, .mov ou .m4v.
O app embala o vídeo sozinho (o título vira o nome do arquivo).
Para controlar título/autor, empacote você mesmo:
meu-video/
manifest.json
content/
video.mp4
{
"schemaVersion": 1,
"id": "com.seunome.ondas",
"version": "1.0.0",
"title": "Ondas",
"author": { "handle": "@seunome" },
"type": "video",
"entry": "content/video.mp4",
"minMacOS": "26.0",
"capabilities": { "network": [] }
}
Dicas: HEVC (H.265) economiza bateria; faça o último quadro casar com o primeiro para o loop ficar
invisível; o vídeo toca sem som e preenche a tela (corta as bordas se a proporção for diferente).
Limite: 256 MB por arquivo. Vídeo não tem ajustes (config é ignorado).
4. Ajustes para o usuário (config)
Cada item de config vira um controle em Personalizar…:
type | Controle | Valor entregue | Campos |
|---|---|---|---|
float | slider | número | min (padrão 0), max (padrão 1), default |
int | slider | número com casas decimais (arredonde você) | igual a float |
bool | chave | true/false | default |
color | seletor de cor | string "#RRGGBB" | default |
Todos aceitam key (obrigatório, único) e label (texto mostrado; padrão = key).
enum ainda não é suportado; o item é ignorado.
- Web: tudo chega em
window.LiveWallpaper.config(objeto{ key: valor }) ewindow.LiveWallpaper.onConfig(cfg)é chamado a cada mudança. - Metal: só
speed(float) etint(color) chegam ao shader, emu.speedeu.tint.
5. Empacotar e checksum
pack.py faz tudo: valida, calcula o checksum e grava o ZIP. Rode de novo sempre que mudar
qualquer arquivo em content/. Se o checksum não bater, o app recusa o pacote.
Quer fazer à mão? O algoritmo (v1):
- Liste todos os arquivos dentro de
content/, com caminho relativo acontent/(ex.:web/index.html), usando/. - Ordene por bytes UTF-8, crescente.
- Para cada um, alimente um SHA-256 com: caminho em UTF-8, um byte
0x00, o conteúdo do arquivo. "checksum": "sha256-" + hex minúsculo.
Depois, zipe manifest.json e content/ na raiz (sem pasta por cima é o ideal):
cd hello-web && zip -r -X ../hello-web.livewallpaper manifest.json content -x '*.DS_Store'
Evite “Comprimir” do Finder: ele pode incluir __MACOSX/ e .DS_Store, que quebram a importação
ou o checksum. Um thumbnail.png opcional na raiz do ZIP aparece como miniatura no app.
6. Importar e compartilhar
- Importar: aba Instalados → + (Importar). Aceita
.livewallpaper,.zipe vídeos. Importar um pacote com o mesmoidsubstitui o anterior. É assim que você publica uma atualização (aumenteversion). - Compartilhar: no menu do wallpaper importado, Share… gera um
.livewallpaper. Mande por AirDrop, mensagem, e-mail, GitHub… Quem receber importa pelo mesmo botão +. - Não há servidor nem moderação: todo arquivo, venha de onde vier, passa pelas mesmas checagens e roda no mesmo isolamento. Só wallpapers que você importou podem ser compartilhados; os do catálogo, não.
Algo deu errado? Veja a tabela de erros.