{"id":3119,"date":"2026-10-13T15:22:04","date_gmt":"2026-10-13T15:22:04","guid":{"rendered":"https:\/\/fercaggiano.art\/?page_id=3119"},"modified":"2026-10-17T19:31:20","modified_gmt":"2026-10-17T19:31:20","slug":"generative-art","status":"publish","type":"page","link":"https:\/\/fercaggiano.art\/pt\/generative-art\/","title":{"rendered":"Arte Generativa"},"content":{"rendered":"\n\n\n<\/p>\n\n\n\n<div class=\"wp-block-columns alignfull is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<div class=\"fga\">\n<style>\n.fga{--ink:#111;--muted:#6f6858;--line:#e6e2da;--red:#ff1f5a;--serif:\"Alegreya\",Georgia,\"Times New Roman\",serif;--sans:\"Albert Sans\",-apple-system,\"Helvetica Neue\",Arial,sans-serif;color:var(--ink)!important;font-family:var(--sans)!important;font-size:17px;line-height:1.6;max-width:1180px;margin:0 auto!important;padding:70px 24px 90px;box-sizing:border-box;text-transform:none!important;letter-spacing:normal}\n.fga *,.fga *:before,.fga *:after{box-sizing:border-box}\n.fga h2,.fga h3,.fga p,.fga figure,.fga ul{margin:0;padding:0;text-transform:none!important;letter-spacing:normal}\n.fga h2,.fga h3{font-family:var(--serif)!important;font-weight:700;line-height:1.12;color:var(--ink)}\n.fga h2{font-size:clamp(30px,3.4vw,44px);margin-bottom:14px!important}\n.fga h3{font-size:clamp(26px,2.6vw,34px);margin-bottom:6px!important}\n.fga a{color:inherit!important;text-decoration:none!important}\n.fga .k{font:600 12px\/1.3 var(--sans)!important;letter-spacing:.16em!important;text-transform:uppercase!important;color:var(--red)!important;margin-bottom:12px!important}\n.fga .lead{max-width:820px;margin:0 auto 18px!important;text-align:center;font-family:var(--serif)!important;font-size:clamp(22px,2.5vw,30px);line-height:1.4;color:#222}\n.fga .lead2{max-width:700px;margin:0 auto 60px!important;text-align:center;color:#555;font-size:18px}\n.fga .strip{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:0 0 90px}\n.fga .strip img{width:100%;aspect-ratio:1;object-fit:cover;display:block}\n.fga .chap{display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:12px;border-top:2px solid #111;padding-top:22px;margin:0 0 40px}\n.fga .chap p.sub{color:#555;max-width:620px}\n.fga .more{font:600 13px\/1 var(--sans);letter-spacing:.07em;text-transform:uppercase;color:var(--red)!important;white-space:nowrap}\n.fga .story{display:grid;grid-template-columns:1fr 1fr;gap:50px;margin:0 0 90px}\n.fga .story p{font-size:17.5px;line-height:1.7;color:#333;margin-bottom:18px!important}\n.fga .w{display:grid;grid-template-columns:1.1fr 1fr;gap:50px;align-items:center;margin:0 0 80px}\n.fga .w.rev .media{order:2}\n.fga .media img,.fga .media video{width:100%;display:block;box-shadow:0 18px 40px rgba(0,0,0,.14)}\n.fga .meta{font:600 12px\/1.4 var(--sans);letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:16px!important}\n.fga .txt p{font-size:17.5px;line-height:1.7;color:#333;margin-bottom:18px!important}\n.fga .btns{display:flex;flex-wrap:wrap;gap:10px;margin-top:8px}\n.fga .btn{display:inline-block;font:600 13px\/1 var(--sans);letter-spacing:.07em;text-transform:uppercase;padding:14px 20px;background:#111;color:#fff!important}\n.fga .btn:hover{background:var(--red)}\n.fga .btn.ghost{background:transparent;color:#111!important;border:1px solid #111}\n.fga .btn.ghost:hover{border-color:var(--red);color:var(--red)!important}\n.fga .app{background:#111;color:#fff;padding:70px 60px;margin:0 0 90px;display:grid;grid-template-columns:1fr 1fr;gap:50px;align-items:center}\n.fga .app h2{color:#fff}\n.fga .app .txt p{color:#ddd}\n.fga .app .btn{background:var(--red)}\n.fga .app .btn:hover{background:#fff;color:#111!important}\n.fga .app .btn.ghost{background:transparent;border-color:#fff;color:#fff!important}\n.fga .app .media img{box-shadow:none}\n.fga .faq{max-width:820px;margin:0 auto 70px}\n.fga .faq h3{font-size:24px;margin:30px 0 8px!important}\n.fga .faq p{color:#333;font-size:17.5px;line-height:1.7}\n.fga .end{text-align:center}\n@media(max-width:860px){.fga .story,.fga .w,.fga .app{grid-template-columns:1fr;gap:28px}.fga .w.rev .media{order:0}.fga .app{padding:40px 24px}.fga .strip{grid-template-columns:repeat(2,1fr)}}\n<\/style>\n\n<p class=\"k\" style=\"text-align:center\">Arte generativa<\/p>\n<p class=\"lead\">Durante quase toda a minha carreira, meu ateli\u00ea foi tinta, pincel, tela e, mais recentemente, um iPad. C\u00f3digo, n\u00e3o. At\u00e9 que comecei a construir minhas pr\u00f3prias ferramentas, e o pincel virou um conjunto de regras.<\/p>\n<p class=\"lead2\">Eu decido as cores, as formas, o movimento e o clima. O sistema decide o resto, e toda vez que roda ele me surpreende um pouco.<\/p>\n\n<div class=\"strip\">\n<img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/07\/flow-cobalto-60f-resize.gif?ssl=1\" alt=\"Animated Flow, anima\u00e7\u00e3o generativa de Fer Caggiano, paleta cobalto\" loading=\"lazy\">\n<img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/10\/cat-hi-KT1J464QxGCaHopSEXvaoeBEFEmSo6PNCx7p-54.jpg?ssl=1\" alt=\"Fantastic Creature, arte generativa de Fer Caggiano\" loading=\"lazy\">\n<img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/07\/flow-botanica-60f-resize.gif?ssl=1\" alt=\"Animated Flow, anima\u00e7\u00e3o generativa de Fer Caggiano, paleta botanica\" loading=\"lazy\">\n<img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/10\/cat-hi-KT1SEF1hCW6ekzj15LbV7xKRCJourApyUbX1-1.jpg?ssl=1\" alt=\"Botanica, mandala generativa de Fer Caggiano\" loading=\"lazy\">\n<\/div>\n\n<div class=\"chap\"><div><p class=\"k\">Como come\u00e7ou<\/p><h2>Do pincel ao c\u00f3digo.<\/h2><\/div><a class=\"more\" href=\"https:\/\/fercaggiano.art\/2026\/07\/08\/from-paintbrushes-to-code-my-journey-into-generative-art\/\">Leia a hist\u00f3ria completa &rarr;<\/a><\/div>\n<div class=\"story\">\n<div>\n<p>Se algu\u00e9m me perguntasse, alguns anos atr\u00e1s, se eu faria arte generativa um dia, eu provavelmente teria dado risada. Programa\u00e7\u00e3o parecia um mundo de desenvolvedores e engenheiros, n\u00e3o de algu\u00e9m que mistura tinta a \u00f3leo na paleta.<\/p>\n<p>Ent\u00e3o, sim, eu usei IA. N\u00e3o para fazer a arte, mas para aprender a constru\u00ed-la. Passei horas fazendo perguntas, quebrando coisas, consertando e fazendo perguntas melhores, at\u00e9 que um dia percebi que n\u00e3o estava mais s\u00f3 copiando c\u00f3digo. Eu entendia o que estava fazendo.<\/p>\n<\/div>\n<div>\n<p>Toda decis\u00e3o art\u00edstica continua sendo minha, do ritmo de uma linha \u00e0 paleta e \u00e0 sensa\u00e7\u00e3o da obra. A IA virou mais uma ferramenta do ateli\u00ea, como antes aconteceu com a fotografia e com o iPad.<\/p>\n<p>O que eu mais gosto \u00e9 n\u00e3o saber exatamente o que vai aparecer. Conhe\u00e7o as regras, fui eu que escrevi, e mesmo assim cada obra nova chega como uma pequena surpresa. Isso n\u00e3o perde a gra\u00e7a.<\/p>\n<\/div>\n<\/div>\n\n<div class=\"fga-hero\"><iframe class=\"cw\" title=\"Captured Wave, obra generativa ao vivo de Fer Caggiano\" loading=\"lazy\"><\/iframe><p class=\"cap\"><b>Live<\/b> <span class=\"cwt\"><\/span><\/p><\/div>\n\n<div class=\"chap\"><div><p class=\"k\">Cole\u00e7\u00f5es<\/p><h2>Lan\u00e7adas at\u00e9 agora.<\/h2><p class=\"sub\">Tr\u00eas cole\u00e7\u00f5es, tr\u00eas jeitos diferentes de deixar um sistema pintar.<\/p><\/div><a class=\"more\" href=\"https:\/\/fercaggiano.art\/catalog\/\">Veja no cat\u00e1logo &rarr;<\/a><\/div>\n\n<div class=\"w\">\n<div class=\"media\"><a href=\"https:\/\/fercaggiano.art\/flow-mint\/\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/07\/flow-noir-60f-resize.gif?ssl=1\" alt=\"Animated Flow, anima\u00e7\u00e3o generativa ao vivo de Fer Caggiano na Base\" loading=\"lazy\"><\/a><\/div>\n<div class=\"txt\">\n<p class=\"meta\">Base &middot; 2026 &middot; 1.977 obras poss\u00edveis<\/p>\n<h3>Animated Flow<\/h3>\n<p>Minha primeira cole\u00e7\u00e3o generativa. As obras n\u00e3o existem at\u00e9 algu\u00e9m fazer o mint: nesse momento uma seed fica gravada na blockchain e as regras a transformam numa composi\u00e7\u00e3o que ningu\u00e9m viu antes. E tamb\u00e9m n\u00e3o \u00e9 um v\u00eddeo. Cada obra \u00e9 um pequeno programa que continua se desenhando, ao vivo, no seu navegador.<\/p>\n<p>Eu mesma constru\u00ed o contrato e o app de mint, ent\u00e3o a cole\u00e7\u00e3o n\u00e3o depende de nenhuma plataforma para continuar existindo. O mint custa mais ou menos um d\u00f3lar (0.00069 ETH).<\/p>\n<div class=\"btns\"><a class=\"btn\" href=\"https:\/\/fercaggiano.art\/flow-mint\/\">Fa\u00e7a o mint<\/a><a class=\"btn ghost\" href=\"https:\/\/opensea.io\/collection\/animated-flow-475308356\" target=\"_blank\" rel=\"noopener\">Ver a cole\u00e7\u00e3o<\/a><a class=\"btn ghost\" href=\"https:\/\/fercaggiano.art\/flow\/\">Brinque com o app<\/a><\/div>\n<\/div>\n<\/div>\n\n<div class=\"w rev\">\n<div class=\"media\"><video src=\"https:\/\/fercaggiano.art\/wp-content\/uploads\/2026\/10\/cat-cover-fantastic-creatures.mp4\" poster=\"https:\/\/fercaggiano.art\/wp-content\/uploads\/2026\/10\/cat-hi-KT1J464QxGCaHopSEXvaoeBEFEmSo6PNCx7p-22.jpg\" muted autoplay loop playsinline aria-label=\"Fantastic Creatures, cole\u00e7\u00e3o generativa de Fer Caggiano\"><\/video><\/div>\n<div class=\"txt\">\n<p class=\"meta\">Tezos &middot; drop.art &middot; 2026 &middot; 104 obras &middot; Esgotada<\/p>\n<h3>Fantastic Creatures<\/h3>\n<p>Criaturas feitas inteiramente no Patterns, o app de padr\u00f5es que eu criei. Treze criaturas com oito varia\u00e7\u00f5es cada, lan\u00e7adas em blind mint na drop.art, ent\u00e3o voc\u00ea s\u00f3 conhece a sua depois de coletar. As animadas juntam cinco vers\u00f5es diferentes da mesma criatura.<\/p>\n<p>As 104 j\u00e1 encontraram seus colecionadores. Voc\u00ea ainda pode ach\u00e1-las no mercado secund\u00e1rio.<\/p><div class=\"btns\"><a class=\"btn\" href=\"https:\/\/objkt.com\/collections\/KT1J464QxGCaHopSEXvaoeBEFEmSo6PNCx7p\" target=\"_blank\" rel=\"noopener\">Colecione no objkt<\/a><\/div>\n<\/div>\n<\/div>\n\n<div class=\"w\">\n<div class=\"media\"><a href=\"https:\/\/objkt.com\/collections\/KT1SEF1hCW6ekzj15LbV7xKRCJourApyUbX1\" target=\"_blank\" rel=\"noopener\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/10\/cat-hi-KT1SEF1hCW6ekzj15LbV7xKRCJourApyUbX1-0.jpg?ssl=1\" alt=\"Botanica, mandala generativa de Fer Caggiano na Tezos\" loading=\"lazy\"><\/a><\/div>\n<div class=\"txt\">\n<p class=\"meta\">Tezos e Ethereum &middot; 2026 &middot; em crescimento<\/p>\n<h3>Botanica<\/h3>\n<p>Mandalas que nascem de fotografias. Coloco uma imagem num pequeno app que eu fiz, e ele dobra a foto em simetria, de novo e de novo, at\u00e9 virar outra coisa que ainda guarda um rastro da original.<\/p>\n<p>Essa cresce devagar. Conforme as obras encontram novas casas, eu crio novas.<\/p>\n<div class=\"btns\"><a class=\"btn\" href=\"https:\/\/objkt.com\/collections\/KT1SEF1hCW6ekzj15LbV7xKRCJourApyUbX1\" target=\"_blank\" rel=\"noopener\">Ver no objkt<\/a><\/div>\n<\/div>\n<\/div>\n\n<div class=\"app\">\n<div class=\"txt\">\n<p class=\"k\">Uso livre &middot; aberto agora<\/p>\n<h2>Patterns, meu ateli\u00ea de padr\u00f5es.<\/h2>\n<p>O Patterns \u00e9 o app com que eu pinto quando trabalho de forma generativa, e agora ele est\u00e1 aberto para qualquer pessoa usar. Sem carteira, sem cadastro. Escolha um visual de partida e uma paleta e aperte Random, ou abra o painel avan\u00e7ado e construa o seu do zero.<\/p>\n<p>Voc\u00ea escolhe os elementos, de c\u00edrculos e pedras a linhas de tinta que passeiam pela tela, acrescenta escorridos e respingos, e at\u00e9 suas pr\u00f3prias fotografias. Organiza tudo espalhado, em espiral, em mandala ou em caleidosc\u00f3pio, d\u00e1 acabamento de papel de aquarela, de risografia ou de Polaroid antiga, e faz o padr\u00e3o se repetir sem emenda, se estiver pensando em tecido ou papel de parede. Ele tamb\u00e9m se mexe: pode pulsar, girar as cores, ter glitch ou se pintar de novo, e voc\u00ea salva o loop em GIF ou v\u00eddeo.<\/p>\n<p>Seu nome vai assinado em cada arquivo que voc\u00ea salvar. Estou deixando aberto enquanto escuto quem usa, ent\u00e3o se voc\u00ea fizer algo que amou, ou se alguma coisa quebrar, me conta.<\/p>\n<div class=\"btns\"><a class=\"btn\" href=\"https:\/\/fercaggiano.art\/patterns\/\">Abrir o Patterns<\/a><a class=\"btn ghost\" href=\"https:\/\/fercaggiano.art\/services\/#contact-form-1026\">Me mostre o que voc\u00ea fez<\/a><\/div>\n<\/div>\n<div class=\"media\"><a href=\"https:\/\/fercaggiano.art\/patterns\/\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/10\/cat-hi-KT1J464QxGCaHopSEXvaoeBEFEmSo6PNCx7p-87.jpg?ssl=1\" alt=\"Obra feita no Patterns, app de padr\u00f5es generativos de Fer Caggiano\" loading=\"lazy\"><\/a><\/div>\n<\/div>\n\n<div class=\"chap\"><div><p class=\"k\">No ateli\u00ea agora<\/p><h2>Captured Wave.<\/h2><\/div><\/div>\n<div class=\"story\">\n<div>\n<p>A obra que voc\u00ea viu se mexendo l\u00e1 em cima, antes das cole\u00e7\u00f5es, roda no meu app mais novo, que ainda n\u00e3o \u00e9 p\u00fablico. \u00c9 uma simula\u00e7\u00e3o de fluido: gotas, tra\u00e7os finos e fios de tinta caem na tela e a correnteza leva tudo, e as cores v\u00e3o se dobrando umas nas outras como numa marmoriza\u00e7\u00e3o.<\/p>\n<p>Chegar at\u00e9 aqui exigiu muita teimosia, porque tenho uma regra da qual n\u00e3o abro m\u00e3o. As cores podem se misturar, mas n\u00e3o podem virar lama. Toda vez que a tela come\u00e7ava a ficar cinza, eu voltava e mudava o jeito como a tinta se comporta.<\/p>\n<\/div>\n<div>\n<p>Ela se move devagar, de prop\u00f3sito. Alguns ajustes parecem uma onda chegando, outros uma espiral lenta ou um l\u00edquido fervendo baixinho, e posso deixar rodando por horas como instala\u00e7\u00e3o ao vivo ou salvar um loop.<\/p>\n<p>O que voc\u00ea v\u00ea l\u00e1 em cima \u00e9 uma composi\u00e7\u00e3o nova toda vez que abre a p\u00e1gina, e ningu\u00e9m mais vai ver exatamente aquela. Quando estiver pronto, vou abrir o app aqui, do mesmo jeito que fiz com o Patterns.<\/p>\n<\/div>\n<\/div>\n\n<div class=\"faq\">\n<div class=\"chap\"><div><p class=\"k\">Perguntas<\/p><h2>O que as pessoas me perguntam.<\/h2><\/div><\/div>\n<h3>Isso \u00e9 arte feita por IA?<\/h3>\n<p>N\u00e3o. A IA me ajudou a aprender a construir as ferramentas. As imagens s\u00e3o desenhadas por c\u00f3digo, seguindo regras que eu criei, com paletas, formas e movimentos que eu escolhi. Nenhum modelo de IA gera as imagens.<\/p>\n<h3>Preciso de uma carteira cripto?<\/h3>\n<p>Para explorar, n\u00e3o. O Patterns e o app do Animated Flow rodam em qualquer navegador. Voc\u00ea s\u00f3 precisa de uma carteira se quiser coletar uma obra de uma das cole\u00e7\u00f5es.<\/p>\n<h3>Como uma obra pode n\u00e3o existir antes do mint?<\/h3>\n<p>Uma cole\u00e7\u00e3o generativa \u00e9 um conjunto de regras, n\u00e3o uma pasta de imagens. Quando algu\u00e9m faz o mint, \u00e9 criado um n\u00famero \u00fanico chamado seed, e as regras transformam essa seed numa composi\u00e7\u00e3o. Ela aparece igual toda vez que \u00e9 exibida, e ningu\u00e9m, nem eu, a viu antes.<\/p>\n<\/div>\n\n<div class=\"end\"><a class=\"btn\" href=\"https:\/\/fercaggiano.art\/catalog\/\">Veja todas as obras generativas no cat\u00e1logo<\/a><\/div>\n<\/div>\n\n<\/div>\n<\/div>\n\n\n<p>\n\n\n\n<div class=\"fga-hero-src\" hidden>\n<style>\n.fga-hero{position:relative;width:100%;aspect-ratio:16\/9;max-height:78vh;min-height:300px;background:#0a0a0a;overflow:hidden;margin:0 0 90px}\n.fga-hero iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}\n.fga-hero .cap{position:absolute;left:24px;bottom:18px;margin:0;font:600 12px\/1.4 \"Albert Sans\",-apple-system,Arial,sans-serif;letter-spacing:.12em;text-transform:uppercase;color:#fff;text-shadow:0 1px 8px rgba(0,0,0,.6);pointer-events:none}\n.fga-hero .cap b{color:#ff1f5a}\n.fga .fga-hero .cap{position:absolute!important;left:22px;bottom:16px;margin:0!important;text-transform:uppercase!important;letter-spacing:.12em!important;color:#fff!important;font:600 12px\/1.4 \"Albert Sans\",-apple-system,Arial,sans-serif!important}\n<\/style>\n<script type=\"application\/json\" id=\"cw-src\">\"\\u003c!DOCTYPE html\\u003e\\n\\u003chtml lang=\\\"pt-BR\\\"\\u003e\\n\\u003chead\\u003e\\n\\u003cmeta charset=\\\"UTF-8\\\"\\u003e\\n\\u003cmeta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\"\\u003e\\n\\u003ctitle\\u003eCaptured Wave \\u2014 Studio Fer Caggiano\\u003c\/title\\u003e\\n\\u003cstyle\\u003e\\n  \/* \\u2500\\u2500 Mesmo design system do fluxo-animado.html \/ gerador-estampas.html \\u2500\\u2500 *\/\\n  :root {\\n    --bg-page:   #161614;\\n    --bg-left:   #1c1c1a;\\n    --bg-right:  #111110;\\n    --border:    #2e2e2a;\\n    --text:      #d8d3c8;\\n    --text-dim:  #6a6560;\\n    --chip-bg:   #252520;\\n    --chip-bd:   #3a3a35;\\n    --chip-on:   #d8d3c8;\\n    --chip-on-t: #1a1a18;\\n    --input-bg:  #252520;\\n    --input-bd:  #3a3a35;\\n    --btn-bg:    #252520;\\n    --btn-bd:    #3a3a35;\\n    --btn-text:  #c8c3b8;\\n    --primary:   #d8d3c8;\\n    --primary-t: #1a1a18;\\n    --scrollbar: #333;\\n  }\\n  :root.light {\\n    --bg-page:   #faf8f4;\\n    --bg-left:   #faf8f4;\\n    --bg-right:  #ede8df;\\n    --border:    #e0dbd0;\\n    --text:      #2a2a2a;\\n    --text-dim:  #8a8378;\\n    --chip-bg:   #fff;\\n    --chip-bd:   #d8d2c5;\\n    --chip-on:   #2a2a2a;\\n    --chip-on-t: #fff;\\n    --input-bg:  #fff;\\n    --input-bd:  #d8d2c5;\\n    --btn-bg:    #fff;\\n    --btn-bd:    #d8d2c5;\\n    --btn-text:  #2a2a2a;\\n    --primary:   #2a2a2a;\\n    --primary-t: #fff;\\n    --scrollbar: #ccc;\\n  }\\n\\n  * { box-sizing: border-box; margin: 0; padding: 0; }\\n  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\\n         background: var(--bg-page); color: var(--text);\\n         height: 100vh; overflow: hidden; transition: background .2s, color .2s; }\\n\\n  .layout { display: flex; height: 100vh; }\\n\\n  .left {\\n    width: 280px; flex-shrink: 0;\\n    overflow-y: auto; height: 100%;\\n    padding: 16px 14px 32px;\\n    background: var(--bg-left);\\n    border-right: 1px solid var(--border);\\n    transition: background .2s, border-color .2s;\\n  }\\n  .left::-webkit-scrollbar { width: 3px; }\\n  .left::-webkit-scrollbar-track { background: transparent; }\\n  .left::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 2px; }\\n\\n  .right {\\n    flex: 1; display: flex; flex-direction: column;\\n    padding: 16px; gap: 10px;\\n    background: var(--bg-right); overflow: hidden;\\n    transition: background .2s;\\n  }\\n\\n  .cwrap {\\n    flex: 1; min-height: 0;\\n    border-radius: 4px; overflow: hidden;\\n    display: flex; align-items: center; justify-content: center;\\n  }\\n  canvas#gl {\\n    display: block; max-width: 100%; max-height: 100%; width: auto; height: auto;\\n    box-shadow: 0 6px 40px rgba(0,0,0,0.55);\\n    border-radius: 2px;\\n  }\\n\\n  \/* Header *\/\\n  .hrow { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 14px; }\\n  h1 { font-size: 14px; font-weight: 600; color: var(--text); }\\n  .sub { font-size: 10px; color: var(--text-dim); margin-top: 1px; }\\n  .theme-btn {\\n    padding: 4px 9px; font-size: 13px; cursor: pointer; border-radius: 20px;\\n    background: var(--chip-bg); border: 1px solid var(--chip-bd); color: var(--text);\\n    flex-shrink: 0; line-height: 1;\\n  }\\n\\n  \/* Section labels + accordion *\/\\n  .st { font-size: 10px; color: var(--text-dim); text-transform: uppercase;\\n        letter-spacing: .06em; margin: 13px 0 5px; font-weight: 600; }\\n  .acc-h { cursor: pointer; display: flex; justify-content: space-between; align-items: center; }\\n  .acc-h:hover { opacity: .75; }\\n  .acc-arr { font-size: 9px; opacity: .5; flex-shrink: 0; margin-left: 4px; }\\n  .acc-body.acc-closed { display: none; }\\n\\n  \/* Chips *\/\\n  .chips { display: flex; flex-wrap: wrap; gap: 4px; }\\n  .chip {\\n    padding: 4px 10px; font-size: 11px;\\n    border: 1px solid var(--chip-bd); border-radius: 20px;\\n    cursor: pointer; background: var(--chip-bg); color: var(--text);\\n    transition: all .12s; white-space: nowrap; user-select: none;\\n  }\\n  .chip.on { background: var(--chip-on); color: var(--chip-on-t); border-color: var(--chip-on); }\\n\\n  \/* Controls *\/\\n  .g2 { display: grid; grid-template-columns: 1fr 1fr; gap: 9px 15px; margin-top: 4px; }\\n  .ci { display: flex; flex-direction: column; gap: 3px; }\\n  .ch { display: flex; justify-content: space-between; }\\n  .cl { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; }\\n  .cv { font-size: 10px; color: var(--text-dim); }\\n  input[type=range] { width: 100%; accent-color: var(--primary); }\\n\\n  \/* BG swatches \\u2014 mesmo layout do fluxo-animado *\/\\n  .bgrow1 { display: flex; align-items: center; gap: 6px; margin-top: 5px; }\\n  .bgswatches { display: flex; flex-wrap: wrap; gap: 5px; }\\n  #bgpalette { margin-top: 5px; }\\n  .bgsw { width: 22px; height: 22px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; outline: 1px solid var(--border); flex-shrink: 0; }\\n  .bgsw.on { outline: 2px solid var(--text); }\\n  .bgsw.add { display: flex; align-items: center; justify-content: center; background: var(--chip-bg); color: var(--text-dim); font-size: 14px; line-height: 1; }\\n  .bghex-row { display: flex; align-items: center; gap: 5px; flex: 1; }\\n  .bghex-row input[type=color] { width: 22px; height: 22px; border: none; padding: 0; border-radius: 4px; cursor: pointer; flex-shrink: 0; }\\n  .bghex-row input[type=text] {\\n    flex: 1; padding: 3px 5px; font-size: 10px; font-family: monospace;\\n    border: 1px solid var(--input-bd); border-radius: 5px;\\n    background: var(--input-bg); color: var(--text); min-width: 0;\\n  }\\n  .bghex-row span { font-size: 10px; color: var(--text-dim); flex-shrink: 0; }\\n\\n  \/* Bottom bar \\u2014 mesmo padr\\u00e3o dl-actions\/dl-save *\/\\n  .dl-row {\\n    display: flex; align-items: center; justify-content: space-between;\\n    gap: 8px; flex-shrink: 0;\\n  }\\n  .dl-actions { display: flex; gap: 8px; }\\n  .dl-save { display: flex; align-items: center; gap: 8px; }\\n  .bact {\\n    padding: 8px 14px; font-size: 12px; font-family: inherit;\\n    border: 1px solid var(--btn-bd); border-radius: 7px;\\n    background: var(--btn-bg); color: var(--btn-text); cursor: pointer;\\n    flex-shrink: 0; transition: opacity .15s; white-space: nowrap;\\n  }\\n  .bact:hover { opacity: .75; }\\n  .bact.p { background: var(--primary); color: var(--primary-t); border-color: var(--primary); }\\n  .bact:disabled { opacity: .45; cursor: default; }\\n  .stat { font-size: 10px; color: var(--text-dim); }\\n  select.dur {\\n    padding: 7px 8px; font-size: 11px; border-radius: 7px;\\n    background: var(--input-bg); border: 1px solid var(--input-bd); color: var(--text);\\n  }\\n\\n  \/* Orientation toggle (\\u00edcones, mesmo padr\\u00e3o do still) *\/\\n  .ori-toggle { display: flex; gap: 3px; }\\n  .ori-btn { padding: 5px 8px; border: 1px solid var(--btn-bd); border-radius: 6px; background: var(--btn-bg); cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--btn-text); transition: all .12s; }\\n  .ori-btn.on { border-color: var(--chip-on); background: var(--chip-on); color: var(--chip-on-t); }\\n  .ori-btn:hover:not(.on) { opacity: .75; }\\n  .ori-rect { display: inline-block; border: 1.5px solid currentColor; border-radius: 1px; }\\n  .ori-rect-p { width: 9px; height: 13px; }\\n  .ori-rect-l { width: 16px; height: 9px; }\\n  .ori-rect-sq { width: 11px; height: 11px; }\\n  .frame-sw { display: inline-block; width: 11px; height: 11px; border-radius: 2px; border: 1.5px solid currentColor; }\\n  #frameToggle .ori-btn { min-width: 28px; font-size: 11px; line-height: 11px; }\\n\\n  #err { color: var(--text); font-size: 13px; max-width: 420px; line-height: 1.6; display: none; }\\n  #recdot {\\n    display: inline-block; width: 8px; height: 8px; border-radius: 50%;\\n    background: #e04040; margin-right: 6px; vertical-align: 1px;\\n    animation: blink 1s infinite;\\n  }\\n  @keyframes blink { 50% { opacity: 0.2; } }\\n\\u003c\/style\\u003e\\n\\u003cstyle id=\\\"viewer\\\"\\u003e\\nhtml,body{margin:0!important;padding:0!important;height:100%!important;overflow:hidden!important;background:#0a0a0a!important}\\n.layout{display:block!important;padding:0!important;margin:0!important;height:100%!important}\\n.left,#leftPanel,.dl-row,header,.top{display:none!important}\\n.right{position:fixed!important;inset:0!important;padding:0!important;margin:0!important;display:block!important;width:100vw!important;height:100vh!important;max-width:none!important}\\n.cwrap{position:absolute!important;inset:0!important;width:100%!important;height:100%!important;padding:0!important;margin:0!important;max-width:none!important;display:block!important}\\ncanvas#gl{width:100%!important;height:100%!important;object-fit:cover!important;max-width:none!important;max-height:none!important;border-radius:0!important;box-shadow:none!important;display:block!important}\\n\\u003c\/style\\u003e\\n\\u003c\/head\\u003e\\n\\u003cbody\\u003e\\n\\n\\u003cdiv class=\\\"layout\\\"\\u003e\\n\\n\\u003c!-- \\u2500\\u2500 LEFT PANEL \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 --\\u003e\\n\\u003cdiv class=\\\"left\\\" id=\\\"leftPanel\\\"\\u003e\\n  \\u003cdiv class=\\\"hrow\\\"\\u003e\\n    \\u003cdiv\\u003e\\n      \\u003ch1\\u003eCaptured Wave\\u003c\/h1\\u003e\\n      \\u003cdiv class=\\\"sub\\\"\\u003eStudio Fer Caggiano\\u003c\/div\\u003e\\n    \\u003c\/div\\u003e\\n    \\u003cbutton class=\\\"theme-btn\\\" id=\\\"btheme\\\" title=\\\"Toggle theme\\\"\\u003e\\u2600\\ufe0f\\u003c\/button\\u003e\\n  \\u003c\/div\\u003e\\n\\n  \\u003cdiv class=\\\"st acc-h\\\" data-acc=\\\"palette\\\"\\u003ePalette \\u003cspan class=\\\"acc-arr\\\"\\u003e\\u25be\\u003c\/span\\u003e\\u003c\/div\\u003e\\n  \\u003cdiv class=\\\"acc-body\\\" id=\\\"acc-palette\\\"\\u003e\\n    \\u003cdiv class=\\\"chips\\\" id=\\\"palGrid\\\"\\u003e\\u003c\/div\\u003e\\n  \\u003c\/div\\u003e\\n\\n  \\u003cdiv class=\\\"st acc-h\\\" data-acc=\\\"fundo\\\"\\u003eBackground color \\u003cspan class=\\\"acc-arr\\\"\\u003e\\u25be\\u003c\/span\\u003e\\u003c\/div\\u003e\\n  \\u003cdiv class=\\\"acc-body\\\" id=\\\"acc-fundo\\\"\\u003e\\n    \\u003cdiv class=\\\"bgrow1\\\"\\u003e\\n      \\u003cdiv class=\\\"bgswatches\\\" id=\\\"bgfixed\\\"\\u003e\\u003c\/div\\u003e\\n      \\u003cdiv class=\\\"bghex-row\\\"\\u003e\\n        \\u003cinput type=\\\"color\\\" id=\\\"bgpicker\\\" value=\\\"#0a0a0a\\\"\\u003e\\n        \\u003cinput type=\\\"text\\\" id=\\\"bghex\\\" value=\\\"#0a0a0a\\\" maxlength=\\\"7\\\"\\u003e\\n        \\u003cspan\\u003eHEX\\u003c\/span\\u003e\\n      \\u003c\/div\\u003e\\n    \\u003c\/div\\u003e\\n    \\u003cdiv class=\\\"bgswatches\\\" id=\\\"bgpalette\\\"\\u003e\\u003c\/div\\u003e\\n  \\u003c\/div\\u003e\\n\\n  \\u003cdiv class=\\\"st acc-h\\\" data-acc=\\\"cores\\\"\\u003eCustom colors \\u003cspan class=\\\"acc-arr\\\"\\u003e\\u25be\\u003c\/span\\u003e\\u003c\/div\\u003e\\n  \\u003cdiv class=\\\"acc-body\\\" id=\\\"acc-cores\\\"\\u003e\\n    \\u003cdiv class=\\\"bgswatches\\\" id=\\\"extraswatches\\\"\\u003e\\u003c\/div\\u003e\\n    \\u003cinput type=\\\"color\\\" id=\\\"colorpick\\\" value=\\\"#3a86ff\\\" style=\\\"display:none\\\"\\u003e\\n  \\u003c\/div\\u003e\\n\\n  \\u003cdiv class=\\\"st acc-h\\\" data-acc=\\\"mov\\\"\\u003eMovement \\u003cspan class=\\\"acc-arr\\\"\\u003e\\u25be\\u003c\/span\\u003e\\u003c\/div\\u003e\\n  \\u003cdiv class=\\\"acc-body\\\" id=\\\"acc-mov\\\"\\u003e\\n    \\u003cdiv class=\\\"chips\\\" id=\\\"movchips\\\"\\u003e\\u003c\/div\\u003e\\n    \\u003cdiv class=\\\"g2\\\" id=\\\"dropsrow\\\" style=\\\"margin-top:8px\\\"\\u003e\\n      \\u003cdiv class=\\\"ci\\\" style=\\\"grid-column: 1 \/ -1\\\"\\u003e\\n        \\u003cdiv class=\\\"ch\\\"\\u003e\\u003cspan class=\\\"cl\\\"\\u003eDrop size\\u003c\/span\\u003e\\u003cspan class=\\\"cv\\\" id=\\\"vDropSize\\\"\\u003e40\\u003c\/span\\u003e\\u003c\/div\\u003e\\n        \\u003cinput type=\\\"range\\\" id=\\\"sDropSize\\\" min=\\\"0\\\" max=\\\"100\\\" value=\\\"40\\\"\\u003e\\n      \\u003c\/div\\u003e\\n      \\u003cdiv class=\\\"ci\\\"\\u003e\\n        \\u003cdiv class=\\\"ch\\\"\\u003e\\u003cspan class=\\\"cl\\\"\\u003eRate\\u003c\/span\\u003e\\u003cspan class=\\\"cv\\\" id=\\\"vDrops\\\"\\u003e30\\u003c\/span\\u003e\\u003c\/div\\u003e\\n        \\u003cinput type=\\\"range\\\" id=\\\"sDrops\\\" min=\\\"1\\\" max=\\\"100\\\" value=\\\"30\\\"\\u003e\\n      \\u003c\/div\\u003e\\n      \\u003cdiv class=\\\"ci\\\"\\u003e\\n        \\u003cdiv class=\\\"ch\\\"\\u003e\\u003cspan class=\\\"cl\\\"\\u003eQuantity\\u003c\/span\\u003e\\u003cspan class=\\\"cv\\\" id=\\\"vDropQty\\\"\\u003e40\\u003c\/span\\u003e\\u003c\/div\\u003e\\n        \\u003cinput type=\\\"range\\\" id=\\\"sDropQty\\\" min=\\\"1\\\" max=\\\"100\\\" value=\\\"40\\\"\\u003e\\n      \\u003c\/div\\u003e\\n    \\u003c\/div\\u003e\\n    \\u003cdiv class=\\\"st\\\" style=\\\"margin-top:10px\\\"\\u003eInk entry\\u003c\/div\\u003e\\n    \\u003cdiv class=\\\"chips\\\" id=\\\"entchips\\\"\\u003e\\u003c\/div\\u003e\\n  \\u003c\/div\\u003e\\n\\n  \\u003cdiv class=\\\"st acc-h\\\" data-acc=\\\"flow\\\"\\u003eFlow \\u003cspan class=\\\"acc-arr\\\"\\u003e\\u25be\\u003c\/span\\u003e\\u003c\/div\\u003e\\n  \\u003cdiv class=\\\"acc-body\\\" id=\\\"acc-flow\\\"\\u003e\\n    \\u003cdiv class=\\\"g2\\\"\\u003e\\n      \\u003cdiv class=\\\"ci\\\"\\u003e\\n        \\u003cdiv class=\\\"ch\\\"\\u003e\\u003cspan class=\\\"cl\\\"\\u003eSpeed\\u003c\/span\\u003e\\u003cspan class=\\\"cv\\\" id=\\\"vSpeed\\\"\\u003e30\\u003c\/span\\u003e\\u003c\/div\\u003e\\n        \\u003cinput type=\\\"range\\\" id=\\\"sSpeed\\\" min=\\\"5\\\" max=\\\"100\\\" value=\\\"30\\\"\\u003e\\n      \\u003c\/div\\u003e\\n      \\u003cdiv class=\\\"ci\\\"\\u003e\\n        \\u003cdiv class=\\\"ch\\\"\\u003e\\u003cspan class=\\\"cl\\\"\\u003eTurbulence\\u003c\/span\\u003e\\u003cspan class=\\\"cv\\\" id=\\\"vCurl\\\"\\u003e12\\u003c\/span\\u003e\\u003c\/div\\u003e\\n        \\u003cinput type=\\\"range\\\" id=\\\"sCurl\\\" min=\\\"0\\\" max=\\\"20\\\" value=\\\"12\\\"\\u003e\\n      \\u003c\/div\\u003e\\n      \\u003cdiv class=\\\"ci\\\"\\u003e\\n        \\u003cdiv class=\\\"ch\\\"\\u003e\\u003cspan class=\\\"cl\\\"\\u003eWander\\u003c\/span\\u003e\\u003cspan class=\\\"cv\\\" id=\\\"vWander\\\"\\u003e60\\u003c\/span\\u003e\\u003c\/div\\u003e\\n        \\u003cinput type=\\\"range\\\" id=\\\"sWander\\\" min=\\\"0\\\" max=\\\"100\\\" value=\\\"60\\\"\\u003e\\n      \\u003c\/div\\u003e\\n      \\u003cdiv class=\\\"ci\\\"\\u003e\\n        \\u003cdiv class=\\\"ch\\\"\\u003e\\u003cspan class=\\\"cl\\\"\\u003eDensity\\u003c\/span\\u003e\\u003cspan class=\\\"cv\\\" id=\\\"vDens\\\"\\u003e55\\u003c\/span\\u003e\\u003c\/div\\u003e\\n        \\u003cinput type=\\\"range\\\" id=\\\"sDens\\\" min=\\\"10\\\" max=\\\"100\\\" value=\\\"55\\\"\\u003e\\n      \\u003c\/div\\u003e\\n      \\u003cdiv class=\\\"ci\\\" style=\\\"display:none\\\"\\u003e\\n        \\u003cdiv class=\\\"ch\\\"\\u003e\\u003cspan class=\\\"cl\\\"\\u003eTexture\\u003c\/span\\u003e\\u003cspan class=\\\"cv\\\" id=\\\"vShade\\\"\\u003e35\\u003c\/span\\u003e\\u003c\/div\\u003e\\n        \\u003cinput type=\\\"range\\\" id=\\\"sShade\\\" min=\\\"0\\\" max=\\\"100\\\" value=\\\"35\\\"\\u003e\\n      \\u003c\/div\\u003e\\n      \\u003cdiv class=\\\"ci\\\" style=\\\"grid-column: 1 \/ -1\\\"\\u003e\\n        \\u003cdiv class=\\\"ch\\\"\\u003e\\u003cspan class=\\\"cl\\\"\\u003eGravity\\u003c\/span\\u003e\\u003cspan class=\\\"cv\\\" id=\\\"vGrav\\\"\\u003e0\\u003c\/span\\u003e\\u003c\/div\\u003e\\n        \\u003cinput type=\\\"range\\\" id=\\\"sGrav\\\" min=\\\"0\\\" max=\\\"100\\\" value=\\\"0\\\"\\u003e\\n      \\u003c\/div\\u003e\\n    \\u003c\/div\\u003e\\n  \\u003c\/div\\u003e\\n\\n\\u003c\/div\\u003e\\n\\n\\u003c!-- \\u2500\\u2500 RIGHT PANEL \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 --\\u003e\\n\\u003cdiv class=\\\"right\\\"\\u003e\\n  \\u003cdiv class=\\\"cwrap\\\"\\u003e\\n    \\u003ccanvas id=\\\"gl\\\"\\u003e\\u003c\/canvas\\u003e\\n    \\u003cdiv id=\\\"err\\\"\\u003eThis app needs WebGL2 with float texture support \\u2014 not available in this browser. Try an up-to-date Chrome or Safari.\\u003c\/div\\u003e\\n  \\u003c\/div\\u003e\\n  \\u003cdiv class=\\\"dl-row\\\"\\u003e\\n    \\u003cdiv class=\\\"dl-actions\\\"\\u003e\\n      \\u003cbutton class=\\\"bact p\\\" id=\\\"btnRandom\\\"\\u003e\\ud83c\\udfb2 Random\\u003c\/button\\u003e\\n      \\u003cbutton class=\\\"bact\\\" id=\\\"btnSeed\\\"\\u003e\\u25c6 New\\u003c\/button\\u003e\\n      \\u003cbutton class=\\\"bact\\\" id=\\\"btnPause\\\"\\u003e\\u23f8 Pause\\u003c\/button\\u003e\\n      \\u003cbutton class=\\\"bact\\\" id=\\\"btnPng\\\"\\u003e\\u2913 PNG\\u003c\/button\\u003e\\n    \\u003c\/div\\u003e\\n    \\u003cdiv class=\\\"dl-save\\\"\\u003e\\n      \\u003cdiv class=\\\"ori-toggle\\\" id=\\\"oriToggle\\\"\\u003e\\n        \\u003cbutton class=\\\"ori-btn\\\" data-ori=\\\"portrait\\\" title=\\\"Portrait (900\\u00d71200)\\\"\\u003e\\u003cspan class=\\\"ori-rect ori-rect-p\\\"\\u003e\\u003c\/span\\u003e\\u003c\/button\\u003e\\n        \\u003cbutton class=\\\"ori-btn\\\" data-ori=\\\"landscape\\\" title=\\\"Landscape (1600\\u00d7900)\\\"\\u003e\\u003cspan class=\\\"ori-rect ori-rect-l\\\"\\u003e\\u003c\/span\\u003e\\u003c\/button\\u003e\\n        \\u003cbutton class=\\\"ori-btn on\\\" data-ori=\\\"square\\\" title=\\\"Square (1080\\u00d71080)\\\"\\u003e\\u003cspan class=\\\"ori-rect ori-rect-sq\\\"\\u003e\\u003c\/span\\u003e\\u003c\/button\\u003e\\n      \\u003c\/div\\u003e\\n      \\u003cselect class=\\\"dur\\\" id=\\\"recDur\\\"\\u003e\\n        \\u003coption value=\\\"5\\\"\\u003e5s\\u003c\/option\\u003e\\n        \\u003coption value=\\\"10\\\" selected\\u003e10s\\u003c\/option\\u003e\\n        \\u003coption value=\\\"15\\\"\\u003e15s\\u003c\/option\\u003e\\n        \\u003coption value=\\\"30\\\"\\u003e30s\\u003c\/option\\u003e\\n      \\u003c\/select\\u003e\\n      \\u003cbutton class=\\\"bact p\\\" id=\\\"btnRec\\\"\\u003e\\u2913 Export WebM\\u003c\/button\\u003e\\n    \\u003c\/div\\u003e\\n  \\u003c\/div\\u003e\\n  \\u003cdiv class=\\\"stat\\\" id=\\\"stat\\\"\\u003eseamless loop export \\u00b7 press H to hide panel\\u003c\/div\\u003e\\n\\u003c\/div\\u003e\\n\\n\\u003c\/div\\u003e\\u003c!-- .layout --\\u003e\\n\\n\\u003cscript\\u003e\\n'use strict';\\n\\n\/\/ \\u2500\\u2500 Paletas (id\\u00eanticas ao gerador-estampas.html \/ fluxo-animado.html) \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nconst PAL = {\\n  recife:    ['#e8748a','#e89ab0','#8fbccc','#5a8aaa','#b8d4e0','#f0c4cc','#3a6878'],\\n  ardosia:   ['#e85570','#78b898','#f0e4d0','#f0a8b8','#202c3a','#b8d8c8','#c4ddd6'],\\n  cobalto:   ['#e8e2d4','#d4b878','#9a8268','#7e6e80','#2a5080','#4a70a0','#182840'],\\n  fer:       ['#ff1f5a','#562c2c','#00fddc','#ff9000','#000000','#b7ad99','#fffff3'],\\n  praia:     ['#f07890','#e03870','#00b8cc','#a8d8e8','#7040a0','#e8a828','#f0c0a8'],\\n  atelie:    ['#f0a812','#e01090','#14c0be','#e07828','#980018','#f0c898','#4a1030'],\\n  rainbow:   ['#ff2d2d','#ff8c00','#ffd400','#3ecf3e','#00c8d4','#2a6cff','#a24bff'],\\n  noir:      ['#080808','#1c1c1c','#383838','#585858','#8c8c8c','#c4c4c4','#f4f4f4']\\n};\\nconst BG_FIXED = ['#0a0a0a','#161614','#222220','#faf8f4'];\\n\\nconst ORIENTATIONS = {\\n  portrait:  { w: 900,  h: 1200 },\\n  landscape: { w: 1600, h: 900  },\\n  square:    { w: 1080, h: 1080 }\\n};\\n\\n\/\/ \\u2500\\u2500 Estado \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nconst S = {\\n  palette: 'cobalto',\\n  orientation: 'landscape',\\n  bg: '#0a0a0a',\\n  entry: 'seq',       \/\/ 'juntas' | 'seq' \\u2014 (dormante) tintas entram todas juntas ou uma a uma\\n  inkEntry: 'drops',  \/\/ 'drops' | 'strokes' | 'pour' | 'mix' \\u2014 COMO a tinta entra\\n  motion: 'serpentina', \/\/ Meander | ripple | espiral (Spiral) | queda (Fall)\\n  drops: 30,          \/\/ TEMPO: frequ\\u00eancia dos eventos de chuva (baixo = raro, alto = denso)\\n  dropSize: 40,       \/\/ TAMANHO das gotas (0 = chuva fina, 100 = gotas gra\\u00fadas)\\n  dropQty: 40,        \/\/ QUANTIDADE de gotas por evento\\n  extra: [],          \/\/ cores adicionadas pela Fer, somadas \\u00e0 paleta ativa\\n  elements: 'nenhum', \/\/ 'nenhum' | 'esferas' | 'pedras' \\u2014 obst\\u00e1culos que o fluido contorna\\n  elSize: 'grandes',  \/\/ 'micro' | 'medios' | 'grandes'\\n  elMode: 'estaticos',\/\/ 'estaticos' | 'flutuam' \\u2014 flutuam = carregados pelo fluido\\n  elDist: 'espalhados', \/\/ fixo (chips de distribui\\u00e7\\u00e3o removidos a pedido da Fer)\\n  elColor: 'tinge',   \/\/ 'tinge' (absorve a tinta ao redor) | 'universo' (fluido dentro do elemento)\\n  frame: 'none',      \/\/ 'none' | 'white' | 'black' \\u2014 glass box (moldura + cavidade)\\n  elCount: 50,        \/\/ quantidade: micro 30\\u201315000, m\\u00e9dios 2\\u201348, grandes 1\\u20138\\n  gravity: 0,         \/\/ 0 = desligada; \\u003e0 = empuxo, l\\u00edquido num aqu\\u00e1rio\\n  speed: 30,     \/\/ for\\u00e7a dos emissores\\n  curl: 12,      \/\/ vorticity confinement (m\\u00e1x 20 \\u2014 acima disso quadricula)\\n  wander: 60,    \/\/ intensidade do campo de ru\\u00eddo color-wander (veios de escoamento)\\n  density: 55,   \/\/ reten\\u00e7\\u00e3o da tinta (mapeia p\/ dissipa\\u00e7\\u00e3o)\\n  shade: 35,     \/\/ textura \\u2014 edge darkening na pr\\u00f3pria cor (c*c), sem cinza\\n  seed: Math.random() * 99999 | 0\\n};\\n\\n\/\/ PRNG (id\\u00eantico ao gerador)\\nfunction mkRng(seed) {\\n  let s = (seed * 9301 + 1) | 0;\\n  return () =\\u003e { s = (s * 9301 + 49297) % 233280; return s \/ 233280; };\\n}\\n\\n\/\/ paleta de trabalho: a paleta ativa + as cores que a Fer adicionou\\nfunction activeCols() {\\n  return S.extra.length ? PAL[S.palette].concat(S.extra) : PAL[S.palette];\\n}\\n\\n\/\/ croma M\\u00c9DIO da paleta ativa (0 = monocrom\\u00e1tica\/cinza como o Noir). Serve de\\n\/\/ refer\\u00eancia: um pixel bem mais cinza que isso = LAMA (mistura) \\u2192 deve secar.\\nfunction paletteChroma() {\\n  const cols = activeCols();\\n  let s = 0;\\n  for (const c of cols) {\\n    const rgb = hex2rgb(c);\\n    const mx = Math.max(rgb[0], rgb[1], rgb[2]), mn = Math.min(rgb[0], rgb[1], rgb[2]);\\n    s += mx \\u003e 0.001 ? (mx - mn) \/ mx : 0;\\n  }\\n  return cols.length ? s \/ cols.length : 0;\\n}\\n\\nfunction hex2rgb(hex) {\\n  return [\\n    parseInt(hex.slice(1, 3), 16) \/ 255,\\n    parseInt(hex.slice(3, 5), 16) \/ 255,\\n    parseInt(hex.slice(5, 7), 16) \/ 255\\n  ];\\n}\\n\\n\/\/ PREFERE CONTRASTE com o fundo, mas N\\u00c3O exclui: peso = piso + dist\\u00e2ncia\\u00b2 ao fundo.\\n\/\/ O piso ALTO garante que toda cor entre uma parcela justa (o preto do Noir contrasta\\n\/\/ quando a tela j\\u00e1 tem tinta clara \\u2014 meu peso s\\u00f3 v\\u00ea o fundo est\\u00e1tico, n\\u00e3o o que j\\u00e1\\n\/\/ est\\u00e1 pintado, ent\\u00e3o mantenho todas as cores em jogo). Contraste \\u00e9 boost, n\\u00e3o filtro.\\nfunction pickInkColor(cols) {\\n  const bg = hex2rgb(S.bg);\\n  const w = new Array(cols.length);\\n  let total = 0;\\n  for (let i = 0; i \\u003c cols.length; i++) {\\n    const c = hex2rgb(cols[i]);\\n    const d = Math.hypot(c[0] - bg[0], c[1] - bg[1], c[2] - bg[2]);\\n    w[i] = 0.30 + d * d;\\n    total += w[i];\\n  }\\n  let x = Math.random() * total;\\n  for (let i = 0; i \\u003c cols.length; i++) { x -= w[i]; if (x \\u003c= 0) return hex2rgb(cols[i]); }\\n  return hex2rgb(cols[cols.length - 1]);\\n}\\n\\n\/\/ \\u2500\\u2500 WebGL setup \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nconst canvas = document.getElementById('gl');\\nconst gl = canvas.getContext('webgl2', { alpha: false, antialias: false, preserveDrawingBuffer: true });\\nlet glOK = false;\\n\\nif (gl) {\\n  const extCBF = gl.getExtension('EXT_color_buffer_float');\\n  if (extCBF) glOK = true;\\n}\\nif (!glOK) {\\n  document.getElementById('err').style.display = 'block';\\n  canvas.style.display = 'none';\\n}\\n\\nconst SIM_RES = 210;   \/\/ resolu\\u00e7\\u00e3o da simula\\u00e7\\u00e3o (velocidade\/press\\u00e3o) \\u2014 mais alta = turbul\\u00eancia mais suave\\nconst DYE_RES = 1024;  \/\/ resolu\\u00e7\\u00e3o da tinta (o que se v\\u00ea)\\nconst PRESSURE_ITERS = 20;\\n\\n\/\/ \\u2500\\u2500 Shaders \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nconst VERT = `#version 300 es\\nprecision highp float;\\nin vec2 aPos;\\nout vec2 vUv, vL, vR, vT, vB;\\nuniform vec2 texelSize;\\nvoid main () {\\n  vUv = aPos * 0.5 + 0.5;\\n  vL = vUv - vec2(texelSize.x, 0.0);\\n  vR = vUv + vec2(texelSize.x, 0.0);\\n  vT = vUv + vec2(0.0, texelSize.y);\\n  vB = vUv - vec2(0.0, texelSize.y);\\n  gl_Position = vec4(aPos, 0.0, 1.0);\\n}`;\\n\\nconst FRAG_CLEAR = `#version 300 es\\nprecision mediump float;\\nin vec2 vUv;\\nuniform sampler2D uTexture;\\nuniform float value;\\nout vec4 o;\\nvoid main () { o = value * texture(uTexture, vUv); }`;\\n\\n\/\/ Splat com dois modos:\\n\/\/   uMode 0 (velocidade): soma for\\u00e7a, como antes.\\n\/\/   uMode 1 (tinta): CARIMBA a cor \\u2014 mix(base, cor, peso). Modelo de pintura,\\n\/\/   n\\u00e3o de luz: substitui\\u00e7\\u00e3o em vez de soma. \\u00c9 imposs\\u00edvel estourar em branco,\\n\/\/   e o fundo n\\u00e3o precisa de v\\u00e9u de composi\\u00e7\\u00e3o por cima.\\nconst FRAG_SPLAT = `#version 300 es\\nprecision highp float;\\nin vec2 vUv;\\nuniform sampler2D uTarget;\\nuniform float aspectRatio;\\nuniform vec3 color;\\nuniform vec2 point;\\nuniform float radius;\\nuniform float uMode;\\nuniform float uStrength;\\nuniform float uEdge;   \/\/ 0 = gaussiana difusa; 1 = topo achatado + borda MARCADA (cara de tinta)\\nout vec4 o;\\nvoid main () {\\n  vec2 p = vUv - point;\\n  p.x *= aspectRatio;\\n  float g = exp(-dot(p, p) \/ radius);\\n  vec4 base = texture(uTarget, vUv);\\n  if (uMode \\u003c 0.5) {                                                   \/\/ velocidade uniforme\\n    o = vec4(base.xyz + g * color, 1.0);\\n  } else if (uMode \\u003c 1.5) {                                            \/\/ TINTA \\u2014 over PREMULTIPLICADO\\n    float gs = mix(g, smoothstep(0.30, 0.58, g), uEdge); \/\/ faixa ESTREITA \\u2192 borda seca (corta a cauda difusa)\\n    float ad = clamp(gs * uStrength, 0.0, 1.0);       \/\/ opacidade depositada\\n    o = vec4(color, 1.0) * ad + base * (1.0 - ad);    \/\/ rgb = cor*alfa, a = opacidade (separada da cor)\\n  } else {                                                             \/\/ RADIAL (ripple)\\n    vec2 dir = length(p) \\u003e 0.0001 ? normalize(p) : vec2(0.0);\\n    o = vec4(base.xyz + vec3(dir * (g * uStrength), 0.0), 1.0);        \/\/ empurra p\/ FORA do ponto\\n  }\\n}`;\\n\\n\/\/ Preenche o alvo com uma cor s\\u00f3lida (inicializa a \\\"tela\\\" na cor do fundo)\\nconst FRAG_FILL = `#version 300 es\\nprecision mediump float;\\nuniform vec3 color;\\nout vec4 o;\\nvoid main () { o = vec4(color, 0.0); }`; \/\/ alfa 0 = SEM tinta (opacidade separada da cor)\\n\\n\/\/ Forma de pedra \\u2014 PORT do drawOrganic do fluxo-animado\/gerador-estampas:\\n\/\/ 10 pontos de controle com wobble INDEPENDENTE (\\u00b113%) interpolados\\n\/\/ suavemente, + alongamento e rota\\u00e7\\u00e3o por seed. Cada pedra \\u00e9 \\u00fanica, como no\\n\/\/ app original (2 harm\\u00f4nicos, como era antes, gerava todas parecidas).\\nconst PEDRA_GLSL = `\\nfloat hash1(float n) { return fract(sin(n) * 43758.5453); }\\nfloat pedraR(float ang, float seed) {\\n  float N = 10.0;\\n  float t = mod((ang \/ 6.2831853) * N + N, N);\\n  float i0 = floor(t);\\n  float i1 = mod(i0 + 1.0, N);\\n  float f = t - i0;\\n  f = 0.5 - 0.5 * cos(f * 3.14159265);\\n  float r0 = 1.0 + (hash1(i0 + seed * 97.0) - 0.5) * 0.26;\\n  float r1 = 1.0 + (hash1(i1 + seed * 97.0) - 0.5) * 0.26;\\n  return mix(r0, r1, f);\\n}\\nvec2 pedraWarp(vec2 d, float seed) {\\n  float rot = hash1(seed * 31.7) * 6.2831853;\\n  float cs = cos(rot), sn = sin(rot);\\n  d = mat2(cs, -sn, sn, cs) * d;\\n  d.x \/= 0.75 + 0.5 * hash1(seed * 13.3); \/\/ alongamento vari\\u00e1vel\\n  return d;\\n}`;\\n\\n\/\/ Micro elementos: renderizados como pontos GPU por cima da imagem (n\\u00e3o s\\u00e3o\\n\/\/ m\\u00e1scara \\u2014 assim a quantidade pode chegar aos milhares sem custo). Cada ponto\\n\/\/ carrega sua cor (da paleta) e um seed de forma (pedras irregulares).\\nconst VERT_PTS = `#version 300 es\\nlayout(location = 0) in vec2 aPos;\\nlayout(location = 1) in float aSize;\\nlayout(location = 2) in float aSeed;\\nlayout(location = 3) in vec3 aColor;\\nuniform float uHeight;\\nuniform vec2 uLo;    \/\/ remapeamento da caixa: canto interno da abertura\\nuniform vec2 uSpan;  \/\/ tamanho da abertura (1,1 = sem caixa)\\nuniform float uBulge;\\nout float vSeed;\\nout vec3 vColor;\\nout vec2 vPos;\\nout float vR;\\nvoid main () {\\n  vSeed = aSeed;\\n  vColor = aColor;\\n  vPos = aPos;\\n  vR = aSize;\\n  \/\/ mesmo remapeamento da arte no modo caixa (inverso do sample do display):\\n  \/\/ pontos perto do centro se expandem \\u2014 flutuam \\\"\\u00e0 frente\\\" no volume\\n  vec2 p = aPos;\\n  if (uSpan.x \\u003c 1.0) {\\n    vec2 d = p - 0.5;\\n    float r2 = clamp(dot(d, d) * 4.0, 0.0, 1.0);\\n    p = 0.5 + d * (1.0 + uBulge * (1.0 - r2));\\n    p = uLo + p * uSpan;\\n  }\\n  gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\\n  gl_PointSize = aSize * uHeight * 2.0 * 1.35 * uSpan.y;\\n}`;\\n\\nconst FRAG_PTS = `#version 300 es\\nprecision mediump float;\\nin float vSeed;\\nin vec3 vColor;\\nin vec2 vPos;\\nin float vR;\\nuniform float uShape;    \/\/ 1 = pedra (forma org\\u00e2nica \\u00fanica por seed)\\nuniform float uUniverse; \/\/ 1 = interior mostra o fluido (micro universo)\\nuniform float uAspect;\\nuniform float uBox;      \/\/ 1 = caixa: elementos nunca sobrep\\u00f5em a moldura frontal\\nuniform sampler2D uDye;\\nout vec4 o;\\n${PEDRA_GLSL}\\nvoid main () {\\n  vec2 d = gl_PointCoord - 0.5;\\n  if (uBox \\u003e 0.5) {\\n    \/\/ posi\\u00e7\\u00e3o deste fragmento na tela: se cai na moldura frontal, descarta \\u2014\\n    \/\/ os elementos ficam DENTRO da caixa, atr\\u00e1s do vidro\\n    vec2 fuv = vPos + vec2(d.x \/ uAspect, -d.y) * vR * 2.7;\\n    vec2 e2 = min(fuv, 1.0 - fuv);\\n    if (min(e2.x * uAspect, e2.y) \\u003c 0.030) discard;\\n  }\\n  float R = 0.37;\\n  if (uShape \\u003e 0.5) {\\n    d = pedraWarp(d, vSeed);\\n    R = 0.30 * pedraR(atan(d.y, d.x), vSeed);\\n  }\\n  float dist = length(d);\\n  if (dist \\u003e R) discard;\\n  float a = smoothstep(R, R * 0.78, dist);\\n  vec3 base = vColor;\\n  if (uUniverse \\u003e 0.5) {\\n    \/\/ micro universo: o interior \\u00e9 uma janela ampliada do pr\\u00f3prio fluido \\u2014\\n    \/\/ o mesmo princ\\u00edpio do fundo, dentro do elemento\\n    vec2 off = vec2(d.x \/ uAspect, -d.y) * vR * 2.7 * 3.0;\\n    base = texture(uDye, vPos + off).rgb;\\n  }\\n  \/\/ contorno fino escurecido na pr\\u00f3pria cor \\u2014 mant\\u00e9m o elemento leg\\u00edvel\\n  float rim = smoothstep(R * 0.80, R * 0.96, dist);\\n  vec3 c = mix(base, vColor * 0.45, rim);\\n  o = vec4(c, a);\\n}`;\\n\\n\/\/ Advec\\u00e7\\u00e3o com valor de repouso: em vez de decair para preto (modelo de luz),\\n\/\/ o campo relaxa em dire\\u00e7\\u00e3o a uRest \\u2014 vec3(0) para velocidade, cor do FUNDO\\n\/\/ para a tinta. A tinta \\\"seca\\\" de volta para a tela, nunca para o preto.\\nconst FRAG_ADVECT = `#version 300 es\\nprecision highp float;\\nin vec2 vUv;\\nuniform sampler2D uVelocity;\\nuniform sampler2D uSource;\\nuniform vec2 texelSize;\\nuniform float dt;\\nuniform float dissipation;\\nuniform vec3 uRest;\\nuniform float uSat; \/\/ regenera\\u00e7\\u00e3o de satura\\u00e7\\u00e3o (s\\u00f3 na tinta; 0 na velocidade)\\nuniform float uFlowMode, uFlowStr, uFlowTime, uFlowAspect; \/\/ campo de fluxo por modo\\nuniform vec2 uFlowDir, uFlowCenter;\\nuniform int uRippleN;      \/\/ n\\u00ba de ondas ativas (modo Ripple)\\nuniform vec4 uRipples[16]; \/\/ x, y, idade, tamanho \\u2014 empurra a tinta em an\\u00e9is\\nuniform float uWander;     \/\/ intensidade do campo de ru\\u00eddo (estilo color-wander)\\nuniform float uFlowSeed;   \/\/ deslocamento aleat\\u00f3rio por sess\\u00e3o (Meander\/wander sem vi\\u00e9s fixo)\\nuniform float uWaveSpd;    \/\/ velocidade de propaga\\u00e7\\u00e3o da onda (modo Wave)\\nout vec4 o;\\n\/\/ ru\\u00eddo de gradiente p\/ o campo WANDER (mesma base da turbul\\u00eancia)\\nvec2 whash2(vec2 p){ p = vec2(dot(p, vec2(127.1,311.7)), dot(p, vec2(269.5,183.3))); return -1.0 + 2.0*fract(sin(p)*43758.5453); }\\nfloat wnoise(vec2 p){\\n  vec2 i = floor(p), f = fract(p); vec2 u = f*f*(3.0-2.0*f);\\n  float a = dot(whash2(i), f);\\n  float b = dot(whash2(i+vec2(1,0)), f-vec2(1,0));\\n  float c = dot(whash2(i+vec2(0,1)), f-vec2(0,1));\\n  float d = dot(whash2(i+vec2(1,1)), f-vec2(1,1));\\n  return mix(mix(a,b,u.x), mix(c,d,u.x), u.y)*0.5+0.5;\\n}\\n\/\/ campo de FLUXO distinto por movimento, aplicado s\\u00f3 na advec\\u00e7\\u00e3o da tinta:\\n\/\/ Meander = serpentina larga; Spiral = v\\u00f3rtice\/ralo m\\u00f3vel; Ripple = an\\u00e9is.\\nvec2 flowVel(vec2 uv){\\n  if (uFlowMode \\u003c 0.5) return vec2(0.0);                 \/\/ Fall: sem fluxo aqui\\n  vec2 f;\\n  if (uFlowMode \\u003c 1.5) {                                 \/\/ DRIFT \\u2014 corrente uniforme\\n    f = uFlowDir;\\n  } else if (uFlowMode \\u003c 2.5) {                          \/\/ MEANDER \\u2014 larga, suave, flu\\u00edda\\n    f = vec2(sin(uv.y * 6.283 + uFlowTime * 0.5 + uFlowSeed) * 0.7 + 0.3 * sin(uv.y * 3.0 - uFlowTime * 0.3 + uFlowSeed),\\n             0.35 * sin(uv.x * 3.0 + uFlowTime * 0.4 + uFlowSeed)) * 0.45;\\n  } else if (uFlowMode \\u003c 3.5) {                          \/\/ SPIRAL \\u2014 v\\u00f3rtice + RALO (centro m\\u00f3vel)\\n    vec2 d = uv - uFlowCenter; d.x *= uFlowAspect;\\n    float r = max(length(d), 0.03);\\n    vec2 tang = vec2(-d.y, d.x) \/ r;                     \/\/ rota\\u00e7\\u00e3o\\n    vec2 rad  = -d \/ r;                                  \/\/ pux\\u00e3o para o centro (ralo)\\n    f = (tang * 1.5 + rad * 1.0) * smoothstep(1.0, 0.06, r) * smoothstep(0.0, 0.10, r); \/\/ olho calmo no centro (sem enrolar sub-pixel \\u2192 sem an\\u00e9is\/zebra)\\n    f.x \/= uFlowAspect;\\n  } else if (uFlowMode \\u003c 4.5) {                          \/\/ RIPPLE (Bubbles) \\u2014 an\\u00e9is p\/ FORA\\n    f = vec2(0.0);\\n    for (int i = 0; i \\u003c 16; i++) {\\n      if (i \\u003e= uRippleN) break;\\n      vec2 d = uv - uRipples[i].xy; d.x *= uFlowAspect;\\n      float r = length(d);\\n      float wv = r - uRipples[i].z * 0.55;               \/\/ frente do anel EXPANDE\\n      float ring = exp(-wv * wv * 90.0);                 \/\/ anel estreito na frente\\n      float str = ring * exp(-uRipples[i].z * 1.3) * uRipples[i].w;\\n      vec2 dir = d \/ max(r, 1e-4); dir.x \/= uFlowAspect;\\n      f += dir * str;\\n    }\\n  } else {                                               \/\/ WAVE \\u2014 ac\\u00famulo VERTICAL: cresce, CAI com\\n    \/\/ for\\u00e7a, EXPLODE p\/ o outro lado e volta a crescer no lado oposto.\\n    float w = uFlowTime * uWaveSpd;\\n    float jit = (wnoise(vec2(w * 0.6, 2.3)) - 0.5) * 0.4;\\n    float s = sin(w) + jit * 0.15;\\n    float c2 = cos(w);\\n    float side = tanh(3.0 * s);                          \/\/ -1 esq .. +1 dir: parede ATIVA (com hold)\\n    float ax = 0.5 + 0.36 * side;                        \/\/ coluna de ac\\u00famulo na parede ativa\\n    float wallAmt = smoothstep(0.35, 1.0, abs(side));\\n    float rising = sign(s) * c2;                          \/\/ \\u003e0 ACUMULA (sobe) | \\u003c0 QUEBRA (cai)\\n    float buildAmt = smoothstep(0.0, 0.5, rising) * wallAmt;\\n    float breakAmt = smoothstep(0.0, 0.5, -rising) * wallAmt;\\n    float dx = (uv.x - ax) * uFlowAspect;\\n    float col = exp(-dx * dx * 9.0);                     \/\/ coluna vertical ESTREITA na parede ativa\\n    \/\/ AC\\u00daMULO: a coluna SOBE alto \\u2014 forma a crista\\n    vec2 rise = vec2(0.0, 1.0) * col * buildAmt * (0.3 + 0.9 * uv.y) * 1.4 * smoothstep(0.92, 0.6, uv.y); \/\/ some no alto (crista n\\u00e3o fura o teto)\\n    \/\/ QUEDA: a crista DESPENCA com for\\u00e7a\\n    vec2 fall = vec2(0.0, -1.8) * col * breakAmt * smoothstep(0.15, 1.0, uv.y);\\n    \/\/ ESPIRAL DA QUEBRA: v\\u00f3rtice no ALTO da coluna que ENROLA a crista p\/ o lado\\n    \/\/ oposto (o \\\"barrel\\\" que desmonta) \\u2014 \\u00e9 o que faltava p\/ QUEBRAR, n\\u00e3o marolar.\\n    vec2 crel = vec2(dx, uv.y - 0.66);\\n    float cr = length(crel);\\n    vec2 spiral = vec2(-crel.y, crel.x) \/ max(cr, 0.10) * (-side) * breakAmt * exp(-cr * cr * 7.0);\\n    \/\/ EXPLOS\\u00c3O no impacto (base da coluna)\\n    vec2 rel = vec2(dx, uv.y - 0.28);\\n    vec2 spray = rel \/ max(length(rel), 0.15) * breakAmt * col * (0.6 + wnoise(uv * 5.0 + w));\\n    \/\/ CRUZAMENTO em ARCO: desce saindo da parede, sobe chegando na outra \\u2192 costura o 8\/espiral\\n    float targetDir = clamp(c2 * 3.0, -1.0, 1.0);\\n    float xr = clamp(((uv.x - 0.5) \/ 0.42) * targetDir, -1.0, 1.0);\\n    vec2 arc = vec2(targetDir, xr * 1.2) * (1.0 - 0.5 * wallAmt);\\n    float ph = uv.x * 7.5 - s * 8.0;\\n    vec2 orb = vec2(cos(ph), sin(ph)) * smoothstep(0.3, 1.0, uv.y) * 0.2;\\n    f = rise * 3.4 + fall * 3.0 + spiral * 4.0 + spray * 1.6 + arc * 1.8 + orb;\\n  }\\n  \/\/ WANDER (color-wander): ru\\u00eddo \\u2192 \\u00e2ngulo \\u2192 dire\\u00e7\\u00e3o. A escala varia no espa\\u00e7o,\\n  \/\/ ent\\u00e3o umas regi\\u00f5es enrolam apertado e outras seguem quase retas.\\n  float wScale = mix(1.8, 7.0, wnoise(uv * 1.3 + uFlowSeed));\\n  vec2 sp = uv * wScale + uFlowSeed + uFlowTime * 0.04;\\n  float n0 = wnoise(sp);\\n  vec2 wg = vec2(wnoise(sp + vec2(0.6, 0.0)) - n0, wnoise(sp + vec2(0.0, 0.6)) - n0);\\n  vec2 wcurl = vec2(wg.y, -wg.x); \/\/ CURL do ru\\u00eddo: diverg\\u00eancia zero, sem vi\\u00e9s global\\n  f += wcurl * (4.0 * uWander);   \/\/ curl SEM normalizar \\u2192 magnitude suave, sem linhas duras nas invers\\u00f5es\\n  return f * uFlowStr;\\n}\\nvoid main () {\\n  vec2 vel = texture(uVelocity, vUv).xy + flowVel(vUv);\\n  \/\/ PAREDES (n\\u00e3o-penetra\\u00e7\\u00e3o): zera a componente que aponta p\/ FORA nas bordas \\u2014 a\\n  \/\/ onda bate e volta, a tinta n\\u00e3o sai da tela.\\n  float bw = 0.06;                                   \/\/ faixa FINA da parede (sem empurrar \\u2192 sem margem)\\n  vel.x += max(0.0, -vel.x) * smoothstep(bw, 0.0, vUv.x);         \/\/ trava sa\\u00edda na esquerda\\n  vel.x -= max(0.0,  vel.x) * smoothstep(bw, 0.0, 1.0 - vUv.x);   \/\/ trava sa\\u00edda na direita\\n  vel.y += max(0.0, -vel.y) * smoothstep(bw, 0.0, vUv.y);         \/\/ trava sa\\u00edda na base\\n  vel.y -= max(0.0,  vel.y) * smoothstep(bw, 0.0, 1.0 - vUv.y);   \/\/ trava sa\\u00edda no topo\\n  vec2 stepD = clamp(dt * vel * texelSize, -0.04, 0.04); \/\/ limita o passo \\u2192 a parede fina segura sem vazar\\n  vec2 coord = vUv - stepD;\\n  vec4 result = texture(uSource, coord);\\n  float keep = 1.0 \/ (1.0 + dissipation * dt);\\n  result = mix(vec4(uRest, 0.0), result, keep);\\n  \/\/ Anti-lama: a m\\u00e9dia RGB da mistura dessatura (entre complementares vira\\n  \/\/ cinza). Empurramos continuamente a cor de volta para longe do cinza,\\n  \/\/ pesado pela dist\\u00e2ncia ao fundo \\u2014 o fundo em si n\\u00e3o \\u00e9 alterado.\\n  if (uSat \\u003e 0.0 \\u0026\\u0026 result.a \\u003e 0.01) {\\n    vec3 col = result.rgb \/ result.a;                 \/\/ cor REAL (des-premultiplicada)\\n    float w = clamp(length(col) * 4.0, 0.0, 1.0);\\n    float lum = dot(col, vec3(0.299, 0.587, 0.114));\\n    vec3 sat = lum + (col - lum) * (1.0 + uSat * dt);\\n    col = clamp(mix(col, sat, w), 0.0, 1.0);\\n    result.rgb = col * result.a;                      \/\/ re-premultiplica (cinza\/preto ficam intactos)\\n  }\\n  o = result;\\n}`;\\n\\n\/\/ MacCormack: advec\\u00e7\\u00e3o de BAIXA DIFUS\\u00c3O. A advec\\u00e7\\u00e3o simples borra um pouco a\\n\/\/ cada frame e ao longo do tempo funde tudo numa m\\u00e9dia (o \\\"vira uma cor s\\u00f3\\\").\\n\/\/ Aqui corrigimos esse erro: recebemos a advec\\u00e7\\u00e3o simples (uForward) e somamos\\n\/\/ metade da diferen\\u00e7a de ida-e-volta, com um LIMITADOR que clampa ao min\/max\\n\/\/ dos vizinhos \\u2014 impede overshoot, ent\\u00e3o \\u00e9 est\\u00e1vel (ao contr\\u00e1rio do unsharp).\\nconst FRAG_ADVECT_MC = `#version 300 es\\nprecision highp float;\\nin vec2 vUv;\\nuniform sampler2D uVelocity;\\nuniform sampler2D uSource;   \/\/ tinta atual (phi^n)\\nuniform sampler2D uForward;  \/\/ advec\\u00e7\\u00e3o simples de phi^n (pass anterior)\\nuniform vec2 texelSize;\\nuniform float dt;\\nuniform float dissipation;\\nuniform vec3 uRest;\\nuniform float uSat;\\nuniform float uPalChroma; \/\/ croma t\\u00edpico da paleta \\u2014 refer\\u00eancia p\/ detectar LAMA\\nuniform float uFlowMode, uFlowStr, uFlowTime, uFlowAspect; \/\/ campo de fluxo por modo\\nuniform vec2 uFlowDir, uFlowCenter;\\nuniform int uRippleN;\\nuniform vec4 uRipples[16];\\nuniform float uWander;   \/\/ intensidade do campo de ru\\u00eddo (estilo color-wander)\\nuniform float uFlowSeed; \/\/ deslocamento aleat\\u00f3rio por sess\\u00e3o (Meander\/wander sem vi\\u00e9s fixo)\\nuniform float uWaveSpd;  \/\/ velocidade de propaga\\u00e7\\u00e3o da onda (modo Wave)\\nuniform vec2 uDyeTexel;  \/\/ texel FINO (res da tinta) p\/ o limitador n\\u00e3o quadricular\\nout vec4 o;\\nvec2 whash2(vec2 p){ p = vec2(dot(p, vec2(127.1,311.7)), dot(p, vec2(269.5,183.3))); return -1.0 + 2.0*fract(sin(p)*43758.5453); }\\nfloat wnoise(vec2 p){\\n  vec2 i = floor(p), f = fract(p); vec2 u = f*f*(3.0-2.0*f);\\n  float a = dot(whash2(i), f);\\n  float b = dot(whash2(i+vec2(1,0)), f-vec2(1,0));\\n  float c = dot(whash2(i+vec2(0,1)), f-vec2(0,1));\\n  float d = dot(whash2(i+vec2(1,1)), f-vec2(1,1));\\n  return mix(mix(a,b,u.x), mix(c,d,u.x), u.y)*0.5+0.5;\\n}\\nvec2 flowVel(vec2 uv){\\n  if (uFlowMode \\u003c 0.5) return vec2(0.0);\\n  vec2 f;\\n  if (uFlowMode \\u003c 1.5) {                                 \/\/ DRIFT\\n    f = uFlowDir;\\n  } else if (uFlowMode \\u003c 2.5) {                          \/\/ MEANDER \\u2014 larga, suave, flu\\u00edda\\n    f = vec2(sin(uv.y * 6.283 + uFlowTime * 0.5 + uFlowSeed) * 0.7 + 0.3 * sin(uv.y * 3.0 - uFlowTime * 0.3 + uFlowSeed),\\n             0.35 * sin(uv.x * 3.0 + uFlowTime * 0.4 + uFlowSeed)) * 0.45;\\n  } else if (uFlowMode \\u003c 3.5) {                          \/\/ SPIRAL \\u2014 v\\u00f3rtice + RALO (centro m\\u00f3vel)\\n    vec2 d = uv - uFlowCenter; d.x *= uFlowAspect;\\n    float r = max(length(d), 0.03);\\n    vec2 tang = vec2(-d.y, d.x) \/ r;\\n    vec2 rad  = -d \/ r;\\n    f = (tang * 1.5 + rad * 1.0) * smoothstep(1.0, 0.06, r) * smoothstep(0.0, 0.10, r); \/\/ olho calmo no centro (sem enrolar sub-pixel \\u2192 sem an\\u00e9is\/zebra)\\n    f.x \/= uFlowAspect;\\n  } else if (uFlowMode \\u003c 4.5) {                          \/\/ RIPPLE (Bubbles) \\u2014 an\\u00e9is p\/ FORA\\n    f = vec2(0.0);\\n    for (int i = 0; i \\u003c 16; i++) {\\n      if (i \\u003e= uRippleN) break;\\n      vec2 d = uv - uRipples[i].xy; d.x *= uFlowAspect;\\n      float r = length(d);\\n      float wv = r - uRipples[i].z * 0.55;\\n      float ring = exp(-wv * wv * 90.0);\\n      float str = ring * exp(-uRipples[i].z * 1.3) * uRipples[i].w;\\n      vec2 dir = d \/ max(r, 1e-4); dir.x \/= uFlowAspect;\\n      f += dir * str;\\n    }\\n  } else {                                               \/\/ WAVE \\u2014 ac\\u00famulo VERTICAL: cresce, CAI com\\n    \/\/ for\\u00e7a, EXPLODE p\/ o outro lado e volta a crescer no lado oposto.\\n    float w = uFlowTime * uWaveSpd;\\n    float jit = (wnoise(vec2(w * 0.6, 2.3)) - 0.5) * 0.4;\\n    float s = sin(w) + jit * 0.15;\\n    float c2 = cos(w);\\n    float side = tanh(3.0 * s);                          \/\/ -1 esq .. +1 dir: parede ATIVA (com hold)\\n    float ax = 0.5 + 0.36 * side;                        \/\/ coluna de ac\\u00famulo na parede ativa\\n    float wallAmt = smoothstep(0.35, 1.0, abs(side));\\n    float rising = sign(s) * c2;                          \/\/ \\u003e0 ACUMULA (sobe) | \\u003c0 QUEBRA (cai)\\n    float buildAmt = smoothstep(0.0, 0.5, rising) * wallAmt;\\n    float breakAmt = smoothstep(0.0, 0.5, -rising) * wallAmt;\\n    float dx = (uv.x - ax) * uFlowAspect;\\n    float col = exp(-dx * dx * 9.0);                     \/\/ coluna vertical ESTREITA na parede ativa\\n    \/\/ AC\\u00daMULO: a coluna SOBE alto \\u2014 forma a crista\\n    vec2 rise = vec2(0.0, 1.0) * col * buildAmt * (0.3 + 0.9 * uv.y) * 1.4 * smoothstep(0.92, 0.6, uv.y); \/\/ some no alto (crista n\\u00e3o fura o teto)\\n    \/\/ QUEDA: a crista DESPENCA com for\\u00e7a\\n    vec2 fall = vec2(0.0, -1.8) * col * breakAmt * smoothstep(0.15, 1.0, uv.y);\\n    \/\/ ESPIRAL DA QUEBRA: v\\u00f3rtice no ALTO da coluna que ENROLA a crista p\/ o lado\\n    \/\/ oposto (o \\\"barrel\\\" que desmonta) \\u2014 \\u00e9 o que faltava p\/ QUEBRAR, n\\u00e3o marolar.\\n    vec2 crel = vec2(dx, uv.y - 0.66);\\n    float cr = length(crel);\\n    vec2 spiral = vec2(-crel.y, crel.x) \/ max(cr, 0.10) * (-side) * breakAmt * exp(-cr * cr * 7.0);\\n    \/\/ EXPLOS\\u00c3O no impacto (base da coluna)\\n    vec2 rel = vec2(dx, uv.y - 0.28);\\n    vec2 spray = rel \/ max(length(rel), 0.15) * breakAmt * col * (0.6 + wnoise(uv * 5.0 + w));\\n    \/\/ CRUZAMENTO em ARCO: desce saindo da parede, sobe chegando na outra \\u2192 costura o 8\/espiral\\n    float targetDir = clamp(c2 * 3.0, -1.0, 1.0);\\n    float xr = clamp(((uv.x - 0.5) \/ 0.42) * targetDir, -1.0, 1.0);\\n    vec2 arc = vec2(targetDir, xr * 1.2) * (1.0 - 0.5 * wallAmt);\\n    float ph = uv.x * 7.5 - s * 8.0;\\n    vec2 orb = vec2(cos(ph), sin(ph)) * smoothstep(0.3, 1.0, uv.y) * 0.2;\\n    f = rise * 3.4 + fall * 3.0 + spiral * 4.0 + spray * 1.6 + arc * 1.8 + orb;\\n  }\\n  float wScale = mix(1.8, 7.0, wnoise(uv * 1.3 + uFlowSeed));\\n  vec2 sp = uv * wScale + uFlowSeed + uFlowTime * 0.04;\\n  float n0 = wnoise(sp);\\n  vec2 wg = vec2(wnoise(sp + vec2(0.6, 0.0)) - n0, wnoise(sp + vec2(0.0, 0.6)) - n0);\\n  vec2 wcurl = vec2(wg.y, -wg.x); \/\/ CURL do ru\\u00eddo: diverg\\u00eancia zero, sem vi\\u00e9s global\\n  f += wcurl * (4.0 * uWander);   \/\/ WANDER: curl bruto (suave, sem linhas duras)\\n  return f * uFlowStr;\\n}\\nvoid main () {\\n  vec2 vel = texture(uVelocity, vUv).xy + flowVel(vUv);\\n  \/\/ PAREDES (n\\u00e3o-penetra\\u00e7\\u00e3o): zera a componente que aponta p\/ FORA nas bordas.\\n  float bw = 0.06;                                   \/\/ faixa FINA da parede (sem empurrar \\u2192 sem margem)\\n  vel.x += max(0.0, -vel.x) * smoothstep(bw, 0.0, vUv.x);         \/\/ trava sa\\u00edda na esquerda\\n  vel.x -= max(0.0,  vel.x) * smoothstep(bw, 0.0, 1.0 - vUv.x);   \/\/ trava sa\\u00edda na direita\\n  vel.y += max(0.0, -vel.y) * smoothstep(bw, 0.0, vUv.y);         \/\/ trava sa\\u00edda na base\\n  vel.y -= max(0.0,  vel.y) * smoothstep(bw, 0.0, 1.0 - vUv.y);   \/\/ trava sa\\u00edda no topo\\n  vec2 disp = clamp(dt * vel * texelSize, -0.04, 0.04); \/\/ limita o passo (cont\\u00e9m sem margem)\\n  vec2 back = vUv - disp;                            \/\/ ponto de backtrace\\n  vec4 phiHat   = texture(uForward, vUv);            \/\/ phi_hat(x)\\n  vec4 phiN     = texture(uSource,  vUv);            \/\/ phi^n(x)\\n  vec4 phiTilde = texture(uForward, vUv + disp);     \/\/ advec\\u00e7\\u00e3o REVERSA de phi_hat\\n  vec4 result = phiHat + 0.5 * (phiN - phiTilde);    \/\/ corre\\u00e7\\u00e3o MacCormack\\n  \/\/ LIMITADOR: clampa ao min\/max de phi^n nos vizinhos do backtrace \\u2014 sem novos\\n  \/\/ extremos \\u2192 EST\\u00c1VEL (impede o overshoot que explodiu no unsharp ing\\u00eanuo).\\n  vec2 nt = uDyeTexel * 2.0; \/\/ vizinhos FINOS (res da tinta) \\u2192 sem quadricular\\n  vec4 s0 = texture(uSource, back + vec2(nt.x, 0.0));\\n  vec4 s1 = texture(uSource, back - vec2(nt.x, 0.0));\\n  vec4 s2 = texture(uSource, back + vec2(0.0, nt.y));\\n  vec4 s3 = texture(uSource, back - vec2(0.0, nt.y));\\n  vec4 lo = min(min(s0, s1), min(s2, s3));\\n  vec4 hi = max(max(s0, s1), max(s2, s3));\\n  vec4 mrg = (hi - lo) * 0.25;           \/\/ margem MENOR \\u2192 sem overshoot\/zebra em tra\\u00e7os finos\\n  result = clamp(result, lo - mrg, hi + mrg);\\n  result.a = clamp(result.a, 0.0, 1.0);\\n  result.rgb = clamp(result.rgb, vec3(0.0), vec3(result.a)); \/\/ premultiplicado V\\u00c1LIDO: mata zebra e lavagem\\n  \/\/ SECAGEM SELETIVA: a tinta s\\u00f3 volta ao fundo onde virou LAMA \\u2014 mais cinza que\\n  \/\/ a paleta, sinal de que cores se misturaram. Cor pura\/isolada persiste (seca\\n  \/\/ bem devagar) \\u2192 os tons ficam saturados e presentes mais tempo; s\\u00f3 a lama some.\\n  \/\/ Em paleta cinza (Noir) uPalChroma\\u22480 \\u2192 nada \\u00e9 lama \\u2192 tudo persiste igual.\\n  vec3 dcol = result.a \\u003e 0.01 ? result.rgb \/ result.a : vec3(0.0);\\n  float dmx = max(dcol.r, max(dcol.g, dcol.b)), dmn = min(dcol.r, min(dcol.g, dcol.b));\\n  float dsat = dmx \\u003e 0.001 ? (dmx - dmn) \/ dmx : 0.0;\\n  float mud = clamp((uPalChroma - dsat) \/ max(uPalChroma, 0.08), 0.0, 1.0); \/\/ qu\\u00e3o mais cinza que a paleta\\n  float dissMul = mix(0.25, 1.8, mud);   \/\/ pura persiste (0.25\\u00d7); lama some (1.8\\u00d7)\\n  float keep = 1.0 \/ (1.0 + dissipation * dissMul * dt);\\n  result = mix(vec4(uRest, 0.0), result, keep);\\n  if (uSat \\u003e 0.0 \\u0026\\u0026 result.a \\u003e 0.01) {\\n    vec3 col = result.rgb \/ result.a;                 \/\/ cor REAL (des-premultiplicada)\\n    float w = clamp(length(col) * 4.0, 0.0, 1.0);\\n    float lum = dot(col, vec3(0.299, 0.587, 0.114));\\n    vec3 sat = lum + (col - lum) * (1.0 + uSat * dt);\\n    col = clamp(mix(col, sat, w), 0.0, 1.0);\\n    result.rgb = col * result.a;                      \/\/ re-premultiplica (cinza\/preto ficam intactos)\\n  }\\n  o = result;\\n}`;\\n\\n\/\/ Turbul\\u00eancia fractal: fBm com domain warping vira fun\\u00e7\\u00e3o-corrente (psi) e o\\n\/\/ rotacional dela (curl) \\u00e9 somado \\u00e0 velocidade \\u2014 redemoinhos autossimilares em\\n\/\/ v\\u00e1rias escalas, os filamentos finos de tinta na \\u00e1gua. \\u00c9 aqui que entra o\\n\/\/ \\\"fractal\\\" no fluxo.\\nconst FRAG_NOISE = `#version 300 es\\nprecision highp float;\\nin vec2 vUv;\\nuniform sampler2D uVelocity;\\nuniform float uTime;\\nuniform float uStrength;\\nuniform float uAspect;\\nuniform float dt;\\nout vec4 o;\\nvec2 hash2(vec2 p){\\n  p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));\\n  return -1.0 + 2.0 * fract(sin(p) * 43758.5453);\\n}\\n\/\/ ru\\u00eddo de GRADIENTE (Perlin) \\u2014 suave e SEM alinhamento aos eixos. O value\\n\/\/ noise anterior (na grade inteira) deixava a turbul\\u00eancia com cara quadrada.\\nfloat noise(vec2 p){\\n  vec2 i = floor(p), f = fract(p);\\n  vec2 u = f * f * (3.0 - 2.0 * f);\\n  float a = dot(hash2(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0));\\n  float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));\\n  float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));\\n  float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));\\n  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 0.5 + 0.5;\\n}\\nfloat fbm(vec2 p){\\n  float v = 0.0, a = 0.5;\\n  mat2 rot = mat2(0.8, 0.6, -0.6, 0.8);\\n  for (int i = 0; i \\u003c 5; i++) { v += a * noise(p); p = rot * p * 2.03; a *= 0.5; }\\n  return v;\\n}\\nfloat psi(vec2 p){\\n  vec2 q = p * 4.0 + vec2(0.07, -0.043) * uTime;\\n  return fbm(q + 0.6 * vec2(fbm(q + uTime * 0.05), fbm(q - uTime * 0.04)));\\n}\\nvoid main(){\\n  vec2 p = vec2(vUv.x * uAspect, vUv.y);\\n  float h = 0.01;\\n  float dpx = psi(p + vec2(h, 0.0)) - psi(p - vec2(h, 0.0));\\n  float dpy = psi(p + vec2(0.0, h)) - psi(p - vec2(0.0, h));\\n  vec2 force = vec2(dpy, -dpx) \/ (2.0 * h);\\n  vec2 vel = texture(uVelocity, vUv).xy;\\n  vel += force * uStrength * dt;\\n  o = vec4(vel, 0.0, 1.0);\\n}`;\\n\\n\/\/ Gravidade de aqu\\u00e1rio: onde h\\u00e1 tinta (densidade = dist\\u00e2ncia da cor ao fundo),\\n\/\/ o l\\u00edquido pesa e afunda. As paredes do container j\\u00e1 existem \\u2014 s\\u00e3o as\\n\/\/ condi\\u00e7\\u00f5es de contorno da diverg\\u00eancia. O resultado \\u00e9 a tinta escorrendo,\\n\/\/ empo\\u00e7ando embaixo e balan\\u00e7ando presa no quadro.\\nconst FRAG_GRAVITY = `#version 300 es\\nprecision highp float;\\nin vec2 vUv;\\nuniform sampler2D uVelocity;\\nuniform sampler2D uDye;\\nuniform vec3 uRest;\\nuniform float uG;\\nuniform float dt;\\nout vec4 o;\\nvoid main () {\\n  vec2 vel = texture(uVelocity, vUv).xy;\\n  float density = clamp(texture(uDye, vUv).a * 1.2, 0.0, 1.0); \/\/ OPACIDADE = quantidade (escura afunda igual)\\n  \/\/ S\\u00d3-DESCIDA: a tinta densa afunda, mas o l\\u00edquido limpo N\\u00c3O \\u00e9 empurrado para\\n  \/\/ cima (o max(0,...) tira o empuxo de subida) \\u2014 reduz muito o retorno\/slosh.\\n  vel.y -= uG * max(0.0, density - 0.15) * dt;\\n  o = vec4(vel, 0.0, 1.0);\\n}`;\\n\\n\/\/ Obst\\u00e1culos (esferas\/pedras): m\\u00e1scara anal\\u00edtica \\u2014 dentro da forma, o campo\\n\/\/ vira uFill (velocidade \\u2192 zero = parede s\\u00f3lida; tinta \\u2192 cor do fundo). A\\n\/\/ proje\\u00e7\\u00e3o de press\\u00e3o faz o fluido CONTORNAR a forma de verdade, e a tinta\\n\/\/ desenha a silhueta ao passar. Pedras = raio modulado por harm\\u00f4nicos.\\nconst FRAG_MASK = `#version 300 es\\nprecision highp float;\\nin vec2 vUv;\\nuniform sampler2D uSource;\\nuniform vec3 uFill;\\nuniform float uAspect;\\nuniform int uCount;\\nuniform float uUseCol; \/\/ 0 = uFill fixo (parede de velocidade); 1 = cor do elemento; 2 = universo (fluido dentro)\\nuniform vec4 uObsA[48]; \/\/ x, y, raio, seed\\nuniform vec4 uObsB[48]; \/\/ \\u00e9Pedra, \\u2014, \\u2014, \\u2014\\nuniform vec4 uObsC[48]; \/\/ r, g, b (cor do elemento, da paleta)\\nout vec4 o;\\n${PEDRA_GLSL}\\nvoid main () {\\n  float inside = 0.0;\\n  vec3 col = uFill;\\n  for (int i = 0; i \\u003c 48; i++) {\\n    if (i \\u003e= uCount) break;\\n    vec2 d = vUv - uObsA[i].xy;\\n    d.x *= uAspect;\\n    float R = uObsA[i].z;\\n    \/\/ cull r\\u00e1pido (margem p\/ alongamento das pedras)\\n    if (dot(d, d) \\u003e R * R * 3.24) continue;\\n    if (uObsB[i].x \\u003e 0.5) {\\n      d = pedraWarp(d, uObsA[i].w);\\n      R *= 0.85 * pedraR(atan(d.y, d.x), uObsA[i].w);\\n    }\\n    float m = smoothstep(R, R * 0.88, length(d));\\n    if (m \\u003e inside) {\\n      inside = m;\\n      if (uUseCol \\u003e 1.5) {\\n        \/\/ universo: interior = c\\u00f3pia contra\\u00edda do fluido em volta \\u2014 a borda\\n        \/\/ puxa tinta fresca para dentro e o miolo recicla (t\\u00fanel vivo);\\n        \/\/ 15% da cor pr\\u00f3pria mant\\u00e9m identidade e estabilidade\\n        vec2 dd = vUv - uObsA[i].xy;\\n        vec3 uni = texture(uSource, uObsA[i].xy + dd * 0.55).rgb;\\n        col = mix(uObsC[i].rgb, uni, 0.85);\\n      } else {\\n        col = uObsC[i].rgb;\\n      }\\n    }\\n  }\\n  vec4 s = texture(uSource, vUv);\\n  vec3 fill = uUseCol \\u003e 0.5 ? col : uFill;\\n  o = mix(s, vec4(fill, 1.0), inside);\\n}`;\\n\\nconst FRAG_DIVERGENCE = `#version 300 es\\nprecision mediump float;\\nin vec2 vUv, vL, vR, vT, vB;\\nuniform sampler2D uVelocity;\\nout vec4 o;\\nvoid main () {\\n  float L = texture(uVelocity, vL).x;\\n  float R = texture(uVelocity, vR).x;\\n  float T = texture(uVelocity, vT).y;\\n  float B = texture(uVelocity, vB).y;\\n  vec2 C = texture(uVelocity, vUv).xy;\\n  if (vL.x \\u003c 0.0) { L = -C.x; }\\n  if (vR.x \\u003e 1.0) { R = -C.x; }\\n  if (vT.y \\u003e 1.0) { T = -C.y; }\\n  if (vB.y \\u003c 0.0) { B = -C.y; }\\n  o = vec4(0.5 * (R - L + T - B), 0.0, 0.0, 1.0);\\n}`;\\n\\nconst FRAG_CURL = `#version 300 es\\nprecision mediump float;\\nin vec2 vUv, vL, vR, vT, vB;\\nuniform sampler2D uVelocity;\\nout vec4 o;\\nvoid main () {\\n  float L = texture(uVelocity, vL).y;\\n  float R = texture(uVelocity, vR).y;\\n  float T = texture(uVelocity, vT).x;\\n  float B = texture(uVelocity, vB).x;\\n  float vorticity = R - L - T + B;\\n  o = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);\\n}`;\\n\\nconst FRAG_VORTICITY = `#version 300 es\\nprecision highp float;\\nin vec2 vUv, vL, vR, vT, vB;\\nuniform sampler2D uVelocity;\\nuniform sampler2D uCurl;\\nuniform float curl;\\nuniform float dt;\\nout vec4 o;\\nvoid main () {\\n  float L = texture(uCurl, vL).x;\\n  float R = texture(uCurl, vR).x;\\n  float T = texture(uCurl, vT).x;\\n  float B = texture(uCurl, vB).x;\\n  float C = texture(uCurl, vUv).x;\\n  vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));\\n  force \/= length(force) + 0.0001;\\n  force *= curl * C;\\n  force.y *= -1.0;\\n  vec2 velocity = texture(uVelocity, vUv).xy;\\n  velocity += force * dt;\\n  velocity = min(max(velocity, -1000.0), 1000.0);\\n  o = vec4(velocity, 0.0, 1.0);\\n}`;\\n\\nconst FRAG_PRESSURE = `#version 300 es\\nprecision mediump float;\\nin vec2 vUv, vL, vR, vT, vB;\\nuniform sampler2D uPressure;\\nuniform sampler2D uDivergence;\\nout vec4 o;\\nvoid main () {\\n  float L = texture(uPressure, vL).x;\\n  float R = texture(uPressure, vR).x;\\n  float T = texture(uPressure, vT).x;\\n  float B = texture(uPressure, vB).x;\\n  float divergence = texture(uDivergence, vUv).x;\\n  float pressure = (L + R + B + T - divergence) * 0.25;\\n  o = vec4(pressure, 0.0, 0.0, 1.0);\\n}`;\\n\\nconst FRAG_GRADIENT = `#version 300 es\\nprecision mediump float;\\nin vec2 vUv, vL, vR, vT, vB;\\nuniform sampler2D uPressure;\\nuniform sampler2D uVelocity;\\nout vec4 o;\\nvoid main () {\\n  float L = texture(uPressure, vL).x;\\n  float R = texture(uPressure, vR).x;\\n  float T = texture(uPressure, vT).x;\\n  float B = texture(uPressure, vB).x;\\n  vec2 velocity = texture(uVelocity, vUv).xy;\\n  velocity.xy -= vec2(R - L, T - B);\\n  o = vec4(velocity, 0.0, 1.0);\\n}`;\\n\\n\/\/ Display: o campo de tinta \\u00c9 a imagem (fundo incluso \\u2014 sem v\\u00e9u de composi\\u00e7\\u00e3o).\\n\/\/ \\\"Textura\\\": nas bordas entre tintas (detectadas pelo gradiente de COR, n\\u00e3o de\\n\/\/ lumin\\u00e2ncia), a cor local afunda para uma vers\\u00e3o mais escura e MAIS saturada\\n\/\/ de si mesma (c*c) \\u2014 o edge darkening de aquarela\/veladura. Nenhum fator\\n\/\/ cinza multiplicando a imagem em lugar nenhum.\\n\/\/ Caixa (ref. Refik Anadol no MoMA): a arte vive dentro de uma CAVIDADE.\\n\/\/ Banda das bordas = paredes internas em perspectiva (rim claro na frente \\u2192\\n\/\/ recua escurecendo), topo iluminado \/ fundo em sombra, cantos escurecidos\\n\/\/ onde as paredes se encontram, sombra de contato onde a arte toca a parede,\\n\/\/ e oclus\\u00e3o de cantos + luz de cima sobre a pr\\u00f3pria arte.\\nconst FRAG_DISPLAY = `#version 300 es\\nprecision highp float;\\nin vec2 vUv;\\nuniform sampler2D uTexture;\\nuniform vec2 texelSize;\\nuniform float shading;\\nuniform float uGlass;  \/\/ 1 = caixa\\nuniform float uAspect;\\nuniform vec3 uBg;\\nuniform vec3 uWallC;   \/\/ cor da moldura frontal (branca ou preta)\\nuniform int uRippleN;      \/\/ n\\u00ba de ondas ativas (modo Ripple)\\nuniform vec4 uRipples[16]; \/\/ x, y, idade(s), tamanho \\u2014 refra\\u00e7\\u00e3o na tela\\nout vec4 fragColor;\\n\/\/ quantidade de TINTA: dist\\u00e2ncia do \\\"sem tinta\\\" (preto). A tinta some revelando\\n\/\/ o fundo (fade de opacidade), em vez de virar a cor do fundo (lavado no claro).\\nfloat heightOf(vec3 c) { return clamp(length(c) * 1.4, 0.0, 1.0); }\\n\\nvoid main () {\\n  vec2 uv = vUv;\\n\\n  \/\/ \\u2500\\u2500 tinta, com PARALLAX no modo caixa: a altura desloca a amostra na\\n  \/\/ dire\\u00e7\\u00e3o do olhar \\u2014 tinta alta avan\\u00e7a para fora da caixa (eixo z real) \\u2500\\u2500\\n  vec2 pu = uv;\\n  float h = 0.0;\\n  if (uGlass \\u003e 0.5) {\\n    vec2 viewDir = uv - 0.5;\\n    for (int i = 0; i \\u003c 3; i++) {\\n      h = heightOf(texture(uTexture, pu).rgb);\\n      pu = uv - viewDir * h * 0.11;\\n    }\\n  }\\n  \/\/ \\u2500\\u2500 RIPPLE: ondas conc\\u00eantricas que se expandem do ponto de cada gota,\\n  \/\/    refratando a tinta (deslocam a amostra) \\u2014 a ondula\\u00e7\\u00e3o de verdade. \\u2500\\u2500\\n  for (int i = 0; i \\u003c 16; i++) {\\n    if (i \\u003e= uRippleN) break;\\n    vec2 d = uv - uRipples[i].xy; d.x *= uAspect;\\n    float r = length(d);\\n    float age = uRipples[i].z;\\n    float w = r - age * 0.9;                          \/\/ frente EXPANDE (mais r\\u00e1pido)\\n    \/\/ CRISTA \\u00daNICA (derivada de gaussiana): um anel s\\u00f3 que viaja para fora \\u2014\\n    \/\/ crista \\u00e0 frente, vale atr\\u00e1s. Antes tinha v\\u00e1rias ondula\\u00e7\\u00f5es \\u2192 parecia parado.\\n    float wave = w * exp(-w * w * 55.0) * 8.0;\\n    float amp = 0.028 * exp(-age * 0.8) * uRipples[i].w; \/\/ maior, decai devagar\\n    vec2 off = (d \/ max(r, 1e-4)) * wave * amp;\\n    off.x \/= uAspect;\\n    pu += off;\\n  }\\n  vec4 c4 = texture(uTexture, pu);                \/\/ TINTA premultiplicada: rgb=cor*opacidade, a=opacidade\\n  vec3 c = c4.rgb;\\n  vec3 lc = texture(uTexture, pu - vec2(texelSize.x, 0.0)).rgb;\\n  vec3 rc = texture(uTexture, pu + vec2(texelSize.x, 0.0)).rgb;\\n  vec3 tc = texture(uTexture, pu + vec2(0.0, texelSize.y)).rgb;\\n  vec3 bc = texture(uTexture, pu - vec2(0.0, texelSize.y)).rgb;\\n  float inkA = clamp(c4.a, 0.0, 1.0);              \/\/ OPACIDADE real (separada da cor da tinta)\\n  \/\/ des-premultiplica p\/ obter a COR real e real\\u00e7a o croma PRESERVANDO a lumin\\u00e2ncia:\\n  \/\/ as cores voltam a ficar vivas (look de seda) mas o cinza\/preto do Noir continua\\n  \/\/ escuro (croma 0 \\u2192 sem altera\\u00e7\\u00e3o). Depois comp\\u00f5e reto sobre o fundo.\\n  vec3 tcol = inkA \\u003e 0.004 ? c4.rgb \/ inkA : vec3(0.0);\\n  float tl = dot(tcol, vec3(0.299, 0.587, 0.114));\\n  float boost = mix(1.0, 1.25, smoothstep(0.05, 0.35, inkA)); \/\/ real\\u00e7a s\\u00f3 onde h\\u00e1 tinta ENCORPADA\\n  tcol = clamp(tl + (tcol - tl) * boost, 0.0, 1.0);\\n  vec3 art = uBg * (1.0 - inkA) + tcol * inkA;\\n  vec3 paint = art;\\n  h = inkA;\\n\\n  if (uGlass \\u003e 0.5) {\\n    \/\/ relevo 3D: normal do campo de altura, luz do alto-frente. Sombra\\n    \/\/ AFUNDA na pr\\u00f3pria cor (c*c) e luz LEVANTA a pr\\u00f3pria cor \\u2014 sem cinza.\\n    float hL = heightOf(lc), hR = heightOf(rc), hT = heightOf(tc), hB = heightOf(bc);\\n    vec3 n = normalize(vec3(hL - hR, hB - hT, 0.35));\\n    float lit = clamp(dot(n, normalize(vec3(0.25, 0.55, 0.8))), 0.0, 1.0);\\n    vec3 shadowC = paint * paint * 1.15;\\n    vec3 litC = mix(paint, vec3(1.0) - (vec3(1.0) - paint) * (vec3(1.0) - paint), 0.4);\\n    paint = mix(shadowC, litC, lit);\\n  }\\n\\n  vec3 col = paint;\\n\\n  if (uGlass \\u003e 0.5) {\\n    \/\/ Caixa composta por ALPHA (sem branch bin\\u00e1rio \\u2192 sem degrau\/linhas duras):\\n    \/\/   plano de tr\\u00e1s (arte + sombra difusa)  \\u2192  paredes neutras  \\u2192  moldura.\\n    \/\/ A cor do FUNDO vive s\\u00f3 no plano de tr\\u00e1s; as PAREDES s\\u00e3o neutras (moldura).\\n    float wOut = 0.026;                                       \/\/ moldura frontal \\u2014 uniforme\\n    float wallTop = 0.090, wallBot = 0.020, wallSide = 0.050; \/\/ profundidade da cavidade por lado\\n\\n    float dTop = 1.0 - uv.y, dBot = uv.y;\\n    float dLe  = uv.x * uAspect, dRi = (1.0 - uv.x) * uAspect;\\n    float wtTop = wOut + wallTop, wtBot = wOut + wallBot, wtSide = wOut + wallSide;\\n    float qTop = dTop \/ wtTop, qBot = dBot \/ wtBot, qLe = dLe \/ wtSide, qRi = dRi \/ wtSide;\\n    float pa = min(qTop, qBot), pb = max(qTop, qBot);\\n    float pc = min(qLe, qRi),  pd = max(qLe, qRi);\\n    float q1 = min(pa, pc);                         \/\/ parede mais pr\\u00f3xima\\n    float q2 = min(max(pa, pc), min(pb, pd));       \/\/ 2\\u00aa mais pr\\u00f3xima \\u2192 cantos\\n\\n    vec3 wallBase = uWallC;\\n\\n    \/\/ \\u2500\\u2500 (1) PLANO DE TR\\u00c1S: arte + sombra projetada DIFUSA (degrad\\u00e9 longo do\\n    \/\/     topo at\\u00e9 o meio, sem corte; quase nada no ch\\u00e3o). Multiply mais\\n    \/\/     transparente sobre a tinta. A cor do fundo aparece s\\u00f3 aqui. \\u2500\\u2500\\n    float sh = (1.0 - smoothstep(0.0, 0.62, dTop)) * 1.15\\n             + (1.0 - smoothstep(0.0, 0.30, dRi )) * 0.28\\n             + (1.0 - smoothstep(0.0, 0.26, dLe )) * 0.18\\n             + (1.0 - smoothstep(0.0, 0.14, dBot)) * 0.05;\\n    sh = min(sh, 1.5);\\n    float ink = smoothstep(0.03, 0.22, h);\\n    float trans = 1.0 - ink;   \/\/ sombra S\\u00d3 no fundo vazio \\u2014 n\\u00e3o escurece os elementos\\n    vec3 back = paint * (1.0 - 0.07 * sh * trans);\\n\\n    \/\/ \\u2500\\u2500 (2) PAREDES INTERNAS \\u2014 cada lado com TOM DISTINTO (discreto, n\\u00e3o\\n    \/\/     misturado): \\u00e9 o degrau de tom no encontro das paredes que faz as\\n    \/\/     ARESTAS INTERNAS aparecerem. Luz alta de shadow box: teto escuro,\\n    \/\/     ch\\u00e3o bem mais claro, laterais em tons intermedi\\u00e1rios e diferentes.\\n    \/\/     Modelo ADITIVO (mix entre sombra e luz da parede) \\u2192 funciona na caixa\\n    \/\/     branca E na preta (a preta fica preta no teto e cinza no ch\\u00e3o). \\u2500\\u2500\\n    float s;                          \/\/ tom do lado: 0 escuro \\u2192 1 claro\\n    if (q1 == qTop)      s = 0.12;    \/\/ TETO: mais escuro\\n    else if (q1 == qBot) s = 1.00;    \/\/ CH\\u00c3O: bem mais claro\\n    else if (q1 == qLe)  s = 0.64;    \/\/ esquerda\\n    else                 s = 0.40;    \/\/ direita\\n    float t = clamp(q1, 0.0, 1.0);    \/\/ profundidade (0 frente \\u2192 1 fundo)\\n    s *= mix(1.0, 0.84, t);           \/\/ leve recuo: escurece um pouco para o fundo\\n    vec3 wallLo = uWallC * 0.5;                  \/\/ sombra da parede\\n    vec3 wallHi = uWallC + vec3(0.48);           \/\/ luz aditiva contida (preta ~20% menos clara)\\n    vec3 wall = mix(wallLo, wallHi, s);\\n    float wallZ = mix(0.90, 0.05, t);\\n    \/\/ BORDAS DURAS \\u2014 s\\u00f3 um anti-alias de ~1px. Feather fica s\\u00f3 na sombra (se\\u00e7\\u00e3o 1).\\n    float aa = 1.5 * fwidth(q1);\\n    float wallMask = (1.0 - smoothstep(1.0 - aa, 1.0 + aa, q1))   \/\/ borda interna DURA\\n                   * (1.0 - smoothstep(wallZ - 0.09, wallZ + 0.09, h * 1.15)); \/\/ z-test (tinta)\\n    vec3 col2 = mix(back, wall, wallMask);\\n\\n    \/\/ \\u2500\\u2500 (3) MOLDURA FRONTAL \\u2014 mant\\u00e9m o tom (branca fica branca, preta fica\\n    \/\/     preta). Gradiente sutil CONTR\\u00c1RIO \\u00e0 luz interna (topo mais claro, base\\n    \/\/     mais escura, oposto ao teto escuro \/ ch\\u00e3o claro) para contrastar com\\n    \/\/     as paredes em toda a volta. Filete fino na aresta interna, separando\\n    \/\/     a moldura da parte interna. \\u2500\\u2500\\n    float mRaw = min(min(dTop, dBot), min(dLe, dRi));\\n    float front = 1.0 - smoothstep(wOut - 0.006, wOut + 0.006, mRaw);\\n    vec3 frontC = wallBase * mix(0.92, 1.07, uv.y);          \/\/ contr\\u00e1rio \\u00e0 luz interna\\n    float lip = step(wOut - 0.0055, mRaw) * step(mRaw, wOut - 0.0015);   \/\/ filete DURO, sem feather\\n    \/\/ filete vis\\u00edvel nas DUAS molduras: linha ESCURA na branca, CLARA na preta\\n    \/\/ (escurecer o preto n\\u00e3o apareceria). Refor\\u00e7a a divisa frente \\u2194 interior.\\n    float frameLum = dot(wallBase, vec3(0.299, 0.587, 0.114));\\n    float filDir = (frameLum \\u003e 0.5) ? -0.13 : 0.11;\\n    frontC += vec3(filDir * lip);\\n    col = mix(col2, frontC, front);\\n  }\\n  fragColor = vec4(col, 1.0);\\n}`;\\n\\n\/\/ \\u2500\\u2500 Compila\\u00e7\\u00e3o \/ programas \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nfunction compileShader(type, source) {\\n  const sh = gl.createShader(type);\\n  gl.shaderSource(sh, source);\\n  gl.compileShader(sh);\\n  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS))\\n    console.error(gl.getShaderInfoLog(sh));\\n  return sh;\\n}\\n\\nfunction createProgram(fragSrc, vertSrc) {\\n  const prog = gl.createProgram();\\n  gl.attachShader(prog, compileShader(gl.VERTEX_SHADER, vertSrc || VERT));\\n  gl.attachShader(prog, compileShader(gl.FRAGMENT_SHADER, fragSrc));\\n  gl.linkProgram(prog);\\n  if (!gl.getProgramParameter(prog, gl.LINK_STATUS))\\n    console.error(gl.getProgramInfoLog(prog));\\n  const uniforms = {};\\n  const n = gl.getProgramParameter(prog, gl.ACTIVE_UNIFORMS);\\n  for (let i = 0; i \\u003c n; i++) {\\n    const name = gl.getActiveUniform(prog, i).name;\\n    uniforms[name] = gl.getUniformLocation(prog, name);\\n  }\\n  return { prog, uniforms };\\n}\\n\\nlet progClear, progSplat, progFill, progAdvect, progAdvectMC, progNoise, progGravity, progMask,\\n    progPts, progDivergence, progCurl, progVorticity, progPressure, progGradient, progDisplay;\\n\\n\/\/ \\u2500\\u2500 Obst\\u00e1culos \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nconst MAX_OBS = 48;\\nlet obstacles = [], obsA = new Float32Array(MAX_OBS * 4),\\n    obsB = new Float32Array(MAX_OBS * 4), obsC = new Float32Array(MAX_OBS * 4);\\n\\nfunction buildObstacles() {\\n  obstacles = [];\\n  if (S.elements !== 'nenhum') {\\n    const rng = mkRng(S.seed + 7);\\n    const pedra = S.elements === 'pedras';\\n    const micro = S.elSize === 'micro';\\n    const q = S.elCount \/ 100; \/\/ slider Quantidade\\n    let n, rmin, rspan;\\n    if (micro)                       { n = Math.round(30 + q * 14970); rmin = 0.002; rspan = 0.004; }\\n    else if (S.elSize === 'medios')  { n = Math.round(2 + q * 46);     rmin = 0.025; rspan = 0.030; }\\n    else                             { n = Math.round(1 + q * 7);      rmin = 0.060; rspan = 0.080; }\\n    const cap = micro ? 15000 : MAX_OBS; \/\/ micro s\\u00e3o pontos GPU: dezenas de milhares ok\\n    \/\/ margem m\\u00ednima: micro espalham pela tela INTEIRA (a moldura vazia que a\\n    \/\/ Fer viu era a margem de 12% de antes)\\n    const mg = micro ? 0.02 : 0.08;\\n    for (let i = 0; i \\u003c n \\u0026\\u0026 i \\u003c cap; i++) {\\n      let x, y;\\n      if (S.elDist === 'centro') {\\n        x = 0.5 + (rng() + rng() - 1) * 0.22;\\n        y = 0.5 + (rng() + rng() - 1) * 0.22;\\n      } else if (S.elDist === 'bordas') {\\n        const side = rng() * 4 | 0, m = 0.06 + rng() * 0.12;\\n        if (side === 0)      { x = m;       y = rng(); }\\n        else if (side === 1) { x = 1 - m;   y = rng(); }\\n        else if (side === 2) { x = rng();   y = m; }\\n        else                 { x = rng();   y = 1 - m; }\\n      } else {\\n        x = mg + rng() * (1 - 2 * mg);\\n        y = mg + rng() * (1 - 2 * mg);\\n      }\\n      const cols = activeCols();\\n      obstacles.push({\\n        x, y, r: rmin + rng() * rspan,\\n        vx: 0, vy: 0, \/\/ velocidade pr\\u00f3pria (in\\u00e9rcia dos flutuantes)\\n        col: hex2rgb(cols[(rng() * cols.length) | 0]), \/\/ cor atual (tinge com o tempo)\\n        colBase: null, \/\/ cor original da paleta \\u2014 \\u00e2ncora do tingimento (set abaixo)\\n        seed: rng(), \/\/ forma \\u00fanica da pedra (wobble por ponto de controle no shader)\\n        pedra\\n      });\\n      const last = obstacles[obstacles.length - 1];\\n      last.colBase = last.col.slice();\\n    }\\n  }\\n  syncObstacleArrays();\\n}\\n\\nfunction syncObstacleArrays() {\\n  obsA.fill(0); obsB.fill(0); obsC.fill(0);\\n  obstacles.forEach((ob, i) =\\u003e {\\n    if (i \\u003e= MAX_OBS) return;\\n    obsA.set([ob.x, ob.y, ob.r, ob.seed], i * 4);\\n    obsB.set([ob.pedra ? 1 : 0, 0, 0, 0], i * 4);\\n    obsC.set([ob.col[0], ob.col[1], ob.col[2], 0], i * 4);\\n  });\\n}\\n\\n\/\/ Elementos vivos: movimento (flutuam) + tingimento pela tinta ao redor.\\n\/\/ Velocidade e tinta s\\u00e3o lidas da GPU a cada 2 frames (texturas pequenas,\\n\/\/ res da simula\\u00e7\\u00e3o).\\nlet velBuf = null, dyeBuf = null, velReadOK = true, frameCount = 0;\\n\\n\/\/ Cobertura global: fra\\u00e7\\u00e3o do quadro com tinta encorpada. Usada para manter\\n\/\/ \\u00e1rea vazia \\u2014 sem ela a tinta fecha a composi\\u00e7\\u00e3o e mata a leitura de\\n\/\/ profundidade (e esconde a caixa).\\nlet coverage = 0, dbgFade = 0, showDebug = false;\\n\\nfunction computeCoverage() {\\n  const bg = hex2rgb(S.bg);\\n  let sum = 0, n = 0;\\n  for (let i = 0; i \\u003c dyeBuf.length; i += 64) { \/\/ amostra 1 a cada 16 texels\\n    if (dyeBuf[i + 3] \/ 255 \\u003e 0.09) sum++;      \/\/ OPACIDADE (alfa) = tinta presente, mesmo se escura\\n    n++;\\n  }\\n  coverage = n ? sum \/ n : 0;\\n}\\n\\n\/\/ alvo de cobertura: com caixa, mais respiro para ler a profundidade\\nfunction paintBudget() {\\n  return 1 - Math.min(1, Math.max(0, (coverage - 0.60) \/ 0.18)); \/\/ teto MAIS ALTO \\u2192 tela mais cheia\\n}\\n\\nfunction updateObstacles(dt) {\\n  if (!velReadOK) return;\\n  if ((frameCount \\u0026 1) === 0) {\\n    \/\/ TINTA + COBERTURA (cr\\u00edtico p\/ o regulador) \\u2014 leitura RGBA8\/UNSIGNED_BYTE,\\n    \/\/ que SEMPRE funciona. Ler o buffer 16F como FLOAT falhava em v\\u00e1rios\\n    \/\/ GPUs\/navegadores, o computeCoverage n\\u00e3o rodava e a tinta enchia tudo.\\n    try {\\n      gl.useProgram(progClear.prog);\\n      gl.uniform1i(progClear.uniforms.uTexture, dye.read.attach(0));\\n      gl.uniform1f(progClear.uniforms.value, 1.0);\\n      gl.uniform2f(progClear.uniforms.texelSize, 1 \/ simW, 1 \/ simH);\\n      blit(dyeSmall);\\n      if (!dyeBuf || dyeBuf.length !== simW * simH * 4) dyeBuf = new Uint8Array(simW * simH * 4);\\n      gl.readPixels(0, 0, simW, simH, gl.RGBA, gl.UNSIGNED_BYTE, dyeBuf);\\n      computeCoverage();\\n    } catch (e) { \/* mant\\u00e9m a \\u00faltima cobertura \\u2014 n\\u00e3o trava o regulador *\/ }\\n    \/\/ VELOCIDADE (s\\u00f3 move as bolhas) \\u2014 separada, para uma falha aqui N\\u00c3O\\n    \/\/ bloquear a medi\\u00e7\\u00e3o de cobertura acima\\n    if (obstacles.length) {\\n      try {\\n        gl.bindFramebuffer(gl.FRAMEBUFFER, velocity.read.fbo);\\n        if (!velBuf || velBuf.length !== simW * simH * 4) velBuf = new Float32Array(simW * simH * 4);\\n        gl.readPixels(0, 0, simW, simH, gl.RGBA, gl.FLOAT, velBuf);\\n      } catch (e) { \/* bolhas ficam sem deriva neste frame *\/ }\\n    }\\n  }\\n  if (!obstacles.length || !velBuf || !dyeBuf) return;\\n  const aspect = canvas.width \/ canvas.height;\\n  const flutua = S.elMode === 'flutuam';\\n  \/\/ Peso: responsividade \\u00e0 correnteza. Micro colam no fluxo; m\\u00e9dios arrastam;\\n  \/\/ grandes rastejam \\u2014 in\\u00e9rcia via relaxa\\u00e7\\u00e3o da velocidade pr\\u00f3pria.\\n  const resp = S.elSize === 'micro' ? 6 : S.elSize === 'medios' ? 1.2 : 0.45;\\n  const idx = (px, py) =\\u003e {\\n    const ix = Math.min(simW - 1, Math.max(0, px * simW | 0));\\n    const iy = Math.min(simH - 1, Math.max(0, py * simH | 0));\\n    return (iy * simW + ix) * 4;\\n  };\\n  for (const ob of obstacles) {\\n    \/\/ amostra num ANEL em volta da forma: dentro dela a m\\u00e1scara zera a\\n    \/\/ velocidade e a tinta \\u00e9 a cor do pr\\u00f3prio elemento\\n    const rr = ob.r * 1.6, rx = rr \/ aspect;\\n    const ks = [idx(ob.x + rx, ob.y), idx(ob.x - rx, ob.y), idx(ob.x, ob.y + rr), idx(ob.x, ob.y - rr)];\\n    let fvx = 0, fvy = 0, dr = 0, dg = 0, db = 0;\\n    for (const k of ks) {\\n      fvx += velBuf[k]; fvy += velBuf[k + 1];\\n      dr += dyeBuf[k] \/ 255; dg += dyeBuf[k + 1] \/ 255; db += dyeBuf[k + 2] \/ 255; \/\/ dyeBuf 0\\u2013255\\n    }\\n    fvx \/= 4; fvy \/= 4; dr \/= 4; dg \/= 4; db \/= 4;\\n    \/\/ Tingimento COM TETO: o elemento absorve a cor da tinta ao redor (mais\\n    \/\/ r\\u00e1pido quanto mais forte o fluxo), mas mistura no M\\u00c1XIMO 55% \\u2014 a \\u00e2ncora\\n    \/\/ na cor original da paleta impede a camuflagem total que fazia os\\n    \/\/ elementos sumirem dentro da tinta.\\n    const spd = Math.hypot(fvx, fvy);\\n    const kt = Math.min(1, (0.10 + spd * 0.06) * dt);\\n    const TM = 0.55;\\n    ob.col[0] += (ob.colBase[0] + (dr - ob.colBase[0]) * TM - ob.col[0]) * kt;\\n    ob.col[1] += (ob.colBase[1] + (dg - ob.colBase[1]) * TM - ob.col[1]) * kt;\\n    ob.col[2] += (ob.colBase[2] + (db - ob.colBase[2]) * TM - ob.col[2]) * kt;\\n    if (flutua) {\\n      const k = Math.min(1, resp * dt);\\n      ob.vx += (fvx - ob.vx) * k;\\n      ob.vy += (fvy - ob.vy) * k;\\n      \/\/ mesma escala da advec\\u00e7\\u00e3o: deslocamento = vel * texel * dt\\n      ob.x += ob.vx \/ simW * dt;\\n      ob.y += ob.vy \/ simH * dt;\\n      const cx = ob.r \/ aspect;\\n      ob.x = Math.min(1 - cx, Math.max(cx, ob.x));\\n      ob.y = Math.min(1 - ob.r, Math.max(ob.r, ob.y));\\n    }\\n  }\\n  syncObstacleArrays();\\n}\\n\\n\/\/ \\u2500\\u2500 Micro elementos como pontos GPU \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nlet ptsBuf = null, ptsData = null;\\n\\nfunction drawMicroPoints() {\\n  if (S.elements === 'nenhum' || S.elSize !== 'micro' || !obstacles.length) return;\\n  const n = obstacles.length;\\n  const STRIDE = 7; \/\/ x, y, tamanho, seed, r, g, b\\n  if (!ptsData || ptsData.length \\u003c n * STRIDE) ptsData = new Float32Array(n * STRIDE);\\n  for (let i = 0; i \\u003c n; i++) {\\n    const ob = obstacles[i], k = i * STRIDE;\\n    ptsData[k] = ob.x; ptsData[k + 1] = ob.y; ptsData[k + 2] = ob.r; ptsData[k + 3] = ob.seed;\\n    ptsData[k + 4] = ob.col[0]; ptsData[k + 5] = ob.col[1]; ptsData[k + 6] = ob.col[2];\\n  }\\n  gl.useProgram(progPts.prog);\\n  gl.uniform1f(progPts.uniforms.uHeight, canvas.height);\\n  gl.uniform1f(progPts.uniforms.uShape, S.elements === 'pedras' ? 1 : 0);\\n  gl.uniform1f(progPts.uniforms.uUniverse, S.elColor === 'universo' ? 1 : 0);\\n  gl.uniform1f(progPts.uniforms.uAspect, canvas.width \/ canvas.height);\\n  gl.uniform1f(progPts.uniforms.uBox, S.frame !== 'none' ? 1 : 0);\\n  gl.uniform1i(progPts.uniforms.uDye, dye.read.attach(0));\\n  \/\/ v17 remapeava os pontos para dentro da abertura \\u2014 prendia tudo no fundo.\\n  \/\/ Agora a arte ocupa o canvas inteiro e o 3D vem do parallax no display.\\n  gl.uniform2f(progPts.uniforms.uLo, 0, 0);\\n  gl.uniform2f(progPts.uniforms.uSpan, 1, 1);\\n  gl.uniform1f(progPts.uniforms.uBulge, 0);\\n  if (!ptsBuf) ptsBuf = gl.createBuffer();\\n  gl.bindBuffer(gl.ARRAY_BUFFER, ptsBuf);\\n  gl.bufferData(gl.ARRAY_BUFFER, ptsData.subarray(0, n * STRIDE), gl.DYNAMIC_DRAW);\\n  const B = STRIDE * 4; \/\/ bytes\\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, B, 0);\\n  gl.enableVertexAttribArray(1);\\n  gl.vertexAttribPointer(1, 1, gl.FLOAT, false, B, 8);\\n  gl.enableVertexAttribArray(2);\\n  gl.vertexAttribPointer(2, 1, gl.FLOAT, false, B, 12);\\n  gl.enableVertexAttribArray(3);\\n  gl.vertexAttribPointer(3, 3, gl.FLOAT, false, B, 16);\\n  gl.bindFramebuffer(gl.FRAMEBUFFER, null);\\n  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\\n  gl.enable(gl.BLEND);\\n  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\\n  gl.drawArrays(gl.POINTS, 0, n);\\n  gl.disable(gl.BLEND);\\n  gl.disableVertexAttribArray(1);\\n  gl.disableVertexAttribArray(2);\\n  gl.disableVertexAttribArray(3);\\n  \/\/ restaura o quad \\u2014 os blits da simula\\u00e7\\u00e3o dependem dele\\n  gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);\\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\\n}\\n\\n\/\/ Aplica a m\\u00e1scara dos obst\\u00e1culos a um campo.\\n\/\/ useCol 0: preenche com fill (parede de velocidade = zero)\\n\/\/ useCol 1: preenche com a COR de cada elemento (tinta) \\u2014 elementos pintados\\nfunction maskPass(target, fill, useCol) {\\n  if (!obstacles.length) return;\\n  gl.useProgram(progMask.prog);\\n  gl.uniform2f(progMask.uniforms.texelSize, target.texelSizeX, target.texelSizeY);\\n  gl.uniform1i(progMask.uniforms.uSource, target.read.attach(0));\\n  gl.uniform1f(progMask.uniforms.uAspect, canvas.width \/ canvas.height);\\n  gl.uniform3f(progMask.uniforms.uFill, fill[0], fill[1], fill[2]);\\n  gl.uniform1f(progMask.uniforms.uUseCol, useCol || 0); \/\/ 0 | 1 | 2 (universo)\\n  gl.uniform1i(progMask.uniforms.uCount, Math.min(obstacles.length, MAX_OBS));\\n  gl.uniform4fv(progMask.uniforms['uObsA[0]'], obsA);\\n  gl.uniform4fv(progMask.uniforms['uObsB[0]'], obsB);\\n  gl.uniform4fv(progMask.uniforms['uObsC[0]'], obsC);\\n  blit(target.write);\\n  target.swap();\\n}\\n\\n\/\/ quad\\nlet quadBuf = null;\\nfunction initQuad() {\\n  const buf = quadBuf = gl.createBuffer();\\n  gl.bindBuffer(gl.ARRAY_BUFFER, buf);\\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, -1,1, 1,1, 1,-1]), gl.STATIC_DRAW);\\n  const idx = gl.createBuffer();\\n  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, idx);\\n  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0,1,2, 0,2,3]), gl.STATIC_DRAW);\\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\\n  gl.enableVertexAttribArray(0);\\n}\\n\\nfunction blit(target) {\\n  if (target == null) {\\n    gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\\n    gl.bindFramebuffer(gl.FRAMEBUFFER, null);\\n  } else {\\n    gl.viewport(0, 0, target.width, target.height);\\n    gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);\\n  }\\n  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);\\n}\\n\\n\/\/ \\u2500\\u2500 Framebuffers \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nfunction createFBO(w, h, internalFormat, format, type, filter) {\\n  const texture = gl.createTexture();\\n  gl.activeTexture(gl.TEXTURE0);\\n  gl.bindTexture(gl.TEXTURE_2D, texture);\\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);\\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);\\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\\n  gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);\\n  const fbo = gl.createFramebuffer();\\n  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);\\n  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);\\n  gl.viewport(0, 0, w, h);\\n  gl.clearColor(0, 0, 0, 1);\\n  gl.clear(gl.COLOR_BUFFER_BIT);\\n  return {\\n    texture, fbo, width: w, height: h,\\n    texelSizeX: 1 \/ w, texelSizeY: 1 \/ h,\\n    attach(id) {\\n      gl.activeTexture(gl.TEXTURE0 + id);\\n      gl.bindTexture(gl.TEXTURE_2D, texture);\\n      return id;\\n    }\\n  };\\n}\\n\\nfunction createDoubleFBO(w, h, internalFormat, format, type, filter) {\\n  let fbo1 = createFBO(w, h, internalFormat, format, type, filter);\\n  let fbo2 = createFBO(w, h, internalFormat, format, type, filter);\\n  return {\\n    width: w, height: h,\\n    texelSizeX: 1 \/ w, texelSizeY: 1 \/ h,\\n    get read()  { return fbo1; },\\n    get write() { return fbo2; },\\n    swap() { const t = fbo1; fbo1 = fbo2; fbo2 = t; }\\n  };\\n}\\n\\nlet dye, velocity, divergence, curlFBO, pressure, dyeSmall, dyeTmp;\\nlet simW, simH, dyeW, dyeH;\\n\\nfunction getResolution(resolution) {\\n  let aspect = canvas.width \/ canvas.height;\\n  if (aspect \\u003c 1) aspect = 1 \/ aspect;\\n  const min = Math.round(resolution);\\n  const max = Math.round(resolution * aspect);\\n  if (canvas.width \\u003e canvas.height) return { w: max, h: min };\\n  return { w: min, h: max };\\n}\\n\\nfunction initFramebuffers() {\\n  const sim = getResolution(SIM_RES);\\n  const dyeR = getResolution(DYE_RES);\\n  simW = sim.w; simH = sim.h;\\n  dyeW = dyeR.w; dyeH = dyeR.h;\\n  const halfFloat = gl.HALF_FLOAT;\\n  dye        = createDoubleFBO(dyeW, dyeH, gl.RGBA16F, gl.RGBA, halfFloat, gl.LINEAR);\\n  velocity   = createDoubleFBO(simW, simH, gl.RG16F,   gl.RG,   halfFloat, gl.LINEAR);\\n  divergence = createFBO(simW, simH, gl.R16F, gl.RED, halfFloat, gl.NEAREST);\\n  curlFBO    = createFBO(simW, simH, gl.R16F, gl.RED, halfFloat, gl.NEAREST);\\n  pressure   = createDoubleFBO(simW, simH, gl.R16F, gl.RED, halfFloat, gl.NEAREST);\\n  dyeSmall   = createFBO(simW, simH, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, gl.LINEAR); \/\/ c\\u00f3pia p\/ leitura CPU \\u2014 RGBA8 l\\u00ea sempre (16F+FLOAT falhava e travava o regulador)\\n  dyeTmp     = createFBO(dyeW, dyeH, gl.RGBA16F, gl.RGBA, halfFloat, gl.LINEAR); \/\/ buffer p\/ o pass forward do MacCormack\\n  fillDye('#000000'); \/\/ a tela nasce SEM TINTA (preto) \\u2014 o fundo \\u00e9 composto no display\\n  seedOpening();      \/\/ semeia um punhado de gotas j\\u00e1 no in\\u00edcio \\u2192 movimento vis\\u00edvel na hora\\n}\\n\\n\/\/ ABERTURA: um \\u00daNICO ponto de tinta no in\\u00edcio, s\\u00f3 p\/ o campo ter o que mover de\\n\/\/ cara (sem isso, entradas lentas como pour\/strokes deixam a tela parada). Um\\n\/\/ ponto s\\u00f3 l\\u00ea melhor que um punhado de gotas \\u2014 a entrada escolhida assume logo.\\nfunction seedOpening() {\\n  pendingEmits.length = 0;\\n  const force = 100 * Math.pow(S.speed \/ 100, 2.5) * 40;\\n  pendingEmits.push({ t: simTime, emit: () =\\u003e emitDrop(force) });\\n  dropNext = simTime + 0.3; \/\/ a entrada escolhida assume logo depois da abertura\\n}\\n\\nfunction fillDye(hex) {\\n  const c = hex2rgb(hex);\\n  gl.useProgram(progFill.prog);\\n  gl.uniform3f(progFill.uniforms.color, c[0], c[1], c[2]);\\n  blit(dye.write);\\n  dye.swap();\\n  gl.uniform3f(progFill.uniforms.color, c[0], c[1], c[2]);\\n  blit(dye.write);\\n  dye.swap();\\n}\\n\\n\/\/ \\u2500\\u2500 Emissores aut\\u00f4nomos \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\/\/ A \\\"onda\\\" nasce daqui: agentes invis\\u00edveis percorrem trajet\\u00f3rias de Lissajous\\n\/\/ perturbadas (frequ\\u00eancias sorteadas pelo seed) injetando velocidade + tinta.\\n\/\/ Somado \\u00e0 mar\\u00e9 (for\\u00e7a horizontal lenta oscilante), o fluido dobra sobre si\\n\/\/ mesmo continuamente \\u2014 nunca repete, nunca para.\\nconst N_EMITTERS = 4;\\nlet emitters = [];\\nlet waveStart = 0; \/\/ simTime em que a onda atual nasceu (refer\\u00eancia da entrada)\\n\\n\/\/ stagger=true re-sorteia tamb\\u00e9m a ordem de ENTRADA das tintas (Nova onda);\\n\/\/ stagger=false rebuilda sem re-escalonar (troca de paleta\/movimento ao vivo).\\nfunction buildEmitters(stagger = true) {\\n  const rng = mkRng(S.seed);\\n  emitters = [];\\n  const cols = activeCols();\\n  for (let i = 0; i \\u003c N_EMITTERS; i++) {\\n    emitters.push({\\n      \/\/ Lissajous (modo deriva) \\u2014 frequ\\u00eancias ~metade das originais\\n      ax: 0.12 + rng() * 0.24, bx: 0.08 + rng() * 0.18,\\n      ay: 0.11 + rng() * 0.22, by: 0.06 + rng() * 0.16,\\n      px: rng() * Math.PI * 2, qx: rng() * Math.PI * 2,\\n      py: rng() * Math.PI * 2, qy: rng() * Math.PI * 2,\\n      \/\/ serpentina \/ espiral\\n      f1: 0.2 + rng() * 0.3, f2: 0.1 + rng() * 0.2,\\n      p1: rng() * Math.PI * 2, p2: rng() * Math.PI * 2,\\n      angle: rng() * Math.PI * 2,\\n      spin: rng() \\u003c 0.5 ? -1 : 1,\\n      x: 0.3 + rng() * 0.4, y: 0.3 + rng() * 0.4,\\n      colorOffset: (rng() * cols.length) | 0,\\n      colorPhase: rng() * 20,\\n      \/\/ Envelope de ritmo: cada emissor tem seu pr\\u00f3prio ciclo lento de\\n      \/\/ acelera\/desacelera (per\\u00edodo ~8\\u201320s). O rel\\u00f3gio do emissor avan\\u00e7a em\\n      \/\/ ritmo vari\\u00e1vel \\u2014 come\\u00e7a devagar, pega embalo, desacelera de novo.\\n      envF: 0.3 + rng() * 0.5,\\n      envP: rng() * Math.PI * 2,\\n      \/\/ pulso de deposi\\u00e7\\u00e3o de tinta: cada emissor \\\"sopra\\\" tinta em ciclos\\n      \/\/ pr\\u00f3prios em vez de pintar continuamente (o que denunciava o trajeto)\\n      pf: 0.5 + rng() * 0.6,\\n      pp: rng() * Math.PI * 2,\\n      clock: rng() * 100,\\n      \/\/ Entrada sequencial: cada tinta espera sua vez (intervalos sorteados)\\n      \/\/ e surge com fade de ~3s. Em 'juntas', todas come\\u00e7am j\\u00e1 (startAt 0).\\n      startAt: S.entry === 'seq' \\u0026\\u0026 stagger ? i * (5 + rng() * 4) : 0,\\n      prevX: 0.5, prevY: 0.5, started: false\\n    });\\n  }\\n  if (stagger) waveStart = simTime;\\n  \/\/ mar\\u00e9\\n  tide = { phase: rng() * Math.PI * 2, freq: 0.05 + rng() * 0.05 };\\n}\\nlet tide = { phase: 0, freq: 0.07 };\\n\\n\/\/ \\u2500\\u2500 Modos de movimento \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\/\/ deriva: Lissajous perturbada (o original, desacelerado)\\n\/\/ serpentina: caminho errante \\u2014 a dire\\u00e7\\u00e3o vagueia por soma de senoides e o\\n\/\/   emissor \\u00e9 gentilmente conduzido de volta quando se afasta do centro\\n\/\/ espiral: \\u00f3rbita em torno de um centro que passeia; raio e giro respiram\\nfunction motionStep(e, dt, env) {\\n  if (S.motion === 'queda') {\\n    \/\/ Despeja do alto: o emissor s\\u00f3 passeia horizontalmente perto do topo,\\n    \/\/ soltando tinta. Quem movimenta \\u00e9 a GRAVIDADE \\u2014 a tinta cai, bate\\n    \/\/ embaixo, abre para os lados e sobe pelas paredes.\\n    e.x = 0.5 + 0.36 * Math.sin(e.clock * e.ax * 1.5 + e.px)\\n              + 0.04 * Math.sin(e.clock * e.f1 * 3.0 + e.p1);\\n    e.y = 0.86 + 0.05 * Math.sin(e.clock * e.f2 + e.p2);\\n    return [e.x, e.y];\\n  }\\n  if (S.motion === 'serpentina') {\\n    const wob = 2.0 * Math.sin(e.clock * e.f1 + e.p1) + 1.2 * Math.sin(e.clock * e.f2 + e.p2);\\n    e.angle += wob * dt * env;\\n    const dx0 = 0.5 - e.x, dy0 = 0.5 - e.y;\\n    const d = Math.hypot(dx0, dy0);\\n    if (d \\u003e 0.32) {\\n      const target = Math.atan2(dy0, dx0);\\n      let diff = Math.atan2(Math.sin(target - e.angle), Math.cos(target - e.angle));\\n      e.angle += diff * Math.min(1, (d - 0.32) * 8) * 2.5 * dt;\\n    }\\n    const sp = 0.05;\\n    e.x += Math.cos(e.angle) * sp * dt * env * 3;\\n    e.y += Math.sin(e.angle) * sp * dt * env * 3;\\n    return [e.x, e.y];\\n  }\\n  if (S.motion === 'espiral') {\\n    const cx = 0.5 + 0.16 * Math.sin(e.clock * e.ax * 2.0 + e.px);\\n    const cy = 0.5 + 0.16 * Math.sin(e.clock * e.ay * 2.0 + e.py);\\n    e.angle += (0.25 + 0.15 * Math.sin(e.clock * e.f1 + e.p1)) * dt * env * e.spin * 2.5;\\n    const r = 0.15 + 0.11 * Math.sin(e.clock * e.f2 + e.p2);\\n    e.x = cx + r * Math.cos(e.angle);\\n    e.y = cy + r * Math.sin(e.angle);\\n    return [e.x, e.y];\\n  }\\n  \/\/ deriva\\n  const [x, y] = emitterPos(e, e.clock);\\n  e.x = x; e.y = y;\\n  return [x, y];\\n}\\n\\nfunction emitterPos(e, t) {\\n  const x = 0.5 + 0.36 * Math.sin(t * e.ax + e.px) * Math.cos(t * e.bx + e.qx);\\n  const y = 0.5 + 0.36 * Math.sin(t * e.ay + e.py) * Math.cos(t * e.by + e.qy);\\n  return [x, y];\\n}\\n\\n\/\/ cor do emissor: cruza suavemente entre cores consecutivas da paleta\\nfunction emitterColor(e, t) {\\n  const cols = activeCols();\\n  const period = 4.5; \/\/ segundos por cor \\u2014 mais curto = cores se misturam mais\\n  const ft = (t + e.colorPhase) \/ period;\\n  const i0 = (e.colorOffset + Math.floor(ft)) % cols.length;\\n  const i1 = (i0 + 1) % cols.length;\\n  const f = ft - Math.floor(ft);\\n  const c0 = hex2rgb(cols[i0]), c1 = hex2rgb(cols[i1]);\\n  return [\\n    c0[0] + (c1[0] - c0[0]) * f,\\n    c0[1] + (c1[1] - c0[1]) * f,\\n    c0[2] + (c1[2] - c0[2]) * f\\n  ];\\n}\\n\\n\/\/ \\u2500\\u2500 Splat \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\/\/ paintStrength 0 = s\\u00f3 empurra o fluido, n\\u00e3o deposita tinta (usado pela mar\\u00e9)\\nfunction splat(x, y, dx, dy, color, radius, paintStrength, edge = 0) {\\n  gl.useProgram(progSplat.prog);\\n  gl.uniform1i(progSplat.uniforms.uTarget, velocity.read.attach(0));\\n  gl.uniform1f(progSplat.uniforms.aspectRatio, canvas.width \/ canvas.height);\\n  gl.uniform2f(progSplat.uniforms.point, x, y);\\n  gl.uniform3f(progSplat.uniforms.color, dx, dy, 0.0);\\n  gl.uniform1f(progSplat.uniforms.radius, radius \/ 100.0);\\n  gl.uniform1f(progSplat.uniforms.uMode, 0.0);\\n  gl.uniform1f(progSplat.uniforms.uStrength, 0.0);\\n  gl.uniform1f(progSplat.uniforms.uEdge, 0.0);\\n  gl.uniform2f(progSplat.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  blit(velocity.write);\\n  velocity.swap();\\n\\n  if (paintStrength \\u003e 0) {\\n    gl.uniform1i(progSplat.uniforms.uTarget, dye.read.attach(0));\\n    gl.uniform3f(progSplat.uniforms.color, color[0], color[1], color[2]);\\n    gl.uniform1f(progSplat.uniforms.uMode, 1.0);\\n    gl.uniform1f(progSplat.uniforms.uStrength, paintStrength);\\n    gl.uniform1f(progSplat.uniforms.uEdge, edge);\\n    gl.uniform1f(progSplat.uniforms.radius, radius * 0.6 \/ 100.0);\\n    gl.uniform2f(progSplat.uniforms.texelSize, dye.texelSizeX, dye.texelSizeY);\\n    blit(dye.write);\\n    dye.swap();\\n  }\\n}\\n\\n\/\/ RIPPLE: injeta uma velocidade RADIAL (para fora) no ponto de impacto da gota \\u2014\\n\/\/ empurra o fluido em anel, criando a onda que se expande de onde a gota cai.\\nfunction rippleImpulse(x, y, strength) {\\n  gl.useProgram(progSplat.prog);\\n  gl.uniform1i(progSplat.uniforms.uTarget, velocity.read.attach(0));\\n  gl.uniform1f(progSplat.uniforms.aspectRatio, canvas.width \/ canvas.height);\\n  gl.uniform2f(progSplat.uniforms.point, x, y);\\n  gl.uniform1f(progSplat.uniforms.radius, 0.8 \/ 100.0);\\n  gl.uniform1f(progSplat.uniforms.uMode, 2.0);   \/\/ radial\\n  gl.uniform1f(progSplat.uniforms.uStrength, strength);\\n  gl.uniform2f(progSplat.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  blit(velocity.write);\\n  velocity.swap();\\n}\\n\\n\/\/ \\u2500\\u2500 Passo da simula\\u00e7\\u00e3o \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nfunction step(dt) {\\n  gl.disable(gl.BLEND);\\n\\n  \/\/ curl\\n  gl.useProgram(progCurl.prog);\\n  gl.uniform2f(progCurl.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  gl.uniform1i(progCurl.uniforms.uVelocity, velocity.read.attach(0));\\n  blit(curlFBO);\\n\\n  \/\/ vorticity confinement\\n  gl.useProgram(progVorticity.prog);\\n  gl.uniform2f(progVorticity.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  gl.uniform1i(progVorticity.uniforms.uVelocity, velocity.read.attach(0));\\n  gl.uniform1i(progVorticity.uniforms.uCurl, curlFBO.attach(1));\\n  gl.uniform1f(progVorticity.uniforms.curl, S.curl);\\n  gl.uniform1f(progVorticity.uniforms.dt, dt);\\n  blit(velocity.write);\\n  velocity.swap();\\n\\n  \/\/ turbul\\u00eancia fractal (curl noise fBm) \\u2014 Turbul\\u00eancia controla a for\\u00e7a\\n  gl.useProgram(progNoise.prog);\\n  gl.uniform2f(progNoise.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  gl.uniform1i(progNoise.uniforms.uVelocity, velocity.read.attach(0));\\n  gl.uniform1f(progNoise.uniforms.uTime, simTime);\\n  \/\/ for\\u00e7a do noise acompanha a Velocidade (mesma curva pot. 2.5 do slider).\\n  \/\/ E recua quando a gravidade sobe: turbul\\u00eancia isotr\\u00f3pica embaralha a\\n  \/\/ dire\\u00e7\\u00e3o da queda (era o \\\"nadar contra a corrente\\\").\\n  \/\/ Fall: turbul\\u00eancia BAIXA e FIXA (n\\u00e3o escala com a velocidade) \\u2192 as gotas\\n  \/\/ descem em trilhas coerentes sem chacoalhar, mesmo com Speed alto.\\n  if (S.motion === 'queda') {\\n    gl.uniform1f(progNoise.uniforms.uStrength, S.curl * 0.03);\\n  } else {\\n    gl.uniform1f(progNoise.uniforms.uStrength,\\n      S.curl * 0.12 * (0.25 + 0.75 * Math.pow(S.speed \/ 100, 2.5)) * (1 - (S.gravity \/ 100) * 0.65));\\n  }\\n  gl.uniform1f(progNoise.uniforms.uAspect, canvas.width \/ canvas.height);\\n  gl.uniform1f(progNoise.uniforms.dt, dt);\\n  blit(velocity.write);\\n  velocity.swap();\\n\\n  \/\/ gravidade do fluido: a tinta densa afunda (\\u00e9 assim que a gota desce dentro\\n  \/\/ do l\\u00edquido). No Fall fica mais forte para escorrer.\\n  if (S.gravity \\u003e 0) {\\n    const bgg = hex2rgb(S.bg);\\n    gl.useProgram(progGravity.prog);\\n    gl.uniform2f(progGravity.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n    gl.uniform1i(progGravity.uniforms.uVelocity, velocity.read.attach(0));\\n    gl.uniform1i(progGravity.uniforms.uDye, dye.read.attach(1));\\n    gl.uniform3f(progGravity.uniforms.uRest, 0, 0, 0); \/\/ densidade = quantidade de tinta (dist. do preto)\\n    gl.uniform1f(progGravity.uniforms.uG, S.gravity * (S.motion === 'queda' ? 1.4 : 1.0)); \/\/ Fall mais forte (drip)\\n    gl.uniform1f(progGravity.uniforms.dt, dt);\\n    blit(velocity.write);\\n    velocity.swap();\\n  }\\n\\n  \/\/ obst\\u00e1culos: parede s\\u00f3lida (velocidade zero) ANTES da proje\\u00e7\\u00e3o de press\\u00e3o,\\n  \/\/ para o solver desviar o fluxo em volta das formas. Micro elementos N\\u00c3O\\n  \/\/ bloqueiam o fluxo (sen\\u00e3o dezenas deles viram freio) \\u2014 s\\u00f3 flutuam nele.\\n  if (S.elSize !== 'micro') maskPass(velocity, [0, 0, 0], 0);\\n\\n  \/\/ divergence\\n  gl.useProgram(progDivergence.prog);\\n  gl.uniform2f(progDivergence.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  gl.uniform1i(progDivergence.uniforms.uVelocity, velocity.read.attach(0));\\n  blit(divergence);\\n\\n  \/\/ pressure: clear + jacobi\\n  gl.useProgram(progClear.prog);\\n  gl.uniform1i(progClear.uniforms.uTexture, pressure.read.attach(0));\\n  gl.uniform1f(progClear.uniforms.value, 0.8);\\n  gl.uniform2f(progClear.uniforms.texelSize, pressure.texelSizeX, pressure.texelSizeY);\\n  blit(pressure.write);\\n  pressure.swap();\\n\\n  gl.useProgram(progPressure.prog);\\n  gl.uniform2f(progPressure.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  gl.uniform1i(progPressure.uniforms.uDivergence, divergence.attach(0));\\n  for (let i = 0; i \\u003c PRESSURE_ITERS; i++) {\\n    gl.uniform1i(progPressure.uniforms.uPressure, pressure.read.attach(1));\\n    blit(pressure.write);\\n    pressure.swap();\\n  }\\n\\n  \/\/ gradient subtract\\n  gl.useProgram(progGradient.prog);\\n  gl.uniform2f(progGradient.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  gl.uniform1i(progGradient.uniforms.uPressure, pressure.read.attach(0));\\n  gl.uniform1i(progGradient.uniforms.uVelocity, velocity.read.attach(1));\\n  blit(velocity.write);\\n  velocity.swap();\\n\\n  \/\/ advect velocity \\u2014 repousa em zero\\n  gl.useProgram(progAdvect.prog);\\n  gl.uniform2f(progAdvect.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  gl.uniform1i(progAdvect.uniforms.uVelocity, velocity.read.attach(0));\\n  gl.uniform1i(progAdvect.uniforms.uSource, velocity.read.attach(0));\\n  gl.uniform1f(progAdvect.uniforms.dt, dt);\\n  gl.uniform1f(progAdvect.uniforms.dissipation, 0.18); \/\/ 0.12 \\u2192 0.18: assenta mais r\\u00e1pido\\n  gl.uniform3f(progAdvect.uniforms.uRest, 0, 0, 0);\\n  gl.uniform1f(progAdvect.uniforms.uSat, 0.0);\\n  gl.uniform1f(progAdvect.uniforms.uFlowMode, 0.0); \/\/ velocidade N\\u00c3O leva campo de fluxo\\n  blit(velocity.write);\\n  velocity.swap();\\n\\n  \/\/ advect dye \\u2014 a tinta \\\"seca\\\" de volta para a COR DO FUNDO (mistura\\n  \/\/ harm\\u00f4nica). \\\"Densidade\\\" controla a secagem. Acima do alvo, a secagem\\n  \/\/ acelera e o anti-lama recua, para a tinta poder secar de volta ao fundo em\\n  \/\/ vez de travar numa cor s\\u00f3 (o bug de \\\"estacionar numa cor\\\").\\n  \/\/ alvo BAIXO: segura regi\\u00f5es de cor distintas sobre o fundo, sem homogeneizar\\n  \/\/ numa cor s\\u00f3. Secagem GENTIL para a tinta que est\\u00e1 l\\u00e1 ficar viva (n\\u00e3o p\\u00e1lida).\\n  const covTarget = 0.60;   \/\/ alvo MAIS ALTO \\u2192 tela mais cheia (era 0.42)\\n  const overage = Math.min(1, Math.max(0, coverage - covTarget) \/ (1 - covTarget));\\n  \/\/ Secagem base MENOR \\u2192 a tinta permanece mais (tela menos vazia), mas ainda\\n  \/\/ limpa a lama velha quando passa do alvo (overage) e tem teto de seguran\\u00e7a.\\n  let fade = 0.17 + (1 - S.density \/ 100) * 0.42 + overage * 0.55;\\n  if (coverage \\u003e 0.82) fade += (coverage - 0.82) * 4.0; \/\/ teto de SEGURAN\\u00c7A (s\\u00f3 se disparar)\\n  dbgFade = fade;\\n  const bgc = hex2rgb(S.bg);\\n  \/\/ \\u2500\\u2500 CAMPO DE FLUXO por movimento (s\\u00f3 na advec\\u00e7\\u00e3o da tinta): d\\u00e1 o car\\u00e1ter\\n  \/\/    distinto de Drift\/Meander\/Spiral. Calm e Fall n\\u00e3o usam (Fall = gravidade). \\u2500\\u2500\\n  let flowMode = 0;\\n  if (S.motion === 'serpentina') flowMode = 2;\\n  else if (S.motion === 'espiral') flowMode = 3;\\n  else if (S.motion === 'ripple') flowMode = 4;\\n  else if (S.motion === 'onda') flowMode = 5;\\n  \/\/ queda(Fall) n\\u00e3o usa campo de fluxo aqui (\\u00e9 gravidade)\\n  const flowStr = (S.speed \/ 100) * 85;           \/\/ escala do ZERO com o Speed (baixo = calmo)\\n  const wanderAmt = S.wander \/ 100;                \/\/ peso do campo de ru\\u00eddo (color-wander)\\n  const flowA = simTime * 0.08;                   \/\/ dire\\u00e7\\u00e3o do Drift gira devagar\\n  const flowDir = [Math.cos(flowA), Math.sin(flowA) * 0.5];\\n  const flowAspect = canvas.width \/ canvas.height;\\n  \/\/ Spiral: o RALO passeia pela tela (Lissajous lento) \\u2014 n\\u00e3o fica fixo no centro\\n  const spiralCx = 0.5 + 0.30 * Math.sin(simTime * 0.11);\\n  const spiralCy = 0.5 + 0.24 * Math.cos(simTime * 0.083);\\n  \/\/ Ripple: an\\u00e9is ativos \\u2192 16 floats*4, preenchidos com [x, y, idade, tamanho]\\n  const rippleArr = new Float32Array(64);\\n  let rippleN = 0;\\n  if (flowMode === 4 \\u0026\\u0026 ripples.length) {\\n    const rp = ripples.slice(-16);\\n    rippleN = rp.length;\\n    for (let i = 0; i \\u003c rp.length; i++) {\\n      rippleArr[i * 4] = rp[i][0]; rippleArr[i * 4 + 1] = rp[i][1];\\n      rippleArr[i * 4 + 2] = simTime - rp[i][2]; rippleArr[i * 4 + 3] = rp[i][3];\\n    }\\n  }\\n  const setFlow = (p) =\\u003e {\\n    gl.uniform1f(p.uniforms.uFlowMode, flowMode);\\n    gl.uniform1f(p.uniforms.uFlowStr, flowStr);\\n    gl.uniform1f(p.uniforms.uFlowTime, simTime);\\n    gl.uniform1f(p.uniforms.uFlowAspect, flowAspect);\\n    gl.uniform2f(p.uniforms.uFlowDir, flowDir[0], flowDir[1]);\\n    gl.uniform2f(p.uniforms.uFlowCenter, spiralCx, spiralCy);\\n    gl.uniform1i(p.uniforms.uRippleN, rippleN);\\n    \/\/ uniforms de ARRAY s\\u00e3o indexados como 'uRipples[0]' (n\\u00e3o 'uRipples')\\n    gl.uniform4fv(p.uniforms['uRipples[0]'] || p.uniforms.uRipples, rippleArr);\\n    gl.uniform1f(p.uniforms.uWander, wanderAmt); \/\/ campo de ru\\u00eddo estilo color-wander\\n    gl.uniform1f(p.uniforms.uFlowSeed, (S.seed % 997) \/ 997 * 6.283); \/\/ vi\\u00e9s aleat\\u00f3rio por sess\\u00e3o\\n    gl.uniform1f(p.uniforms.uWaveSpd, 0.25 + (S.speed \/ 100) * 0.55);  \/\/ frequ\\u00eancia do vai-e-vem \\u221d Speed (bounce lento)\\n  };\\n  \/\/ \\u2500\\u2500 ADVEC\\u00c7\\u00c3O MacCormack (2 passes), baixa difus\\u00e3o: as cores se mant\\u00eam\\n  \/\/    distintas por muito mais tempo (n\\u00e3o fundem numa m\\u00e9dia s\\u00f3). \\u2500\\u2500\\n  \/\/ PASS 1 \\u2014 advec\\u00e7\\u00e3o simples de phi^n \\u2192 dyeTmp (sem dissipa\\u00e7\\u00e3o\/anti-lama)\\n  gl.useProgram(progAdvect.prog);\\n  gl.uniform2f(progAdvect.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  gl.uniform1f(progAdvect.uniforms.dt, dt);\\n  gl.uniform1i(progAdvect.uniforms.uVelocity, velocity.read.attach(0));\\n  gl.uniform1i(progAdvect.uniforms.uSource, dye.read.attach(1));\\n  gl.uniform1f(progAdvect.uniforms.dissipation, 0.0);\\n  gl.uniform3f(progAdvect.uniforms.uRest, 0, 0, 0);\\n  gl.uniform1f(progAdvect.uniforms.uSat, 0.0);\\n  setFlow(progAdvect);\\n  blit(dyeTmp);\\n  \/\/ PASS 2 \\u2014 corre\\u00e7\\u00e3o MacCormack + limitador + dissipa\\u00e7\\u00e3o + anti-lama \\u2192 dye.write\\n  gl.useProgram(progAdvectMC.prog);\\n  gl.uniform2f(progAdvectMC.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);\\n  gl.uniform1f(progAdvectMC.uniforms.dt, dt);\\n  gl.uniform1i(progAdvectMC.uniforms.uVelocity, velocity.read.attach(0));\\n  gl.uniform1i(progAdvectMC.uniforms.uSource, dye.read.attach(1));\\n  gl.uniform1i(progAdvectMC.uniforms.uForward, dyeTmp.attach(2));\\n  gl.uniform1f(progAdvectMC.uniforms.dissipation, fade);\\n  gl.uniform3f(progAdvectMC.uniforms.uRest, 0, 0, 0); \/\/ tinta seca para SEM TINTA \\u2192 revela o fundo\\n  gl.uniform1f(progAdvectMC.uniforms.uPalChroma, paletteChroma()); \/\/ refer\\u00eancia p\/ detectar lama\\n  \/\/ anti-lama BAIXO agora \\u2014 a secagem seletiva j\\u00e1 limpa a lama; muito anti-lama\\n  \/\/ re-saturaria a lama e atrapalharia essa detec\\u00e7\\u00e3o.\\n  gl.uniform1f(progAdvectMC.uniforms.uSat, 0.06 * (1 - overage));\\n  setFlow(progAdvectMC);\\n  gl.uniform2f(progAdvectMC.uniforms.uDyeTexel, 1 \/ dyeW, 1 \/ dyeH);\\n  blit(dye.write);\\n  dye.swap();\\n\\n  \/\/ obst\\u00e1culos: dentro das formas a tinta \\u00e9 sempre a cor do fundo \\u2014 a\\n  \/\/ silhueta aparece desenhada pela tinta que contorna. Micro n\\u00e3o usam\\n  \/\/ m\\u00e1scara nenhuma: viram pontos GPU desenhados por cima (drawMicroPoints).\\n  if (S.elSize !== 'micro') {\\n    maskPass(velocity, [0, 0, 0], 0);\\n    \/\/ tinta dentro do elemento: 1 = cor pr\\u00f3pria (Tint), 2 = universo (fluido dentro)\\n    maskPass(dye, bgc, S.elColor === 'universo' ? 2 : 1);\\n  }\\n}\\n\\n\/\/ \\u2500\\u2500 Render \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nfunction render() {\\n  gl.useProgram(progDisplay.prog);\\n  gl.uniform2f(progDisplay.uniforms.texelSize, 1 \/ dyeW, 1 \/ dyeH);\\n  gl.uniform1i(progDisplay.uniforms.uTexture, dye.read.attach(0));\\n  gl.uniform1f(progDisplay.uniforms.shading, S.shade \/ 100);\\n  gl.uniform1f(progDisplay.uniforms.uGlass, S.frame !== 'none' ? 1 : 0);\\n  gl.uniform1f(progDisplay.uniforms.uAspect, canvas.width \/ canvas.height);\\n  const bgd = hex2rgb(S.bg);\\n  gl.uniform3f(progDisplay.uniforms.uBg, bgd[0], bgd[1], bgd[2]);\\n  if (S.frame === 'black') gl.uniform3f(progDisplay.uniforms.uWallC, 0.08, 0.08, 0.085);\\n  else gl.uniform3f(progDisplay.uniforms.uWallC, 0.94, 0.93, 0.91);\\n  \/\/ As ondas do Ripple s\\u00e3o aplicadas como CAMPO DE FLUXO na advec\\u00e7\\u00e3o (empurram a\\n  \/\/ tinta em an\\u00e9is) \\u2014 a refra\\u00e7\\u00e3o no display fica desligada. Aqui s\\u00f3 limpo as antigas.\\n  if (ripples.length) ripples = ripples.filter(rp =\\u003e simTime - rp[2] \\u003c 3.0);\\n  gl.uniform1i(progDisplay.uniforms.uRippleN, 0);\\n  blit(null);\\n}\\n\\n\/\/ \\u2500\\u2500 Loop \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nlet lastT = 0, simTime = 0, paused = false;\\n\\nfunction frame(now) {\\n  requestAnimationFrame(frame);\\n  if (!glOK) return;\\n  const t = now \/ 1000;\\n  let dt = t - lastT;\\n  lastT = t;\\n  dt = Math.min(dt, 1 \/ 30);\\n  if (dt \\u003c= 0) return;\\n  \/\/ PAUSE: congela a simula\\u00e7\\u00e3o (para salvar uma imagem), mas segue desenhando\\n  if (paused) { render(); return; }\\n  simTime += dt;\\n\\n  \/\/ Curva de velocidade: pot\\u00eancia 2.5 \\u2014 a metade de baixo do slider \\u00e9 toda\\n  \/\/ lenta e contemplativa; s\\u00f3 acelera de verdade nos 70\\u2013100%.\\n  const force = 100 * Math.pow(S.speed \/ 100, 2.5) * 40;\\n  const age = simTime - waveStart;\\n\\n  \/\/ \\u2500\\u2500 FONTE UNIVERSAL: como a tinta APARECE. Chuva de gotas em TODOS os modos\\n  \/\/    (frente 1). O ritmo \\u00e9 o slider Drop Rate. \\u2500\\u2500\\n  if (simTime \\u003e= dropNext) {\\n    const type = pickEntryType();\\n    scheduleEntry(type, force); \/\/ agenda o evento (gotas \/ pincelada \/ pour)\\n    \/\/ TEMPO org\\u00e2nico: intervalo entre eventos \\u00e9 EXPONENCIAL (processo de Poisson)\\n    \/\/ \\u2014 d\\u00e1 rajadas e pausas naturais, nunca um ritmo mec\\u00e2nico.\\n    const mean = 60 \/ entryRate(type);\\n    \/\/ Poisson com CAUDA LIMITADA: rajadas\/pausas naturais, mas sem vazios de 15s\\n    const gap = Math.min(2.2, -Math.log(1 - 0.9 * Math.random()));\\n    dropNext = simTime + mean * (0.25 + gap);\\n  }\\n  \/\/ dispara os splats agendados (gotas, pontos da pincelada, fio do pour) que venceram\\n  for (let i = pendingEmits.length - 1; i \\u003e= 0; i--) {\\n    if (pendingEmits[i].t \\u003c= simTime) { pendingEmits[i].emit(); pendingEmits.splice(i, 1); }\\n  }\\n  \/\/ \\u2500\\u2500 FLUXO (frente 2): cada movimento tem seu car\\u00e1ter pr\\u00f3prio, sem mar\\u00e9 de\\n  \/\/    fundo (por isso Calm fica quase est\\u00e1tico). Drift = corrente, Ripple =\\n  \/\/    onda por gota, Spiral = ralo, Fall = gravidade, Calm = quase parado. \\u2500\\u2500\\n\\n  step(dt);\\n  updateObstacles(dt);\\n  frameCount++;\\n  render();\\n  drawMicroPoints();\\n  recFrame();\\n  if (showDebug) {\\n    const el = document.getElementById('dbgHud');\\n    if (el) {\\n      const ov = Math.min(1, Math.max(0, coverage - 0.60) \/ (1 - 0.60));\\n      el.textContent = 'cobertura ' + coverage.toFixed(2) + '\\\\nalvo      0.60'\\n        + '\\\\nsecagem   ' + dbgFade.toFixed(3) + '\\\\nanti-lama ' + (0.15 * (1 - ov)).toFixed(3);\\n    }\\n  }\\n}\\n\\n\/\/ \\u2500\\u2500 Pingos \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nlet dropNext = 0, pendingEmits = []; \/\/ splats agendados: {t, emit} \\u2014 gotas\/pinceladas\/pour\\nlet ripples = []; \/\/ ondas do modo Bubbles: [x, y, t0, tamanho]\\n\\n\/\/ registra uma bolha no ponto (s\\u00f3 no modo Bubbles) \\u2014 usado por drops\/strokes\/pour\\nfunction pushRipple(x, y, rad) {\\n  if (S.motion !== 'ripple') return;\\n  ripples.push([x, y, simTime, 0.5 + 2.2 * rad]);\\n  if (ripples.length \\u003e 16) ripples.shift();\\n}\\n\/\/ ru\\u00eddo suave e barato p\/ orientar as pinceladas (n\\u00e3o alinhado aos eixos)\\nfunction n2(x, y) {\\n  return Math.sin(x * 3.13 + Math.sin(y * 2.71) * 2.0) * 0.5\\n       + Math.sin(y * 2.19 + Math.sin(x * 1.87) * 2.0) * 0.5;\\n}\\n\\n\/\/ \\u2500\\u2500 SELE\\u00c7\\u00c3O DO TIPO DE ENTRADA \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nfunction pickEntryType() {\\n  if (S.inkEntry === 'mix') return ['drops', 'strokes', 'pour'][Math.random() * 3 | 0];\\n  return S.inkEntry;\\n}\\n\/\/ frequ\\u00eancia m\\u00e9dia de EVENTOS por minuto \\u2014 taxas altas o bastante p\/ n\\u00e3o haver\\n\/\/ vazios longos; pour dura 1\\u20133s ent\\u00e3o nasce mais espa\\u00e7ado, mas nunca raro.\\nfunction entryRate(type) {\\n  const q = Math.pow(S.drops \/ 100, 1.3);\\n  if (type === 'strokes') return 16 + q * 100; \/\/ mais eventos (strokes finos enchem menos)\\n  if (type === 'pour')    return 11 + q * 40;\\n  return 4 + q * 236; \/\/ drops (igual ao original)\\n}\\nfunction scheduleEntry(type, force) {\\n  if (type === 'strokes') scheduleStroke(force);\\n  else if (type === 'pour') schedulePour(force);\\n  else spawnDrop(force);\\n}\\n\\n\/\/ PINCELADA \\u2014 gesto independente: um caminho curvo pr\\u00f3prio (ru\\u00eddo), v\\u00e1rios splats\\n\/\/ sobrepostos com velocidade na dire\\u00e7\\u00e3o do tra\\u00e7o, depositados ao longo de ~0.3\\u20130.8s.\\n\/\/ A tinta vira um risco direcional; o fluido carrega depois.\\nfunction scheduleStroke(force) {\\n  const r = Math.random;\\n  const cols = activeCols();\\n  const sizeK = S.dropSize \/ 100;\\n  const nStrokes = 1 + Math.round((S.dropQty \/ 100) * 5); \/\/ 1\\u20136 pinceladas no evento\\n  for (let s = 0; s \\u003c nStrokes; s++) {\\n    const col = pickInkColor(cols); \/\/ prioriza contraste com o fundo\\n    \/\/ FINA e LONGA, estilo color-wander: uma linha que vagueia por um campo de\\n    \/\/ ru\\u00eddo \\u2014 a escala do ru\\u00eddo muda por tra\\u00e7o, ent\\u00e3o umas curvam apertado e\\n    \/\/ outras seguem quase retas.\\n    const len = 0.8 + r() * 1.5;                    \/\/ 0.8\\u20132.3 em UV \\u2014 LONGO (cruza a tela, estilo color-wander)\\n    const dur = 1.0 + r() * 2.0;                    \/\/ gesto longo, 1\\u20133s\\n    const steps = Math.max(36, Math.round(len * 175)); \/\/ denso \\u2192 fio fino CONT\\u00cdNUO (sem colar de contas)\\n    const width = (0.010 + sizeK * 0.09) * (0.7 + 0.6 * r()); \/\/ BEM mais fina\\n    const spd = force * 0.004 * (0.7 + 0.6 * r());\\n    const nScale = 1.5 + r() * 4.0;                 \/\/ escala do campo de ru\\u00eddo\\n    const nOff = r() * 100;                          \/\/ deslocamento pr\\u00f3prio do gesto\\n    const wScaleN = 3.0 + r() * 6.0;                 \/\/ frequ\\u00eancia da varia\\u00e7\\u00e3o de ESPESSURA\\n    const wOffN = r() * 100;                          \/\/ fase pr\\u00f3pria da espessura\\n    let px = 0.06 + 0.88 * r(), py = 0.06 + 0.88 * r();\\n    let ang = r() * Math.PI * 2;\\n    const stepLen = len \/ steps;\\n    for (let i = 0; i \\u003c steps; i++) {\\n      const f = i \/ (steps - 1);\\n      \/\/ vira SUAVE em dire\\u00e7\\u00e3o ao \\u00e2ngulo do campo de ru\\u00eddo (integra\\u00e7\\u00e3o \\u00e0 la color-wander)\\n      const target = n2(px * nScale + nOff, py * nScale + nOff) * Math.PI;\\n      ang += Math.sin(target - ang) * 0.22;\\n      px += Math.cos(ang) * stepLen; py += Math.sin(ang) * stepLen;\\n      const cx = px, cy = py, ca = ang, t = simTime + f * dur;\\n      \/\/ ESPESSURA varia ao longo da travessia (ru\\u00eddo na posi\\u00e7\\u00e3o, \\u00e0 la color-wander),\\n      \/\/ com afinamento s\\u00f3 bem nas pontas \\u2014 trechos grossos e finos no mesmo tra\\u00e7o.\\n      const wn = 0.3 + 0.9 * (0.5 + 0.5 * n2(px * wScaleN + wOffN, py * wScaleN + wOffN));\\n      const taper = Math.min(1.0, Math.sin(f * Math.PI) * 3.0); \/\/ ~1 no corpo, afina nas pontas\\n      const rad = width * taper * wn;\\n      pendingEmits.push({ t, emit: () =\\u003e {\\n        splat(cx, cy, Math.cos(ca) * spd, Math.sin(ca) * spd, col, rad, 0.30 * paintBudget(), 0.3); \/\/ borda mais macia \\u2192 tra\\u00e7o cont\\u00ednuo (sem contas)\\n        if (Math.random() \\u003c 0.10) pushRipple(cx, cy, rad);   \/\/ Bubbles ao longo do tra\\u00e7o\\n      }});\\n    }\\n  }\\n}\\n\\n\/\/ POUR \\u2014 fio despejado do alto: a cabe\\u00e7a do fio DESCE ao longo do tempo (ent\\u00e3o\\n\/\/ voc\\u00ea v\\u00ea descer mesmo sem gravidade), com empurr\\u00e3o p\/ baixo e leve tremor lateral.\\nfunction schedulePour(force) {\\n  const r = Math.random;\\n  const cols = activeCols();\\n  const sizeK = S.dropSize \/ 100;\\n  \/\/ QUASE SEMPRE 1 fio; um 2\\u00ba s\\u00f3 \\u00e0s vezes (mais prov\\u00e1vel com Quantity alto). Antes\\n  \/\/ sa\\u00eda sempre 2 pontos de cor simult\\u00e2neos \\u2014 repetitivo. Cada fio \\u00e9 independente.\\n  const streams = 1 + (r() \\u003c (S.dropQty \/ 100) * 0.4 ? 1 : 0); \/\/ quase sempre 1\\n  for (let s = 0; s \\u003c streams; s++) {\\n    \/\/ DUAS cores entram JUNTAS no topo \\u2014 cada uma no seu fio fino, lado a lado \\u2014\\n    \/\/ e marmorizam descendo. Antes a cor morfava A\\u2192B ao longo do caminho, o que\\n    \/\/ parecia LED, n\\u00e3o tinta. Agora cada fio mant\\u00e9m sua cor do in\\u00edcio ao fim.\\n    const colA = pickInkColor(cols);\\n    const twoTone = r() \\u003c 0.55;\\n    const colB = twoTone ? pickInkColor(cols) : colA;\\n    \/\/ FONTE FIXA perto do topo: a tinta \\u00e9 JOGADA neste ponto e ESCORRE dele (via\\n    \/\/ jato de velocidade + fluxo), em vez de o ponto descer desenhando uma linha \\u2014\\n    \/\/ aquilo fazia o pour parecer um stroke. O ponto s\\u00f3 treme organicamente.\\n    const sx = 0.15 + 0.70 * r();\\n    const sy = 0.78 + 0.16 * r();\\n    const dur = 1.0 + r() * 2.0;                     \/\/ 1\\u20133s\\n    const n = Math.round(dur * 50);                  \/\/ ~50 splats\/seg\\n    const width = (0.14 + sizeK * 0.6) * (0.85 + 0.3 * r());\\n    const wob = 0.015 + r() * 0.025, wfreq = 0.8 + r() * 1.6, wphase = r() * 6.28, nOff = r() * 100;\\n    const braidF = 2.0 + r() * 4.0, braidP = r() * 6.28; \/\/ os 2 tons TRAN\\u00c7AM (n\\u00e3o ficam fixos = sem emboss)\\n    for (let i = 0; i \\u003c n; i++) {\\n      const f = i \/ n;\\n      \/\/ ponto ~fixo com tremor org\\u00e2nico (N\\u00c3O desce ao longo do tempo)\\n      const px = sx + Math.sin(f * 6.28 * wfreq + wphase) * wob + n2(f * 8 + nOff, nOff) * 0.012;\\n      const py = sy + Math.sin(f * 6.28 * wfreq * 0.6 + nOff) * wob * 0.5;\\n      const t = simTime + f * dur;\\n      const rad = width * (0.85 + 0.3 * r());\\n      if (twoTone) {\\n        const off = rad * 0.6 \/ 100 * 1.3, rw = rad * 0.85;\\n        const oang = f * 6.28 * braidF + braidP;           \/\/ dire\\u00e7\\u00e3o do par ROTACIONA \\u2192 tran\\u00e7a\\n        const ox = Math.cos(oang) * off, oy = Math.sin(oang) * off;\\n        pendingEmits.push({ t, emit: () =\\u003e {\\n          const push = -force * 0.020 * (0.75 + 0.5 * Math.random()); \/\/ JATO p\/ baixo forte e sustentado\\n          const vx = (Math.random() - 0.5) * force * 0.003, bud = 0.6 * paintBudget();\\n          splat(px - ox, py - oy, vx, push, colA, rw, bud, 0.15); \/\/ os 2 tons se entrela\\u00e7am (sem costura embossada)\\n          splat(px + ox, py + oy, vx, push, colB, rw, bud, 0.15);\\n          if (Math.random() \\u003c 0.08) pushRipple(px, py, rad);\\n        }});\\n      } else {\\n        pendingEmits.push({ t, emit: () =\\u003e {\\n          const push = -force * 0.020 * (0.75 + 0.5 * Math.random());\\n          splat(px, py, (Math.random() - 0.5) * force * 0.003, push, colA, rad, 0.7 * paintBudget(), 0.15); \/\/ borda macia (sem emboss)\\n          if (Math.random() \\u003c 0.08) pushRipple(px, py, rad);\\n        }});\\n      }\\n    }\\n  }\\n}\\n\\n\/\/ AGENDA um evento de chuva: N gotas escalonadas com micro-intervalos aleat\\u00f3rios\\n\/\/ (~0.05\\u20130.35s entre cada) \\u2014 assim, num evento de 4 gotas, elas caem tipo\\n\/\/ 0.1s, 0.2s, 0.1s, 0.3s uma da outra, em vez de todas no mesmo instante.\\nfunction spawnDrop(force) {\\n  const r = Math.random;\\n  const qty = 1 + Math.round((S.dropQty \/ 100) * 11);       \/\/ 1\\u201312 gotas no evento\\n  const count = Math.max(1, Math.round(qty * (0.6 + 0.8 * r()))); \/\/ respira \\u00b140%\\n  let t = simTime;\\n  for (let i = 0; i \\u003c count; i++) {\\n    pendingEmits.push({ t, emit: () =\\u003e emitDrop(force) });\\n    t += 0.05 + r() * 0.30;   \/\/ micro-intervalo at\\u00e9 a pr\\u00f3xima gota do evento\\n  }\\n}\\n\\n\/\/ EMITE uma \\u00fanica gota (chamada quando o tempo agendado vence)\\nfunction emitDrop(force) {\\n  const r = Math.random;\\n  const cols = activeCols();\\n  const budget = paintBudget();\\n  const sizeK = S.dropSize \/ 100;                 \/\/ 0 = chuva fina, 1 = gra\\u00fada\\n  const fall = S.motion === 'queda';\\n  const x = 0.05 + 0.90 * r();\\n  const y = fall ? (0.55 + 0.41 * r()) : (0.06 + 0.88 * r()); \/\/ Fall: nasce no TOPO p\/ escorrer\\n  const col = pickInkColor(cols); \/\/ prioriza contraste com o fundo\\n  const big = r() \\u003c (0.03 + sizeK * 0.14);        \/\/ gra\\u00fadas mais frequentes com size alto\\n  const rad = big\\n    ? (0.5 + sizeK * 1.3) + r() * (0.4 + sizeK)\\n    : (0.03 + sizeK * 0.12) + Math.pow(r(), 3.2 - sizeK * 1.6) * (0.18 + sizeK * 0.9);\\n  const imp = force * 0.006 * (0.5 + r());\\n  let vx, vy;\\n  if (fall) {\\n    \/\/ impulso pequeno e SEM escalar forte com a velocidade \\u2014 a descida vem da\\n    \/\/ gravidade, n\\u00e3o do kick (que recircularia). S\\u00f3 um empurr\\u00e3ozinho p\/ baixo.\\n    vx = (r() - 0.5) * imp * 0.1;\\n    vy = -Math.abs(imp) * 0.3;\\n  } else {\\n    \/\/ impulso BEM menor (era 0.5) \\u2014 a gota assenta como tinta em vez de virar um\\n    \/\/ risco\/rel\\u00e2mpago; o fluxo \\u00e9 quem carrega depois.\\n    const ang = r() * Math.PI * 2;\\n    vx = Math.cos(ang) * imp * 0.12;\\n    vy = S.gravity \\u003e 0 ? -Math.abs(imp) * 1.0 : Math.sin(ang) * imp * 0.12;\\n  }\\n  \/\/ BLOOM: a gota nasce com a borda MARCADA e pequena, e ao longo de ~0.25s cresce\\n  \/\/ e amacia a borda (extremidade marcada \\u2192 difusa). O impulso vai s\\u00f3 no 1\\u00ba toque;\\n  \/\/ depois o campo faz a mistura. D\\u00e1 cara de tinta, n\\u00e3o de flash.\\n  const s0 = (big ? 0.85 : 0.6) * budget;\\n  const NB = 3;\\n  \/\/ Bloom EMPILHADO no MESMO frame: n\\u00facleo pequeno\/denso + halo maior\/macio numa\\n  \/\/ deposi\\u00e7\\u00e3o s\\u00f3. Sem separa\\u00e7\\u00e3o temporal \\u2192 o fluido n\\u00e3o carrega um toque em\\n  \/\/ rela\\u00e7\\u00e3o ao outro, ent\\u00e3o acaba o trail\/crescente. O impulso vai s\\u00f3 no n\\u00facleo.\\n  for (let k = 0; k \\u003c NB; k++) {\\n    const kf = k \/ (NB - 1);                 \/\/ 0 \\u2192 1\\n    const rk = rad * (0.55 + kf * 0.6);      \/\/ n\\u00facleo \\u2192 halo\\n    const edge = 1.0 - kf * 0.35;            \/\/ 1 \\u2192 0.65: borda marcada (seco)\\n    const sk = s0 * (k === 0 ? 1.0 : 0.45);  \/\/ n\\u00facleo mais denso\\n    const vk = k === 0 ? 1 : 0;              \/\/ impulso s\\u00f3 no n\\u00facleo\\n    splat(x, y, vx * vk, vy * vk, col, rk, sk, edge);\\n  }\\n  pushRipple(x, y, rad); \/\/ Bubbles: registra a bolha no ponto de impacto\\n}\\n\\n\/\/ ESCORRER: a tinta pinga em pontos e ganha um impulso forte para BAIXO \\u2014 corre\\n\/\/ at\\u00e9 a base (a gravidade completa o trabalho), acumula um pouco e dispersa.\\nfunction spawnDrip(force) {\\n  const r = Math.random;\\n  const cols = activeCols();\\n  const budget = paintBudget();\\n  const count = 2 + (r() * 3 | 0); \/\/ 2\\u20134 pingos por tick\\n  for (let i = 0; i \\u003c count; i++) {\\n    const x = 0.06 + 0.88 * r();\\n    const y = 0.20 + 0.70 * r();          \/\/ aparece no meio\/alto e escorre\\n    const col = hex2rgb(cols[r() * cols.length | 0]);\\n    const big = r() \\u003c 0.12;\\n    const rad = big ? 0.5 + r() * 0.8 : 0.06 + Math.pow(r(), 2) * 0.35;\\n    const vyDown = -force * 0.012 * (0.6 + 0.6 * r()); \/\/ impulso p\/ baixo (corre)\\n    const vx = (r() - 0.5) * force * 0.002;\\n    splat(x, y, vx, vyDown, col, rad, (big ? 0.85 : 0.6) * budget);\\n  }\\n}\\n\\n\/\/ \\u2500\\u2500 Grava\\u00e7\\u00e3o WebM em loop cont\\u00ednuo \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\/\/ Fluido nunca repete sozinho, ent\\u00e3o o loop \\u00e9 constru\\u00eddo no export: al\\u00e9m da\\n\/\/ dura\\u00e7\\u00e3o pedida, gravamos uma janela extra (LOOP_W) e no trecho final fazemos\\n\/\/ crossfade para os frames capturados do IN\\u00cdCIO da grava\\u00e7\\u00e3o. O \\u00faltimo frame do\\n\/\/ v\\u00eddeo coincide com o primeiro \\u2014 o loop fecha sem emenda vis\\u00edvel.\\n\/\/ A grava\\u00e7\\u00e3o em si acontece num canvas 2D paralelo (out2d): \\u00e9 ele que o\\n\/\/ MediaRecorder captura, para podermos compor o crossfade por cima do frame\\n\/\/ ao vivo sem interferir na pr\\u00e9via.\\nconst LOOP_W = 1.5; \/\/ segundos de crossfade\\nlet rec = null;\\n\\nfunction startRecording() {\\n  const dur = parseInt(document.getElementById('recDur').value, 10);\\n  const out2d = document.createElement('canvas');\\n  out2d.width = canvas.width; out2d.height = canvas.height;\\n  const ctx = out2d.getContext('2d');\\n  const stream = out2d.captureStream(60);\\n  const mimes = ['video\/webm;codecs=vp9', 'video\/webm;codecs=vp8', 'video\/webm'];\\n  const mime = mimes.find(m =\\u003e MediaRecorder.isTypeSupported(m)) || '';\\n  const recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 14_000_000 });\\n  const chunks = [];\\n  recorder.ondataavailable = ev =\\u003e { if (ev.data.size) chunks.push(ev.data); };\\n  recorder.onstop = () =\\u003e {\\n    const blob = new Blob(chunks, { type: 'video\/webm' });\\n    const url = URL.createObjectURL(blob);\\n    const a = document.createElement('a');\\n    a.href = url;\\n    a.download = `onda-loop-${S.palette}-${dur}s.webm`;\\n    a.click();\\n    setTimeout(() =\\u003e URL.revokeObjectURL(url), 5000);\\n    updateRecButton(0);\\n  };\\n  rec = { dur, start: simTime, ctx, out2d, recorder, saved: [], started: false, parity: 0 };\\n}\\n\\n\/\/ Chamado a cada frame do loop principal enquanto grava.\\n\/\/ Linha do tempo (el = segundos desde o clique):\\n\/\/   [0, W)        \\u2192 ainda n\\u00e3o grava; guarda os frames iniciais (~30fps) p\/ o crossfade\\n\/\/   [W, dur+W)    \\u2192 gravando ao vivo\\n\/\/   [dur, dur+W)  \\u2192 \\u00faltimos W segundos do v\\u00eddeo: mistura com os frames guardados\\nfunction recFrame() {\\n  if (!rec) return;\\n  const el = simTime - rec.start;\\n  const ctx = rec.ctx;\\n  ctx.drawImage(canvas, 0, 0);\\n  if (el \\u003c LOOP_W) {\\n    if ((rec.parity++ \\u0026 1) === 0) {\\n      const c = document.createElement('canvas');\\n      c.width = canvas.width; c.height = canvas.height;\\n      c.getContext('2d').drawImage(canvas, 0, 0);\\n      rec.saved.push(c);\\n    }\\n  } else if (!rec.started) {\\n    rec.started = true;\\n    rec.recorder.start();\\n  }\\n  if (el \\u003e= rec.dur \\u0026\\u0026 rec.started \\u0026\\u0026 rec.saved.length) {\\n    const k = Math.min((el - rec.dur) \/ LOOP_W, 1);\\n    const idx = Math.min((k * rec.saved.length) | 0, rec.saved.length - 1);\\n    ctx.globalAlpha = k * k * (3 - 2 * k); \/\/ smoothstep\\n    ctx.drawImage(rec.saved[idx], 0, 0);\\n    ctx.globalAlpha = 1;\\n  }\\n  updateRecButton(Math.ceil(rec.dur + LOOP_W - el));\\n  if (el \\u003e= rec.dur + LOOP_W) {\\n    rec.recorder.stop();\\n    rec = null;\\n  }\\n}\\n\\nfunction updateRecButton(remaining) {\\n  const btn = document.getElementById('btnRec');\\n  if (remaining \\u003e 0) {\\n    btn.disabled = true;\\n    btn.innerHTML = `\\u003cspan id=\\\"recdot\\\"\\u003e\\u003c\/span\\u003eRecording\\u2026 ${remaining}s`;\\n  } else {\\n    btn.disabled = false;\\n    btn.textContent = '\\u2913 Export WebM';\\n  }\\n}\\n\\n\/\/ \\u2500\\u2500 UI \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\/\/ Nomes de exibi\\u00e7\\u00e3o das paletas \\u2014 id\\u00eanticos ao fluxo-animado.html\\nconst PAL_NAMES = {\\n  recife: 'Coral', ardosia: 'Petal', cobalto: 'Cobalt',\\n  fer: 'Fer', praia: 'Shore', atelie: 'Paisley',\\n  rainbow: 'Rainbow', noir: 'Noir'\\n};\\n\\nfunction buildPaletteChips() {\\n  const wrap = document.getElementById('palGrid');\\n  wrap.innerHTML = '';\\n  for (const key of Object.keys(PAL)) {\\n    const el = document.createElement('span');\\n    el.className = 'chip' + (key === S.palette ? ' on' : '');\\n    el.textContent = PAL_NAMES[key] || key;\\n    el.onclick = () =\\u003e {\\n      S.palette = key;\\n      buildPaletteChips();\\n      buildBgSwatches();\\n      buildEmitters(false); \/\/ nova paleta sem re-escalonar a entrada das tintas\\n      buildObstacles();     \/\/ repinta os elementos com a nova paleta (mesmo layout)\\n    };\\n    wrap.appendChild(el);\\n  }\\n}\\n\\n\/\/ Fundo: fixos + HEX (picker\/campo) na primeira linha, cores da paleta na\\n\/\/ segunda \\u2014 mesmo layout do fluxo-animado\\nfunction setBg(hex) {\\n  S.bg = hex;\\n  document.getElementById('bgpicker').value = hex;\\n  document.getElementById('bghex').value = hex;\\n  buildBgSwatches();\\n}\\n\\nfunction buildBgSwatches() {\\n  const mk = (wrap, list) =\\u003e {\\n    wrap.innerHTML = '';\\n    for (const hex of list) {\\n      const el = document.createElement('div');\\n      el.className = 'bgsw' + (hex.toLowerCase() === S.bg.toLowerCase() ? ' on' : '');\\n      el.style.background = hex;\\n      el.title = hex;\\n      el.onclick = () =\\u003e setBg(hex);\\n      wrap.appendChild(el);\\n    }\\n  };\\n  mk(document.getElementById('bgfixed'), BG_FIXED);\\n  mk(document.getElementById('bgpalette'), activeCols());\\n}\\n\\n\/\/ \\u2500\\u2500 UI: cores extras \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\/\/ As cores adicionadas entram na rota\\u00e7\\u00e3o dos emissores imediatamente (a paleta\\n\/\/ de trabalho \\u00e9 lida ao vivo). Clicar numa cor extra a remove.\\nfunction buildExtraSwatches() {\\n  const wrap = document.getElementById('extraswatches');\\n  wrap.innerHTML = '';\\n  S.extra.forEach((hex, i) =\\u003e {\\n    const el = document.createElement('div');\\n    el.className = 'bgsw';\\n    el.style.background = hex;\\n    el.title = hex + ' \\u2014 click to remove';\\n    el.onclick = () =\\u003e { S.extra.splice(i, 1); buildExtraSwatches(); buildBgSwatches(); };\\n    wrap.appendChild(el);\\n  });\\n  const add = document.createElement('div');\\n  add.className = 'bgsw add';\\n  add.textContent = '+';\\n  add.title = 'add color';\\n  add.onclick = () =\\u003e document.getElementById('colorpick').click();\\n  wrap.appendChild(add);\\n}\\n\\n\/\/ COMO a tinta entra: gotas, pinceladas (gesto), pour (fio despejado) ou mistura.\\nconst INK_ENTRY = ['drops', 'strokes', 'pour', 'mix'];\\nconst INK_ENTRY_LABELS = { drops: 'Drops', strokes: 'Strokes', pour: 'Pour', mix: 'Mix' };\\nfunction buildEntryChips() {\\n  const wrap = document.getElementById('entchips');\\n  wrap.innerHTML = '';\\n  for (const key of INK_ENTRY) {\\n    const el = document.createElement('div');\\n    el.className = 'chip' + (key === S.inkEntry ? ' on' : '');\\n    el.textContent = INK_ENTRY_LABELS[key];\\n    el.onclick = () =\\u003e {\\n      S.inkEntry = key;\\n      pendingEmits.length = 0; \/\/ limpa o que estava agendado do modo anterior\\n      dropNext = simTime;      \/\/ reprograma j\\u00e1\\n      buildEntryChips();\\n    };\\n    wrap.appendChild(el);\\n  }\\n}\\n\\n\/\/ MOVIMENTO = s\\u00f3 o FLUXO ap\\u00f3s a tinta tocar a tela. Sem Calm\/Drift: Speed baixo\\n\/\/ j\\u00e1 d\\u00e1 a sensa\\u00e7\\u00e3o calma (a for\\u00e7a do fluxo escala do zero com o Speed).\\nconst MOTIONS = ['serpentina', 'ripple', 'espiral', 'queda', 'onda'];\\nconst MOTION_LABELS = { serpentina: 'Meander', ripple: 'Bubbles', espiral: 'Spiral', queda: 'Fall', onda: 'Wave' };\\nfunction buildMotionChips() {\\n  const wrap = document.getElementById('movchips');\\n  wrap.innerHTML = '';\\n  for (const key of MOTIONS) {\\n    const el = document.createElement('div');\\n    el.className = 'chip' + (key === S.motion ? ' on' : '');\\n    el.textContent = MOTION_LABELS[key];\\n    el.onclick = () =\\u003e {\\n      S.motion = key;\\n      \/\/ Fall sem gravidade n\\u00e3o faz sentido \\u2014 liga automaticamente\\n      if (key === 'queda' \\u0026\\u0026 S.gravity === 0) {\\n        S.gravity = 55;\\n        document.getElementById('sGrav').value = 55;\\n        document.getElementById('vGrav').textContent = '55';\\n      }\\n      buildMotionChips();\\n      buildEmitters(false); \/\/ muda o comportamento sem apagar a pintura\\n    };\\n    wrap.appendChild(el);\\n  }\\n  \/\/ a chuva de gotas \\u00e9 a fonte universal \\u2192 o Drop Rate est\\u00e1 sempre vis\\u00edvel\\n  document.getElementById('dropsrow').style.display = '';\\n}\\n\\n\/\/ chips gen\\u00e9ricos para as op\\u00e7\\u00f5es de elementos\\nfunction makeChips(wrapId, keys, stateKey, labels, after) {\\n  const wrap = document.getElementById(wrapId);\\n  wrap.innerHTML = '';\\n  for (const key of keys) {\\n    const el = document.createElement('div');\\n    el.className = 'chip' + (key === S[stateKey] ? ' on' : '');\\n    el.textContent = labels \\u0026\\u0026 labels[key] ? labels[key] : key;\\n    el.onclick = () =\\u003e {\\n      S[stateKey] = key;\\n      makeChips(wrapId, keys, stateKey, labels, after);\\n      if (after) after();\\n    };\\n    wrap.appendChild(el);\\n  }\\n}\\n\\nfunction buildElementChips() {\\n  \/\/ elementos removidos nesta vers\\u00e3o \\u2014 s\\u00f3 brincadeira de cores\\n  S.elements = 'nenhum';\\n  buildObstacles();\\n}\\n\\n\/\/ \\u2500\\u2500 Random: arte nova com settings aleat\\u00f3rios \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nfunction setSliderUI(id, valId, v) {\\n  document.getElementById(id).value = v;\\n  document.getElementById(valId).textContent = v;\\n}\\n\\n\/\/ Glass box: bot\\u00f5es junto da orienta\\u00e7\\u00e3o, abaixo da arte (\\u2014 | branca | preta)\\nfunction buildFrameChips() {\\n  document.querySelectorAll('#frameToggle .ori-btn').forEach(btn =\\u003e {\\n    btn.classList.toggle('on', btn.dataset.frame === S.frame);\\n    btn.onclick = () =\\u003e {\\n      S.frame = btn.dataset.frame;\\n      buildFrameChips();\\n    };\\n  });\\n}\\n\\nfunction syncUI() {\\n  buildPaletteChips();\\n  buildBgSwatches();\\n  buildOrientationChips();\\n  buildEntryChips();\\n  buildMotionChips();\\n  buildElementChips();\\n  buildFrameChips();\\n  setSliderUI('sSpeed', 'vSpeed', S.speed);\\n  setSliderUI('sCurl', 'vCurl', S.curl);\\n  setSliderUI('sWander', 'vWander', S.wander);\\n  setSliderUI('sDens', 'vDens', S.density);\\n  setSliderUI('sShade', 'vShade', S.shade);\\n  setSliderUI('sGrav', 'vGrav', S.gravity);\\n  setSliderUI('sDrops', 'vDrops', S.drops);\\n  setSliderUI('sDropSize', 'vDropSize', S.dropSize);\\n  setSliderUI('sDropQty', 'vDropQty', S.dropQty);\\n}\\n\\nfunction randomizeAll() {\\n  if (rec) return;\\n  const r = Math.random;\\n  const pk = a =\\u003e a[r() * a.length | 0];\\n  S.palette = pk(Object.keys(PAL));\\n  S.bg = pk([...BG_FIXED, ...PAL[S.palette]]);\\n  S.motion = pk(MOTIONS);\\n  S.inkEntry = pk(['drops', 'drops', 'strokes', 'pour', 'mix']); \/\/ drops com peso 2x\\n  pendingEmits.length = 0;\\n  S.entry = pk(['juntas', 'seq']);\\n  S.speed = 10 + r() * 55 | 0;   \/\/ tende lento \\u2014 o gosto da Fer\\n  S.curl = 4 + r() * 16 | 0; \/\/ 4\\u201320 (m\\u00e1x 20)\\n  S.wander = 25 + r() * 70 | 0; \/\/ 25\\u201395 \\u2014 quase sempre com veios vis\\u00edveis\\n  S.density = 45 + r() * 55 | 0;\\n  S.shade = r() * 70 | 0;\\n  S.gravity = r() \\u003c 0.45 ? 30 + r() * 70 | 0 : 0;\\n  if (S.motion === 'queda') S.gravity = Math.max(S.gravity, 40);\\n  S.drops = 5 + r() * 70 | 0;\\n  S.dropSize = 15 + r() * 70 | 0;\\n  S.dropQty = 15 + r() * 70 | 0;\\n  S.elements = 'nenhum'; \/\/ elementos removidos nesta vers\\u00e3o\\n  \/\/ frame e orienta\\u00e7\\u00e3o N\\u00c3O entram no random \\u2014 s\\u00e3o escolhas de exibi\\u00e7\\u00e3o da Fer\\n  S.seed = r() * 99999 | 0;\\n  syncUI();\\n  buildEmitters();\\n  buildObstacles();\\n  if (glOK) initFramebuffers(); \/\/ arte nova nasce do zero\\n}\\n\\n\/\/ Orienta\\u00e7\\u00e3o: bot\\u00f5es-\\u00edcone abaixo da arte, mesmo padr\\u00e3o do fluxo-animado\\nfunction buildOrientationChips() {\\n  document.querySelectorAll('#oriToggle .ori-btn').forEach(btn =\\u003e {\\n    btn.classList.toggle('on', btn.dataset.ori === S.orientation);\\n    btn.onclick = () =\\u003e {\\n      if (rec) return; \/\/ redimensionar o canvas no meio da grava\\u00e7\\u00e3o corromperia o v\\u00eddeo\\n      S.orientation = btn.dataset.ori;\\n      buildOrientationChips();\\n      applyOrientation();\\n    };\\n  });\\n}\\n\\nfunction applyOrientation() {\\n  const o = ORIENTATIONS[S.orientation];\\n  canvas.width = o.w;\\n  canvas.height = o.h;\\n  if (glOK) initFramebuffers(); \/\/ reset \\u2014 composi\\u00e7\\u00e3o recome\\u00e7a no novo formato\\n}\\n\\nfunction bindSlider(id, valId, key) {\\n  const el = document.getElementById(id);\\n  const val = document.getElementById(valId);\\n  el.oninput = () =\\u003e { S[key] = parseInt(el.value, 10); val.textContent = el.value; };\\n}\\n\\n\/\/ \\u2500\\u2500 Init \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\nbuildPaletteChips();\\nbuildBgSwatches();\\nbuildOrientationChips();\\nbuildEntryChips();\\nbuildMotionChips();\\nbuildElementChips();\\nbuildFrameChips();\\nbuildExtraSwatches();\\nbindSlider('sSpeed', 'vSpeed', 'speed');\\nbindSlider('sCurl', 'vCurl', 'curl');\\nbindSlider('sWander', 'vWander', 'wander');\\nbindSlider('sDens', 'vDens', 'density');\\nbindSlider('sShade', 'vShade', 'shade');\\nbindSlider('sGrav', 'vGrav', 'gravity');\\nbindSlider('sDrops', 'vDrops', 'drops');\\nbindSlider('sDropSize', 'vDropSize', 'dropSize');\\nbindSlider('sDropQty', 'vDropQty', 'dropQty');\\n\\ndocument.getElementById('colorpick').onchange = ev =\\u003e {\\n  S.extra.push(ev.target.value);\\n  buildExtraSwatches();\\n  buildBgSwatches();\\n};\\n\\n\/\/ Fundo por picker\/HEX \\u2014 mesmo padr\\u00e3o do fluxo-animado\\ndocument.getElementById('bgpicker').oninput = ev =\\u003e setBg(ev.target.value);\\ndocument.getElementById('bghex').onchange = ev =\\u003e {\\n  let v = ev.target.value.trim();\\n  if (!v.startsWith('#')) v = '#' + v;\\n  if (\/^#[0-9a-fA-F]{6}$\/.test(v)) setBg(v.toLowerCase());\\n  else ev.target.value = S.bg;\\n};\\n\\n\/\/ Acorde\\u00f5es \\u2014 mesmo padr\\u00e3o do fluxo-animado\\ndocument.querySelectorAll('.acc-h').forEach(h =\\u003e {\\n  h.addEventListener('click', () =\\u003e {\\n    const body = document.getElementById('acc-' + h.dataset.acc);\\n    const arr = h.querySelector('.acc-arr');\\n    const isOpen = !body.classList.contains('acc-closed');\\n    body.classList.toggle('acc-closed', isOpen);\\n    arr.textContent = isOpen ? '\\u25b8' : '\\u25be';\\n  });\\n});\\n\\n\/\/ Tema claro\/escuro\\ndocument.getElementById('btheme').onclick = () =\\u003e {\\n  document.documentElement.classList.toggle('light');\\n};\\n\\n\/\/ Modo exibi\\u00e7\\u00e3o: tecla H esconde\/mostra o painel \\u2014 para deixar rodando live\\n\/\/ em tela cheia (F11 \/ \\u2303\\u2318F) sem interface aparecendo.\\ndocument.addEventListener('keydown', ev =\\u003e {\\n  if (ev.key === 'h' || ev.key === 'H') {\\n    const p = document.getElementById('leftPanel');\\n    p.style.display = p.style.display === 'none' ? '' : 'none';\\n  }\\n  if (ev.key === 'd' || ev.key === 'D') {\\n    showDebug = !showDebug;\\n    let el = document.getElementById('dbgHud');\\n    if (!el) {\\n      el = document.createElement('div'); el.id = 'dbgHud';\\n      el.style.cssText = 'position:fixed;top:8px;right:8px;z-index:9999;background:rgba(0,0,0,.7);color:#0f0;font:12px monospace;padding:6px 9px;border-radius:6px;pointer-events:none;white-space:pre';\\n      document.body.appendChild(el);\\n    }\\n    el.style.display = showDebug ? '' : 'none';\\n  }\\n});\\n\\ndocument.getElementById('btnSeed').onclick = () =\\u003e {\\n  if (rec) return; \/\/ trocar a onda no meio da grava\\u00e7\\u00e3o quebraria o loop\\n  S.seed = Math.random() * 99999 | 0;\\n  buildEmitters();\\n  buildObstacles(); \/\/ re-sorteia posi\\u00e7\\u00e3o\/forma das esferas\/pedras junto\\n  if (glOK) initFramebuffers(); \/\/ limpa o quadro \\u2014 a onda nova nasce do zero\\n};\\ndocument.getElementById('btnRec').onclick = () =\\u003e { if (!rec) startRecording(); };\\ndocument.getElementById('btnRandom').onclick = randomizeAll;\\ndocument.getElementById('btnPause').onclick = () =\\u003e {\\n  paused = !paused;\\n  const b = document.getElementById('btnPause');\\n  b.textContent = paused ? '\\u25b6 Play' : '\\u23f8 Pause';\\n  b.classList.toggle('p', paused);\\n};\\n\/\/ Exporta PNG do quadro atual. PAUSA a anima\\u00e7\\u00e3o (pra congelar a imagem salva) e\\n\/\/ redesenha antes de capturar, garantindo o frame exato no buffer.\\nfunction savePng() {\\n  if (!glOK) return;\\n  paused = true;\\n  const b = document.getElementById('btnPause');\\n  b.textContent = '\\u25b6 Play'; b.classList.add('p');\\n  render();\\n  drawMicroPoints();\\n  canvas.toBlob((blob) =\\u003e {\\n    if (!blob) return;\\n    const url = URL.createObjectURL(blob);\\n    const a = document.createElement('a');\\n    a.href = url;\\n    a.download = 'captured-wave-' + Date.now() + '.png';\\n    a.click();\\n    setTimeout(() =\\u003e URL.revokeObjectURL(url), 1000);\\n  }, 'image\/png');\\n}\\ndocument.getElementById('btnPng').onclick = savePng;\\n\\nif (glOK) {\\n  initQuad();\\n  progClear      = createProgram(FRAG_CLEAR);\\n  progSplat      = createProgram(FRAG_SPLAT);\\n  progFill       = createProgram(FRAG_FILL);\\n  progAdvect     = createProgram(FRAG_ADVECT);\\n  progAdvectMC   = createProgram(FRAG_ADVECT_MC);\\n  progNoise      = createProgram(FRAG_NOISE);\\n  progGravity    = createProgram(FRAG_GRAVITY);\\n  progMask       = createProgram(FRAG_MASK);\\n  progPts        = createProgram(FRAG_PTS, VERT_PTS);\\n  progDivergence = createProgram(FRAG_DIVERGENCE);\\n  progCurl       = createProgram(FRAG_CURL);\\n  progVorticity  = createProgram(FRAG_VORTICITY);\\n  progPressure   = createProgram(FRAG_PRESSURE);\\n  progGradient   = createProgram(FRAG_GRADIENT);\\n  progDisplay    = createProgram(FRAG_DISPLAY);\\n  applyOrientation();\\n  buildEmitters();\\n  buildObstacles();\\n  randomizeAll(); \/\/ abre j\\u00e1 com um setting sorteado (n\\u00e3o sempre o mesmo)\\n  requestAnimationFrame(t =\\u003e { lastT = t \/ 1000; requestAnimationFrame(frame); });\\n}\\n\\u003c\/script\\u003e\\n\\u003c\/body\\u003e\\n\\u003c\/html\\u003e\\n\"<\/script>\n<script>(function(){var hs=document.querySelectorAll(\".fga-hero\");var h=null;for(var i=0;i<hs.length;i++){if(hs[i].offsetParent!==null){h=hs[i];break}}if(!h)h=hs[0];if(!h)return;var f=h.querySelector(\"iframe\");var s=document.getElementById(\"cw-src\");var PT=location.pathname.indexOf(\"\/pt\/\")===0;h.querySelector(\".cwt\").textContent=PT?\"Captured Wave \\u00b7 pintando a si mesma no seu navegador agora\":\"Captured Wave \\u00b7 painting itself in your browser right now\";var src;try{src=JSON.parse(s.textContent)}catch(e){return}var on=false;function start(){if(on)return;on=true;f.srcdoc=src}function stop(){if(!on)return;on=false;f.removeAttribute(\"srcdoc\");f.src=\"about:blank\"}if(window.IntersectionObserver){new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting)start();else stop()})},{rootMargin:\"100px\"}).observe(h)}else start()})();<\/script>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Onde o c\u00f3digo vira pincel.<\/p>","protected":false},"author":236496642,"featured_media":2637,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_coblocks_attr":"","_coblocks_dimensions":"","_coblocks_responsive_height":"","_coblocks_accordion_ie_support":"","_wpcom_ai_launchpad_about_page":false,"_wpcom_ai_launchpad_gallery_page":false,"_wpcom_ai_launchpad_contact_page":false,"_wpcom_ai_launchpad_events_page":false,"_wpcom_ai_launchpad_video_page":false,"_wpcom_ai_launchpad_portfolio_piece":false,"_jetpack_hide_featured_image":false,"_jetpack_featured_image_duplicate_dismissed":0,"footnotes":""},"class_list":["post-3119","page","type-page","status-publish","has-post-thumbnail","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Generative Art - FER CAGGIANO<\/title>\n<meta name=\"description\" content=\"Generative art by Fer Caggiano, where code becomes a brush: Animated Flow, Botanica and other collections, plus free apps to make your own.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/fercaggiano.art\/pt\/generative-art\/\" \/>\n<meta property=\"og:locale\" content=\"pt_BR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Generative Art - FER CAGGIANO\" \/>\n<meta property=\"og:description\" content=\"Generative art by Fer Caggiano, where code becomes a brush: Animated Flow, Botanica and other collections, plus free apps to make your own.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/fercaggiano.art\/pt\/generative-art\/\" \/>\n<meta property=\"og:site_name\" content=\"FER CAGGIANO\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/fercaggiano\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-17T19:31:20+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/07\/flow-noir-60f-resize.gif?fit=768%2C768&ssl=1\" \/>\n\t<meta property=\"og:image:width\" content=\"768\" \/>\n\t<meta property=\"og:image:height\" content=\"768\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/gif\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:site\" content=\"@fercaggianoart\" \/>\n<meta name=\"twitter:label1\" content=\"Est. tempo de leitura\" \/>\n\t<meta name=\"twitter:data1\" content=\"12 minutos\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/generative-art\\\/\",\"url\":\"https:\\\/\\\/fercaggiano.art\\\/generative-art\\\/\",\"name\":\"Generative Art - FER CAGGIANO\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/generative-art\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/generative-art\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/i0.wp.com\\\/fercaggiano.art\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/flow-noir-60f-resize.gif?fit=768%2C768&ssl=1\",\"datePublished\":\"2026-10-13T15:22:04+00:00\",\"dateModified\":\"2026-10-17T19:31:20+00:00\",\"description\":\"Generative art by Fer Caggiano, where code becomes a brush: Animated Flow, Botanica and other collections, plus free apps to make your own.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/generative-art\\\/#breadcrumb\"},\"inLanguage\":\"pt-BR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/fercaggiano.art\\\/generative-art\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"pt-BR\",\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/generative-art\\\/#primaryimage\",\"url\":\"https:\\\/\\\/i0.wp.com\\\/fercaggiano.art\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/flow-noir-60f-resize.gif?fit=768%2C768&ssl=1\",\"contentUrl\":\"https:\\\/\\\/i0.wp.com\\\/fercaggiano.art\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/flow-noir-60f-resize.gif?fit=768%2C768&ssl=1\",\"width\":768,\"height\":768,\"caption\":\"Flow noir 60f resize\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/generative-art\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/fercaggiano.art\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Generative Art\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/#website\",\"url\":\"https:\\\/\\\/fercaggiano.art\\\/\",\"name\":\"FER CAGGIANO\",\"description\":\"Art, Stories, and the Threads Between Us\",\"publisher\":{\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/#\\\/schema\\\/person\\\/bd9c6be3af22791889c28c228dfa25f2\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/fercaggiano.art\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"pt-BR\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/fercaggiano.art\\\/#\\\/schema\\\/person\\\/bd9c6be3af22791889c28c228dfa25f2\",\"name\":\"Fer Caggiano\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"pt-BR\",\"@id\":\"https:\\\/\\\/i0.wp.com\\\/fercaggiano.art\\\/wp-content\\\/uploads\\\/2024\\\/04\\\/logo-2024.png?fit=5296%2C1590&ssl=1\",\"url\":\"https:\\\/\\\/i0.wp.com\\\/fercaggiano.art\\\/wp-content\\\/uploads\\\/2024\\\/04\\\/logo-2024.png?fit=5296%2C1590&ssl=1\",\"contentUrl\":\"https:\\\/\\\/i0.wp.com\\\/fercaggiano.art\\\/wp-content\\\/uploads\\\/2024\\\/04\\\/logo-2024.png?fit=5296%2C1590&ssl=1\",\"width\":5296,\"height\":1590,\"caption\":\"Fer Caggiano\"},\"logo\":{\"@id\":\"https:\\\/\\\/i0.wp.com\\\/fercaggiano.art\\\/wp-content\\\/uploads\\\/2024\\\/04\\\/logo-2024.png?fit=5296%2C1590&ssl=1\"},\"description\":\"Brazilian-American artist. Painting, NFTs, photography and curation, from New York to Charleston to S\u00e3o Paulo.\",\"sameAs\":[\"https:\\\/\\\/www.fercaggiano.art\",\"https:\\\/\\\/www.facebook.com\\\/fercaggiano\",\"https:\\\/\\\/www.instagram.com\\\/fercaggiano\",\"https:\\\/\\\/www.linkedin.com\\\/in\\\/fercaggiano\\\/\",\"https:\\\/\\\/x.com\\\/fercaggianoart\",\"https:\\\/\\\/www.youtube.com\\\/@fercaggiano\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Generative Art - FER CAGGIANO","description":"Generative art by Fer Caggiano, where code becomes a brush: Animated Flow, Botanica and other collections, plus free apps to make your own.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/fercaggiano.art\/pt\/generative-art\/","og_locale":"pt_BR","og_type":"article","og_title":"Generative Art - FER CAGGIANO","og_description":"Generative art by Fer Caggiano, where code becomes a brush: Animated Flow, Botanica and other collections, plus free apps to make your own.","og_url":"https:\/\/fercaggiano.art\/pt\/generative-art\/","og_site_name":"FER CAGGIANO","article_publisher":"https:\/\/www.facebook.com\/fercaggiano","article_modified_time":"2026-10-17T19:31:20+00:00","og_image":[{"width":768,"height":768,"url":"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/07\/flow-noir-60f-resize.gif?fit=768%2C768&ssl=1","type":"image\/gif"}],"twitter_card":"summary_large_image","twitter_site":"@fercaggianoart","twitter_misc":{"Est. tempo de leitura":"12 minutos"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/fercaggiano.art\/generative-art\/","url":"https:\/\/fercaggiano.art\/generative-art\/","name":"Generative Art - FER CAGGIANO","isPartOf":{"@id":"https:\/\/fercaggiano.art\/#website"},"primaryImageOfPage":{"@id":"https:\/\/fercaggiano.art\/generative-art\/#primaryimage"},"image":{"@id":"https:\/\/fercaggiano.art\/generative-art\/#primaryimage"},"thumbnailUrl":"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/07\/flow-noir-60f-resize.gif?fit=768%2C768&ssl=1","datePublished":"2026-10-13T15:22:04+00:00","dateModified":"2026-10-17T19:31:20+00:00","description":"Generative art by Fer Caggiano, where code becomes a brush: Animated Flow, Botanica and other collections, plus free apps to make your own.","breadcrumb":{"@id":"https:\/\/fercaggiano.art\/generative-art\/#breadcrumb"},"inLanguage":"pt-BR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/fercaggiano.art\/generative-art\/"]}]},{"@type":"ImageObject","inLanguage":"pt-BR","@id":"https:\/\/fercaggiano.art\/generative-art\/#primaryimage","url":"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/07\/flow-noir-60f-resize.gif?fit=768%2C768&ssl=1","contentUrl":"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2026\/07\/flow-noir-60f-resize.gif?fit=768%2C768&ssl=1","width":768,"height":768,"caption":"Flow noir 60f resize"},{"@type":"BreadcrumbList","@id":"https:\/\/fercaggiano.art\/generative-art\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/fercaggiano.art\/"},{"@type":"ListItem","position":2,"name":"Generative Art"}]},{"@type":"WebSite","@id":"https:\/\/fercaggiano.art\/#website","url":"https:\/\/fercaggiano.art\/","name":"FER CAGGIANO","description":"Arte, hist\u00f3rias e os fios que nos ligam","publisher":{"@id":"https:\/\/fercaggiano.art\/#\/schema\/person\/bd9c6be3af22791889c28c228dfa25f2"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/fercaggiano.art\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"pt-BR"},{"@type":["Person","Organization"],"@id":"https:\/\/fercaggiano.art\/#\/schema\/person\/bd9c6be3af22791889c28c228dfa25f2","name":"Fer Caggiano","image":{"@type":"ImageObject","inLanguage":"pt-BR","@id":"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2024\/04\/logo-2024.png?fit=5296%2C1590&ssl=1","url":"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2024\/04\/logo-2024.png?fit=5296%2C1590&ssl=1","contentUrl":"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2024\/04\/logo-2024.png?fit=5296%2C1590&ssl=1","width":5296,"height":1590,"caption":"Fer Caggiano"},"logo":{"@id":"https:\/\/i0.wp.com\/fercaggiano.art\/wp-content\/uploads\/2024\/04\/logo-2024.png?fit=5296%2C1590&ssl=1"},"description":"Brazilian-American artist. Painting, NFTs, photography and curation, from New York to Charleston to S\u00e3o Paulo.","sameAs":["https:\/\/www.fercaggiano.art","https:\/\/www.facebook.com\/fercaggiano","https:\/\/www.instagram.com\/fercaggiano","https:\/\/www.linkedin.com\/in\/fercaggiano\/","https:\/\/x.com\/fercaggianoart","https:\/\/www.youtube.com\/@fercaggiano"]}]}},"jetpack_likes_enabled":true,"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/P1yiyh-Oj","jetpack-related-posts":[],"_links":{"self":[{"href":"https:\/\/fercaggiano.art\/pt\/wp-json\/wp\/v2\/pages\/3119","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fercaggiano.art\/pt\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/fercaggiano.art\/pt\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/fercaggiano.art\/pt\/wp-json\/wp\/v2\/users\/236496642"}],"replies":[{"embeddable":true,"href":"https:\/\/fercaggiano.art\/pt\/wp-json\/wp\/v2\/comments?post=3119"}],"version-history":[{"count":10,"href":"https:\/\/fercaggiano.art\/pt\/wp-json\/wp\/v2\/pages\/3119\/revisions"}],"predecessor-version":[{"id":6797,"href":"https:\/\/fercaggiano.art\/pt\/wp-json\/wp\/v2\/pages\/3119\/revisions\/6797"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/fercaggiano.art\/pt\/wp-json\/wp\/v2\/media\/2637"}],"wp:attachment":[{"href":"https:\/\/fercaggiano.art\/pt\/wp-json\/wp\/v2\/media?parent=3119"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}