Vakker kode er en glede å skrive, men det er vanskelig å dele den glede med andre programmerere, for ikke å nevne med ikke-programmører. I mitt fritid mellom min dagjobb og familietid har jeg spilt rundt med ideen om et programmeringsdikt med lerretelementet til å tegne i nettleseren. Det finnes en rekke vilkår der ute for å beskrive visuelle eksperimenter på datamaskinen, for eksempel dev art, kodeskisse, demo og interaktiv kunst, men til slutt slo jeg meg på programmeringsdiktet for å beskrive denne prosessen. Ideen bak et dikt er et polert stykke prosa som lett kan deles, konsist og estetisk. Det er ikke en halv ferdig ide i en skissebok, men et sammenhengende stykke presentert til seeren for deres fornøyelse. Et dikt er ikke et verktøy, men eksisterer for å fremkalle en følelse.

For min egen glede har jeg lest bøker om matte, beregning, fysikk og biologi. Jeg har lært veldig fort at når jeg drømmer på en idé, borer det folk ganske raskt. Visuelt kan jeg ta noen av disse ideene som jeg finner fascinerende, og raskt gi noen en følelse av rart, selv om de ikke forstår teorien bak koden og konseptene som driver den. Du trenger ikke et håndtak på noen hard filosofi eller matte for å skrive et programmeringsdikt, bare et ønske om å se noe leve og puste på skjermen.

Koden og eksemplene jeg har satt sammen nedenfor, vil starte en forståelse av hvordan du faktisk drar av denne raske og svært tilfredsstillende prosessen. Hvis du vil følge med koden du kan last ned kildefilene her.

Hovedtricket når du faktisk lager et dikt er å holde det lett og enkelt. Ikke bruk tre måneder på å bygge en veldig kul demo. I stedet lager 10 dikt som utvikler en ide. Skriv eksperimentell kode som er spennende, og vær ikke redd for å mislykkes.

Intro til lærred

For en rask oversikt er lerretet i hovedsak et 2d bitmap bildeelement som lever i DOM som kan trekkes på. Tegning kan gjøres ved hjelp av enten en 2d-kontekst eller en WebGL-kontekst. Konteksten er JavaScript-objektet du bruker for å få tilgang til tegneverktøyene. JavaScript-hendelsene som er tilgjengelige for lerret, er svært barebones, i motsetning til de tilgjengelige for SVG. Enhver hendelse som utløses, er for elementet som helhet, ikke noe som er trukket på lerretet, akkurat som et normalt bildeelement. Her er et grunnleggende lerreteksempel:

var canvas = document.getElementById('example-canvas');var context = canvas.getContext('2d');//Draw a blue rectanglecontext.fillStyle = '#91C0FF';context.fillRect(100, // x100, // y400, // width200 // height);//Draw some textcontext.fillStyle = '#333';context.font = "18px Helvetica, Arial";context.textAlign = 'center';context.fillText("The wonderful world of canvas", // text300, // x200 // y);

Det er ganske greit å komme i gang. Det eneste som kan være litt forvirrende, er at konteksten må konfigureres med innstillingene som fillStyle, lineWidth, font og strokeStyle før selve tegneanropet brukes. Det er lett å glemme å oppdatere eller tilbakestille disse innstillingene og få noen utilsiktede resultater.

Gjør ting flytte

Det første eksemplet løp bare en gang og trakk et statisk bilde på lerretet. Det er ok, men når det virkelig blir gøy, er det oppdatert med 60 bilder per sekund. Moderne nettlesere har den innebygde funksjonsforespørselenAnimationFrame som synkroniserer egendefinert tegningskode til tegningssyklusene i nettleseren. Dette bidrar til effektivitet og glatthet. Målet med en visualisering bør være kode som tåler sammen med 60 bilder per sekund.

(Et notat om støtte: Det finnes noen enkle polyfilger tilgjengelig hvis du trenger å støtte eldre nettlesere.)

var canvas = document.getElementById('example-canvas');var context = canvas.getContext('2d');var counter = 0;var rectWidth = 40;var rectHeight = 40;var xMovement;//Place rectangle in the middle of the screenvar y = ( canvas.height / 2 ) - ( rectHeight / 2 );context.fillStyle = '#91C0FF';function draw() {//There are smarter ways to increment time, but this is for demonstration purposescounter++;//Cool math below. More explanation in the text following the code.xMovement = Math.sin(counter / 25) * canvas.width * 0.4 + canvas.width / 2 - rectWidth / 2;//Clear the previous drawing resultscontext.clearRect(0, 0, canvas.width, canvas.height);//Actually draw on the canvascontext.fillRect(xMovement,y,rectWidth,rectHeight);//Request once a new animation frame is available to call this function againrequestAnimationFrame( draw );}draw();

Det er fint at det er litt mer intern struktur til koden, men det gjør egentlig ikke noe som er mye mer interessant. Det er der en sløyfe kommer inn. I sceneobjektet oppretter vi et nytt DotManager- objekt. Det er praktisk å samle denne funksjonaliteten i en egen gjenstand, da det er enklere og renere å argumentere med da flere og mer kompleksitet blir lagt til simuleringen.

var DotManager = function( numberOfDots, scene ) {this.dots = [];this.numberOfDots = numberOfDots;this.scene = scene;for(var i=0; i < numberOfDots; i++) {this.dots.push( new Dot(Math.random() * this.canvas.width,Math.random() * this.canvas.height,this.scene));}};DotManager.prototype = {update : function( dt ) {for(var i=0; i < this.numberOfDots; i++) {this.dots[i].update( dt );}}};

Nå i scenen, i stedet for å opprette og oppdatere en prikk , oppretter og oppdaterer vi DotManager . Vi lager 5000 punkter for å komme i gang.

function Scene() {...this.dotManager = new DotManager(5000, this);...};Scene.prototype = {...update : function( dt ) {this.dotManager.update( dt );}...};

For hver ny prikk opprettet, ta den innledende posisjonen, og sett fargen sin til hvor den ligger langs lerretets bredde. Funksjonen Utils.hslToFillStyle er en liten hjelpefunksjon som jeg har lagt til for å omforme noen inngangsvariabler i riktig formatert fillStyle- streng. Allerede ser ting ser mer spennende ut. Punkter vil etter hvert slå seg sammen og miste regnbueffekten etter at de har tid til å spre seg. Igjen, dette er et eksempel på kjørebilder med litt matematikk eller variable innganger. Jeg liker virkelig å gjøre farger med HSL-fargemodellen med generativ kunst i stedet for RGB på grunn av brukervennligheten. RGB er litt abstrakt.

Brukerinteraksjon med en mus

Det har ikke vært noen ekte brukerinteraksjon opp til dette punktet.

var Mouse = function( scene ) {this.scene = scene;this.position = new THREE.Vector2(-10000, -10000);$(window).mousemove( this.onMouseMove.bind(this) );};Mouse.prototype = {onMouseMove : function(e) {if(typeof(e.pageX) == "number") {this.position.x = e.pageX;this.position.y = e.pageY;} else {this.position.x = -100000;this.position.y = -100000;}}};

Dette enkle objektet inkapsler logikken til musoppdateringene fra resten av scenen. Den oppdaterer bare posisjonsvektoren på et musekryss. Resten av objektene kan da prøve fra musens posisjonsvektor hvis de blir sendt en referanse til objektet. En advarsel jeg ignorerer her er om lerretets bredde ikke er en til en med dimensjonens dimensjonsdimensjoner, det vil si et responsivt resizing lerret eller et høyeste pikseldensitet (retina) lerret eller om lerretet ikke er plassert på øverst til venstre. Koordinatene til musen må justeres tilsvarende.

var Scene = function() {...this.mouse = new Mouse( this );...};

Det eneste som var igjen for musen var å lage musen objektet inne i scenen. Nå som vi har en mus, la oss tiltrekke prikkene til den.

function Dot( x, y, scene ) {...this.attractSpeed = 1000 * Math.random() + 500;this.attractDistance = (150 * Math.random()) + 180;...}

Jeg har lagt til noen skalarverdier til prikken, slik at hver enkelt oppfører seg litt annerledes i simuleringen for å gi den litt realisme. Spill rundt med disse verdiene for å få en annen følelse. Nå videre til å tiltrekke musemetoden. Det er litt lenge med kommentarene.

attractMouse : function() {//Again, create some private variables for this methodvar vectorToMouse = new THREE.Vector2(),vectorToMove = new THREE.Vector2();//This is the actual public methodreturn function(dt) {var distanceToMouse, distanceToMove;//Get a vector that represents the x and y distance from the dot to the mouse//Check out the three.js documentation for more information on how these vectors workvectorToMouse.copy( this.scene.mouse.position ).sub( this.position );//Get the distance to the mouse from the vectordistanceToMouse = vectorToMouse.length();//Use the individual scalar values for the dot to adjust the distance movedmoveLength = dt * (this.attractDistance - distanceToMouse) / this.attractSpeed;//Only move the dot if it's being attractedif( moveLength > 0 ) {//Resize the vector to the mouse to the desired move lengthvectorToMove.copy( vectorToMouse ).divideScalar( distanceToMouse ).multiplyScalar( moveLength );//Go ahead and add it to the current position now, rather than in the draw callthis.position.add(vectorToMove);}};}()

Denne metoden kan være litt forvirrende hvis du ikke er oppdatert på vektormatningen din. Vektorer kan være veldig visuelle og kan hjelpe hvis du trekker noen scribbles ut på et kaffe farget papirskrap. På vanlig engelsk, får denne funksjonen avstanden mellom musen og prikken. Det beveger da prikken litt nærmere prikken basert på hvor nær det er til prikken og hvor lang tid det har gått. Det gjør dette ved å finne ut avstanden for å flytte (et normalt skalar nummer), og deretter multiplisere det med den normaliserte vektoren (en vektor med lengde 1) av punktpunktet som peker mot musen. Ok, den siste setningen var ikke nødvendigvis vanlig engelsk, men det er en start.