Livecoding: Programació creativa en temps real

Visuals amb Hydra

Hydra és un sintetitzador de vídeo per a livecoding, escrit en JavaScript i executable directament des d’un navegador web.

El JavaScript és un llenguatge de programació que s’utilitza principalment per crear pàgines web interactives. Tots els navegadors web són capaços d’interpretar i executar codi JavaScript.

Internament, Hydra tradueix el nostre codi en un shader de WebGL, fet que permet executar efectes visuals complexos de forma eficient utilitzant la targeta gràfica de l’ordinador.

Un shader és un petit programa que s’executa directament a la targeta gràfica en comptes del processador principal. La targeta gràfica compta amb centenars o milers de petits processadors que poden executar el programa simultàniament per a cada píxel de la pantalla, cosa que fa que l’obtenció del resultat final sigui molt ràpida.

WebGL és una API (interfície de programació d’aplicacions) que permet crear gràfics 3D i 2D en un navegador utilitzant shaders.

Hydra s’inspira en el funcionament dels sintetitzadors modulars. En un sintetitzador modular, un mòdul produeix un senyal, i aquest senyal es condueix mitjançant cables cap a altres mòduls que el modifiquen, per acabar dirigint-lo a una sortida on es pot escoltar el resultat. En sistemes complexos, altres senyals es poden utilitzar per modificar paràmetres dins els mòduls, en un procés que s’anomena modulació, aconseguint que el so resultant sigui dinàmic i vagi mutant amb el temps.

Seguint aquesta lògica, Hydra distingeix tres tipus de funcions:

  • Generadores: creen un resultat visual inicial.

  • Transformadores: modifiquen una imatge existent.

  • De sortida: permeten mostrar el resultat final a la pantalla.

Primers exemples

Els exemples d’aquesta guia són interactius. Pots modificar el codi i executar-lo amb el botó ▶ o amb la combinació de tecles Ctrl + Enter.

El primer exemple senzill utilitza un generador, osc(), i l’envia a la sortida amb out():

osc().out()

Totes les funcions s’escriuen amb parèntesis al final del nom. Utilitzem el punt . per enllaçar la sortida d’una funció amb l’entrada de la següent.

La majoria de funcions poden rebre paràmetres per modificar-ne el comportament. Per exemple, podem canviar el valor 10 per altres nombres:

osc(10).out()

Aquest primer paràmetre d'osc() controla la freqüència de l’oscil·lador. Si no s’especifica, Hydra hi assigna un valor per defecte, en aquest cas 60.

osc() accepta fins a tres paràmetres: la freqüència (per defecte 60), la velocitat (0.1) i la desviació (0). La desviació separa les diferents components de color. Pots experimentar amb diferents valors, utilitzant velocitats negatives o augmentant progressivament la desviació:

osc(60, 0.1, 0.1).out()

En programació, s’utilitza la notació anglesa per als decimals: el punt . fa de separador decimal.

Els colors en un ordinador es formen combinant diferents intensitats de vermell, verd i blau (RGB). El paràmetre de desviació mou la component vermella cap a una banda i la blava cap a l’altra, mentre manté la verda fixa.

Un cop hem vist com funciona un generador com osc(), podem afegir transformacions. Per exemple, rotate() permet girar el patró generat.

rotate() rep dos paràmetres opcionals: l’angle inicial (en radiants) i la velocitat de rotació.

Prova a modificar els valors d'osc() o rotate() per veure’n el resultat:

osc(50,0).rotate(0, 0.2).out()

Pots utilitzar Math.PI per fer referència al nombre pi: osc().rotate(Math.PI/4).out()

Les transformacions es poden encadenar. Al següent exemple, apliquem una rotació i després un efecte de repetició (3 vegades en horitzontal i 3 en vertical):

osc(10).rotate(0,0.1).repeat(3,3).out()

L’ordre en què apliquem les transformacions és important:

osc(10).repeat(3,3).rotate(0,0.1).out()

Per escriure més ràpidament, pots ometre el zero inicial en decimals entre 0 i 1: osc(.1).out() en lloc de osc(0.1).out().

L’editor d’Hydra

L’editor oficial d’hydra és https://hydra.ojack.xyz/(en aquesta pàgina web).

Quan hi accedim, es carrega automàticament un patró visual aleatori i es mostra una finestra d’ajuda, que podem tancar clicant la icona .

Adreces de teclat

L’editor ofereix diverses dreceres per facilitar la codificació:

  • Ctrl+Enter: executa una línia de codi.

  • Ctrl+Shift+Enter: executa tot el codi del document.

  • Alt+Enter: executa un bloc de codi delimitat per línies en blanc (ideal per preparar diferents seccions).

  • Ctrl+Shift+H: amaga o mostra el codi per mostrar només el patró visual.

  • Ctrl+Shift+F: aplica format automàtic al codi.

  • Ctrl+Shift+S: fa una captura de pantalla del patró visual i permet descarregar el codi.

Interfície

A la part superior dreta hi ha diverses icones amb les següents funcionalitats:

  • circle play solid: executa tot el codi.

  • trash solid: neteja la pantalla, esborra el patró actual i tot el codi. Útil per eliminar el patró d’exemple que apareix quan entrem a l’editor i començar des de 0.

  • puzzle piece solid: permet afegir extensions a Hydra. Les extensions són funcions addicionals que han programat els usuaris i que permeten ampliar les funcionalitats de Hydra.

  • shuffle solid: carrega un patró aleatori. Aquests patrons són contribucions dels usuaris.

  • dice solid: fa un canvi aleatori al patró actual. Pot ser interessant per explorar noves possibilitats a partir d’un patró ja existent.

  • upload solid: afegeix el patró actual com a contribució a la llista de patrons aleatoris. Un diàleg demanarà un nom o descripció abans de realment compartir-lo.

  • circle question solid: mostra l’ajuda (la mateixa finestra que s’obre quan entrem a l’editor).

Errors

Si cometem un error de sintaxi, l’editor mostrarà un missatge d’error, però el patró anterior continuarà executant-se.

Per exemple, si escrivim ocs() en lloc de osc(), obtindrem el missatge: "ocs is not defined".

Comentaris

Podem comentar una línia escrivint // o comentar blocs complets amb / i /.

Els comentaris no s’executen. Poden servir per comunicar-nos amb el públic, preparar codi que s’executarà més endavant o documentar fragments.

Funcions generadores

A continuació veurem les funcions generadores que ens ofereix Hydra. Per a cadascuna, es mostra el nom, els paràmetres, i els seus valors per defecte (en cas que no l’especifiquem):

  • noise( scale = 10, offset = 0.1 ): genera soroll aleatori. scale és la mida dels píxels del soroll, i offset és la velocitat de canvi.

    noise(5, 0.3).out()
  • voronoi( scale = 5, speed = 0.3, blending = 0.3 ): un patró de Voronoi és una partició de l’espai en regions segons la proximitat a un conjunt de punts. A cada regió se li assigna un color. Scale és la mida, a més gran més particions, speed és la velocitat de canvi, i blending controla com de suaus són les transicions entre regions (amb 0 veurem perfectament definides les diferents àrees).

    voronoi(10, 0.3, 0.3).out()
  • osc( frequency = 60, sync = 0.1, offset = 0 ): genera un oscil·lador. frequency és la freqüència de l’oscil·lador, sync és la velocitat de canvi, i offset és la desviació dels colors.

  • shape( sides = 3, radius = 0.3, smoothing = 0.01 ): un polígon regular. sides és el nombre de costats, radius és el radi del polígon, i smoothing és la suavitat dels angles. Si posem sides igual a 2 obtenim una línia horitzontal. Podem simular una circumferència posant un nombre prou gran de costats, com 99.

    shape(5, 0.3, 0.01).out()
  • gradient( speed = 0): crea un gradient de color. speed és la velocitat de canvi. El gradient inicial té verd a la part inferior i vermell a la part dreta. A la cantonada inferior dreta obtenim groc (la combinació de vermell i verd), i a la cantonada superior esquerra, on no arriba cap dels dos colors, veiem negre. Si assignem una velocitat, la component blava de tots els punts anirà canviant amb el temps.

    gradient(0.1).out()
  • solid( r = 0, g = 0, b = 0, a = 1 ): crea un color sòlid. R, G i B són els components de color vermell, verd i blau, respectivament, i A és la transparència o canal alfa (1 és completament opac, i 0 completament transparent). Tots els valors van de 0 a 1.

    solid(0.6, 0.2, 0.8).out()
  • src( tex ): permet utilitzar una textura com a font de colors. Tex és la textura que volem utilitzar. Aquesta textura pot ser una imatge o un altre patró generat per Hydra. Estudiarem aquestes possibilitats amb detall més endavant.

Seqüenciació

Hem vist que algunes funcions tenen paràmetres que ja ens permeten generar patrons dinàmics. Però Hydra ens ofereix un sistema que ens permet crear seqüeǹcies de valors que es van repetint en el temps, i que es poden utilitzar en el lloc de qualsevol paràmetre.

Ho veurem clar amb un exemple:

shape([3,8,5,4]).out()

Podem veure com la quantitat de costats del polígon va canviant, seguint la seqüència que li hem passat com a paràmetre. La seqüència l’hem escrita com un array: una llista d’elements separats per comes i delimitada per claudàtors.

Podem fer el mateix amb qualsevol altre paràmetre:

voronoi(8, 0.3, [5,3,1,10]).out()

Els nostres arrays també accepten algunes transformacions:

  • fast( speed ): accelera la seqüència (o la frena si speed és menor a 1).

    shape([3,8,5,4].fast(4), [.2, 0.4, 0.6].fast(.37)).out()
  • smooth(): fa que el canvi entre els valors d’un array sigui gradual en comptes de sobtat:

    solid([0,1].smooth()).out()

Podem combinar les dues transformacions:

shape([3,8,3,12].fast(.2).smooth()).out()

Funcions transformadores

Hi ha moltes més funcions transformadores que no pas generadores. En veurem només algunes, però pots trobar la llista completa a la documentació oficial de Hydra.

Transformacions geomètriques

Aquestes funcions modifiquen les coordenades dels punts de la textura.

  • rotate( angle = 10, speed = 0 ): gira el patró. angle és l’angle en radiants, i speed és la velocitat de canvi.

  • scale( amount = 1.5, xMult = 1, yMult = 1, offsetX = 0.5, offsetY = 0.5 ): aplica un reescalat a la textura. amount és la proporció d’escalat: 1 manté la mida original, 0.5 la redueix a la meitat i 2 la duplica. xMult i yMult permeten escalar de forma diferent en horitzontal i vertical. offsetX i offsetY permeten canviar el centre de l’escalat (0.5 és el centre de la imatge, així que, per defecte, el reescalat es fa centrat).

  • repeat( repeatX = 3, repeatY = 3, offsetX = 0, offsetY = 0 ): repeteix la textura els cops que s’indiquin. repeatX i repeatY indiquen la quantitat de repeticions a l’eix X i a l’eix Y. offsetX i offsetY apliquen un desplaçament alternatiu: només afecta una de cada dues repeticions.

    shape(4).repeat().out()
    shape(4,.6).rotate(Math.PI/4).repeat(6,6,0.5).out()
  • repeatX( repeat = 3, offset = 0 ) i repeatY: com repeat, però només apliquen la repetició en un dels eixos.

    noise(1).repeatX(9,.5).out()
  • scroll( scrollX = 0.5, scrollY = 0.5, speedX = 0, speedY = 0 ): aplica un desplaçament a la textura. scrollX i scrollY és el desplaçament en cada eix. speedX i speedY s’utilitzen per moure dinàmicament la textura.

    shape().scroll(.2,.3).out()
    shape().scroll([0,.3,-.4],[.2,0,-.7].fast(1.21)).out()
    shape(3).scroll(0,0,.2,.2).out()
  • scrollX( scroll = 0.5, speed = 0 ) i scrollY: apliquen un desplaçament només en un eix.

  • kaleid( nSides = 4 ): aplica un efecte de calidoscopi: s’agafa la part de la textura que queda a la cantonada superior esquerra, se centra, i es repeteix en cercle tantes vegades com nSides.

    noise().kaleid(6).out()
    osc(4,-.1,1).kaleid(50).out()
  • pixelate( pixelX = 20, pixelY = 20 ): crea un efecte de pixelació: s’uneixen molts punts de la textura en un de sol, cosa que redueix la resolució de la textura. pixelX i pixelY són la quantitat de punts resultant en cada eix.

    noise().pixelate(10,10).out()
    voronoi(100).pixelate(10,50).kaleid(8).out()

Funcions de transformació de color

Hi ha moltes funcions a Hydra relacionades amb el color, en veurem algunes d’elles. Pots veure’n la llista completa a aquest apartat de la documentació oficial.

  • posterize( bins = 3, gamma = 0.6 ): redueix la quantitat de colors de la textura. bins és el nombre de colors resultants, i gamma és un paràmetre que controla la intensitat del color resultant.

    gradient(0.1).posterize(3, 0.6).out()
    osc(20,0.2,[0.8,2,5].fast(1.1))
    .posterize([2,10].smooth())
    .out()
  • invert( amount = 1 ): inverteix els colors de la textura. amount és la quantitat d’inversió (1 és totalment invertit, 0 és sense inversió).

    solid(0.2, 0.5, 0.8).invert([0,1]).out()
    gradient(0).invert([0,1].smooth()).out()
  • color( r = 1, g = 1, b = 1, a = 1 ): aplica un color a la textura. r, g i b són els components de color vermell, verd i blau, respectivament, i a és la transparència (1 és completament opac, i 0 completament transparent). Tots els valors van de 0 a 1.

    shape(5,.3,.8).color(1,.3,0).out()
    shape(5,.3,.8)
      .repeat(5,5)
      .scroll(0,0,.3,.3)
      .color([0,1].smooth(), .3, 0)
      .out()
  • colorama( amount = 0.005): aplica un efecte de color automàtic, més intens com més gran sigui amount. El color es converteix internament a l’espai HSV (Hue, Saturation, Value); després, se suma amount a cadascun dels components, i finalment se’n conserven només els decimals per mantenir-los dins el rang [0, 1].

    gradient()
      .colorama([-1,1].fast(.1).smooth())
      .out()

Sortides

Hydra, per defecte, té fins a quatre sortides que es poden utilitzar. Això ens permet crear patrons intermitjos, o utilitzar-los com a memòries temporals.

Les quatre sortides s’anomenen o0, o1, o2 i o3. Per defecte, out() envia el resultat a o0, i o0 és la sortida que es visualitza a la pantalla. Podem modificar la sortida d’una expressió indicant-la explícitament a out(), i podem utilitzar render() per mostrar per pantalla qualsevol de les quatre sortides. render() sense cap paràmetre mostrarà les quatre sortides alhora.

osc().out(o0)
voronoi().out(o1)
noise().out(o2)
gradient().out(o3)

render(o0)

Prova a modificar el paràmetre de render() per veure cadascuna de les sortides, o a no passar-li res per veure-les totes alhora.

La funció src() permet fer servir el contingut d’una de les sortides com a textura d’entrada. Per defecte, src() agafa el resultat de o0, però podem especificar quina sortida volem utilitzar.

voronoi().out(o1)
noise().out(o2)
gradient().out(o3)
src(o1).out()
render(o0)

En aquest exemple, prova de modificar el paràmetre de src().

render() modifica la configuració interna d’Hydra i es manté encara que l’esborrem i tornem a executar el codi. Per canviar la configuració hem d’utilitzar render() una altra vegada.

Barreja de patrons

Disposem de diverses funcions per combinar patrons entre ells.

  • blend( textura, amount = 0.5 ): barreja dues textures. Per cada punt, el resultat és la mitjana ponderada entre el valor de la primera textura i el valor de la segona. amount indica la proporció de cada textura que s’agafa, on 0 correspon només a la primera textura i 1 només a la segona.

    gradient().out(o0)
    voronoi().out(o1)
    src(o0).blend(o1, [0,.25,.5,.75,1]).out(o2)
    gradient().blend(voronoi(), [0,.25,.5,.75,1]).out(o3)
    render()

    En aquest exemple veiem:

    • o0: un gradient.

    • o1: el resultat de voronoi().

    • o2: el resultat de barrejar la sortida 0 amb la sortida 1, amb una proporció que va variant entre 0 i 1.

    • o3: el resultat de barrejar el gradient amb la textura de Voronoi, amb una proporció que va variant entre 0 i 1.

  • add ( textura, amount = 1): similar a blend(), cada punt és la suma de la primera textura sense modificar-se i la proporció de la segona textura indicada a amount.

    gradient().out(o0)
    voronoi().out(o1)
    src(o0).add(o1, [0,.25,.5,.75,1]).out(o2)
    gradient().add(voronoi(), [0,.25,.5,.75,1]).out(o3)
    render()

    És el mateix exemple d’abans, canviant blend() per add(). Noteu com aquí el gradient sempre és visible, i hi afegim el patró de Voronoi per sobre com si fos una capa lluminosa.

  • diff ( textura ): substrau la segona textura de la primera, amb la particularitat que si algun nombre queda per sota de 0, es resta d'1 per obtenir sempre nombres entre 0 i 1. Per exemple, si el color del primer punt és (0.4, 1, 0.8) i el color del segon punt és (0.6, 0.2, 0.4), el resultat serà (0.8, 0.8, 0.4). El primer 0.8 surt de 0.4-0.6 = -0.2, i com que és negatiu, 1-0.2=0.8.

    Aquesta manera de calcular evita que el resultat sigui massa fosc, i permet generar colors vius a partir de la diferència entre textures.

    gradient().out(o0)
    voronoi().out(o1)
    src(o0).diff(o1).out(o2)
    gradient().diff(voronoi()).out(o3)
    render()
  • layer ( textura ): és similar a blend(), però utilitza el canal alfa de la segona textura per determinar quina part d’aquesta s’ha de superposar sobre la primera.

    gradient().out(o0)
    voronoi().color(1,1,1,[0,.25,.5,.75,.1]).out(o1)
    src(o0).layer(o1).out(o2)
    gradient().layer(
      voronoi().color(1,1,1,[0,.25,.5,.75,.1])
    ).out(o3)
    render()
  • mask ( textura ): aplica una màscara a la primera textura, basada en la segona: si un punt és blanc, es mostra la primera textura tal qual; si és negre, es veu el negre; si és gris, s’atenua. En cas que la segona textura tingui color, es fa un càlcul previ de la lluminositat total per determinar el nivell de màscara a aplicar.

    gradient().out(o0)
    voronoi().out(o1)
    src(o0).mask(o1).out(o2)
    gradient().mask(voronoi()).out(o3)
    render()

Feedback

Ara que ja sabem com combinar textures, podem utilitzar la sortida d’un patró com a una de les textures d’entrada. Dit d’una altra manera, podem utilitzar el resultat d’un frame per construir el següent frame. Aquesta tècnica es coneix com a feedback o retroalimentació.

Un frame és la imatge que es mostra en pantalla en un instant donat. Els videos i animacions són una successió de frames que es mostren a una velocitat determinada, anomenada frame rate i que es mesura en frames per segon (FPS).

Les possibilitats artístiques del feedback són infinites, especialment si ho combinem amb les funcions de modulació que veurem al següent apartat.

Veiem algun exemple d’ús del feedback amb funcions que hem vist fins ara:

shape(3,.3,.8)
  .repeat(3)
  .diff(
    gradient(.6)
  ).diff(
    src(o0)
      .rotate([.04,-.04].smooth().fast(.3))
    , .9
  )
  .out()
shape(99,.5,.9)
  .blend(src(o0).repeat(4), 1)
  .diff(
    shape(240,.5,0)
      .scrollX(.05)
      .rotate(0,.3)
      .color([.1,.9].smooth().fast(.2),0,.7)
    ).diff(
      shape(99,.4,.002)
        .scrollY(.1)
        .rotate(0,-.2)
        .color(.6,0,[.1,.8].smooth().fast(.31)
    )
  )
.out()

En contextos visuals, el feedback pot generar patrons fractals, moviments espirals o formes orgàniques que evolucionen de manera autònoma. Jugar amb la rotació, la translació o l’escala de la textura de feedback pot donar lloc a composicions molt expressives.

Modulació

La modulació consisteix en modificar la geometria d’un patró utilitzant la informació continguda en un patró diferent. Per exemple, en comptes d’aplicar una rotació constant, podem fer que la rotació a cada punt depengui de la intensitat del punt corresponent d’un altre patró.

Hi ha moltes funcions de modulació a Hydra. Aquí només en veurem algunes, però pots experimentar amb qualsevol d’elles.

  • modulate ( textura, amount = 0.1 ): desplaça cada punt de la textura original depenent del color de la segona textura. Específicament, la component vermella de la segona textura determina el desplaçament horitzontal, i la verda, el vertical. amount és la quantitat de desplaçament que s’aplica.

    osc(20).out(o0)
    voronoi(5,.5,1.5).out(o1)
    src(o0).modulateRotate(o1).out(o2)
    osc(20).modulateRotate(voronoi(5,.5,1.5)).out(o3)
    render()
    osc(5)
      .blend(gradient(.3))
      .modulate(
        src(o0)
          .colorama(
            [0,1].smooth().fast(.1)
          )
        .rotate(.1),.5
      ).out()
  • modulateRotate ( texture, multiple = 1, offset = 0 ): com rotate, però la rotació de cada punt depèn del color de la segona textura (en particular, de la seva component vermella). multiple és la quantitat de rotació que s’aplica, i offset és una quantitat fixa de rotació que s’afegeix al resultat.

    osc(20).out(o0)
    voronoi(5,.5,1.5).out(o1)
    src(o0).modulateRotate(o1).out(o2)
    osc(20).modulateRotate(voronoi(5,.5,1.5)).out(o3)
    render()
    src(o0)
      .colorama(.1)
      .modulateRotate(src(o1),2)
      .layer(
        src(o1).diff(gradient(1)))
      .out(o0)
    
    noise(8,.2)
      .rotate(.2)
      .diff(src(o0)
      .kaleid(3))
      .out(o1)
  • modulateScale( texture, multiple = 1, offset = 0 ): funciona com modulateRotate(), però modifica l’escalat de cada punt en comptes de la rotació

    shape(5,.6,.1).out(o0)
    voronoi(5,.5,1.5).out(o1)
    src(o0).modulateRotate(o1).out(o2)
    shape(5,.6,.1).modulateRotate(voronoi(5,.5,1.5)).out(o3)
    render()
    shape(5,.2,.4)
      .repeat(10)
      .blend(o0)
      .kaleid(6)
      .modulateScale(o1,.2)
      .out()
    
    src(o0)
      .pixelate(50,50)
      .rotate(0,.02)
      .out(o1)

Fonts externes

A més de les funcions generadores que hem vist al principi, Hydra pot fer servir altres fonts com a textures d’entrada. Podem utilitzar la imatge d’una webcam, vídeos, o imatges externes.

Disposem de quatre entrades externes que podem configurar i utilitzar: s0, s1, s2 i s3. Cadascuna pot assignar-se a un recurs extern.

Càmeres

Per utilitzar una càmera web, hem d’inicialitzar-la amb la funció initCam(). Per exemple, s0.initCam() assignarà el flux de la càmera a s0. Després, el podem utilitzar com hem fet abans amb les sortides: src(s0).

Si disposem de més d’una càmera, podem indicar quina volem utilitzar amb un nombre: initCam(2).

Encara que parlem de webcam perquè és el cas més comú, es pot utilitzar qualsevol dispositiu físic o virtual que generi un flux continu d’imatge. Per exemple, un microscopi USB, o una càmera virtual configurada a OBS.

s0.initCam()
src(s0).colorama(.5).diff(noise()).out()

Imatges i videos

Podem utilitzar imatges i vídeos externs com a textures d’entrada. Per fer-ho, hem d’inicialitzar la textura amb initImage() o initVideo(), respectivament, indicant l’adreça del recurs.

Per motius de seguretat, el navegador web no pot accedir directament als fitxers del teu ordinador. Tot i que aquest problema es pot solucionar de diverses maneres, la seva relativa complexitat tècnica fa que no les tractem en aquest curs.

Molts serveis d’ús habitual per allotjar vídeos (com YouTube o Vimeo) no permeten accedir al seu contingut des d’altres pàgines web.

Un servei que sí que permet utilitzar els seus vídeos i imatges és Wikimedia Commons. Per fer-ho, necessitem l’adreça directa del fitxer que volem utilitzar:

s0.initImage("https://upload.wikimedia.org/wikipedia/commons/6/6b/Bronze-winged_jacana_%28Metopidius_indicus%29.jpg")
src(s0)
  .modulateRotate(osc(3))
  .posterize(8)
  .invert(.8)
  .out()
s0.initVideo("https://upload.wikimedia.org/wikipedia/commons/1/1c/Lightning_strikes_in_slow_motion_%28240fps%29_in_Muurame%2C_Finland.webm")
src(s0)
  .scroll(.5,.3)
  .kaleid(5)
  .add(o0,.7)
  .out()