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.
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.
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();
Nå skal jeg omskrive formelen min fra forrige kodeeksempel som en mer brutt versjon som er enklere å lese.
var a = 1 / 25, //Make the oscillation happen a lot slowerx = counter, //Move along the graph a little bit each time draw() is calledb = 0, //No need to adjust the graph up or downc = width * 0.4, //Make the oscillation as wide as a little less than half the canvasd = canvas.width / 2 - rectWidth / 2; //Tweak the position of the rectangle to be centeredxMovement = Math.sin( a * x + b ) * c + d;
Hvis du vil leke med koden så langt, vil jeg foreslå å legge litt bevegelse i y-retningen. Prøv å endre verdiene i synd-funksjonen, eller bytte til en annen form for funksjon for å spille rundt og se hva som skjer.
Utover kjørebevegelse med matte, ta et øyeblikk til å forestille deg hva du kan gjøre med forskjellige brukerinnmatingsenheter for å flytte et firkant rundt en side. Det finnes alle mulige alternativer i nettleseren, inkludert mikrofon, webkamera, mus, tastatur og gamepad. Ekstra plugin-drevne alternativer er tilgjengelige med noe som Leap Motion eller Kinect. Ved hjelp av WebSockets og en server kan du koble til en visualisering til hjemmebygget maskinvare. Koble en mikrofon til Web Audio API og kjør pikslene med lyd. Du kan til og med bygge en bevegelsessensor ut av en webkamera og skremme en skole med virtuell fisk (ok jeg gjorde den siste i Flash for fem eller så år siden.)
Så nå som du har din store ide, la oss hoppe tilbake til noen flere eksempler. Ett torg er kjedelig, la oss gå opp ante. Først av, la oss lage en firkantet funksjon som kan gjøre mye. Vi kaller det en prikk. En ting som hjelper når du arbeider med bevegelige objekter, er å bruke vektorer i stedet for å skille mellom x og y-variabler. I disse kodeprøverene har jeg trukket inn three.js Vector2-klassen. Det er enkelt å bruke med en gang med vector.x og vector.y, men det har også en rekke praktiske metoder for å jobbe med dem. Se på dokumentene for et dypere dykk.
Kodens eksempel blir litt mer komplekst fordi det samhandler med objekter, men det vil være verdt det. Sjekk ut eksempelkoden for å se et nytt scenobjekt som styrer grunnleggende om å tegne på lerretet. Vår nye Dot- klasse vil få et håndtak til denne scenen for å få tilgang til eventuelle variabler som lerretskonteksten som den trenger.
function Dot( x, y, scene ) {var speed = 0.5;this.color = '#000000';this.size = 10;this.position = new THREE.Vector2(x,y);this.direction = new THREE.Vector2(speed * Math.random() - speed / 2,speed * Math.random() - speed / 2);this.scene = scene;}
Til å begynne med setter konstruktøren for prikken opp konfigurasjonen av sin oppførsel, og setter inn noen variabler å bruke. Igjen, dette bruker tre.js vektorklassen. Når du gjengir 60fps, er det viktig å forhåndsinnstille objektene dine, og ikke opprette nye mens du animerer. Dette spiser inn i ditt tilgjengelige minne og kan gjøre visualiseringen din hakkete. Legg også merke til hvordan punktet er bestått en kopi av scenen ved referanse. Dette holder ting rent.
Dot.prototype = {update : function() {...},draw : function() {...}}
Hele resten av koden vil bli satt på prikkens prototypeobjekt, slik at hver ny prikk som blir opprettet, har tilgang til disse metodene. Jeg skal fungere med funksjon i forklaringen.
update : function( dt ) {this.updatePosition( dt );this.draw( dt );},
Jeg skiller ut tegningskoden fra oppdateringskoden min. Dette gjør det mye enklere å vedlikeholde og justere objektet ditt, akkurat som MVC-mønsteret skiller ut kontrollen din og ser logikken. Dt- variabelen er tidsendringen i millisekunder siden det siste oppdateringssamtalen. Navnet er fint og kort og kommer fra (ikke vær redd) kalkulatoriske derivater. Hva dette gjør er å skille ut bevegelsen din fra hastigheten på bildesatsen. På denne måten får du ikke NES-stil-slowdowns når ting blir for komplisert. Din bevegelse vil slippe rammer hvis det virker hardt, men det vil forbli i samme hastighet.
updatePosition : function() {//This is a little trick to create a variable outside of the render loop//It's expensive to allocate memory inside of the loop.//The variable is only accessible to the function below.var moveDistance = new THREE.Vector2();//This is the actual functionreturn function( dt ) {moveDistance.copy( this.direction );moveDistance.multiplyScalar( dt );this.position.add( moveDistance );//Keep the dot on the screenthis.position.x = (this.position.x + this.scene.canvas.width) % this.scene.canvas.width;this.position.y = (this.position.y + this.scene.canvas.height) % this.scene.canvas.height;}}(), //Note that this function is immediately executed and returns a different function
Denne funksjonen er litt merkelig i sin struktur, men praktisk for visualiseringer. Det er veldig dyrt å allokere minne i en funksjon. Flyttningsvariabelen er satt en gang, og gjenbrukes når funksjonen kalles.
Denne vektoren brukes kun til å beregne den nye posisjonen, men ikke brukes utenfor funksjonen. Dette er den første vektormatrisen som blir brukt. Akkurat nå multipliseres retningsvektoren mot tidsendringen, og legges deretter til posisjonen. På slutten er det en liten modulo handling som skjer for å holde prikken på skjermen.
draw : function(dt) {//Get a short variable name for conveniencevar ctx = this.scene.context;ctx.beginPath();ctx.fillStyle = this.color;ctx.fillRect(this.position.x, this.position.y, this.size, this.size);}
Til slutt de enkle greiene. Få en kopi av konteksten fra sceneobjektet, og trekk deretter et rektangel (eller hva du vil). Rektangler er sannsynligvis den raskeste tingen du kan tegne på skjermen.
På dette punktet legger jeg til en ny prikk ved å ringe this.dot = ny prikk (x, y, dette) i hovedscene-konstruktøren, og deretter i sceneoppdateringsmetoden legger jeg til denne.dot.update (dt) og det er en prikk zoome rundt på skjermen. (Sjekk kildekoden for hele koden i kontekst.)
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 );}...};
Det er litt forvirrende i en linje, så her er det brutt ned som syndfunksjonen fra tidligere.
var a = 1 / 500, //Make the oscillation happen a lot slowerx = this.scene.currTime, //Move along the graph a little bit each time draw() is calledb = this.position.x / this.scene.canvas.width * 4, //No need to adjust the graph up or downc = 20, //Make the oscillation as wide as a little less than half the canvasd = 0; //Tweak the position of the rectangle to be centeredxMovement = Math.sin( a * x + b ) * c + d;
Kommer groovy ...
En ekstra liten tweak. Monokrom er en liten drap, så la oss legge til litt farge.
var hue = this.position.x / this.scene.canvas.width * 360;this.color = Utils.hslToFillStyle(hue, 50, 50, 0.5);
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.