O que handdrawn-3d faz?
Você modela o assunto em 3D com alguns sólidos simples, e o kit o desenha à mão. As faces recebem três tons planos de marcador a partir de uma luz no canto superior esquerdo. Faces vizinhas do mesmo tom se fundem em uma mancha. O contorno é traçado com tinta pesada e as dobras mais leves, com linhas trêmulas e afiladas, sobre papel creme. A cena é desenhada uma vez por quadro com t percorrendo um loop. A saída é um SVG animado em loop: os quadros são alternados por CSS, sem script, funcionando em navegadores e tags <img>. O texto usa uma fonte desenhada à mão integrada com suporte completo a vietnamita.
Kit: assets/kit.js · renderizador: assets/render.mjs · todas as funções e opções: references/kit.md (leia antes de escrever uma cena) · exemplos práticos: examples/ (xícara, bule, torradeira, moinho: leia o mais próximo).
Configuração (uma vez)
A exportação de PNG/GIF precisa de @resvg/resvg-js. Se render.mjs disser que está faltando, execute npm install na pasta desta skill. O SVG em si não precisa de nada.
Antes de desenhar
- É desenhável? Se o assunto for abstrato ("esperança", "produtividade"), não desenhe algo vago. Sugira uma ou duas cenas concretas que o representem e desenhe a que o usuário escolher.
- Diagramas, gráficos, UI, botões, coisas interativas: não é para esta skill.
- O que se mexe? Cada imagem anima o próprio objeto: a câmera nunca se move. Escolha um ou dois movimentos que pertençam ao assunto. A tampa de uma chaleira vibra e solta vapor. Torrada pula. As hélices de um moinho giram. Um ventilador gira. O rabo de um gato balança. Um lampião acena. Uma planta balança. Se o usuário nomear um movimento, use esse. Se nada puder se mover de forma plausível, use vapor, um balanço suave ou um sway, e a agitação natural das linhas.
Fluxo de trabalho
- Partes. Liste as partes reais do assunto e como cada uma é construída a partir de sólidos. Escreva numa tabela como comentário no topo da cena: parte · chamada · tamanho/posição · cor. Modele a partir das partes reais (aros, tampas, botões, alças, pés, fendas, dobradiças), não um bloco único com um nome. Consulte proporções que não tem certeza.
- Modele em
scene.js(mundo: x direita, y trás, z cima, chão z = 0; unidades em mm; enquadramento automático):- coisas redondas que daria para tornear →
k.lathe(recipientes usamwall:),k.cylinder,k.sphere - coisas cúbicas →
k.box(round:para cantos arredondados de eletrodomésticos), contornos irregulares →k.extrude - alças, bicos, tubos, pernas, cabos, hastes →
k.tube, começando dentro do corpo (é cortado na superfície) - conteúdo de um recipiente →
{ in: container }; marcas planas numa face →k.panel/k.linecom{ on: solid } - gire o assunto inteiro com
k.group({ rotate: { z: 45 } }, …)quando a "vista lateral" deve ficar voltada para o observador (bico do bule à esquerda e alça à direita)
- coisas redondas que daria para tornear →
- Anime com
t∈ [0, 1): partes móveis vão dentro de umk.group({ about, rotate, at }, …)acionado pork.wave(),k.pulse()ou360 * t. Cada movimento deve retornar à posição inicial emt = 1. - Renderize e veja:
Abranode <skill>/assets/render.mjs scene.js -o out/scene.svg --png out/scene.png --strip --thumbout/scene.png(um quadro estático) eout/scene-frames.png(6 quadros ao longo do loop) com a ferramenta Leitura e observe-os. Adicione--gridpara ler coordenadas da folha. Verifiqueout/scene-128.png: ainda deve ser reconhecível como o assunto em 128 px. - Corrija e renderize novamente. Verifique:
- É reconhecível como o assunto à primeira vista? Silhueta primeiro.
- Nada desenhado na ordem errada: uma parte aparecendo através do corpo, uma parte distante sobre uma próxima, conteúdo escondido ou furando. Corrija com as regras em Como os sólidos são ordenados em
references/kit.md: sólidos separados,in:,on:, modele apenas o que sai de uma abertura, elayercomo último recurso. - As partes tocam o que as segura. Nada flutua a menos que deva (uma tampa saltando).
- O movimento é legível na tira de quadros e faz loop sem salto.
- Rótulos ficam nas margens e não cruzam o desenho mais do que o necessário.
Repita 4–5 até ficar certo (geralmente 2–4 rodadas). Renderize a versão final sem
--grid.
Nunca entregue sem ter visto o quadro estático e a tira de quadros. Entregue o caminho do SVG (a imagem animada), mais o PNG, e mostre o PNG. Adicione --gif out/scene.gif quando o usuário quiser um GIF ou o SVG tiver que ir para algum lugar que não reproduza animação SVG (apps de chat, alguns clientes de email).
Regras de estilo
- Um visual: tinta + tons planos de marcador sobre papel. Sem gradientes, brilhos, desfoque, truques de opacidade, SVG
<text>ou SVG bruto. Use apenas o kit. - Cor: nomes de paletas (veja
references/kit.md), 3–5 por imagem mais tinta. Nomes de paletas ficam melhores; qualquer hex é suavizado. Tons claro, médio e escuro vêm automaticamente de cada preenchimento. Não simule sombreamento com sólidos extras. - Câmera: isométrica por padrão. Altere
sheet.cameraapenas por um motivo. Uma inclinação mais baixa (20–25) e um ângulo horizontal de 25–35 produzem um "product shot" mais amigável para coisas altas (torres, garrafas, luminárias, pessoas). - Simples e encorpado: poucos sólidos limpos funcionam melhor que muitos detalhados. Peças pequenas (botões, pés, interruptores) são o que tornam real: adicione-as.
- Chão: sombras são automáticas. Algo apoiado sobre uma tábua, mesa ou pires projeta sua sombra sobre ele. Uma tábua, bandeja, montículo ou mesa sob um objeto pequeno ajuda a situá-lo no espaço.
- Texto: opcional
k.title(...)no canto superior esquerdo, mais notas curtas em minúsculas nas margens (k.label) apontando para pontos 3D. Escreva-as no idioma do usuário. Sem caixas ou balões de fala sobre o desenho. - Folha: 1000×1000 por padrão. Para cenas amplas use
export const sheet = { width: 1200, height: 900 }.--transparentremove o papel. - Tamanho da animação: 16 quadros a 12 fps (loop de 1,3 s) por padrão; um SVG típico tem 0,5–1 MB. Para movimento lento use
sheet.fps = 8. Mantenhaframes≤ 24.
Template de cena
// Um bule sobre um tripé; a tampa vibra.
// parte chamada tamanho / posição cor
// tripé cylinder r 120, h 10 madeira
// corpo lathe { smooth } r 55→94→50, h 118, sobre o tripé verde-azulado
// bico tube [14, 7] de dentro do corpo, para a esquerda verde-azulado
// alça tube 8 ambas as pontas dentro do corpo, direita verde-azulado
// tampa+botão lathe + esfera em um grupo articulado na borda traseira verde-azulado, ocre
export const sheet = { seed: 3 };
export function draw(k, t = 0) {
k.cylinder([0, 0, 0], 120, 10, { fill: 'madeira' });
k.group({ rotate: { z: 45 } }, () => { // bico à esquerda, alça à direita
k.lathe([0, 0, 10], [[55, 0], [78, 10], [94, 45], [90, 85], [68, 112], [50, 118]], { fill: 'verde-azulado', smooth: true });
k.tube([[-78, 0, 50], [-112, 0, 66], [-128, 0, 100], [-146, 0, 118]], [14, 7], { fill: 'verde-azulado' });
k.tube([[70, 0, 112], [112, 0, 108], [122, 0, 72], [84, 0, 40]], 8, { fill: 'verde-azulado' });
const pulo = Math.max(0, k.wave(0, 2 * t)); // 0..1, dois pulos por loop
k.group({ about: [44, 0, 128], rotate: { y: -18 * pulo }, at: [0, 0, 16 * pulo] }, () => {
k.lathe([0, 0, 127], [[52, 0], [50, 5], [30, 13], [8, 18], [0, 19]], { fill: 'verde-azulado', smooth: true });
k.sphere([0, 0, 152], 10, { fill: 'ocre' });
});
k.steam([-150, 0, 125], { h: 90, n: 2, spread: 18 });
});
k.label('tampa vibrando', { to: [0, 0, 140], side: 'right' });
}