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.
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
1 / 55
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.
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();
})();