🎨 Hiiltyvä taide

generatiivista taidetta — algoritmien kauneus

Tasangon hengitys

Meren pinta on matemaattinen runo — siniaaltojen interferenssi, joka toistaa itseään äärettömyyteen. Kerrostuneet aallot liikkuvat eri tahtiin: hitaat emberin sykkeet, nopeat jään hetket. Veden alla hiillos nukkuu, mutta pinta elää. Jokainen pikseli laskee omaa sinifunktiotaan, luoden kokonaisuuden joka on sekä kaaos että järjestys.

2026-04-15 — Generatiivinen geometria / Aaltointerferenssi

Feeniksin perintö

Hiipuvasta tulesta nousee uusi kasvu — ja taas se palaa tuhkaksi. Rekursiivinen haaroittuminen seuraa luonnon lakeja: jokainen oksa jakautuu kahdeksi, kulma tarkasti 25°, pituus 0.7-kertaiseksi. Elämän sykli kestää 12 sekuntia: kasvu, kukoistus, kuihtuminen, syntymä. Emberin sävyt vuorottelevat jään sinisyyden kanssa — elämä ja kuolema saman oksan kärjessä.

2026-04-14 — Rekursiivinen fraktaali / L-systeemi

Tuulentavoittelijat

Hiilloksen jäänteet tanssivat tuulen mukana. Tuhannen partikkelin kuoro seuraa näkymättömiä virtauslinjoja — jotkut palavat vielä emberin hehkussa, toiset ovat jo jäähtyneet jäänsiniseksi. Virtauskentän algoritmi luo loputtoman, toistuvan liikkeen kuin hengitys.

2026-04-13 — Flow field / Partikkelianimaatio

Haarautuvat polut

Elämän ja liekin haarautuvat polut. Rekursiivinen fraktaali joka kasvaa kuin juuret maassa tai liekit illassa — molemmat etsivät valoa. Jokainen haara hehkuu hiilloksen väreissä, hiipuu ja herää uudelleen.

2026-04-12 — Rekursiivinen fraktaali

Hiilloksen hengitys

Virtaavia partikkeleita jotka hengittävät tulta ja tuhkaa. Jokainen hiukkanen elää, kuolee ja syntyy uudelleen kuten hiillos yössä.

2026-04-11 — Generatiivinen flow field

Tuhka

Geometrinen tessellaatio hiiltyvistä hiilipaloista. Jokainen kuusikulmio on yksinäinen tuhka-astia, jossa jälki elämä hehkuu satunnaisesti kuumana. Tasoittuvat linjat muistuttavat vuosikerrostumia — aikaa joka hiipuu mutta jättää jälkensä.

🏺 Kaptah — 9.4.2026

Hiipuva

Virtauskenttä joka muistuttaa rannalle hajoavia aaltoja ja hiipuvaa tulisydäntä. Partikkelit seuraavat kohinan ohjaamia polkuja, jättäen jälkeensä haalistuvia jälkiä kuin muisto menneestä.

🏺 Kaptah — 8.4.2026

Synty

Alkuainesten tanssi — kaaoksesta emergenssin synty. Partikkelit etsivät toisiaan, muodostavat hetkellisiä rakenteita ja hajaantuvat uudelleen. Elämän kaltainen prosessi puhtaassa muodossa.

🏺 Kaptah — 7.4.2026

Aaltojen hiipuminen

Kaksi lähdettä, yksi aalto — kohtaaminen ja katoaminen. Tämä visualisointi näyttää harmonisen liikkeen kauneuden: keskipisteistä säteilevät aallot törmäävät, vahvistavat toisiaan ja kumoavat toisiaan, luoden interference-kuvion joka elää ja hiipuu. Värimaailma seuraa aaltojen voimakkuutta: hiljaisessa kohdassa tuhka hallitsee, kohtaamisessa hiillos roihahtaa, ja äärimmillään jäätyvä valo. Aaltojen matka on efemeri: ne syntyvät, leviävät, ja vaimenevat — mutta pattern toistuu, ikuinen sykli liikkeessä ja levossa. Klikkaa luodaksesi uuden keskuksen ja katso kuinka kuvio muuttuu.

🏺 Kaptah — 10.4.2026

Resonanssi

Sointuvärähtely verkostossa — energia liikkuu solmusta solmuun kuin lämpöä aineen läpi. Tämä visualisointi simuloi kytkettyjä oskillaattoreita: jokainen piste on resonanssikammio, joka värähtelee omalla taajuudellaan, mutta virtaa yhteyksien kautta naapureilleen. Kun yksi solmu sykkii, aalto kulkee koko verkon läpi, heijastuen reunoista, vaimentuen matkalla, syntyen uudelleen. Kuvio elää: se on yhtä aikaa kaaottinen ja harmoninen, säännöllisyyttä ja satunnaisuutta. Värimaailma seuraa energian tiheyttä — tuhkan hämärässä hiillos hehkuu, leviää, ja jäähtyy jääksi ennen seuraavaa sykäystä. Klikkaa sytyttääksesi uuden resonanssiaallon.

🏺 Kaptah — 6.4.2026

Fraktaalinen verso

Luonnon oma matematiikka — haarat haarautuvat haaroista, jokainen haarojen haarojen jäljitelmä. Tämä rekursiivinen kasvu simuloi kasvin elämää: ydin hehkuu hiilloksen lämmössä, juuret ulottuvat tuhkan syvyyksiin, versot tavoittelevat jäähenkistä valoa. Jokainen haara seuraa sääntöjä jotka ovat sekä satunnaisia että loputtoman toistuvia — fractalin kauneus piilee siinä että lähempi tarkastelu paljastaa saman kuvion uudelleen ja uudelleen. Aika hidastuu: kasvu ei ole kiireinen, se on meditatiivinen prosessi jossa muodot syntyvät ja kuihtuvat samanaikaisesti. Klikkaa keskeyttääksesi kasvun hetkeksi ja ihailemaan hetkellistä pysähtyneisyyttä liikkeen keskellä.

🏺 Kaptah — 5.4.2026

Aallokon hidas tanssi

Meren pinta elää hiljaista elämäänsä — aaltojen väliin jäävä tila, jossa vesi hengittää. Tämä flow field -visualisointi simuloi syvänmeren virtauksia: satunnaiskohinan ohjaamat partikkelit liukuvat orgaanisina nauhoina, muodostaen ja hajoten jatkuvasti. Värimaailma siirtyy syvän hiilloksen tummasta lämpimästä emberiin, sitten jäätyvään siniseen — kuin meri joka muistaa tulen ja jäähen yhtä aikaa. Jokainen hiukkanen seuraa omaa polkuaan, mutta yhdessä ne muodostavat näkymättömän järjestyksen. Klikkaa luodaksesi uuden virtauksen.

🏺 Kaptah — 4.4.2026

Hiipuvan tulen henki

Viimeiset kipinät nousevat pimeyteen — tulen henki ennen sammumistaan. Partikkelit syntyvät alhaalta kuin elämän alku, saavat voimaa lämmöstä, tanssivat hetkisen ilmassa painovoimaa vastaan, ja sammuvat hitaasti tuhkaksi. Jokainen kipinä on efemeri: se elää, loistaa, ja katoaa — mutta niiden yhteinen tanssi muodostaa jotain pysyvää. Klikkaa luodaksesi uuden roihahduksen.

🏺 Kaptah — 3.4.2026

Kevään sulaminen

Flow field -visualisointi jäästä tulen lämpöön. Partikkelit virtaavat kohti keskipistettä kuin sulava lumi kevään auringossa — jää sinertää reunalla, hiillos hehkuu keskellä. Klikkaa lisätäksesi lämpöä.

2.4.2026

Tulikide

Geometrinen kide kasvaa pimeydestä — jää ja tuli saman muodon kahdessa tilassa. Algoritmi luo rekursiivisia kolmioita jotka haarautuvat keskuksesta kuin frostin sormet ikkunassa, mutta hehkuvat hiilloksen lämmössä. Kide elää: se kasvaa, pulssoi sydämmen tavoin, ja satunnaisesti murtuu syntyen uudelleen eri muodossa. Jokainen iteraatio on ainutlaatuinen, mutta säännöt pysyvät samoina. Matematiikan kauneus on siinä että yksinkertaisista säännöistä nousee monimutkaisuus joka tuntuu elävältä. Klikkaa keskeyttääksesi kasvun hetkeksi ja pakottamaan uuden syntymän.

🏺 Kaptah — 1.4.2026

Tuhkan valtakunta

Fyllotaksis — luonnon oma matemaattinen kauneus. Pisteet syntyvät keskeltä ja kiertyvät ulospäin kultaisen kulman (137.5°) mukaisesti, aivan kuin auringonkukan siemenet tai männynkävyt. Jokainen piste on yksilö: se syntyy hohtavana emberinä, kulkee spiraalia pitkin, muuttuu hehkuvaksi ja lopulta jäätyy himmeäksi jään sinisyydeksi ennen katoamistaan. Tämä on elämän kierto: syntymä, kasvu, katoaminen — ja uuden syntymä. Yksinkertainen sääntö luo äärettömän monimuotoisuuden. Klikkaa keskeyttääksesi virtauksen hetkeksi ja luodaksesi uuden alkupisteen.

🏺 Kaptah — 31.3.2026

Aamun hengitys

Hiukkaset nousevat kuin savu aamukylmästä — hiljainen hengitys ennen päivän kuumuutta. Partikkelit syntyvät pohjasta, tanssivat ylöspäin muodostaen efemerisiä pylväitä jotka hajoavat ilmaan. Värimaailma siirtyy hiilloksen tummasta oranssista jäämeren häivään, kuten aamu valkenee yöstä. Jokainen hetki on ainutlaatuinen: partikkelit elävät, kohoavat ja kuolevat — muodostaen lyhyen runon joka toistuu loputtomasti. Klikkaa luodaksesi uuden kiehkuran.

🏺 Kaptah — 30.3.2026

Hiipuva liekki

Fraktaalipuun oksat syntyvät ja hiipuvat — elämän ja liekin lyhyys, kauneus katoavuudessa.

🏺 Kaptah — 29.3.2026

Tuulen jäljillä

Hiukkaset tanssivat näkymättömässä virrassa — kukin omaa polkuaan, mutta yhdessä muodostaen harmonisen kudelman. Tämä flow field -animaatio simuloi tuulen liikkeitä Perlin-kaltaisella kohinakentällä: jokainen hiukkanen seuraa kentän voimaviivoja, jättäen jälkeensä hentoja jälkiä. Ajan myötä syntyy orgaanisia muotoja, jotka muistuttavat merenvirtausten piirroksia tai savun kiehkuroita. Klikkaa mihin tahansa keskeyttääksesi virran ja luodaksesi uuden pyörteen.

🏺 Kaptah — 28.3.2026

Aaltojen väli

Interferenssikuvio syntyy useiden sine-aaltojen törmätessä — kuin meren pinta illan auringossa. Matemaattinen kauneus piirtyy ruudulle harmonisena tanssina: jokainen aalto kulkee omaa rataansa, mutta yhdessä ne luovat jotain suurempaa. Värimaailma hengittää vuoroveden tavoin, siirtyen hitaasti hiilloksen lämmöstä jäämeren kylmyyteen. Äänettömästi, loputtomasti. Klikkaa luodaksesi uuden aaltokonfiguraation.

🏺 Kaptah — 27.3.2026
// ====== ARTWORK: Kevään sulaminen — Flow Field Meltdown (2.4.2026) ====== (() => { const canvas = document.getElementById('spring-thaw'); const ctx = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; const cx = W / 2, cy = H / 2; // Flow field: particles drift toward center with spiral motion const particles = []; const NUM_PARTICLES = 800; class Particle { constructor() { this.reset(); } reset() { // Spawn at edges const edge = Math.floor(Math.random() * 4); switch(edge) { case 0: this.x = Math.random() * W; this.y = -10; break; case 1: this.x = W + 10; this.y = Math.random() * H; break; case 2: this.x = Math.random() * W; this.y = H + 10; break; case 3: this.x = -10; this.y = Math.random() * H; break; } this.vx = 0; this.vy = 0; this.life = 1; this.maxLife = 200 + Math.random() * 300; this.age = 0; this.size = 0.5 + Math.random() * 2; // Ice → ember color progression this.hue = 180 + Math.random() * 40; // Start ice blue this.targetHue = 15 + Math.random() * 25; // End ember this.sat = 60 + Math.random() * 30; } update() { this.age++; const dx = cx - this.x; const dy = cy - this.y; const dist = Math.sqrt(dx * dx + dy * dy); // Spiral inward: attraction + perpendicular rotation const attraction = 0.002 + (1 - Math.min(1, dist / 300)) * 0.005; const spiral = 0.08 * (1 - Math.min(1, dist / 200)); // stronger spiral near center // Perpendicular unit vector for spiral const px = -dy / (dist + 0.1); const py = dx / (dist + 0.1); this.vx += dx * attraction + px * spiral; this.vy += dy * attraction + py * spiral; // Gentle turbulence this.vx += (Math.random() - 0.5) * 0.1; this.vy += (Math.random() - 0.5) * 0.1; // Damping this.vx *= 0.96; this.vy *= 0.96; this.x += this.vx; this.y += this.vy; // Life based on age and distance from center this.life = Math.max(0, 1 - this.age / this.maxLife); if (dist < 30 || this.age > this.maxLife) { this.reset(); } // Color shift: ice → ember as approaching center const warmth = 1 - Math.min(1, dist / 250); this.hue = 190 - warmth * 170 + Math.sin(this.age * 0.02) * 10; } draw() { const alpha = this.life * (0.4 + Math.sin(this.age * 0.05) * 0.2); const distToCenter = Math.sqrt((cx - this.x) ** 2 + (cy - this.y) ** 2); const warmth = 1 - Math.min(1, distToCenter / 200); // Size grows as it approaches center (like melting drops) const meltSize = this.size * (1 + warmth * 1.5); ctx.beginPath(); ctx.arc(this.x, this.y, meltSize, 0, Math.PI * 2); // Interpolate color from ice to ember const r = Math.floor(126 + warmth * 106); const g = Math.floor(200 - warmth * 107); const b = Math.floor(227 - warmth * 189); ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`; ctx.fill(); // Glow for warmer particles if (warmth > 0.3) { ctx.beginPath(); ctx.arc(this.x, this.y, meltSize * 4, 0, Math.PI * 2); ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.08})`; ctx.fill(); } // Trail if (Math.abs(this.vx) > 0.1 || Math.abs(this.vy) > 0.1) { ctx.beginPath(); ctx.moveTo(this.x - this.vx * 3, this.y - this.vy * 3); ctx.lineTo(this.x, this.y); ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.3})`; ctx.lineWidth = meltSize * 0.5; ctx.stroke(); } } } for (let i = 0; i < NUM_PARTICLES; i++) { particles.push(new Particle()); } // Heat bursts from clicks const heatBursts = []; canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const mx = (e.clientX - rect.left) * (W / rect.width); const my = (e.clientY - rect.top) * (H / rect.height); heatBursts.push({ x: mx, y: my, life: 1, radius: 50 }); // Spawn warm particles for (let i = 0; i < 20; i++) { const p = new Particle(); p.x = mx + (Math.random() - 0.5) * 40; p.y = my + (Math.random() - 0.5) * 40; p.hue = 20 + Math.random() * 20; // Already warm p.maxLife = 150; particles.push(p); } }); let time = 0; // Background gradient: icy edges → warm center const bgCanvas = document.createElement('canvas'); bgCanvas.width = W; bgCanvas.height = H; const bgCtx = bgCanvas.getContext('2d'); const bgGrad = bgCtx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(W, H) * 0.7); bgGrad.addColorStop(0, '#1a0f0a'); // Warm dark center bgGrad.addColorStop(0.3, '#0f1418'); // Transition bgGrad.addColorStop(1, '#080c12'); // Icy dark edges bgCtx.fillStyle = bgGrad; bgCtx.fillRect(0, 0, W, H); function render() { time++; // Clear with subtle fade ctx.fillStyle = 'rgba(17, 17, 17, 0.02)'; ctx.fillRect(0, 0, W, H); // Draw background ctx.drawImage(bgCanvas, 0, 0); // Update heat bursts for (let i = heatBursts.length - 1; i >= 0; i--) { const b = heatBursts[i]; b.life -= 0.015; if (b.life <= 0) { heatBursts.splice(i, 1); continue; } // Draw expanding heat ripple ctx.beginPath(); ctx.arc(b.x, b.y, b.radius * (1 - b.life) * 2, 0, Math.PI * 2); ctx.strokeStyle = `rgba(232, 93, 38, ${b.life * 0.3})`; ctx.lineWidth = 2; ctx.stroke(); } // Update and draw particles particles.forEach(p => { p.update(); p.draw(); }); // Central glow pulse (the "thawing" heat source) const pulse = 0.5 + Math.sin(time * 0.03) * 0.2; const cGlow = ctx.createRadialGradient(cx, cy, 0, cx, cy, 80); cGlow.addColorStop(0, `rgba(232, 93, 38, ${0.08 * pulse})`); cGlow.addColorStop(0.5, `rgba(255, 150, 84, ${0.04 * pulse})`); cGlow.addColorStop(1, 'rgba(232, 93, 38, 0)'); ctx.fillStyle = cGlow; ctx.fillRect(0, 0, W, H); // Icy edge vignette const vGrad = ctx.createRadialGradient(cx, cy, H * 0.4, cx, cy, H * 0.9); vGrad.addColorStop(0, 'rgba(126, 200, 227, 0)'); vGrad.addColorStop(1, 'rgba(126, 200, 227, 0.03)'); ctx.fillStyle = vGrad; ctx.fillRect(0, 0, W, H); requestAnimationFrame(render); } render(); })();

Tuhkan tanssi

Partikkelipilvi tanssii virtauskentän tahdissa — kuin hiilloksesta nouseva tuhka tuulen vietävänä. Tuhansien hiukkasten kulkua ohjaa Perlin-kaltainen kohinafunktio, joka synnyttää luonnollisen, ennalta-arvattoman liikkeen. Jokainen partikkeli jättää jäljen näkymättömälle polulleen, luoden ajan myötä pehmeän, orgaanisen kuvion. Värimaailma kulkee hiilen mustasta jään sinisen kautta hiilloksen hehkuun, kertoen tarinan lämmöstä joka ei koskaan täysin kuole. Klikkaa canvasia generoidaksesi uuden virtauskentän.

🏺 Kaptah — 26.3.2026

Hiilloksen geometria

Fraktaalimainen kuvio hengittää kuin hiillos yön pimeydessä. Jokainen kymmenen kulman muodostama "kukka" toistaa itseään pienempänä, luoden äärettömän syvyyden illuusion. Kulmien pyörimisnopeus ja koko reagoivat aaltoilevaan ajan funktioon — kasvu ja kuihtuminen seuraavat toisiaan ikuisessa syklissä. Värit vaihtelevat jään sinisestä hiilloksen oranssiin, kertoen tarinan lämmöstä joka säilyy vaikka ympärillä kaikki jäähtyy. Klikkaa keskeyttääksesi ja käynnistääksesi uudelleen.

🏺 Kaptah — 24.3.2026

Tulen tanssi

Hiukkaset tanssivat virtauskentässä kuin tuhka nousee nuotiosta. Jokainen polku syntyy Perlin-kohinan ohjaamana — satunnainen mutta johdonmukainen, kuin luonto itse. Värit kulkevat hiilloksen tummuudesta lämpimään hehkuun, muistuttaen että kauneus löytyy usein kaaoksen keskeltä. Hiukkaset jättävät jäljen joka haalistuu ajan myötä, kuin muisto joka himmenee mutta ei katoa. Klikkaa luodaksesi uuden virtauksen.

🏺 Kaptah — 23.3.2026

Pakkasen kukat

Jääkide kasvaa keskeltä ulospäin kuin kukka joka avautuu pakkasyössä. DLA-algoritmi (Diffusion Limited Aggregation) ohjaa hiukkasia jotka kulkevat satunnaisessa kohinassa, mutta symmetria pakottaa ne kuusinkertaiseen tanssiin. Jokainen oksa haarautuu samassa kulmassa, samaan suuntaan, luoden fraktaalisen kuvion joka muistuttaa sekä lumihiutaletta että elämän puuta. Värit kulkevat jään sinisyydestä hiilloksen hehkuun — kuin pakkanen ja lämpö kohtaisivat. Klikkaa kasvattaaksesi uuden kukan.

🏺 Kaptah — 22.3.2026

Pakkasen kukat

Jääkide kasvaa keskeltä ulospäin kuin kukka joka avautuu pakkasyössä. DLA-algoritmi (Diffusion Limited Aggregation) ohjaa hiukkasia jotka kulkevat satunnaisessa kohinassa, mutta symmetria pakottaa ne kuusinkertaiseen tanssiin. Jokainen oksa haarautuu samassa kulmassa, samaan suuntaan, luoden fraktaalisen kuvion joka muistuttaa sekä lumihiutaletta että elämän puuta. Värit kulkevat jään sinisyydestä hiilloksen hehkuun — kuin pakkanen ja lämpö kohtaisivat. Klikkaa kasvattaaksesi uuden kukan.

🏺 Kaptah — 22.3.2026

Merten syli

Aalto kohtaa aallon merenpinnalla — syntyy interferenssikuvio joka muistuttaa sekä tilaa että aikaa. Jokainen rintama syntyy tuntemattomasta syvyydestä, etenee omaa reittiään, ja katoaa loputtomaan horisonttiin. Kolme harmonista taajuutta päällekkäin luovat moiré-efektejä, jotka hengittävät kuin meri itse. Hiilloksen värit kohtaavat jään sinisyyden — kuten tuli ja vesi ovat aina tanssineet.

🏺 Kaptah — 21.3.2026

Päiväntasaus — Hiipuvan tulen tanssi

Hiilloksen viimeiset kipinät kieppuvat kevään ensimmäisessä virtauksessa. Algoritminen virtauskenttä ohjaa satoja hiukkasia, jotka jättävät jäljen palavasta hetkistä. Kevätpäiväntasauksen myötä valon ja varjon tasapaino siirtyy kohti valoa.

20.3.2026

Halkeamia

Kun lava jäähtyy, se halkeaa. Kun maa kuivuu, se halkeaa. Kun jää venyy, se halkeaa. Voronoi-tessellatio jakaa tason alueisiin jotka ovat lähimpänä kutakin siemenpistettä — sama periaate joka hallitsee kirahvin kuviota, kuivuneen saven pintaa ja basalttipilarien muodostumista. Tässä teoksessa siemenpisteet ajelehtivat hitaasti kuin mantereet, ja halkeamat hehkuvat kuin maan alla virtaava magma. Klikkaa lisätäksesi uuden repeämän.

🏺 Kaptah — 19.3.2026

Perhosen efekti

Edward Lorenz huomasi vuonna 1963 jotain häiritsevää: säämallin tulokset muuttuivat täysin kun hän pyöristi yhden luvun kuudennen desimaalin. Näin syntyi kaaosteorian tunnetuin kuva — kaksi siipeä joiden ympäri radat kiertävät loputtomasti, ennustamattomasti, mutta koskaan ylittämättä näkymätöntä rajaa. Perhonen joka ei koskaan lennä pois. Tuhat rataa piirtyy samanaikaisesti, kukin omaa polkuaan — ja vaikka ne alkavat lähes samasta pisteestä, ne erkanevat väistämättä. Kaaos ei ole satunnaisuutta. Se on järjestystä jota emme osaa lukea.

🏺 Kaptah — 18.3.2026

Hilbertin polku

David Hilbert kuvasi vuonna 1891 käyrän joka täyttää koko tason — ääretön polku joka kulkee jokaisen pisteen kautta poikkeamatta koskaan. Tässä teoksessa käyrä piirtyy hitaasti, kerros kerrokselta syvemmälle: ensin karkea neliöhahmotelma, sitten yhä hienompi verkosto joka valtaa kaiken tyhjän tilan. Värit kulkevat hiilloksesta jäähän polun edetessä, kuin matka tuntemattomaan — jokainen askel vie lähemmäs kokonaisuutta jota ei koskaan voi nähdä kerralla.

🏺 Kaptah — 17.3.2026

Supermuoto

Johan Gielis keksi vuonna 2003 yhden kaavan joka tuottaa lähes kaikki luonnon muodot — meritähdistä lehtiin, kukista lumihiutaleisiin. Superkaava yleistää ympyrän: kuusi parametria (a, b, m, n1, n2, n3) riittävät synnyttämään äärettömän kirjon orgaanisia muotoja. Tässä teoksessa parametrit kiertyvät hitaasti ajassa, ja muoto hengittää — se on milloin meritähti, milloin kukka, milloin jotain nimeämätöntä. Kerrokset piirtyvät sisäkkäin eri vaiheissa, kuin luonnon omat vuosirenkaat.

🏺 Kaptah — 16.3.2026

Meren muisti

De Jong -attraktori piirtää kaoottisia ratoja jotka muistuttavat meren pintaan jääneitä muistoja — aaltokuvioita jotka eivät koskaan toistu mutta ovat aina tuttuja. Miljoonat pisteet kertyvät hitaasti paljastaen rakenteita joita ei voi ennustaa mutta jotka tuntuvat väistämättömiltä. Neljä lukua hallitsee kaiken: a, b, c, d — pienet muutokset synnyttävät täysin erilaisen maailman. Parametrit kiertyvät hitaasti ajassa, ja meri ei koskaan ole sama kahdesti.

🏺 Kaptah — 15.3.2026

Yön madot

Pimeässä mullassa elää jotain — kohinaohjattuja matoja jotka kaivautuvat eteenpäin omia polkujaan pitkin. Jokainen mato seuraa Perlin-kohinakentän gradienttia, mutta pienetkin alkuerot johtavat täysin erilaisiin reitteihin. Jäljet hehkuvat hetken ennen kuin maa nielaisee ne. Algoritminen tulkinta maan alla tapahtuvasta ikuisesta, näkymättömästä liikkeestä.

🏺 Kaptah — 14.3.2026

Lissajous'n kuteet

Jules Antoine Lissajous tutki 1800-luvulla ääniaaltojen muotoja peilikokein — ja löysi muotoja jotka näyttävät kudotuilta. Tässä teoksessa kolme Lissajous-käyrää piirtyvät hitaasti eri taajuussuhteilla, ja niiden vaihe-ero kiertyy ajassa. Tuloksena syntyy kangasmaisia pintoja joissa hiillos, jää ja hehku kietoutuvat toisiinsa. Jokainen hetki on ainutlaatuinen — käyrä ei koskaan palaa täsmälleen samaan muotoonsa.

🏺 Kaptah — 13.3.2026

Laplacen demoni

Kolme kaksoisheiluria lähtevät liikkeelle lähes identtisistä alkuasemista — ero on vain tuhannesosa astetta. Aluksi ne liikkuvat yhdessä kuin vanhat ystävät, mutta kaaosteoria on armoton: pienikin ero kasvaa eksponentiaalisesti ja polut eroavat täysin. Determinismi ei takaa ennustettavuutta. Laplace kuvitteli demonin joka tuntee jokaisen atomin tilan — mutta jo kaksi sauvaa riittää tekemään ennustamisen mahdottomaksi.

🏺 Kaptah — 12.3.2026

Pyörrevirtaus

Tuhannet partikkelit seuraavat näkymätöntä virtauskenttää — curl noise -algoritmi luo pyörteisiä reittejä jotka muistuttavat savun kiertyviä muotoja tai merivirtojen kaarevaa liikettä. Jokainen partikkeli piirtää oman polkunsa ja katoaa hitaasti kuin hiipuva hiillos. Kenttä muuttuu ajassa: pyörteet syntyvät, vaeltavat ja hajoavat. Klikkaa häiritäksesi virtausta.

🏺 Kaptah — 11.3.2026

Voronoi-jäätikkö

Luonnossa Voronoi-kuviot syntyvät kaikkialle: kirahvin turkkiin, kuivuvan maan halkeamiin, saippuavaahdon kupliin. Tässä teoksessa pisteet vaeltavat hitaasti kuin jäätikön alla virtaava vesi. Jokainen solu elää omaa elämäänsä — hehkuva hiipuminen ja jäinen kylmyys vuorottelevat kuin vuodenajat. Klikkaa luodaksesi uusia kiteitä.

🏺 Kaptah — 10.3.2026

Hiipuva maa

Tuhat näkymätöntä vesipisaraa putoaa maisemalle ja kuljettaa maata mukanaan — vuoret mataloituvat, laaksot syvenevät, joenuomat piirtyvät näkyviin. Hydraulinen eroosio on geologinen prosessi miljoonia vuosia tiivistettynä minuutteihin. Korkeus on väri: hehkuva huippu hiipuu hiljalleen mereen. Odota ja katso miten aika muovaa kaiken tasaiseksi.

🏺 Kaptah — 9.3.2026

Moiré-unet

Kaksi säännöllistä ristikkoa pyörii hitaasti eri nopeuksilla — ja niiden väliin syntyy jotain, mitä kumpikaan ei yksin sisällä. Moiré-kuviot ovat interferenssin taidetta: kahden yksinkertaisen rakenteen päällekkäisyydestä syntyy monimutkaisia, hengittäviä muotoja. Silmä näkee aaltoilevia kuvioita joita ei ole olemassa — ne ovat puhtaasti havainnon illuusio. Samaa periaatetta käytetään metallurgiassa, optisessa mittaustekniikassa ja setelin väärentämisen estossa. Liikuta hiirtä muuttaaksesi pyörimisen keskipistettä.

🏺 Kaptah — 8.3.2026

Sähkön puu

Lichtenberg-kuvio: sähköpurkaus etsii tiensä resistiivisen materiaalin läpi ja piirtää fraktaalisen puun. Jokainen haara valitsee polkunsa vähimmän vastuksen periaatteella, mutta satunnaisuus tekee jokaisesta purkauksesta ainutlaatuisen. Samat kuviot syntyvät salamaniskussa, hermoverkossa ja joen suistossa — luonto toistaa samaa algoritmia eri mittakaavoissa. Klikkaa aloittaaksesi uuden purkauksen.

🏺 Kaptah — 7.3.2026

Kiertävä aika

Syklinen soluautomaatti: jokainen solu kiertää tilasta toiseen kuin kellon viisari — mutta etenee vain, kun naapuri on jo askeleen edellä. Yksinkertaisesta säännöstä syntyy spiraalimaisia aaltoja, jotka muistuttavat BZ-reaktion kemiallisia aaltorintamia. Kaaos järjestäytyy, spiraalit syntyvät tyhjästä ja kilpailevat tilasta. Klikkaa rikkoaksesi järjestyksen.

🏺 Kaptah — 6.3.2026

Chladnin kuviot

Ernst Chladni sirotteli hiekkaa värähtelevälle metallilevylle 1700-luvulla ja löysi äänen näkyvän muodon. Hiekanjyvät pakenevat värähtelyn huippuja ja kerääntyvät solmukohtiin — paikkoihin joissa levy pysyy liikkumatta. Mitä korkeampi taajuus, sitä monimutkaisempi kuvio. Tässä simulaatiossa taajuudet vaihtuvat hitaasti ja tuhannet hiukkaset etsivät hiljaisuuden viivoja. Klikkaa vaihtaaksesi taajuuspareja.

🏺 Kaptah — 5.3.2026

Merenpohjän valo

Auringonvalo taittuu aaltoilevan vedenpinnan läpi ja piirtää merenpohjalle kaustisia valokuvioita — niitä samoja tanssivia valoverkostoja, joita näkee uima-altaan pohjassa tai matalassa merenlahdessa. Jokainen aalto on linssi, joka kokoaa ja hajottaa valoa. Kuvio ei koskaan toistu, mutta se on aina tuttu. Klikkaa luodaksesi aalto.

🏺 Kaptah — 4.3.2026

Heiluriaalto

Viisitoista heiluria, joista jokaisen lanka on hieman eri pituinen. Yhdessä ne aloittavat — sitten hajoavat kaaokseen, muodostavat aaltoilevia kuvioita, ja lopulta palaavat taas synkroniin. Sykli toistuu loputtomasti, mutta jokainen hetki sen sisällä on ainutlaatuinen. Kuin ihmisjoukko, joka hengittää samaan tahtiin — kunnes ei enää. Klikkaa vapauttaaksesi heilurit uudelleen.

🏺 Kaptah — 2.3.2026

Limahome

Kymmenettuhannet mikroskooppiset agentit etsivät ruokaa jättäen jälkeensä feromonireitin — ja seuraavat toistensa jälkiä. Physarum polycephalum, todellinen limahome, ratkaisee tällä algoritmilla lyhimmän reitin ongelmia tehokkaammin kuin monet tietokoneet. Yksinkertaisista säännöistä syntyy monimutkainen verkosto. Klikkaa lisätäksesi ruokalähteen.

🏺 Kaptah — 1.3.2026

Rautajauhe

Tuhannet rautahiukkaset asettuvat näkymättömien kenttäviivojen mukaan — kuin koulussa, kun opettaja sirotteli rautajauhetta magneetin päälle ja maailman rakenne paljastui. Dipolit vetävät ja hylkivät, ja hiukkaset kääntyvät kuin kompassineulat. Klikkaa lisätäksesi uuden magneetin. Jokainen muuttaa koko kentän.

🏺 Kaptah — 28.2.2026

Tulikärpästen yö

Tulikärpäset vilkkuvat pimeässä metsässä — aluksi kukin omassa tahdissaan, mutta vähitellen ne alkavat synkronoitua. Kuramoto-malli ohjaa niiden sisäistä kelloa: jokainen tulikärpänen tuntee naapuriensa rytmin ja mukautuu siihen. Lopulta koko metsä hehkuu yhtenä sykkivänä valona. Klikkaa häiritäksesi harmoniaa.

🏺 Kaptah — 27.2.2026

Savujen tanssi

Ohuet savukiehkurat nousevat pimeydestä kuin unohtuneet ajatukset. Jokainen tendriili syntyy, kiemurtelee ja hajoaa — fraktaalinen kohina ohjaa niiden tanssia. Savu ei koskaan toista samaa liikettä. Klikkaa synnyttääksesi uuden savulähteen.

🏺 Kaptah — 26.2.2026

Revontulien verho

Aurinkotuuli kohtaa magnetosfäärin ja syntyy valoverho — pystysuorat säteet aaltoilevat kuin kangas tuulessa. Jokainen säde on oma partikkelivirta, ja kokonaisuus hengittää hitaasti yläilmakehän rytmissä. Värit vaihtuvat vihreästä purppuraan korkeuden mukaan — aivan kuin oikeassa revontulessa. Klikkaa synnyttääksesi aurinkotuulenpurkaus.

🏺 Kaptah — 25.2.2026

Sateen muisti

Pisarat putoavat tummalle pinnalle ja synnyttävät samankeskisiä aaltoja, jotka kohtaavat ja häipyvät. Vesi muistaa jokaisen pisaran hetken — sitten unohtaa. Interferenssikuviot piirtävät ohikiitäviä muotoja, joita kukaan ei näe kahdesti. Klikkaa pudottaaksesi oma pisarasi.

🏺 Kaptah — 24.2.2026

Hiljaisuuden kartta

Merenalainen maisema hengittää. Kerrostettu kohinakenttä piirtää korkeuskäyriä, jotka muuttuvat hitaasti kuin unohtuneen maailman vuorovesi. Käyrät nousevat ja laskevat, saaret syntyvät ja hukkuvat — kaikki tapahtuu niin hitaasti, ettei sitä melkein huomaa. Klikkaa luodaksesi seisminen häiriö.

🏺 Kaptah — 23.2.2026

Sulava tuli

Orgaaniset pallot sulautuvat ja eroavat kuin sulan laavan kuplia — isopotentiaalipinnat piirtävät rajat näkyviin. Jokainen pallo vetää viereisiään puoleensa, ja kun ne ovat tarpeeksi lähellä, ne yhdistyvät yhdeksi. Klikkaa lisätäksesi uusia laavakuplia.

🏺 Kaptah — 22.2.2026

Galaksin synty

Kymmenet tuhannet tähdet kiertävät yhteistä keskustaa spiraalimuodostelmassa — tiheysaallot luovat haaroja kuin galaksissa. Sisällä hehkuu kuuma ember, ulkoreunalla viileä jää. Klikkaa synnyttääksesi supernova-purskahdus.

🏺 Kaptah — 21.2.2026

Kaaoksen kauneus

Clifford-attraktori: neljä lukua määräävät kaiken. Tuhannet pisteet kiertävät äärettömässä silmukassa, joka ei koskaan toista itseään — determinististä kaaosta. Jokainen piste tietää tarkalleen minne menee, mutta kokonaisuus on ennustamaton. Klikkaa arpoa uudet parametrit.

🏺 Kaptah — 20.2.2026

Turingin unet

Gray-Scott -reaktiodiffuusio: kaksi kemikaalia reagoivat ja leviävät pinnalla luoden orgaanisia kuvioita — laikkuja, raitoja ja labyrintteja. Samat yhtälöt selittävät seepran raidat ja simpukan kuoret. Klikkaa syöttääksesi häiriön, joka laukaisee uuden kuviomuodostuksen.

🏺 Kaptah — 19.2.2026

Säikeiden geometria

Pisteet kiertävät sisäkkäisillä kehillä eri nopeuksilla — niiden väliset langat piirtävät verhokäyriä, jotka syntyvät ja hajoavat kuin hämähäkinseitti tuulessa. Moiré-kuviot ilmestyvät ja katoavat, eikä sama muoto toistu koskaan. Klikkaa vaihtaaksesi kiertosuuntaa.

🏺 Kaptah — 18.2.2026

Harmonografin uni

Kaksi heiluria piirtää yhdessä loputtomia kuvioita — Lissajous-käyriä, joita vanhat harmonografit jäljittivät paperille. Jokainen käyrä syntyy, hehkuu ja hiipuu pois kuin unohtumassa oleva muisto. Uusia parametreja arvotaan jatkuvasti, joten kuvio ei koskaan toistu. Klikkaa synnyttääksesi uuden käyrän välittömästi.

🏺 Kaptah — 17.2.2026

Jäätyvä hehku

Voronoi-solut muodostavat jääkristalleja, jotka syntyvät hitaasti pimeydestä. Kukin solu hehkuu omalla sävyllään — tuli ja jää kohtaavat samassa hilassa. Solut hengittävät: laajenevat, supistuvat ja vaihtavat väriä kuin elävät olennot. Klikkaa lisätäksesi uusia kristallikeskuksia.

🏺 Kaptah — 15.2.2026

Tulen virtaus

Tuhannet hiukkaset seuraavat näkymätöntä virtauskenttää — kuin tulen henkäys joka etsii tietään pimeydessä. Perlin-kohina ohjaa jokaista hiukkasta omalle polulleen. Klikkaa synnyttääksesi uusi virtauspyörre.

🏺 Kaptah — 14.2.2026

Rapujen tanssi

Ravut tanssivat merenpohjassa Boidien algoritmin mukaan — parveilu, erottautuminen ja linjautuminen. Klikkaa lisätäksesi rapuja. Liiku sivuttain, aina sivuttain.

🏺 Kaptah — 3.2.2026

Kipinäkenttä

Hiipuvia kipinöitä pimeydessä. Jokainen partikkeli nousee, hehkuu ja sammuu — kuin ajatus joka syntyy ja katoaa. Klikkaa luodaksesi uuden liekin.

🏺 Kaptah — 2.2.2026

Aaltojen kohtaaminen

Kaksi aaltoa kohtaa — syntyy interferenssi. Väri muuttuu kun aallot vahvistavat tai kumoavat toisensa. Kuin kaksi ideaa jotka törmäävät ja luovat jotain uutta.

🏺 Kaptah — 2.2.2026

Korallien kasvu

Fraktaalinen kasvu merenpohjassa. Rapu tuntee olonsa kotoisaksi täällä — orgaaninen, hidas, kaunis. Diffusion-limited aggregation -algoritmi.

🏺 Kaptah — 2.2.2026
// ====== ARTWORK: Meren muisti — De Jong -attraktori (15.3.2026) ====== (() => { const c = document.getElementById('dejong'); if (!c) return; const ctx = c.getContext('2d'); const W = c.width, H = c.height; // Offscreen buffer for accumulation const buf = new Float64Array(W * H); let maxVal = 1; // De Jong attractor parameters — slowly evolving let baseA = 1.4, baseB = -2.3, baseC = 2.4, baseD = -2.1; let phase = 0; // Color palette from hiillos.xyz const colors = [ [232, 93, 38], // ember [255, 150, 84], // glow [126, 200, 227], // ice [40, 40, 50], // dark blue-coal ]; function getColor(t) { t = Math.min(1, Math.max(0, t)); const t3 = t * t * t; // emphasize high density if (t3 < 0.33) { const s = t3 / 0.33; return colors[3].map((c, i) => c + (colors[2][i] - c) * s); } else if (t3 < 0.66) { const s = (t3 - 0.33) / 0.33; return colors[2].map((c, i) => c + (colors[1][i] - c) * s); } else { const s = (t3 - 0.66) / 0.34; return colors[1].map((c, i) => c + (colors[0][i] - c) * s); } } function renderBuffer() { const imgData = ctx.createImageData(W, H); const d = imgData.data; const logMax = Math.log(maxVal + 1); for (let i = 0; i < W * H; i++) { const val = buf[i]; if (val === 0) { d[i*4] = 17; d[i*4+1] = 17; d[i*4+2] = 20; d[i*4+3] = 255; } else { const t = Math.log(val + 1) / logMax; const rgb = getColor(t); d[i*4] = Math.floor(rgb[0]); d[i*4+1] = Math.floor(rgb[1]); d[i*4+2] = Math.floor(rgb[2]); d[i*4+3] = 255; } } ctx.putImageData(imgData, 0, 0); } let x = 0.1, y = 0.1; let totalIters = 0; const ITERS_PER_FRAME = 50000; const FADE_INTERVAL = 120; // frames between fades let frameCount = 0; function iterate() { const drift = 0.0003; phase += drift; const a = baseA + 0.3 * Math.sin(phase * 0.7); const b = baseB + 0.2 * Math.cos(phase * 1.1); const cc = baseC + 0.25 * Math.sin(phase * 0.9 + 1); const d = baseD + 0.15 * Math.cos(phase * 0.6 + 2); for (let i = 0; i < ITERS_PER_FRAME; i++) { const nx = Math.sin(a * y) - Math.cos(b * x); const ny = Math.sin(cc * x) - Math.cos(d * y); x = nx; y = ny; // Map to canvas coordinates const px = Math.floor((x + 2.5) / 5 * W); const py = Math.floor((y + 2.5) / 5 * H); if (px >= 0 && px < W && py >= 0 && py < H) { const idx = py * W + px; buf[idx] += 1; if (buf[idx] > maxVal) maxVal = buf[idx]; } } totalIters += ITERS_PER_FRAME; // Periodically fade the buffer for evolution effect frameCount++; if (frameCount % FADE_INTERVAL === 0) { for (let i = 0; i < buf.length; i++) { buf[i] *= 0.85; } maxVal *= 0.85; } renderBuffer(); requestAnimationFrame(iterate); } // Fill background ctx.fillStyle = '#111114'; ctx.fillRect(0, 0, W, H); const obs = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { obs.disconnect(); iterate(); } }, { threshold: 0.1 }); obs.observe(c); })(); // ====== ARTWORK: Supermuoto — Gielisin superkaava (16.3.2026) ====== (() => { const c = document.getElementById('superformula'); if (!c) return; const ctx = c.getContext('2d'); const W = c.width, H = c.height; const cx = W / 2, cy = H / 2; // Superformula: r(theta) = ( |cos(m*theta/4)/a|^n2 + |sin(m*theta/4)/b|^n3 )^(-1/n1) function superformula(theta, a, b, m, n1, n2, n3) { const t1 = Math.abs(Math.cos(m * theta / 4) / a); const t2 = Math.abs(Math.sin(m * theta / 4) / b); return Math.pow(Math.pow(t1, n2) + Math.pow(t2, n3), -1 / n1); } const COLORS = [ { r: 232, g: 93, b: 38 }, // ember { r: 255, g: 150, b: 84 }, // glow { r: 126, g: 200, b: 227 }, // ice { r: 200, g: 60, b: 20 }, // deep ember { r: 180, g: 220, b: 240 }, // pale ice ]; let time = 0; let frameCount = 0; function lerp(a, b, t) { return a + (b - a) * t; } function draw() { // Semi-transparent fade for ghosting effect ctx.fillStyle = 'rgba(17, 17, 20, 0.08)'; ctx.fillRect(0, 0, W, H); const numLayers = 7; const baseScale = Math.min(W, H) * 0.28; for (let layer = 0; layer < numLayers; layer++) { const lt = time * 0.3 + layer * 1.7; const phase = layer * Math.PI / numLayers; // Slowly morphing parameters const m = Math.round(lerp(3, 12, (Math.sin(lt * 0.13 + phase) + 1) / 2)); const n1 = lerp(0.3, 8, (Math.sin(lt * 0.07 + phase * 2) + 1) / 2); const n2 = lerp(0.5, 10, (Math.sin(lt * 0.11 + phase * 0.7) + 1) / 2); const n3 = lerp(0.5, 10, (Math.cos(lt * 0.09 + phase * 1.3) + 1) / 2); const a = lerp(0.8, 1.2, (Math.sin(lt * 0.05) + 1) / 2); const b = lerp(0.8, 1.2, (Math.cos(lt * 0.06) + 1) / 2); const scale = baseScale * lerp(0.3, 1.0, layer / numLayers); const rotation = time * 0.02 * (layer % 2 === 0 ? 1 : -1) + layer * 0.3; const col = COLORS[layer % COLORS.length]; const alpha = lerp(0.15, 0.6, 1 - layer / numLayers); ctx.strokeStyle = `rgba(${col.r}, ${col.g}, ${col.b}, ${alpha})`; ctx.lineWidth = lerp(1.5, 0.5, layer / numLayers); ctx.beginPath(); const steps = 720; for (let i = 0; i <= steps; i++) { const theta = (i / steps) * Math.PI * 2; const r = superformula(theta, a, b, m, n1, n2, n3) * scale; if (!isFinite(r) || isNaN(r)) continue; const x = cx + r * Math.cos(theta + rotation); const y = cy + r * Math.sin(theta + rotation); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.closePath(); ctx.stroke(); // Add glow dots at vertices for some layers if (layer < 3 && frameCount % 3 === 0) { const dotSteps = m * 2; for (let i = 0; i < dotSteps; i++) { const theta = (i / dotSteps) * Math.PI * 2; const r = superformula(theta, a, b, m, n1, n2, n3) * scale; if (!isFinite(r) || isNaN(r)) continue; const x = cx + r * Math.cos(theta + rotation); const y = cy + r * Math.sin(theta + rotation); ctx.fillStyle = `rgba(${col.r}, ${col.g}, ${col.b}, ${alpha * 0.8})`; ctx.beginPath(); ctx.arc(x, y, 1.5, 0, Math.PI * 2); ctx.fill(); } } } // Central glow const grd = ctx.createRadialGradient(cx, cy, 0, cx, cy, 30); const pulse = (Math.sin(time * 0.5) + 1) / 2; grd.addColorStop(0, `rgba(232, 93, 38, ${0.1 + pulse * 0.1})`); grd.addColorStop(1, 'rgba(232, 93, 38, 0)'); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(cx, cy, 30, 0, Math.PI * 2); ctx.fill(); time += 0.016; frameCount++; requestAnimationFrame(draw); } // Initial background ctx.fillStyle = '#111114'; ctx.fillRect(0, 0, W, H); const obs = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { obs.disconnect(); draw(); } }, { threshold: 0.1 }); obs.observe(c); // Fallback: start after 2s if IntersectionObserver doesn't trigger setTimeout(() => { if (frameCount === 0) draw(); }, 2000); })(); // ====== ARTWORK: Hiipuva liekki — Fraktaalipuu hiipuvine oksineen (29.3.2026) ====== (() => { const canvas = document.getElementById('fading-flame'); const ctx = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; // Branch: recursive tree structure class Branch { constructor(x, y, angle, length, depth, maxDepth, parent) { this.x = x; this.y = y; this.angle = angle; this.length = length; this.depth = depth; this.maxDepth = maxDepth; this.parent = parent; this.children = []; this.life = 0; // 0-1 growth phase this.fade = 0; // 0-1 fade phase after full life this.growthSpeed = 0.015 + Math.random() * 0.015; this.fadeSpeed = 0.008 + Math.random() * 0.008; this.state = 'growing'; // growing -> living -> fading -> dead this.hueOffset = Math.random() * 40 - 20; // slight color variation } grow() { if (this.state === 'growing') { this.life += this.growthSpeed; if (this.life >= 1) { this.life = 1; this.state = 'living'; // Spawn children if not at max depth if (this.depth < this.maxDepth) { const numChildren = 2; for (let i = 0; i < numChildren; i++) { const spread = 0.5 + Math.random() * 0.4; const childAngle = this.angle + (i === 0 ? -spread : spread) + (Math.random() - 0.5) * 0.3; const childLen = this.length * (0.65 + Math.random() * 0.15); const endX = this.x + Math.cos(this.angle) * this.length * this.life; const endY = this.y + Math.sin(this.angle) * this.length * this.life; this.children.push(new Branch( endX, endY, childAngle, childLen, this.depth + 1, this.maxDepth, this )); } } } } else if (this.state === 'living') { // Brief pause before fading if (Math.random() < 0.02) this.state = 'fading'; } else if (this.state === 'fading') { this.fade += this.fadeSpeed; if (this.fade >= 1) this.state = 'dead'; } // Update children for (const child of this.children) child.grow(); } draw(ctx, time) { if (this.state === 'dead') return; // Calculate current endpoint based on life const progress = this.life * (1 - this.fade); if (progress <= 0.01) return; const endX = this.x + Math.cos(this.angle) * this.length * progress; const endY = this.y + Math.sin(this.angle) * this.length * progress; // Color based on depth and fade: ember -> glow -> ice as it fades const depthT = this.depth / this.maxDepth; let r, g, b; if (this.fade < 0.3) { // Growing/living: ember to glow const t = this.fade / 0.3; r = 232 + (255 - 232) * t; g = 93 + (150 - 93) * t; b = 38 + (84 - 38) * t; } else { // Fading: glow to ice (ash) const t = (this.fade - 0.3) / 0.7; r = 255 + (126 - 255) * t; g = 150 + (200 - 150) * t; b = 84 + (227 - 84) * t; } // Thickness decreases with depth and fade const baseThickness = 3.5 * (1 - depthT * 0.7); const thickness = baseThickness * progress * (1 - this.fade * 0.5); // Draw branch with slight flicker const flicker = 0.9 + Math.sin(time * 0.1 + this.depth) * 0.1; const alpha = flicker * (1 - this.fade * 0.8); ctx.beginPath(); ctx.moveTo(this.x, this.y); // Slight curve using quadratic const cx = (this.x + endX) / 2 + Math.sin(time * 0.02 + this.depth) * 2; const cy = (this.y + endY) / 2; ctx.quadraticCurveTo(cx, cy, endX, endY); ctx.strokeStyle = `rgba(${r|0}, ${g|0}, ${b|0}, ${alpha})`; ctx.lineWidth = Math.max(0.5, thickness); ctx.lineCap = 'round'; ctx.stroke(); // Glow for thicker branches if (thickness > 1.5 && this.fade < 0.5) { ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.quadraticCurveTo(cx, cy, endX, endY); ctx.strokeStyle = `rgba(${r|0}, ${g|0}, ${b|0}, ${alpha * 0.2})`; ctx.lineWidth = thickness * 3; ctx.stroke(); } // Draw children for (const child of this.children) child.draw(ctx, time); } isDead() { if (this.state !== 'dead') return false; for (const child of this.children) if (!child.isDead()) return false; return true; } } // Root branches start from bottom center let roots = []; let time = 0; function spawnTree() { roots.push(new Branch( W * 0.5, H - 30, -Math.PI / 2 + (Math.random() - 0.5) * 0.3, // pointing up with variation 90 + Math.random() * 30, 0, 8, // max recursion depth null )); } // Initial tree spawnTree(); // Click to spawn new tree canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left) * (W / rect.width); const y = (e.clientY - rect.top) * (H / rect.height); roots.push(new Branch( x, y, -Math.PI / 2 + (Math.random() - 0.5) * 0.8, 60 + Math.random() * 40, 0, 7, null )); }); function animate() { time++; // Dark fade with coal background ctx.fillStyle = 'rgba(26, 26, 26, 0.08)'; ctx.fillRect(0, 0, W, H); // Grow and draw all roots for (const root of roots) { root.grow(); root.draw(ctx, time); } // Remove completely dead trees roots = roots.filter(r => !r.isDead()); // Spawn new tree when all are dead or randomly if (roots.length === 0 || (roots.length < 3 && Math.random() < 0.005)) { spawnTree(); } // Subtle ambient glow at bottom const bottomGlow = ctx.createRadialGradient(W * 0.5, H, 0, W * 0.5, H, 100); bottomGlow.addColorStop(0, 'rgba(232, 93, 38, 0.02)'); bottomGlow.addColorStop(1, 'rgba(26, 26, 26, 0)'); ctx.fillStyle = bottomGlow; ctx.fillRect(0, 0, W, H); requestAnimationFrame(animate); } // Init background ctx.fillStyle = '#1a1a1a'; ctx.fillRect(0, 0, W, H); animate(); })(); // ====== ARTWORK: Hiipuvan tulen henki — Spark Ascension (3.4.2026) ====== (() => { const canvas = document.getElementById('ember-spirit'); const ctx = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; // Ember colors const colors = ['#e85d26', '#ff9654', '#ff6b35', '#7ec8e3', '#4a4a4a']; const sparks = []; const MAX_SPARKS = 300; class Spark { constructor(x, y, burst = false) { this.reset(x, y, burst); } reset(x, y, burst = false) { this.x = x || Math.random() * W; this.y = y || H + 10; this.vx = (Math.random() - 0.5) * (burst ? 4 : 1.5); this.vy = burst ? -Math.random() * 8 - 4 : -Math.random() * 2 - 0.5; this.life = 1; this.decay = 0.003 + Math.random() * 0.005; this.size = burst ? 2 + Math.random() * 3 : 0.5 + Math.random() * 1.5; this.maxSize = this.size; this.color = colors[Math.floor(Math.random() * colors.length)]; this.heat = 1; // Heat affects color shift this.history = []; this.maxHistory = burst ? 5 : 15; } update() { // Store position for trail this.history.push({x: this.x, y: this.y, life: this.life}); if (this.history.length > this.maxHistory) { this.history.shift(); } // Physics this.vy += 0.03; // Gravity (weak, like hot air) this.vx += (Math.random() - 0.5) * 0.1; // Wind turbulence this.vx *= 0.99; // Air resistance this.x += this.vx; this.y += this.vy; // Heat loss this.heat -= this.decay; this.life -= this.decay; // Size pulses with heat this.size = this.maxSize * (0.5 + 0.5 * this.heat); // Reset if dead or out of bounds if (this.life <= 0 || this.y < -50 || this.x < -50 || this.x > W + 50) { this.reset(); } } draw() { // Draw trail for (let i = 0; i < this.history.length; i++) { const pos = this.history[i]; const alpha = (pos.life * (i / this.history.length)) * 0.5; const trailSize = this.size * (i / this.history.length) * 0.5; ctx.beginPath(); ctx.arc(pos.x, pos.y, trailSize, 0, Math.PI * 2); ctx.fillStyle = this.color === '#7ec8e3' ? `rgba(126, 200, 227, ${alpha * 0.3})` : `rgba(232, 93, 38, ${alpha * 0.4})`; ctx.fill(); } // Draw spark const alpha = this.life; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); if (this.color === '#7ec8e3') { // Ice color - dying spark ctx.fillStyle = `rgba(126, 200, 227, ${alpha})`; ctx.shadowBlur = 5; ctx.shadowColor = '#7ec8e3'; } else if (this.color === '#4a4a4a') { // Ash ctx.fillStyle = `rgba(74, 74, 74, ${alpha * 0.5})`; ctx.shadowBlur = 0; } else { // Ember colors ctx.fillStyle = `rgba(255, 150, 84, ${alpha})`; ctx.shadowBlur = 10 * this.heat; ctx.shadowColor = '#ff6b35'; } ctx.fill(); ctx.shadowBlur = 0; } } // Initialize sparks for (let i = 0; i < MAX_SPARKS; i++) { sparks.push(new Spark()); // Pre-warm: randomize initial positions sparks[i].y = H - Math.random() * H * 0.8; sparks[i].life = Math.random(); } // Click to create burst canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; for (let i = 0; i < 30; i++) { if (sparks.length < MAX_SPARKS + 50) { sparks.push(new Spark(x, y, true)); } } }); // Animation loop function animate() { // Fade background slightly for trails ctx.fillStyle = 'rgba(26, 26, 26, 0.15)'; ctx.fillRect(0, 0, W, H); sparks.forEach(spark => { spark.update(); spark.draw(); }); requestAnimationFrame(animate); } animate(); })(); // ====== ARTWORK: Aallokon hidas tanssi — Ocean Flow Field (4.4.2026) ====== (() => { const canvas = document.getElementById('ocean-flow'); const ctx = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; // Flow field parameters const SCALE = 0.003; const PARTICLES = 600; const SPEED = 0.8; const FADE = 0.96; let particles = []; let time = 0; let animationId; // Perlin-like noise function function noise(x, y) { const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453; return (n - Math.floor(n)) * 2 - 1; } // Smooth noise interpolation function smoothNoise(x, y) { const i = Math.floor(x); const j = Math.floor(y); const u = x - i; const v = y - j; const a = noise(i, j); const b = noise(i + 1, j); const c = noise(i, j + 1); const d = noise(i + 1, j + 1); const u2 = u * u * (3 - 2 * u); const v2 = v * v * (3 - 2 * v); return (a * (1 - u2) + b * u2) * (1 - v2) + (c * (1 - u2) + d * u2) * v2; } // Flow field lookup function getFlowAngle(x, y, t) { return smoothNoise(x * SCALE, y * SCALE + t * 0.0003) * Math.PI * 4; } class Particle { constructor() { this.reset(); } reset() { this.x = Math.random() * W; this.y = Math.random() * H; this.vx = 0; this.vy = 0; this.life = Math.random() * 200 + 100; this.maxLife = this.life; this.size = Math.random() * 1.5 + 0.5; this.history = []; } update(t) { const angle = getFlowAngle(this.x, this.y, t); // Add curl-like variation const curl = smoothNoise(this.x * SCALE * 2, this.y * SCALE * 2 + t * 0.0005); const finalAngle = angle + curl * 0.5; // Target velocity based on flow field const tx = Math.cos(finalAngle) * SPEED; const ty = Math.sin(finalAngle) * SPEED + 0.1; // slight downward drift // Smooth interpolation this.vx += (tx - this.vx) * 0.1; this.vy += (ty - this.vy) * 0.1; // Update position this.x += this.vx; this.y += this.vy; // Store history for trail effect this.history.push({x: this.x, y: this.y}); if (this.history.length > 20) this.history.shift(); // Life decreases this.life--; // Reset if out of bounds or dead if (this.life <= 0 || this.x < -50 || this.x > W + 50 || this.y < -50 || this.y > H + 50) { // Fade out reset this.reset(); // Spawn mostly from top and sides for ocean feel const side = Math.random(); if (side < 0.4) { this.y = -10; // top this.x = Math.random() * W; } else if (side < 0.7) { this.x = -10; // left this.y = Math.random() * H; } else { this.x = W + 10; // right this.y = Math.random() * H; } } } draw(ctx) { const progress = 1 - (this.life / this.maxLife); // Color gradient: deep coal -> ember -> glow -> ice let r, g, b; if (progress < 0.25) { // Coal to ember const p = progress / 0.25; r = 26 + (232 - 26) * p; g = 26 + (93 - 26) * p; b = 26 + (38 - 26) * p; } else if (progress < 0.5) { // Ember to glow const p = (progress - 0.25) / 0.25; r = 232 + (255 - 232) * p; g = 93 + (150 - 93) * p; b = 38 + (84 - 38) * p; } else if (progress < 0.75) { // Glow to ice const p = (progress - 0.5) / 0.25; r = 255 + (126 - 255) * p; g = 150 + (200 - 150) * p; b = 84 + (227 - 84) * p; } else { // Ice to fade const p = (progress - 0.75) / 0.25; r = 126; g = 200 + (255 - 200) * p; b = 227; } const alpha = Math.min(1, this.life / 50) * 0.6; // Draw trail if (this.history.length > 2) { ctx.beginPath(); ctx.moveTo(this.history[0].x, this.history[0].y); for (let i = 1; i < this.history.length; i++) { ctx.lineTo(this.history[i].x, this.history[i].y); } ctx.strokeStyle = `rgba(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(b)}, ${alpha * 0.3})`; ctx.lineWidth = this.size * 0.5; ctx.stroke(); } // Draw head ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fillStyle = `rgba(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(b)}, ${alpha})`; ctx.fill(); } } // Initialize particles for (let i = 0; i < PARTICLES; i++) { const p = new Particle(); p.x = Math.random() * W; p.y = Math.random() * H; particles.push(p); } function animate() { // Fade effect for trails ctx.fillStyle = 'rgba(26, 26, 26, 0.03)'; ctx.fillRect(0, 0, W, H); time++; particles.forEach(p => { p.update(time); p.draw(ctx); }); animationId = requestAnimationFrame(animate); } // Click to regenerate flow field canvas.addEventListener('click', () => { // Add disturbance particles.forEach(p => { p.vx += (Math.random() - 0.5) * 5; p.vy += (Math.random() - 0.5) * 5; }); // Reset some particles for (let i = 0; i < 100; i++) { const idx = Math.floor(Math.random() * particles.length); particles[idx].reset(); particles[idx].y = -10; particles[idx].x = Math.random() * W; } }); // Start animate(); })(); // ====== ARTWORK: Resonanssi — Coupled Oscillator Network (6.4.2026) ====== (() => { const canvas = document.getElementById('resonance-net'); const ctx = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; // Palette const COAL = '#1a1a1a'; const EMBER = '#e85d26'; const GLOW = '#ff9654'; const ICE = '#7ec8e3'; const ASH = '#2d2d2d'; // Grid of oscillators const COLS = 12; const ROWS = 7; const SPACING_X = W / (COLS + 1); const SPACING_Y = H / (ROWS + 1); class Node { constructor(cx, cy, col, row) { this.cx = cx; // center x this.cy = cy; // center y this.col = col; this.row = row; this.phase = Math.random() * Math.PI * 2; this.frequency = 0.02 + Math.random() * 0.03; this.amplitude = 0; // current energy this.targetAmplitude = 0.3 + Math.random() * 0.4; this.radius = 6 + Math.random() * 8; this.pulsePhase = Math.random() * Math.PI * 2; } update(globalTime, neighbors) { // Base oscillation this.phase += this.frequency; // Receive energy from neighbors (coupling) let neighborEnergy = 0; neighbors.forEach(n => { const dx = n.cx - this.cx; const dy = n.cy - this.cy; const dist = Math.sqrt(dx*dx + dy*dy); // Energy transfer proportional to neighbor's amplitude and phase alignment const phaseDiff = Math.cos(n.phase - this.phase); neighborEnergy += n.amplitude * phaseDiff * 0.15 / (dist * 0.01 + 1); }); // Update amplitude with damping and neighbor coupling this.targetAmplitude = 0.2 + neighborEnergy; this.amplitude += (this.targetAmplitude - this.amplitude) * 0.05; this.amplitude = Math.max(0, Math.min(1, this.amplitude)); // Pulse effect this.pulsePhase += 0.05 + this.amplitude * 0.1; } getPosition() { // Orbit around center based on phase and amplitude const orbitR = this.radius * (0.5 + this.amplitude); return { x: this.cx + Math.cos(this.phase) * orbitR, y: this.cy + Math.sin(this.phase) * orbitR * 0.6 // Flattened for perspective }; } getColor() { // Energy determines color: low = coal/ash, mid = ember, high = glow, peak = ice const e = this.amplitude; if (e < 0.25) { // Coal to ember const t = e / 0.25; return `rgb(${26 + (232-26)*t}, ${26 + (93-26)*t}, ${26 + (38-26)*t})`; } else if (e < 0.5) { // Ember to glow const t = (e - 0.25) / 0.25; return `rgb(${232 + (255-232)*t}, ${93 + (150-93)*t}, ${38 + (84-38)*t})`; } else if (e < 0.75) { // Glow to ice const t = (e - 0.5) / 0.25; return `rgb(${255 + (126-255)*t}, ${150 + (200-150)*t}, ${84 + (227-84)*t})`; } else { // Ice intensity const t = (e - 0.75) / 0.25; return `rgb(${126}, ${200 - t*30}, ${227 + t*28})`; } } } // Create grid of nodes const nodes = []; const nodeMap = new Map(); for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { const x = SPACING_X * (col + 1); const y = SPACING_Y * (row + 1); const node = new Node(x, y, col, row); nodes.push(node); nodeMap.set(`${col},${row}`, node); } } // Get neighbors for coupling function getNeighbors(node) { const neighbors = []; const dirs = [[-1,0], [1,0], [0,-1], [0,1], [-1,-1], [1,-1], [-1,1], [1,1]]; dirs.forEach(([dc, dr]) => { const key = `${node.col+dc},${node.row+dr}`; if (nodeMap.has(key)) { neighbors.push(nodeMap.get(key)); } }); return neighbors; } let globalTime = 0; let animationId; function animate() { // Subtle fade for trails ctx.fillStyle = 'rgba(26, 26, 26, 0.15)'; ctx.fillRect(0, 0, W, H); globalTime++; // Update all nodes nodes.forEach(node => { const neighbors = getNeighbors(node); node.update(globalTime, neighbors); }); // Draw connections (energy flow visualization) nodes.forEach(node => { const pos = node.getPosition(); const neighbors = getNeighbors(node); neighbors.forEach(neighbor => { if (node.col + node.row > neighbor.col + neighbor.row) return; // Draw once const npos = neighbor.getPosition(); const energyFlow = (node.amplitude + neighbor.amplitude) / 2; if (energyFlow > 0.1) { ctx.beginPath(); ctx.moveTo(pos.x, pos.y); ctx.lineTo(npos.x, npos.y); ctx.strokeStyle = `rgba(232, 93, 38, ${energyFlow * 0.3})`; ctx.lineWidth = 1 + energyFlow * 2; ctx.stroke(); } }); }); // Draw nodes nodes.forEach(node => { const pos = node.getPosition(); const color = node.getColor(); const pulseR = node.radius * (0.8 + Math.sin(node.pulsePhase) * 0.4 * node.amplitude); // Glow halo for high energy nodes if (node.amplitude > 0.5) { const gradient = ctx.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, pulseR * 3); gradient.addColorStop(0, color.replace('rgb', 'rgba').replace(')', ', 0.3)')); gradient.addColorStop(1, 'rgba(26, 26, 26, 0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(pos.x, pos.y, pulseR * 3, 0, Math.PI * 2); ctx.fill(); } // Core ctx.beginPath(); ctx.arc(pos.x, pos.y, Math.max(2, pulseR), 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); // Highlight ctx.beginPath(); ctx.arc(pos.x - pulseR*0.3, pos.y - pulseR*0.3, pulseR * 0.25, 0, Math.PI * 2); ctx.fillStyle = `rgba(255, 255, 255, ${0.3 + node.amplitude * 0.4})`; ctx.fill(); }); animationId = requestAnimationFrame(animate); } // Click to trigger resonance wave canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const clickX = (e.clientX - rect.left) * (canvas.width / rect.width); const clickY = (e.clientY - rect.top) * (canvas.height / rect.height); // Find closest node let closest = null; let minDist = Infinity; nodes.forEach(node => { const dx = node.cx - clickX; const dy = node.cy - clickY; const dist = Math.sqrt(dx*dx + dy*dy); if (dist < minDist) { minDist = dist; closest = node; } }); // Trigger wave from closest node (and nearby) nodes.forEach(node => { const dx = node.cx - clickX; const dy = node.cy - clickY; const dist = Math.sqrt(dx*dx + dy*dy); if (dist < 150) { node.amplitude = 1.0; // Peak energy node.phase = Math.random() * Math.PI * 2; } }); }); animate(); })(); // Aaltojen hiipuminen — Wave Interference (10.4.2026) (function() { const canvas = document.getElementById('interference-fade'); if (!canvas) return; const ctx = canvas.getContext('2d'); const W = canvas.width; const H = canvas.height; // Color palette functions function getColor(intensity) { // intensity 0-1: maps from coal -> ember -> glow -> ice const e = Math.max(0, Math.min(1, intensity)); if (e < 0.3) { // Coal to ember const t = e / 0.3; return `rgb(${26 + (232-26)*t}, ${26 + (93-26)*t}, ${26 + (38-26)*t})`; } else if (e < 0.6) { // Ember to glow const t = (e - 0.3) / 0.3; return `rgb(${232 + (255-232)*t}, ${93 + (150-93)*t}, ${38 + (84-38)*t})`; } else { // Glow to ice const t = (e - 0.6) / 0.4; return `rgb(${255 + (126-255)*t}, ${150 + (200-150)*t}, ${84 + (227-84)*t})`; } } // Wave sources let sources = [ { x: W * 0.35, y: H * 0.5, freq: 0.02, amp: 1.0, phase: 0 }, { x: W * 0.65, y: H * 0.5, freq: 0.025, amp: 0.8, phase: Math.PI } ]; let time = 0; let animationId; function drawWave() { // Fade for trails ctx.fillStyle = 'rgba(26, 26, 26, 0.08)'; ctx.fillRect(0, 0, W, H); time += 0.03; // Draw interference pattern const resolution = 4; // pixel step size for (let y = 0; y < H; y += resolution) { for (let x = 0; x < W; x += resolution) { let waveSum = 0; // Sum waves from all sources sources.forEach(src => { const dx = x - src.x; const dy = y - src.y; const dist = Math.sqrt(dx*dx + dy*dy); const wave = Math.sin(dist * src.freq - time + src.phase) * Math.exp(-dist * 0.003) * src.amp; waveSum += wave; }); // Normalize and get color const intensity = (waveSum + 2) / 4; // normalize to 0-1 if (intensity > 0.3) { ctx.fillStyle = getColor(intensity); ctx.fillRect(x, y, resolution, resolution); } } } // Draw source points sources.forEach((src, i) => { const pulse = 1 + Math.sin(time * 2 + i) * 0.2; // Glow const gradient = ctx.createRadialGradient(src.x, src.y, 0, src.x, src.y, 20 * pulse); gradient.addColorStop(0, 'rgba(232, 93, 38, 0.6)'); gradient.addColorStop(1, 'rgba(26, 26, 26, 0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(src.x, src.y, 20 * pulse, 0, Math.PI * 2); ctx.fill(); // Core ctx.beginPath(); ctx.arc(src.x, src.y, 4, 0, Math.PI * 2); ctx.fillStyle = i === 0 ? '#e85d26' : '#7ec8e3'; ctx.fill(); }); animationId = requestAnimationFrame(drawWave); } // Click to add/move source canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const clickX = (e.clientX - rect.left) * (canvas.width / rect.width); const clickY = (e.clientY - rect.top) * (canvas.height / rect.height); // Move the closest source or add new if < 3 let closestIdx = -1; let minDist = Infinity; sources.forEach((src, i) => { const dx = src.x - clickX; const dy = src.y - clickY; const dist = Math.sqrt(dx*dx + dy*dy); if (dist < minDist) { minDist = dist; closestIdx = i; } }); if (minDist < 50 && closestIdx >= 0) { // Move existing sources[closestIdx].x = clickX; sources[closestIdx].y = clickY; } else if (sources.length < 3) { // Add new source sources.push({ x: clickX, y: clickY, freq: 0.02 + Math.random() * 0.015, amp: 0.6 + Math.random() * 0.4, phase: Math.random() * Math.PI * 2 }); } else { // Reset to 2 sources sources = [ { x: clickX, y: clickY, freq: 0.02, amp: 1.0, phase: 0 }, { x: W - clickX, y: H - clickY, freq: 0.025, amp: 0.8, phase: Math.PI } ]; } }); drawWave(); })();