Koder Vector Graphics Format (.kvg)

proposal-v0.1

Universal open vector format for the Koder Stack: 2D, animation, and 3D in a single self-contained document. Layered profile model (Core → Motion → Solid) lets renderers declare conformance progressively. Self-hosted-first: no external CDN refs, no proprietary dependencies, fully editable in Koder Dok and rasterizable by kicon. This document is the v0.1 proposal — scope and primitives are open for redesign before ratification.

Version: 0.1 — Proposal Status: Pre-normative — open for redesign Editors: Rodrigo Mendonça (lead) Reference impl target: Koder Dok (rasterizer + editor) and kicon (rasterizer-only)


0. Why a new format

A typical Koder product today juggles three vector-adjacent formats:

Use case Today Pain
Static 2D icons, illustrations, logos SVG Pseudo-3D só. Sem semântica de objeto.
2D animated UI (splash, micro-interaction) Rive / Lottie Outro pipeline, outro editor, outro runtime
3D real (PBR, hero, viewer) glTF + Filament Não edita no Inkscape; pesado pra ícone

Mantemos três sources (.svg, .riv, .glb) com três pipelines de tooling para descrever a mesma família de objetos. KVG propõe um source único: uma cena que pode ser 2D, animada, 3D — ou as três simultaneamente — descritas no mesmo grafo. Renderizadores declaram qual profile suportam; quem só faz 2D ignora as camadas Motion e Solid sem quebrar.

Premissa Koder-específica: self-hosted-first. Nenhum .kvg pode depender de CDN externo, font remota, shader hospedado em terceiros. O documento é fechado, distribuído como bytes, executável offline.


stylekoder-kvg { margin: 1.5rem 0; }/style

0.3 Galeria live — exemplos no browser (Core)

Renderizado ao vivo no seu navegador pelo renderer canônico do KVG compilado para WebAssembly (elemento <koder-kvg>, KVG RFC-001) — o mesmo renderer.Render que o kicon usa, sem reimplementação. A galeria cobre as features de Core (formas, path, traço, texto, gradientes, filtros, generativo), Motion (animação) e Solid (3D) — todas abaixo, ao vivo. Sem WebGL: rasterização em CPU → <canvas>.

Formas + cor

kvg-version "0.1.0"
profile     Core
viewport    0 0 240 240
scene {
  ellipse #a  80  90 r=46 fill="#3B82F6"
  ellipse #b 160  90 r=46 fill="#F59E0B"
  ellipse #c 120 165 r=46 fill="#10B981"
}

<koder-kvg src"assetskvgexamplesshapes.kvg" width"240" height"240" alt"Three filled circles in Koder accent blue, amber and emerald">/koder-kvg

Retângulos + linha

scene {
  rect  30 60 70 130 fill="#3B82F6"
  rect 110 40  90  90 fill="#F59E0B"
  line  20 25 220  25 stroke="#1E3A8A" stroke-width=6
}

<koder-kvg src"assetskvgexamplescore-rect-line.kvg" width"240" height"240" alt"Filled rectangles and a horizontal line">/koder-kvg

Path — curvas Bézier

scene { path d="M120,40 Q205,80 172,178 Q120,150 68,178 Q35,80 120,40 Z" fill="#8B5CF6" }

<koder-kvg src"assetskvgexamplescore-path.kvg" width"240" height"240" alt"A filled shape built from quadratic Bézier path segments">/koder-kvg

Traço — largura, tracejado, ponta (stroke-width / stroke-dasharray / stroke-cap)

scene {
  line 30  60 210  60 stroke="#3B82F6" stroke-width=14 stroke-cap="round"
  line 30 120 210 120 stroke="#F59E0B" stroke-width=10 stroke-dasharray=[18,12]
  line 30 180 210 180 stroke="#10B981" stroke-width=4
}

<koder-kvg src"assetskvgexamplescore-stroke.kvg" width"240" height"240" alt"Three lines: thick round-capped, dashed, and thin">/koder-kvg

Texto (fonte embutida; font=#id para TTF/OTF)

scene { text 120 132 "KVG" size=64 fill="#3B82F6" anchor="middle" }

<koder-kvg src"assetskvgexamplescore-text.kvg" width"240" height"240" alt"The word KVG rendered as vector text">/koder-kvg

Gradiente radial

gradient #sphere radial center=[0.35, 0.35] radius=0.7 {
  stop 0.00 "#BFDBFE"
  stop 0.35 "#3B82F6"
  stop 1.00 "#1E3A8A"
}
scene { ellipse #core 120 120 r=92 fill=#sphere }

<koder-kvg src"assetskvgexamplesgradient.kvg" width"240" height"240" alt"A sphere shaded with a radial gradient from light to deep blue">/koder-kvg

Gradiente linear

gradient #sky linear from=[0,0] to=[1,1] {
  stop 0.0 "#F59E0B"  stop 0.5 "#EC4899"  stop 1.0 "#3B82F6"
}
scene { rect 24 24 192 192 fill=#sky }

<koder-kvg src"assetskvgexamplesgradient-linear.kvg" width"240" height"240" alt"A square filled with a diagonal linear gradient amber to pink to blue">/koder-kvg

Mesh gradient (Coons) — o diferencial do KVG vs SVG: uma cor por canto, interpolada bilinearmente.

gradient #patch mesh corners=[[36,36],[204,36],[204,204],[36,204]]
                     colors=["#3B82F6","#F59E0B","#EC4899","#10B981"]
scene { rect 36 36 168 168 fill=#patch }

<koder-kvg src"assetskvgexamplesgradient-mesh.kvg" width"240" height"240" alt"A square with a four-corner Coons mesh gradient blending blue, amber, pink and green">/koder-kvg

Efeitos — desfoque (blur)

filter #soft { blur stdDev=6 }
scene { rect 60 60 120 120 fill="#3B82F6" filter=#soft  ellipse #b 120 120 r=30 fill="#F59E0B" }

<koder-kvg src"assetskvgexampleseffect-blur.kvg" width"240" height"240" alt"A Gaussian-blurred blue square with a sharp amber dot in the center">/koder-kvg

Efeitos — sombra projetada (drop-shadow)

filter #ds { drop-shadow dx=10 dy=10 blur=6 color="#1E3A8A99" }
scene { ellipse #card 120 110 r=72 fill="#3B82F6" filter=#ds }

<koder-kvg src"assetskvgexampleseffect-shadow.kvg" width"240" height"240" alt"A blue circle casting a soft offset drop shadow">/koder-kvg

Efeitos — matriz de cor (color-matrix)saturate / hue-rotate / luminance-to-alpha.

filter #mute { color-matrix saturate=0.25 }
scene { rect 30 30 180 180 fill=#vivid filter=#mute }

<koder-kvg src"assetskvgexampleseffect-color-matrix.kvg" width"240" height"240" alt"A vivid gradient square desaturated by a color-matrix filter">/koder-kvg

Generativo (kgen) — geometria proceduralmente emitida no mesmo grafo:

kgen #satellites { body """
  let r = 125
  each s in satellites
    emit_connector(from: [256,256], to: [cx, cy])
    emit_satellite(s.shape, cx, cy, gradient: s.gradient)
  end
""" }

<koder-kvg src"assetskvgexamplescore-kgen.kvg" width"240" height"240" alt"The Koder Hub icon: a central node with procedurally placed satellite shapes">/koder-kvg

Movimento (Motion profile) — animação vetorial no mesmo grafo:

timeline #pulse duration=1.5 loop="forever" {
  track target=#dot property="opacity"
        keyframes=[[0, 1.0], [0.75, 0.2], [1.5, 1.0]] easing="ease-in-out"
}
scene { ellipse #dot 120 120 r=80 fill="#10B981" }

<koder-kvg src"assetskvgexamplesmotion.kvg" animate width"240" height"240" alt"A green circle pulsing its opacity in a loop">/koder-kvg

Sob prefers-reduced-motion, o quadro de repouso (t=0) é renderizado estático.

Cena animada — "Arkanoid" — muitas tracks no mesmo timeline: a bola quica pela quadra (cx/cy animados — geometria 2D direta) enquanto cada tijolo some via opacity numa varredura, em loop. É uma cena coreografada (a trajetória são keyframes), não um jogo — gameplay, colisão e áudio vivem num runtime host, não no formato (§2).

stylekoder-kvg[src*="arkanoid"] canvas { max-width: 360px !important; height: 400px !important; margin: 0 auto !important; display: block !important; }/style <koder-kvg src"assetskvgexamplesarkanoid.kvg" animate width"400" height"300" alt"An Arkanoid-style board with position triggers: bricks vanish when the ball reaches them (compound cx+cy check), not at fixed times; a pink ball bounces across the board clearing bricks bottom-to-top">/koder-kvg

3D (Solid profile) — geometria sólida no mesmo grafo (renderer CPU + z-buffer):

profile Spatial
// polígono regular (cx cy r sides):
scene { extrude #gem cx=120 cy=120 r=70 sides=6 depth=60 fill="#3B82F6" }
// shape arbitrário, côncavo (points=[[x,y],…]):
scene { extrude #star depth=44 fill="#F59E0B" points=[[120,40],[138,95],[196,95],[150,130],[167,185],[120,151],[73,185],[90,130],[44,95],[102,95]] }
// reusar um shape 2D do Core (of=#id) — aqui um círculo vira cilindro:
kdef  { circle #disc cx=120 cy=120 r=72 }
scene { extrude #coin of=#disc depth=40 fill="#10B981" }
// primitivas 3D (S3): box, sphere, mesh (triângulos explícitos):
scene { box    #cube cx=120 cy=120 w=118 h=118 depth=118 fill="#8B5CF6" }
scene { sphere #ball cx=120 cy=120 r=90 fill="#EC4899" }
scene { mesh   #tetra fill="#14B8A6" verts=[[120,202,0],[52,84,-58],[188,84,-58],[120,92,78]] faces=[[0,1,2],[0,2,3],[0,3,1],[1,3,2]] }

<div style="display:flex;flex-wrap:wrap;gap:1rem;justify-content:center;align-items:center"> <koder-kvg src"assetskvgexamplessolid.kvg" width"200" height"200" alt"A blue hexagonal prism, extruded and shaded in 3D">/koder-kvg <koder-kvg src"assetskvgexamplessolid-star.kvg" width"200" height"200" alt"An amber five-pointed star extruded into a 3D prism, shaded with perspective">/koder-kvg <koder-kvg src"assetskvgexamplessolid-of-ref.kvg" width"200" height"200" alt"A green circular disc extruded into a 3D coin via of=#shape">/koder-kvg <koder-kvg src"assetskvgexamplessolid-box.kvg" width"200" height"200" alt"A purple cuboid (box primitive) shaded in 3D">/koder-kvg <koder-kvg src"assetskvgexamplessolid-sphere.kvg" width"200" height"200" alt"A pink UV-sphere primitive, latitudelongitude faceted and shaded"><koder-kvg> <koder-kvg src"assetskvgexamplessolid-mesh.kvg" width"200" height"200" alt"A teal tetrahedron from an explicit vertexface mesh"><koder-kvg> /div

Slices S1–S3 (RFC-002): extrude aceita polígono regular (cx cy r sides), shape arbitrário (points=[[x,y],…], côncavo via ear-clipping) ou reuso de um shape 2D do Core via of=#id (rectcircleellipse/ polygon — incl. shapes sob kdef, não desenhados). S3 adiciona as primitivas box (cuboide), sphere (UV-sphere rings/segs) e mesh (verts=[[x,y,z],…] faces=[[i,j,k],…] — triângulos explícitos). Todas compartilham o mesmo passo: projeção perspectiva (dist, default 4×; dist=0 → ortográfica), z-buffer e flat-shade pela luz do rig (themes/lighting.kmd).

of=#path — extrudar uma curvaof=#id aceita também um path (curvas Bézier/arcos): o contorno é achatado em world-space e extrudado — o que points= (só retas) não expressa.

kdef  { path #blob d="M120,40 Q205,80 172,178 Q120,150 68,178 Q35,80 120,40 Z" }
scene { extrude #petal of=#blob depth=40 fill="#EC4899" }

<koder-kvg src"assetskvgexamplessolid-of-path.kvg" width"240" height"240" alt"A pink Bézier-curved petal outline extruded into a 3D solid">/koder-kvg

Câmera explícita (camera) — um nó camera no grafo vira a projeção global da cena (look-at world-space; substitui a vista iso por-nó):

profile Spatial
camera #cam type=perspective eye=[210,210,360] target=[120,120,55] fov=40
scene { box #cube cx=120 cy=120 w=110 h=110 depth=110 fill="#3B82F6" }

<koder-kvg src"assetskvgexamplessolid-camera.kvg" width"240" height"240" alt"A blue cube viewed through an explicit look-at perspective camera">/koder-kvg

eyetargetup em unidades do viewport (world); fov em graus; type=ortho → projeção ortográfica. Normais de face em view-space mantêm o flat-shade correto sob a câmera.

Solid × Motion (profile Full) — o mesmo grafo anima a transformação 3D: a timeline interpola roty (graus; rotx/dist também) e o cubo gira ao vivo.

profile Full
timeline #spin duration=4 loop="forever" {
  track target=#cube property="roty" keyframes=[[0, -180], [4, 180]] easing="linear"
}
scene { box #cube cx=120 cy=120 w=110 h=110 depth=110 fill="#8B5CF6" rotx=-20 }

<koder-kvg src"assetskvgexamplessolid-motion.kvg" animate width"240" height"240" alt"A purple cube spinning around its vertical axis in 3D">/koder-kvg

2D dentro do 3D (z= num nó 2D) — uma forma 2D (rectcircleellipse/ polygon/path) com z= dentro de uma cena com transform vira um decal plano projetado pela mesma view e compostado no z-buffer compartilhado (KVG-055): o disco deita no slab, o card azul fica atrás do bloco verde e é ocluído por ele. O gradiente sobrevive à projeção (KVG-065, amostra de paint por pixel); texto/path-textura ficam pra um sampler futuro:

profile Spatial
gradient #sun radial center=[0.4,0.4] radius=0.7 { stop 0.00 "#FDE047"  stop 1.00 "#B45309" }
scene {
  g #s cx=120 cy=120 cz=60 rotx=55 roty=-26 dist=620 {
    box     #slab cx=120 cy=110 w=180 h=8 depth=130 fill="#9AA7B0"
    ellipse #disc cx=86 cy=120 rx=32 ry=32 z=118 fill=#sun        // decal com gradiente
    rect    #card x=118 y=120 w=44 h=30 z=12 fill="#3B82F6"       // ocluído pelo bloco
    box     #blk  cx=148 cy=128 w=40 h=40 depth=40 z=44 fill="#2E7D32"
  }
}

<koder-kvg src"assetskvgexamplessolid-decal.kvg" width"240" height"240" alt"A 3D platform with a radial-gradient disc lying on it and a flat blue card standing behind a green block that occludes it — 2D shapes (incl. a gradient) composited by depth in a 3D scene">/koder-kvg

Rótulo billboard (text … billboard=true) — um nó text com billboard=true dentro de uma cena com transform (grupo ou camera) vira um rótulo alinhado à tela (KVG-097 / RFC-004 C3, paridade com three-spritetext): a âncora 3D (x y + z=) é projetada pela view ativa, mas os glifos são estampados sempre de frente para o observador (não foreshortenam ao orbitar) e testados contra o z-buffer compartilhado — então o rótulo oclui e é ocluído pelos solids. anchor default é middle (o rótulo centra no centro do nó). Em Core (2D) billboard é no-op (o texto já encara a tela). Fonte embutida apenas (billboard TTF é follow-up).

profile Spatial
camera #cam type=perspective eye=[120,120,320] target=[120,120,0] fov=45
scene {
  sphere #node cx=120 cy=120 r=44 fill="#3B82F6"
  text   120 120 "engines" billboard=true z=0 fill="#FFFFFF" size=20
}

Rótulo de aresta (linklabel from=#a to=#b text=…) — um nó linklabel ancora um texto numa fração at (default 0.5) ao longo da aresta entre dois nós (KVG-097 / RFC-004 C5). Os extremos são resolvidos por id; seus centros 3D (cx/cy + z=) são interpolados e o rótulo é estampado como billboard ali — numa cena Solid ele encara a câmera e é z-testado como um rótulo de nó; numa cena Core vira texto 2D no midpoint. No-op se um extremo não existir:

profile Spatial
camera #cam type=perspective eye=[120,120,320] target=[120,120,0] fov=45
scene {
  sphere    #a cx=60  cy=120 r=18 fill="#3B82F6"
  sphere    #b cx=180 cy=120 r=18 fill="#3B82F6"
  linklabel #e from=#a to=#b text="depends on" at=0.5 fill="#94A3B8" size=12
}

Instância de forma (kdef + use of=#tpl) — um kdef define uma forma reutilizável que não é desenhada sozinha; use of=#tpl a instancia numa posição com overrides (KVG-097 / RFC-004 C4 — node-shape binding). O kdef declara o tipo primitivo via extends= (sphereboxmeshextrudecircle/ …); o use resolve a cadeia de extends, mescla os atributos do template (o mais derivado vence) e sobrepõe os próprios (posição + overrides). Assim um grafo define a forma uma vez e a liga a cada nó por id, sem repetir geometria (hyperscale-first). No-op se a referência não existir:

profile Spatial
camera #cam type=perspective eye=[120,120,320] target=[120,120,0] fov=45
scene {
  kdef #node extends=sphere r=20 fill="#3B82F6"
  use  of=#node cx=80  cy=120 z=0
  use  of=#node cx=160 cy=120 z=40 fill="#10B981"   // override de cor + profundidade
}

Cena composta (g + grafo 3D) — primitivas agrupadas sob um nó g compartilham uma transformação de cena. O grupo carrega um pivô (cxcycz), um tilt (rotx) e uma distância de perspectiva (dist) únicos: todo solid descendente roda como um corpo rígido em torno do mesmo pivô (uma timeline no grupo orbita a cena inteira) e um z-buffer compartilhado resolve a oclusão entre as peças — uma cena coerente, não primitivas isoladas. Como o box/extrude ancoram em z=0, o atributo z= (por primitiva) e tz= (no grupo) deslocam objetos em profundidade, permitindo dispor um lote em planta. O grupo também aceita rotz (roll), scalesxsy/sz e tx/ty. Aqui uma propriedade inteira num único grafo — casa de dois andares (corpo + telhado extrude + chaminé, porta, varanda e cinco janelas), garagem com carro, três árvores (ipê amarelo + duas verdes), piscina com trampolim, campinho com traves e linha central, canteiro de flores, girassóis, sebe, poste e cerca — cada objeto em sua posição (x, z) sobre o gramado. As arestas são suavizadas por anti-aliasing no próprio renderer (SSAA, KVG-053). Arraste para orbitar em 3D (interactive — arrasto horizontal = guinadaroty, vertical = arfagemrotx; sem ele, animate orbita sozinho pela timeline):

profile Full
timeline #orbit duration=18 loop="forever" {
  track target=#lot property="roty" keyframes=[[0, -54], [9, 30], [18, -54]] easing="linear"
}
scene {
  // pivô + tilt + perspectiva compartilhados; arraste controla rotx/roty (KVG-056)
  g #lot cx=160 cy=88 cz=120 rotx=50 roty=-24 dist=1100 {
    box     #grass cx=160 cy=50 w=300 h=10 depth=250 fill="#5FA04A"
    box     #body  cx=170 cy=88 w=96 h=66 depth=70 z=135 fill="#EBDFC4"  // casa 2 andares
    extrude #roof  depth=70 z=135 fill="#B9502E" points=[[122,121],[170,150],[218,121]]
    box     #gar   cx=92  cy=80 w=52 h=50 depth=58 z=140 fill="#E3D6BE"  // garagem
    box     #carb  cx=92  cy=61 w=24 h=11 depth=13 z=92  fill="#C0392B"  // carro
    extrude #cnp1  cx=256 cy=104 r=25 sides=10 depth=26 z=98 fill="#F2B705" // ipê
    box     #pool  cx=236 cy=57 w=46 h=3 depth=34 z=23 fill="#2FA8DC"     // piscina
    box     #pitch cx=92  cy=55 w=82 h=1.6 depth=52 z=14 fill="#2E7D32"   // campinho
    // … varanda, 5 janelas, 2 árvores verdes, trampolim, traves, flores,
    //    girassóis, calçada, sebe, poste, cerca — ~50 objetos no total …
  }
}

stylekoder-kvg[interactive] { overflow: hidden; } koder-kvg[interactive] canvas { width: 100% !important; height: auto !important; aspect-ratio: 43; margin: 0 auto !important; }<style> <koder-kvg src"assetskvgexampleshouse-3d.kvg" interactive width"480" height"360" alt"A detailed 3D property lot — a two-storey cream villa with a terracotta hip roof, a garage with a red car, a yellow-canopied ipê plus two green trees, a swimming pool with diving board, a small soccer pitch with goals, a flower bed, sunflowers, a hedge, a lamp post and a fence on a green lawn; drag to orbit in 3D">/koder-kvg


0.4 Filosofia de desenvolvimento

KVG é spec de longa duração: arquivos .kvg escritos em 2026 devem renderizar em 2036. Isso impõe uma assimetria sobre como decisões são tomadas neste projeto:

  • Decisões de design fundamentais — profile boundaries, sintaxe,

    primitivas, sandbox da DSL, sistema de coordenadas, semântica de notações — são feitas uma vez para durar 10+ anos. Justificam investimento desproporcional em análise, alternativas comparadas contra diretrizes, e completude antes de congelar.

  • Decisões de implementação iterativa — qual renderer escrever

    primeiro, ordem de fases do roadmap, quais bugs prioritizar, qual algoritmo interno escolher, qual cache adotar — seguem ship discipline normal: pequenas, reversíveis, frequentes.

Relação com policies/hyperscale-first.kmd

A policy diz: "esforço desproporcional → ship simples + ticket de follow-up". No KVG, a barra de "desproporcional" para decisões de design da spec é mais alta que para tickets de produto — porque o custo de errar a spec é viver com migração de breaking change em todo arquivo .kvg em circulação.

Para decisões de implementação interna dentro do projeto KVG (renderer, parser, CLI, editor, kicon integration), aplicar hyperscale-first sem ajuste.

Implicação prática

Quando uma decisão de design v0.1 for ambígua entre "ship enxuto agora + adicionar v0.2" vs "incluir agora com mais investimento":

  • Se afeta forma do .kvg distribuído (sintaxe, primitivas,

    semântica observável, contrato de runtime) → escolher completude. Custo de migração é alto e atinge todos os autores.

  • Se afeta só implementação interna (qual algoritmo, qual cache,

    qual ordem de pipeline, qual linguagem do renderer) → ship enxuto + ticket. Custo de migração é zero — é detalhe de produto, não contrato de spec.

Esta filosofia justifica decisões como "todas as constraints em Motion v0.1" (Pergunta 5, decidida 2026-04-30) e "Coons patch em v0.1" (Pergunta 6) que sob hyperscale-first puro pareceriam over-investment.


0.5 Princípios e diretrizes

Estas dez diretrizes são o contrato de design do formato. Toda decisão posterior — sintaxe, primitivas, profile boundaries, runtime — pode ser justificada (ou contestada) contra esta lista.

  1. Limpo / simples visualmente. O source canônico em texto é

    confortável de ler sem ferramenta especial. Nada de marcação ruidosa em volta de geometria simples.

  2. Extensível via DSL embarcada (kgen). Documento pode declarar

    [[kdef]] (primitivas customizadas reutilizáveis) e [[kgen]] (geração procedural pontual) para domínios específicos — CAD, VFX, schematics, ou qualquer cenário onde as primitivas-base do KVG-Core não bastam. A DSL chama-se kgen, é purpose-built (não é subset de Koder Koda), e está especificada na §13. Ver §11 para o modelo de extensão.

  3. Prático. A coisa mínima que faz uma coisa útil cabe em poucas

    linhas. Casos comuns têm caminho curto.

  4. Objetos em todas as dimensões. 1D (curvas e sequências), 2D

    (vetorial clássico), 3D (sólido), 4D (3D no tempo), N-D (paramétrico, volume rendering, dados hiperdimensionais). Coordenadas usam vetor genérico — [x, y], [x, y, z], [x, y, z, t] — sem tag de tipo manual no caso comum.

  5. Estáticos ou dinâmicos. Mesmo formato cobre desde ícone congelado

    até cena animada com state machine. Profile Motion é opt-in.

  6. Cores em todas as notações. Qualquer primitiva aceita cor por

    sólido, gradient, mesh-gradient, textura ou shader. Sem casos onde "este tipo não suporta cor" só por limitação do formato.

  7. Aplicável de desenho simples a CADVFXcinema. Ícone do Hub e cena

    de filme rodam no mesmo formato — não no mesmo profile, mas no mesmo formato. Profiles específicos (CAD-kit, VFX-kit) são montados como metaspecs reutilizáveis, não como dialetos separados.

  8. Escalável. Em duas dimensões: tamanho do documento (de 50 linhas

    até GB com streaming) e complexidade da cena (de 6 nós até milhões de triângulos com LOD, instancing, culling).

  9. Não verboso. Default agressivo: o que não foi declarado, herda do

    profile ou do parent. Nada de boilerplate por linha.

  10. Rápida leitura e rápida renderização. Source de texto parseia em

    O(n); binário .kvgb carrega em mmap-friendly layout; cenas grandes suportam streaming progressivo; primitivas mapeiam diretamente em chamadas GPU sem tradução pesada.

Tensões reconhecidas

Algumas diretrizes brigam — registramos para tratar conscientemente:

  • #1 simples ⚔ #7 todos cenários: cinema/CAD são intrinsecamente

    ricos. Resolução: complexidade vive em [[kdef]] reusáveis (#2), não em primitivas-base.

  • #9 não verboso ⚔ #4 todas dimensões: 5D paramétrico exige notação

    extra. Resolução: 2D/3D são otimizados no caso comum; N-D paga o preço só quando usado.

  • #10 rápida leitura ⚔ #7 cinema: cenas grandes obrigam streaming.

    Resolução: o binário aceita chunks lidos sob demanda; source texto é sempre carregado todo (assumido pequeno).

  • #1 simples ⚔ #2 extensão: blocos [[kdef]]/[[kgen]] inline

    tornam o source mais poderoso mas mais difícil de ler. Resolução: 95 % dos arquivos não definem extensão; os outros 5 % são feitos por autores avançados que aceitam o trade-off.

  • #2 extensão ⚔ self-hosted-first: imports externos quebram

    auto-suficiência. Resolução em §11: [[kdef]] mora dentro do mesmo arquivo que a usa; tooling (Dok) inlina ao inserir.


1. Goals

  1. Universal scene description: 2D, motion, 3D no mesmo grafo, com

    primitivas que se compõem em vez de competirem.

  2. Layered profiles: o mínimo viável é Core (2D estático). Motion e Solid

    são profiles aditivos, opt-in por documento e por renderer.

  3. Deterministic rendering: dado o mesmo .kvg e o mesmo profile, dois

    renderers conformes produzem o mesmo bitmap (dentro de tolerância de anti-aliasing definida).

  4. Self-contained: zero referências externas. Fonts, raster textures,

    meshes, scripts — tudo embarcado.

  5. Editable: o formato source é texto humano-legível, gerável por código

    e editável em Koder Dok com preview ao vivo.

  6. Object semantics: cada nó tem identidade e metadata estruturada que

    tooling (kicon, koder_kit, search) consulta — "todos os satélites do hub icon" é uma query, não uma heurística sobre bbox.

  7. Embeddable runtime: o renderer KVG-Core deve caber em < 200 KB

    compactado (alvo: WASM no Web, FFI lib no Flutter, biblioteca KL nativa).


2. Non-goals

  • Substituir CAD: STEP/OpenSCAD continuam para engenharia.
  • Substituir vídeo: KVG-Motion é animação vetorial, não codec.
  • Virar linguagem de programação: scripts inline são opcionais e

    sandboxed (Koder Koda restrito); a base é declarativa.

  • Compatibilidade exata com SVGRiveglTF: KVG é um formato novo. Os

    conversores fazem best-effort.

  • Substituir HTML / Flutter widgets: KVG descreve uma cena, não uma

    UI com layout responsivo, foco e acessibilidade WAI-ARIA.


3. Architecture — Profiles

KVG é especificada em três profiles aditivos. Cada documento declara o mínimo profile que requer; cada renderer declara o máximo profile que suporta. Documento abre num renderer ⇔ profiledoc ⊆ profilerenderer.

3.0 Declaração — aliases canônicos

Documentos declaram o profile no header com um destes aliases:

Alias Capacidades incluídas
Core 2D estático
Animated Core + Motion
Spatial Core + Solid
Full Core + Motion + Solid
profile Core      // ícone simples, splash estático
profile Animated  // logo animado, splash com motion
profile Spatial   // hero icon 3D estático
profile Full      // cena completa (3D animado)

A forma concat-explícita também é aceita pelo parser e equivale ao alias correspondente; kvg fmt normaliza para o alias canônico:

profile Core+Motion+Solid   // aceito; kvg fmt → "Full"
profile Core+Motion         // aceito; kvg fmt → "Animated"

Combinações inválidas (ex: Motion+Solid sem Core) são rejeitadas pelo validador — Core é sempre mandatório.

3.1 KVG-Core (mandatório)

Equivalente a SVG estático. Cobre:

  • Primitivas 2D: path, rect, ellipse, line, polyline, polygon,

    text (com fonts embarcadas). Rich text: text … { run "…" fill=… size=… } sub-runs (equivalente a tspan), em baseline compartilhada, com letter-spacing. textPath (texto em path) e per-glyph dxdyrotate são v0.2 follow-up; shaping/bidi ficam em KVG-069. (KVG-080)

  • Estilo: fill (sólido + gradient linearradialmesh), stroke, opacity,

    blend modes, clipping, masks.

  • Filters: blur, drop-shadow, color-matrix, displacement-map, morphology

    (dilateerode), flood, composite (overinoutatopxorarithmetic). Lighting (feSpecularLighting) e turbulence (feTurbulence) são v0.2 follow-up. (KVG-082)

  • Markers: marker #id w=W h=H { … } + marker-start, marker-mid,

    marker-end (ou shorthand marker=) em path/line; orient=auto|auto-start- reverse|<graus>; placement por vértice com rotação à tangente (amostragem afim inversa). markerUnits=strokeWidth, context-fillstroke, polylinepolygon são v0.2 follow-up. (KVG-083)

  • Grupos hierárquicos com transforms 2D (matrix, translate, rotate, scale,

    skew).

  • Viewport e units (user space, viewBox como SVG).

Renderer KVG-Core é equivalente em capacidade a um renderer SVG bom. Footprint alvo: < 200 KB, 100 % offline.

3.2 KVG-Motion (opcional)

Adiciona ao Core:

  • Timeline: keyframes, easing curves (cubic-bezier nomeados + custom

    splines), loopping-pongonce.

  • State machine: estados nomeados, transições com triggers (input

    pointer, evento custom, fim de timeline), interpolação entre estados.

  • Constraints (todas em v0.1, decidido 2026-04-30):
    • look-at 2D/3D: nó rotaciona para apontar a um alvo (point ou

      outro nó). Solver: 5-10 LoC, determinístico.

    • follow-path: nó percorre uma path com parâmetro t ∈ [0, 1],

      com modos loop, ping-pong, once. Suporta amostragem por distância de arco para velocidade uniforme.

    • IK 2-bone: solver de cinemática inversa para cadeia de 2 ossos

      (braço/perna estilo). Suporta pole-target, joint limits, fallback suave em singularidade. Inspiração de API: Rive constraints (modelo bem desenhado e validado em produção).

  • Inputs: pointer (hoverpressdrag), value bindings (data drivers).

Inspiração: Rive (state machine + constraints API), Lottie (timeline).

3.3 KVG-Solid (opcional)

Adiciona ao Core ou Core+Motion:

  • Mesh primitives: triangle mesh (índices + atributos), CSG primitives

    (sphere, cylinder, box, torus) parametricamente expandidos no carregamento.

  • Material PBR: metallic-roughness model (alinhado a glTF 2.0), normal

    maps, emissive, IBL.

  • Lighting: directional, point, spot, IBL environment cube ou equirect

    embarcado.

  • Camera: perspective/orthographic, field-of-view, clip planes.
  • 3D transforms: matrix4, quaternion rotation.
  • Per-vertex animation: skinning (joints+weights), morph targets.

Em modo Core+Solid sem Motion, geometria 3D é estática. Em Core+Motion+Solid, animação 3D usa skinning ou morph driven pelo state machine.

Compositional rule: 2D e 3D coexistem na mesma cena. Um nó 2D vive no plano z=0 do espaço 3D. Um nó 3D pode ser raster-bakeado em runtime para compor com 2D (drop-shadow do 3D no plano 2D, etc.).

Subconjunto implementado (v0.1 — renderer CPU)

O renderer canônico já cobre o caminho Solid usado pela galeria:

  • Primitivas: box (cuboide), extrude (prisma de polígono regular,

    points=, ou of=#shape), sphere (UV-sphere), mesh (verts/faces explícitos). Cada uma aceita fill=, rotx/roty (view iso por nó) e dist (perspectiva).

  • Layout em profundidade (KVG-052): box/extrude ancoram a face frontal

    em z0. O atributo *z** (por primitiva) desloca essa face ao longo de Z, e **tz=`*(no grupo) desloca o subgrafo inteiro — sem isso, dois objetos só diferem em Z pela própria espessura, não pela posição.

  • Transformação de grupo (sceneggroup): rotx/roty

(bloco). #` é reservado para IDs.

Exemplo — ícone Koder Hub (núcleo + 1 satélite + 1 gradient)

kvg-version "0.1.0"
profile     Core
viewport    0 0 512 512
title       "Koder Hub icon"

meta {
  author  "rpm"
  created 2026-04-30
  license "CC-BY-4.0"
  units   px
}

asset font #inter {
  data "base64:AAEAAAAR..."
}

gradient #core-grad radial center=[0.35, 0.35] radius=0.7 {
  stop 0.00 "#BFDBFE"
  stop 0.35 "#3B82F6"
  stop 1.00 "#1E3A8A"
}

scene {
  ellipse #core role="hub.center" 256 256 r=48 fill=#core-grad
}

Estrutura de um node statement

TYPE [#ID] [POSITIONAL_ARGS] [NAMED_ATTRS] [BODY]
  • TYPE: bare identifier (ellipse, rect, path, gradient,

    kdef, kgen, scene, group, meta, asset, ...)

  • #ID: opcional; quando presente, vira identidade local do nó.
  • POSITIONAL_ARGS: zero ou mais valores anônimos. Cada tipo

    documenta a ordem aceita (ex: ellipse cx cy r, rect x y w h, stop offset color).

  • NAMED_ATTRS: pares chave=valor em qualquer ordem.
  • BODY: bloco { ... } com mais statements aninhados. Opcional

    para nós-folha.

Tipos de valor reconhecidos

Forma Tipo Exemplo
Inteiro int 256, -7
Decimal float 3.14, 0.667, 1e-5
Booleano bool true, false
Bare identifier ident Core, radial, ellipse
ID/ref id-ref #core, #blue-sphere
String quotada string "texto com espaços"
Vec (KVN) vec [256, 256], [0.35, 0.35, 0]
Hex color (KCN, em fillstrokecolor/stop) color "#3B82F6", "#3B82F6CC"
Multi-line string block-string """...kgen body...."""

Gramática EBNF resumida

file        = (statement)*
statement   = node-stmt | comment
node-stmt   = type id? value* attr* body? NEWLINE
type        = IDENT
id          = '#' IDENT
attr        = IDENT '=' value
value       = number | bool | ident | id-ref | string | vec | block-string
vec         = '[' value (',' value)* ']'
block-string = '"""' .* '"""'
body        = '{' statement* '}'
comment     = '//' .* NEWLINE | ''

Convenções de leitura

  • Multi-line agressivo é OK: cenas longas devem usar uma

    declaração por linha. Compactação inline com separador ; é permitida (várias declarações na mesma linha) mas desencorajada para fontes versionadas em git (diffs piores).

  • Indent é livre: 2 espaços é convenção; o parser ignora.
  • Trailing comma em vecs e listas é permitida e ignorada.

Self-host parser

Implementação de referência: ~700 LoC em Koder Koda, distribuída em engines/lang/kvg/parser. Reescrita em Dart (para Flutter) e em Go (para ferramenta CLI) seguem a mesma EBNF — sem dependência de parser externo.

4.2 Binary serialization — .kvgb

Equivalente ao .glb do glTF: CBOR-encoded scene graph + concatenated binary buffers. Usado em runtime para parse rápido, distribuído quando tamanho importa. Conversão kvg ↔ kvgb é round-trip lossless.

4.3 Container variant — .kvg.zip (opcional)

Para projetos com muitos assets pesados (vários .glb mesh, raster textures de alta resolução, audio bindings de motion), KVG aceita empacotamento como ZIP com manifest.kvg na raiz. Apenas conveniência — semanticamente idêntico a um .kvg com tudo embarcado.

4.4 Versionamento

Documento declara versão da spec no header com semver:

kvg-version "0.1.0"
profile     Full

Regras de versionamento (decidido 2026-04-30):

  • Header obrigatório. Validador erra se ausente.
  • Major (1.0 → 2.0) = breaking change permitido. Ferramenta

    obrigatória kvg upgrade migra arquivos do major anterior.

  • Minor (0.1 → 0.2) = aditivo apenas. Documento v0.1 renderiza

    inalterado em renderer v0.2. Renderer v0.1 ignora features novas silenciosamente quando declaradas via requires.

  • Patch (0.1.0 → 0.1.1) = clarificação de spec, zero mudança de

    capacidade observável.

Imutabilidade do legado: nenhuma primitiva presente em v0.1 desaparece em minor ou patch. Major bump pode remover, sempre com migração ferramenta-assistida.

Capability strings por nó (opcional, ortogonal à versão):

group #character requires=["ik-2bone"] {
  // árvore que precisa do constraint IK
  fallback {
    // árvore alternativa se renderer não tem ik-2bone
  }
}

Permite graceful degradation dentro de um mesmo profile/versão. Um renderer KVG-Motion pode ter shipado look-at mas não ik-2bone ainda; o campo requires permite ao autor declarar dependência e fornecer fallback. A lista de capability strings é parte da spec da versão e cresce monotonicamente.

Renderer declara compat:

renderer.kvg = {
  version-max: "0.2.0",
  capabilities: ["Core", "Motion", "look-at", "follow-path",
                 "ik-2bone", "mesh-coons", ...]
}

Validação: documento abre se doc.kvg-version ≤ renderer.version-max E todos os requires declarados em nós estão presentes em renderer.capabilities (ou os nós têm fallback).


5. Coordinate system

Decisão 2026-04-30: Y-up everywhere (Cartesiano padrão). Razão: convenção matemática universal ensinada em escola desde Descartes (1637); alinha com 3D inteiro (glTF, Blender, Maya, Unity, USD, OpenGL); alinha com CADengenhariaplots científicos. Y-down do SVG/Canvas é artefato histórico de raster scan de CRT — não convenção humana. Importadores de SVG aplicam flip uma vez no kvg convert.

  • Sistema canônico: right-handed Cartesiano, Y-up, X-right,

    Z-out (saindo da tela em direção ao observador).

  • Origem: por default no canto inferior-esquerdo da viewport.

    Documento pode declarar origin center no header para colocar (0,0) no centro — útil para ícones simétricos.

  • Plano 2D = z=0: nós 2D vivem no plano XY com z=0 e normal +Z.
  • Viewport declara um retângulo no plano 2D: viewport x y w h,

    onde (x, y) é o canto inferior-esquerdo.

  • Units: padrão px; aceita mm, pt, em, vw/vh

    resolvidos no contexto do consumer.

  • Migração SVG: o importer aplica transform="scale(1, -1) translate(0, -H)"

    no nó raiz para inverter Y e ancorar a origem. Autor não precisa pensar nisso — kvg convert foo.svg resolve.

  • Migração glTF: importação direta sem flip (mesma convenção).

6. Object model

Cada [[node]] tem:

Campo Tipo Obrigatório Descrição
id string sim Identidade local; única no documento
role string (dotted) não Semântica para tooling. Ex: hub.satellite.square
type enum sim group, path, rect, ellipse, text, mesh, ...
parent id não Default: root
transform matrix/keyed não 2D ou 3D conforme profile
tags string[] não Para queries amplas (ex: ["interactive"])
data.* livre não Metadata arbitrária para consumers
visible bool ou 0/1 não false/0 → renderer pula o nó e todos os descendentes. Ausente → true. Animável via trigger (set="visible" to=0). Diferente de opacity=0 (que reserva espaço e hit area); visible=false não renderiza nada.

Queryability: a runtime expõe scene.query("role:hub.satellite.*") → todos os 6 nós satélite. Substitui o problema atual do kicon que precisa heurística de bbox para entender o que é "objeto" no SVG.

6.1 Acessibilidade

Toda cena KVG deve ser consumível por leitor de tela e outras tecnologias assistivas. Cinco campos a11y.* cobrem o leque relevante para conteúdo gráfico (decidido 2026-04-30):

Campo Tipo Obrigatório Descrição
a11y.label string sim no nó-raiz da scene Nome curto que o leitor anuncia
a11y.role enum não (default img) img, figure, diagram, decoration, presentation
a11y.description string não Descrição longa; serializada em aria-describedby na renderização web
a11y.decoration bool não (default false) Atalho para role="decoration"; leitor pula o nó
a11y.labelledby id-ref não Aponta pra outro nó (geralmente text) cujo conteúdo serve como label, evitando duplicação

Regras de exposição:

  1. Nó-raiz da scene deve ter a11y.label. Validador erra se ausente.
  2. Sub-nós com a11y.label próprio viram sub-elementos navegáveis

    tecnologia assistiva trata o conjunto como árvore, à semelhança de <figure> aninhado em HTML.

  3. Sub-nós sem a11y.label são opacos para AT (parte da composição

    visual, não da estrutura semântica).

  4. a11y.decoration=true salta o nó inteiramente — válido para

    ornamentos puramente visuais.

  5. a11y.labelledby="#some-text-node" resolve em runtime: o leitor lê

    o conteúdo de texto daquele nó como label.

Diferido para v0.2+: a11y.live (anúncio de mudanças em Motion) e a11y.flowto (ordem custom de leitura em diagramas editoriais). Adicionados quando demanda concreta aparecer.

Exemplo:

scene {
  group #hub-icon a11y.label="Koder Hub icon" a11y.role="img" {

    ellipse #core 256 256 r=48
    rect    #sat-top 232 107 48 48 a11y.decoration=true

    group #annotation a11y.label="6 connected satellites" a11y.role="figure" {
      // ... sub-árvore navegável
    }
  }
}

7. Notações embarcadas

Dentro do KVG, três sub-formatos são notações simbólicas — pequenas linguagens de strings interpretadas por sub-parsers próprios. Não são estrutura TOML; são strings com gramática própria embutidas em campos. Renderer KVG-Core conforme deve entender as três.

7.1 KPN — KVG Path Notation

Notação compacta para descrever curvas e contornos. Inspirada em SVG path data: cada comando é uma letra, seguida de números separados por espaço ou vírgula. Maiúscula = absoluto, minúscula = relativo.

Comando Args Significado
M / m x y Move (caneta sobe, vai para o ponto)
L / l x y Line (linha reta até o ponto)
H / h x Horizontal line
V / v y Vertical line
C / c x1 y1 x2 y2 x y Cubic Bézier (dois controles + endpoint)
Q / q x1 y1 x y Quadratic Bézier (um controle + endpoint)
A / a rx ry rot large sweep x y Elliptical arc
T / t x y Smooth quadratic continuation
S / s x2 y2 x y Smooth cubic continuation
Z / z Close path (volta para o último M)
[[node]]
id   = "leaf"
type = "path"
d    = "M 100 100 C 130 50 170 50 200 100 Q 200 150 150 180 Z"

KPN aceita extensão para 3D em KVG-Solid (comandos M3, L3, C3 com três coordenadas), e para N-D paramétrico (sufixo N mais um vetor).

7.2 KCN — KVG Color Notation

Herda do CSS Color Module Level 4 — qualquer renderer KVG aceita as notações abaixo. Um campo fill, stroke, color ou stop-color pode receber qualquer forma:

Forma Exemplo
Hex curto #3B8
Hex longo #3B82F6
Hex com alpha #3B82F6CC
RGB rgb(59, 130, 246)
RGBA rgb(59, 130, 246, 0.8)
HSL hsl(217, 91%, 60%)
OKLCH oklch(60% 0.18 250) (recomendado para perceptual)
Named transparent, currentColor
Gradient ref gradient(id) (aponta para [gradient.id] declarado)

Recomendação na §0.5 diretriz #6: produtos Koder devem usar OKLCH para máxima consistência perceptual entre dispositivos.

7.3 KVN — KVG Coordinate Notation

Notação vetorial unificada para 1D a N-D. Vetor é uma lista TOML; a dimensionalidade é inferida pelo número de componentes:

Forma Significado
[x, y] 2D — plano padrão
[x, y, z] 3D — espaço cartesiano
[x, y, z, w] 4D — homogêneo (projetivo) ou 3D + tempo
[x, y, z, t, p] N-D — paramétrico (e.g., volume rendering)

Operadores e funções da kgen (§13) trabalham polimorficamente sobre KVN — length([3,4]) → 5; length([1,2,2]) → 3; lerp(a,b,t) opera componente a componente.

7.4 Por que notações ao invés de TOML estruturado?

Notações são mais densas, mais legíveis para olho treinado e mais performáticas para parse + render. Compare:

# Notação compacta
d = "M 0 0 L 100 0 L 100 100 Z"

# Forma estruturada equivalente (rejeitada)
[[d.cmd]]
op = "moveTo"
to = [0, 0]
[[d.cmd]]
op = "lineTo"
to = [100, 0]
[[d.cmd]]
op = "lineTo"
to = [100, 100]
[[d.cmd]]
op = "close"

Para 50 comandos numa path, a forma estruturada custa ~10× mais bytes e é mais lenta de parsear. A notação ganha em todos os quesitos — desde que seja simples o suficiente pra autor ler na mão.


8. Color & material

8.1 2D (Core)

  • Cor: #RRGGBB, #RRGGBBAA, rgb(), oklch() (recomendado para perceptual

    consistency entre dispositivos).

  • Fill: sólido, linear-gradient, radial-gradient, mesh-gradient Coons-patch

    (decidido 2026-04-30; ver §8.1.1).

  • Stroke: width, cap, join, miter, dasharray.
  • Filter graph: nós conectáveis (blur → color-matrix → composite), idêntico

    ao modelo SVG <filter> mas com tipos checados.

  • Blend modes (atributo blend="<modo>" por nó; CSS Compositing & Blending

    Level 1; default normal, não-herdado): separáveis normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion; não-separáveis hue, saturation, color, luminosity. Modo desconhecido degrada para normal. isolation (buffer offscreen de grupo) é v0.2 follow-up (KVG-078). (KVG-078)

  • Clipping & masking (CSS Masking Level 1):
    • clip=#id — recorta o nó à geometria do subtree referenciado

      (shapepathgroup renderizado fill-only); pixels fora da região somem.

    • mask=#id — modula a alpha do nó pela cobertura do subtree

      referenciado: luminância·alpha por default, ou só alpha quando mask-mode="alpha". clip e mask combinam (interseção multiplicativa).

    • #id desconhecido = no-op (nó renderiza sem recorte; comportamento CSS).
    • v0.1 (slice 1): em drawables-folha. Clip/mask em grupos, clip-rule,

      refs aninhadas e luminância linearizada são v0.2 follow-up. (KVG-077)

  • Pattern (tiling): pattern #id w=W h=H { … } declara um tile W×H (world

    units) de conteúdo KVG arbitrário; fill=#id preenche repetindo o tile. rotate=N gira a malha. v0.1 (slice 1): userSpaceOnUse; objectBoundingBox, patternTransform (scale/translate) e tile animado são v0.2 follow-up. (KVG-079)

8.1.1 Mesh gradient (Coons patch)

Mesh gradients permitem transições de cor em 2D ao longo de uma área 4-corner com lados curvos. Modelo: Coons patch — define cores nos 4 cantos + curvas Bézier nos 4 lados. Bilinear (lados retos) é caso particular sem custo extra de sintaxe.

gradient #fruit-skin mesh {
  // 4 cantos em ordem TL → TR → BR → BL (Y-up, então TL = top-left = [0,1])
  corner [0, 1] color="#FFE5B0"
  corner [1, 1] color="#FFA640"
  corner [1, 0] color="#D04020"
  corner [0, 0] color="#700"

  // 4 lados — cada um straight ou bezier (até 2 controles)
  edge top    bezier=[[0.3, 1.1], [0.7, 1.1]]
  edge right  bezier=[[1.1, 0.7], [1.1, 0.3]]
  edge bottom straight
  edge left   straight
}

Todos os 4 lados straight ⇒ degenera em bilinear sem boilerplate extra. Coordenadas dos cantos no espaço normalizado [0, 1].

Razão da escolha (2026-04-30): bilinear puro é meio-caminho que envelhece mal em ilustração premium e packaging design (diretrizes 6 e 7); Coons cobre ambos os casos com um único modelo. Custo de impl ~150 LoC amortizado para sempre.

8.2 3D (Solid)

  • PBR metallic-roughness (gltf 2.0 model):
    • baseColor, metallic, roughness, normal, emissive, occlusion.
  • Texturas: raster embarcado (PNG/WebP) ou procedural (gradient como

    texture).

  • Lighting: directional, point, spot, IBL (cube ou equirect HDR

    embarcado, comprimido com Koder kodec-img).

8.3 Shader graph (extensão futura)

Profile KVG-Shade (não-v0.1, deixado para v0.2+): grafo declarativo de shader unificando filter graph 2D e PBR 3D. Excluído do escopo desta proposta para limitar v0.1.


9. Animation & Motion

9.1 Timeline

[[timeline]]
id       = "intro"
duration = 1.2  # seconds
loop     = "once"

[[timeline.track]]
target   = "core"          # node id
property = "transform.scale"
keyframes = [
  { t = 0.0, value = 0.0,  easing = "ease-out-cubic" },
  { t = 0.6, value = 1.05, easing = "ease-in-out" },
  { t = 1.0, value = 1.0 },
]

Reduced motion (KVG-072)

A timeline may declare a reduced-motion= policy for when the host signals prefers-reduced-motion:

timeline #spin loop="forever" reduced-motion="hold-first" { … }
// reduced-motion: play | hold-first (default) | hold-last | crossfade=<seconds>
  • Default hold-first — render the rest frame at t=0, no animation.

    The web renderer applies this automatically, so a looping/parallax KVG is motion-safe by default even without the attr.

  • play opts a document back into motion under reduced-motion (use

    sparingly — only for motion that is non-vestibular and essential).

  • The policy is a render input (like the profile), so output stays

    deterministic given (.kvg, profile, reduced-motion preference).

  • A document with no reduced-motion declared is not an error

    (motion-safe default is inferred).

9.2 State machine

[[state-machine]]
id      = "idle-hover-press"
default = "idle"

[[state-machine.state]]
name = "idle"
[[state-machine.state]]
name = "hover"
[[state-machine.state]]
name = "press"

[[state-machine.transition]]
from    = "idle"
to      = "hover"
trigger = "pointer.over"
duration = 0.15
easing  = "ease-out"

[[state-machine.transition]]
from    = "hover"
to      = "press"
trigger = "pointer.down"
duration = 0.08

State change interpola valores de propriedades animáveis declaradas em cada estado.

9.3 Constraints

IK 2-bone, follow-path, look-at 2D. Lista exaustiva fica para v0.2+.


10. 3D solids

Estrutura mínima para Solid:

[[node]]
id       = "hero-glb"
type     = "mesh"
mesh     = "hub-mesh-01"
material = "hub-material-01"

[[asset.mesh]]
id          = "hub-mesh-01"
primitives  = [
  { mode = "triangles", indices = "buf:0", positions = "buf:1", normals = "buf:2", uvs = "buf:3" }
]

[[asset.material]]
id           = "hub-material-01"
type         = "pbr-mr"
baseColor    = "#3B82F6"
metallic     = 0.3
roughness    = 0.4
normalTexture = "tex:hub-normal"

[[asset.buffer]]
id   = "buf:0"
data = "base64:..."

Importação direta de .glb é suportada via converter kvg import gltf, que aterriza meshesmaterialsanimations como [[asset.*]] blocks.


11. Extensão: kdef e kgen

KVG suporta dois mecanismos de extensão no source. Ambos vivem no próprio arquivo que os consome — nunca em arquivos separados, nunca buscados por URL. Auto-suficiência do documento é regra dura (§12).

Ambos os mecanismos rodam código escrito na DSL kgen, especificada em §13. Não há campo lang — só existe uma DSL.

11.1 kdef — primitivas customizadas reutilizáveis

kdef define um tipo novo de nó como uma expansão para primitivas-base ou para outras kdefs já definidas no mesmo arquivo:

kdef #spring extends=path params=[start, end, coils, amplitude] {
  expand """
    let pts = [start]
    let segments = coils * 2
    for i in range(segments)
      let t = (i + 1) / segments
      let mid = lerp(start, end, t)
      let off = if i % 2 == 0 then amplitude else -amplitude
      pts.push([mid.x, mid.y + off])
    end
    pts.push(end)
    pts
  """
}

scene {
  spring #shock-absorber start=[50,100] end=[200,100] coils=8 amplitude=12
}

Pipeline do parser:

  1. Pass 1: coleta todas as declarações kdef do documento. Ordem

    de aparição não importa.

  2. Pass 2: ao encontrar um nó cujo TYPE casa com um id de kdef

    (spring, no exemplo), busca primeiro as kdefs locais; se não achar, cai nas primitivas-base; se não achar, erro KVG-PARSE-UNKNOWN-TYPE-NNNN.

  3. Expand: roda o bloco expand no sandbox kgen; o resultado vira

    nó da primitiva apontada por extends.

  4. Render: renderer só vê primitivas-base — nunca soube que

    spring existe.

11.2 kgen — geração procedural pontual

Para repetições e parametrizações que não merecem virar tipo nomeado:

kgen #build-satellites {
  body """
    let r = 125
    for i in range(6)
      let angle = (90 - i * 60) * PI / 180
      emit_node(
        id: "sat-" + i.to_s,
        role: "hub.satellite",
        type: shape_for(i),
        cx: 256 + r * cos(angle),
        cy: 256 - r * sin(angle)
      )
    end
  """
}

kgen roda no load-time, no mesmo sandbox que o expand de kdef. A diferença: kgen emite nós direto, sem virar tipo reutilizável.

11.3 Regras duras (validador rejeita)

  1. Escopo local ao documento. kdef definida em A não é visível em

    B. Cada .kvg é mundo fechado.

  2. Sem rebinding de primitivas-base. [[kdef]] id = "ellipse" é

    erro. Só extensão, nunca substituição.

  3. Sem ciclos no grafo de dependência. Se kdef A usa B e B usa A,

    validador detecta e falha.

  4. Sandbox kgen — sem I/O, sem rede, sem syscall. O bloco expand

    da [[kdef]] e o body da [[kgen]] só podem usar a stdlib kgen (§13) e os helpers emit_* providos pelo runtime.

  5. Determinismo. Dado o mesmo input, expansão produz o mesmo output

    bit-a-bit. Sem random sem seed, sem timestamp, sem leitura externa.

11.4 Distribuição

O .kvgb binário inlina o resultado das expansões para parse mais rápido em runtime. O source .kvg texto preserva os blocos [[kdef]] e [[kgen]] para edição. Conversão kvg ↔ kvgb é round-trip do source completo (incluindo extensões), não só do output expandido — o autor nunca perde sua extensão ao ir e voltar.

11.5 Biblioteca de kdefs (camada de tooling, não de formato)

O Koder Dok mantém um catálogo de kdefs comuns (spring, gear, bezier-arrow, dimension-callout, gauss-bell, ...). Quando o autor "insere" uma do catálogo, o Dok copia a definição para dentro do arquivo aberto. O autor experimenta autocomplete e snippets; o arquivo salvo permanece auto-suficiente. Igual ao padrão de snippets do VSCode.

A biblioteca não é parte da especificação do formato — é conveniência da ferramenta. Outras ferramentas KVG podem implementar suas próprias bibliotecas, ou nenhuma. O .kvg resultante funciona em qualquer renderer compliant.


12. Self-hosted-first constraints

Regras duras (decidido 2026-04-30 — strict em v0.1, sem exceções):

  1. Zero URL externa em nenhum campo do documento. Validador rejeita

    http://, https://, //cdn.x.y/, file://, koder://, e qualquer outro scheme — apenas referências internas (#id) são válidas.

  2. Fonts embarcadas como asset font em base64 (TTFOTFWOFF2).
  3. Texturas raster embarcadas como asset texture em base64

    (PNG, WebP, AVIF). Recomenda-se passar pela compressão do kodec-img antes de embed.

  4. Scripts são kgen inline ou referência por id a outro kgen

    interno. Sem import ou require de qualquer fonte externa.

  5. Profile dependencies (Motion, Solid) são parte da especificação

    carregada pelo runtime — nunca buscadas em runtime.

kvg validate <file> falha o documento se alguma regra acima for violada.

Razão (2026-04-30)

A flexibilização foi avaliada e rejeitada para v0.1. Opções consideradas:

  • koder:// URI scheme com cache local — fragmenta determinismo, doc

    deixa de ser auto-suficiente.

  • Refs content-addressable por hash (IPFS-style) — interessante mas

    complexidade desproporcional pra v0.1.

  • Header opt-in (chave external-refs allow) — fragmenta em dois mundos KVG.

Para casos pesados (texturas 4K, brand kit compartilhado), o pacote .kvg.zip (§4.3) já cobre — embarca binários num zip de conveniência sem violar self-hosted-first.

Sob filosofia §0.4 (long-term): é mais fácil flexibilizar v0.2 se demanda concreta aparecer do que retirar permissão depois. Strict primeiro é a aposta segura. Caminho preferencial para v0.2+ se for o caso: hash content-addressable (preserva determinismo, agnóstico de transporte).


13. kgen — language reference v0.1

A DSL embarcada usada pelos blocos [[kdef]] e [[kgen]]. Frozen em v0.1; mudanças futuras viram v0.2 com profile opt-in. Esta seção é a spec completa — implementadores não precisam de mais nada.

13.1 Filosofia

kgen é purpose-built, não é subset de Koder Koda. Sintaxe-família de KL (mesmas keywords) mas semântica própria. Alvo de implementação: ~500 LoC parser, ~1.000 LoC interpreter tree-walking, total ~50-100 KB compilado por renderer.

13.2 Tipos primitivos

Tipo Exemplo Notas
int 42, -7 64 bits sinalizado
float 3.14, 1e-5 IEEE 754 double
bool true, false
string "abc", 'abc' UTF-8; sem regex, sem format
vec [1, 2, 3] 1D-Nd; KVN compatível
mat mat([[1,0],[0,1]]) matriz NxM
list [1, "a", true] heterogênea
hash {a: 1, b: 2} string→valor
range range(0, 10) iterador finito
null null ausência

13.3 Operadores

aritméticos:   + - * / % **
comparação:    == != < > <= >=
lógicos:       && || !
acesso:        . [ ]

Todos componentwise sobre vec e mat ([1,2] + [3,4][4,6]).

13.4 Controle de fluxo

let x = 10
if x > 5 then
  ...
else
  ...
end

for i in range(0, 6)
  ...
end

each item in lista
  ...
end

Não tem: while, loop, goto, break, continue, recursão, closures, exceptions. Loops são bounded por construção (range ou each).

13.5 Funções declaradas

fn distance(a, b)
  let d = b - a
  return sqrt(d.x * d.x + d.y * d.y)
end

Funções são puras, top-level, sem closures. Podem chamar outras funções declaradas no mesmo bloco. Recursão proibida — validador detecta e falha. Para repetição, usar for ou each.

13.6 Builtins (~30, congelados em v0.1)

Matemática escalar: abs floor ceil round trunc sqrt pow exp log min max clamp sign

Trigonométricas: sin cos tan asin acos atan atan2

Vetoriais: vec length normalize dot cross dist angle_between

Matriciais: mat identity transpose inverse multiply translate rotate scale

Interpolação: lerp smoothstep mix step

Conversão / string: to_s to_i to_f concat

Constantes: PI E TAU INF NAN

13.7 Helpers de runtime (injetados pelo KVG)

Apenas dentro de [[kgen]] body ou [[kdef]] expand:

Helper Retorno O que faz
emit_node({...}) cria um [[node]] na cena
emit_path(d, opts) cria nó path com KPN
emit_connector(from, to, opts) linha entre dois pontos
param(name) valor acessa [[kdef]].params[name]
viewport() [w, h] ou [w,h,d] dimensões da viewport

13.8 Sandbox — proibições

  1. Sem I/O. Nenhuma função abre/lê arquivo, socket, dispositivo.
  2. Sem rede. Nenhuma chamada HTTP, DNS, etc.
  3. Sem syscalls. Sem exec, fork, signal, time.now.
  4. Sem fontes não-determinísticas. random exige seed explícito;

    time não existe; ordem de iteração de hash é estável.

  5. Sem dynamic code. Sem eval, parse, import, require.
  6. Sem mutação global. Variáveis declaradas com let são locais

    ao bloco; sem globals.

  7. Bounded execution. Loop limit configurável (default: 1.000.000

    iterações totais por bloco); estouro = erro KGEN-LIMIT-EXCEEDED.

13.9 Determinismo

Implementação compliant deve garantir bit-exact reprodução entre renderers para o mesmo input. Especificamente:

  • Aritmética float segue IEEE 754, ordem de operações canônica

    (esquerda→direita, respeitando precedência).

  • for i in range(...) itera em ordem crescente.
  • each sobre hash itera em ordem de inserção.
  • random(seed) usa xorshift64 com algoritmo specificado em §13.11.

13.10 Gramática EBNF (resumida)

program     = (statement | function-decl)*
statement   = let-stmt | if-stmt | for-stmt | each-stmt | expr-stmt | return-stmt
let-stmt    = 'let' IDENT '=' expression
if-stmt     = 'if' expression 'then' statement* ('else' statement*)? 'end'
for-stmt    = 'for' IDENT 'in' expression statement* 'end'
each-stmt   = 'each' IDENT 'in' expression statement* 'end'
function-decl = 'fn' IDENT '(' params ')' statement* 'end'
expression  = literal | IDENT | unary | binary | call | access
binary      = expression op expression
op          = '+' | '-' | '*' | '/' | '%' | '**' | '==' | '!=' | '<' | '>' | '<=' | '>=' | '&&' | '||'
literal     = INT | FLOAT | STRING | BOOL | NULL | vec-lit | hash-lit
vec-lit     = '[' (expression (',' expression)*)? ']'

13.11 Implementação de referência

A spec do parser, interpretador, e algoritmos de determinismo (xorshift seed, ordem de hash, IEEE 754 rounding) vivem em engines/lang/kvg/kgen/SPEC.kmd (a criar quando começar implementação). Esta §13 é o contrato; aquela é o blueprint de código.


14. Profile conformance

Documento declara profile Full (alias canônico — equivale a Core+Motion+Solid). Renderer declara capabilities como conjunto, ex: ["Core", "Motion"] (= Animated). Algoritmo:

if profile_doc ⊆ profile_renderer:
    render normally
else:
    render best-effort (drop unsupported nodes)
    emit conformance warning to host

Nó individual pode declarar requires = "Solid"; se renderer não tem, nó é silenciosamente substituído pelo seu fallback opcional (um nó 2D estático equivalente). Mecanismo análogo ao <switch> do SVG.


15. Reference toolchain

Componente Papel Status v0.1
engines/lang/kvg/spec Esta spec, schemas JSON/CBOR Esta proposta
engines/lang/kvg/parser Lib Koder Koda: parse .kvg → scene tree A implementar
engines/lang/kvg/render Renderer KVG-Core (Skia backend LinuxAndroid, CoreGraphics iOSmacOS, Canvas2D web) A implementar
engines/lang/kvg/render-3d Renderer KVG-Solid (Filament-based) A implementar
engines/sdk/kvg-flutter Flutter widget <KvgScene> A implementar
products/horizontal/dok Editor canônico, preview ao vivo, export para SVGPNGglTF Integração a planejar
products/dev/kicon Consome .kvg como source preferencial; gera variants per-platform usando role semantics Integração a planejar
kvg CLI kvg validate / convert / build / explode A implementar

16. Migration paths

De Para KVG Estratégia
SVG KVG-Core kvg convert <file>.svg mapeia 1:1 (path, gradient, filter); roles inferidos por classes/ids; viewport + transform mantidos
Rive KVG-Core+Motion parser do .riv (open spec) extrai timeline + state machine; primitivas vetoriais convertem para KVG paths
Lottie KVG-Core+Motion parser JSON; expressões After Effects (subconjunto) convertem para easing curves nomeadas
glTF KVG-Core+Solid meshmaterialanimation 1:1; cenas multi-mesh viram grupos KVG
PNG/WebP raster KVG-Core wrapper embed como [[asset.texture]] num único nó image — fallback para quem só tem raster

Round-trip não é garantido em geral (ex: KVG → SVG perde Motion); apenas SVG → KVG e KVG-Core → SVG são lossless por design.


17. Open questions (a resolver antes de v0.2)

  1. Source syntax (decidido 2026-04-30): KSS — KVG Source Syntax

    purpose-built, KDL-inspired, com hierarquia nativa via { }, identificadores bare, IDs #prefixados (CSS-style), atributos posicionais + nomeados, notações inline (KPNKCNKVN), comentários // e . Spec da gramática em §4.1. Razão: TOML emprestado força hierarquia flat-com-parent-ref (diretriz #1 e #2 sofrem) e [[node]] repetitivo (diretriz #9 sofre). KSS coerente com a filosofia purpose-built do resto da stack (kgen, KPNKCNKVN).

  2. Profile name (decidido 2026-04-30): aliases canônicos Core

    / Animated / Spatial / Full no header; forma concat-explícita (Core+Motion+Solid) também aceita pelo parser e normalizada para alias canônico pelo kvg fmt. Declaração e mapeamento em §3.0. Razão: diretrizes #1 (limpo) e #9 (não verboso) ganham com aliases; kvg-N numérico foi rejeitado porque profiles não são cumulativos linearmente (não há número natural para Core+Solid sem Motion).

  3. Mesh-gradient encoding (decidido 2026-04-30): Coons patch em

    v0.1. Bilinear (lados retos) é caso particular sem boilerplate extra. Razão: diretrizes #6 (cores em todas notações) e #7 (CAD a cinema) só ficam ★★★★★ com Coons; bilinear envelhece mal em ilustração premium. API e exemplo em §8.1.1.

  4. Acessibilidade (decidido 2026-04-30): cinco campos a11y.*

    por nó (label, role, description, decoration, labelledby). Spec em §6.1. Razão: cobre 100 % dos casos relevantes para conteúdo gráfico sem invadir o leque de WAI-ARIA específico de widgets interativos. a11y.live e a11y.flowto diferidos para v0.2+ — só fazem sentido com Motion shipado e demanda concreta de diagramas editoriais.

  5. Streaming / progressive load: assets pesados podem ser carregados

    sob demanda? (Conflita com self-hosted-first se requer rede; aceitável se tudo está num .kvg.zip lido stream do disco.)

  6. Versionamento (decidido 2026-04-30): semver no header

    (kvg-version "0.1.0" obrigatório) + capability strings opcionais por nó (requires=[...] com fallback). Imutabilidade do legado: nenhuma primitiva removida em minor/patch; major bump exige ferramenta kvg upgrade. Detalhes em §4.4.


18. Roadmap (decidido 2026-04-30)

Pacing: spec + Core impl em paralelo (co-evolução, modelo A2). Filosofia §0.4 não exige serial puro — exige cuidado de design; co-evolução com loops curtos satisfaz e revela ambiguidades cedo.

Companhia da spec: híbrido (B3) — source em meta/docs/stack/specs/kvg/format.kmd (single source of truth); projeção HTML pública em kvg.koder.dev (renderizada via kmd render --standalone ao build).

Fase Escopo Estimativa
Spec v0.1 freeze Este documento marcado como tag kvgspecv0.1.0. done
Core impl em paralelo Parser KSS + renderer SkiaCanvas + CLI kvg validate / convert / build, em engineslangkvg. Loops curtos com a spec — refinos viram patch v0.1.1. 4-6 semanas
Revisão externa 1-2 autores externos (industry contacts) + devs Koder revisam spec após primeiras 2 semanas de impl. Ajustes ⇒ patch v0.1.1. +1 semana
Dok integration Editor live preview + export para SVGPNGglTF. +3 semanas
Subir kvg.koder.dev Site público com spec rendered + playground + Hub icon vivo + link pro repo. 2 semanas em paralelo
Motion impl Timeline + state machine + 3 constraints (look-at, follow-path, IK 2-bone). Migrar splash de 1 produto piloto. 6-8 semanas
Solid impl Filament backend + Coons mesh-gradient renderer. Migrar hero icon piloto para 3D. 8-10 semanas
kicon integration kicon lê .kvg como source preferred; usa role semantics para layout per-platform. Resolve o problema que originou esta proposta (safe-zone rendering automático). +2 semanas

Total para state-of-the-art utilizável em produção: ~5-6 meses com 1-2 engenheiros dedicados.


19. Decision needed from the user

Antes de avançar para implementação, esta proposta v0.1 precisa de direção em:

  • [x] Profile boundaries — CoreMotionSolid é o slice certo, ou

    mover algo (constraints? text-on-path?) entre profiles? → Decidido (2026-04-30): Profiles aditivos CoreMotionSolid conforme §3. Razão: única opção compatível com diretriz #10 (Core renderer < 200 KB factível) e #9 (não verboso — paga só o que usa). Refinamento permitido: nós individuais podem declarar requires + fallback para graceful degradation dentro do profile (mecanismo já em §14).

  • [x] Source syntax — TOML-superset, ou outra escolha?

    Decidido (2026-04-30): KSS — KVG Source Syntax purpose-built, KDL-inspired, descrita em §4.1. Coerente com a filosofia purpose-built do resto da stack (kgen, KPNKCNKVN).

  • [x] Sistema de coordenadas — Y-up (matemático, glTF) ou Y-down

    (SVG, Canvas)? → Decidido (2026-04-30): Y-up everywhere (Cartesiano padrão). Razão: convenção matemática ensinada em escola universalmente; alinha com 3D inteiro (glTF, Blender, Maya, Unity, USD, OpenGL); alinha com CADengenhariaplots científicos. Y-down do SVG é artefato histórico de raster scan de CRT, não convenção humana. Importadores de SVG aplicam flip uma vez no kvg convert. Detalhes na §5.

  • [x] Constraints scope no profile Motion — IK / follow-path /

    look-at todos em v0.1 ou diferir parte? → Decidido (2026-04-30): todas as três em v0.1. Razão: constraints são primitivas universais em ferramentas vetoriais maduras (Rive, Lottie, AE, Maya, Blender); API design já bem-trodden; performance nativa ordens de magnitude > emulação kgen; autores não querem reimplementar matemática vetorial. Detalhes em §3.2.

  • [x] Self-hosted-first regras — alguma flexibilização aceitável (ex:

    permitir koder:// URI scheme para resolução offline em cache local)? → Decidido (2026-04-30): zero flexibilização em v0.1. Self-hosted é diretriz dura; abrir exceção convida outras. Para casos pesados, .kvg.zip (§4.3) já cobre. Caminho preferencial v0.2+ se demanda aparecer: hash content-addressable. Detalhes em §12.

  • [x] Roadmap pacing — começamos pelo Core impl + Dok preview, ou

    ratificamos a spec primeiro com revisão externa? → Decidido (2026-04-30): modelo A2 — spec v0.1 freeze + Core impl em paralelo. Revisão externa após 2-3 semanas de impl, refinos viram patch v0.1.1. Filosofia §0.4 não exige serial puro — exige cuidado de design; co-evolução com loops curtos revela ambiguidades. Detalhes em §18.

  • [x] Companhia da spec — abrir um repo público

    meta/docs/stack/specs/kvg/ (este path) ou domínio próprio kvg.koder.dev desde o dia 1? → Decidido (2026-04-30): modelo B3 — híbrido. Source em meta/docs/stack/specs/kvg/format.kmd (single source of truth); projeção HTML pública em kvg.koder.dev rendered via kmd render --standalone ao build. Self-hosted-first identity preservada + presença pública estratégica para "padrão aberto". Detalhes em §18.

Todas as 10 perguntas críticas de v0.1 estão decididas. Próximo passo: freeze v0.1 da spec + começar Core impl em engines/lang/kvg/.


Apêndice A — Exemplo completo: ícone Koder Hub em KVG-Core

kvg-version "0.1.0"
profile     Core
viewport    0 0 512 512
title       "Koder Hub icon"

meta {
  role  "product-icon"
  brand "Koder Hub"
  slug  "khub"
}

// --- gradients ---
gradient #blue-sphere radial center=[0.35, 0.35] radius=0.7 {
  stop 0.00 "#BFDBFE"
  stop 0.35 "#3B82F6"
  stop 1.00 "#1E3A8A"
}
gradient #amber-square radial center=[0.35, 0.35] radius=0.7 {
  stop 0.00 "#FDE68A"
  stop 0.35 "#F59E0B"
  stop 1.00 "#B45309"
}
gradient #emerald-triangle radial center=[0.35, 0.35] radius=0.7 {
  stop 0.00 "#A7F3D0"
  stop 0.35 "#10B981"
  stop 1.00 "#047857"
}

// --- procedural satellites (kgen) ---
kgen #satellites {
  body """
    let r = 125
    let satellites = [
      { angle:  90, shape: "rect",     gradient: "amber-square",     role: "hub.satellite.square.top" },
      { angle:  30, shape: "ellipse",  gradient: "blue-sphere",      role: "hub.satellite.circle.upper-right" },
      { angle: -30, shape: "triangle", gradient: "emerald-triangle", role: "hub.satellite.triangle.lower-right" },
      { angle: -90, shape: "rect",     gradient: "amber-square",     role: "hub.satellite.square.bottom" },
      { angle:-150, shape: "ellipse",  gradient: "blue-sphere",      role: "hub.satellite.circle.lower-left" },
      { angle: 150, shape: "triangle", gradient: "emerald-triangle", role: "hub.satellite.triangle.upper-left" },
    ]
    each s in satellites
      let cx = 256 + r * cos(s.angle * PI / 180)
      let cy = 256 + r * sin(s.angle * PI / 180)  // Y-up: positive sin → up
      emit_connector(from: [256, 256], to: [cx, cy])
      emit_satellite(s.shape, cx, cy, gradient: s.gradient, role: s.role)
    end
  """
}

// --- core ---
scene {
  ellipse #core role="hub.center" 256 256 r=48 fill=#blue-sphere
}

Quando rasterizado em 512×512 produz a v2.29.96 atual do ícone do Hub — em ~40 linhas de source contra 47 do SVG equivalente, ~50% mais compacto que a versão TOML anterior, mais expressivo sobre a estrutura ("o que é cada elemento"), e queryable por role.


Fim do documento v0.1. Comentários inline ou via PR no monorepo.