Vacker kod är en glädje att skriva, men det är svårt att dela den glädjen med andra programmerare, för att inte tala med icke-programmerare. På min fritid mellan mitt dagjobb och familjetid har jag spelat med idén om en programmeringsdikt med hjälp av dukelementet för att rita i webbläsaren. Det finns en mängd termer där ute för att beskriva visuella experiment på datorn som dev art, kodskiss, demo och interaktiv konst, men i slutändan bestämde jag mig för programmeringsdikt för att beskriva denna process. Tanken bakom en dikt är ett polerat stycke prosa som lätt kan delas, koncis och estetiskt. Det är inte en halvfärdig idé i en skissbok, men en sammanhållen bit presenterad för tittaren för deras njutning. En dikt är inte ett verktyg, men existerar för att framkalla en känsla.
För min egen njutning har jag läst böcker om matematik, beräkning, fysik och biologi. Jag har lärt mig riktigt snabbt att när jag rysar på en idé borar det människor ganska snabbt. Visuellt kan jag ta några av dessa idéer som jag tycker är fascinerande och snabbt ge någon en känsla av underverk, även om de inte förstår teorin bakom koden och begreppen som driver den. Du behöver inget handtag på någon hård filosofi eller matematik för att skriva en programmeringsdikt, bara en önskan att se något leva och andas på skärmen.
Koden och exemplen som jag har sammanställt nedan kommer att starta en förståelse för hur man faktiskt tar bort denna snabba och mycket tillfredsställande process. Om du vill följa med koden du kan ladda ner källfilerna här.
Det viktigaste tricket när man faktiskt skapar en dikt är att hålla det lätt och enkelt. Spendera inte tre månader på att bygga en riktigt cool demo. Istället skapar 10 dikter som utvecklar en idé. Skriv experimentell kod som är spännande, och var inte rädd för att misslyckas.
För en snabb överblick är duken väsentligen ett 2d bitmap bildelement som bor i DOM som kan dras in på. Ritning kan göras med hjälp av antingen ett 2d-kontext eller ett WebGL-kontext. Kontextet är det JavaScript-objekt som du använder för att få tillgång till ritverktygen. JavaScript-händelserna som finns tillgängliga för duk är mycket barebones, till skillnad från de som är tillgängliga för SVG. Varje händelse som utlöses är för elementet som helhet, inte något som draget på duken, precis som ett normalt bildelement. Här är ett basalt lerret exempel:
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 är ganska enkelt att komma igång. Det enda som kan vara lite förvirrande är att sammanhanget måste konfigureras med inställningarna som fillStyle, lineWidth, font och strokeStyle innan det aktuella rita samtalet används. Det är lätt att glömma att uppdatera eller återställa dessa inställningar och få några oavsiktliga resultat.
Det första exemplet körde bara en gång och ritade en statisk bild på duken. Det är okej, men när det blir roligt är det när det uppdateras med 60 bilder per sekund. Moderna webbläsare har den inbyggda funktionsförfråganAnimationFrame som synkroniserar anpassad ritningskod till webbläsarens rita cykler. Detta bidrar till effektivitet och jämnhet. Målet för en visualisering bör vara kod som längtar vid 60 bilder per sekund.
(En anteckning om support: det finns några enkla polyprofiler tillgängliga om du behöver stödja äldre webbläsare.)
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();
Nu skriver jag om min formel från tidigare kodexempel som en mer uppbruten version som är lättare att läsa.
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;
Om du vill leka med koden hittills, skulle jag föreslå att du lägger till lite rörelse i y-riktningen. Försök ändra värdena i syndfunktionen, eller byt till någon annan typ av funktion för att leka och se vad som händer.
Utöver körrörelse med matte, ta en stund att föreställa dig vad du kan göra med olika användarinmatningsenheter för att flytta en ruta runt en sida. Det finns alla möjliga alternativ i webbläsaren inklusive mikrofonen, webbkamera, mus, tangentbord och gamepad. Ytterligare plugin-driven alternativ är tillgängliga med något som Leap Motion eller Kinect. Med hjälp av WebSockets och en server kan du koppla in en visualisering till den inbyggda hårdvaran. Haka upp en mikrofon till Web Audio API och kör dina pixlar med ljud. Du kan till och med bygga en rörelsessensor ur en webbkamera och skrämma en skola med virtuell fisk (okej jag gjorde den sista i Flash för fem eller så år sedan.)
Så nu när du har din stora idé låt oss hoppa tillbaka till några fler exempel. En torg är tråkig, låt oss ante. Först av, låt oss skapa en fyrkantig funktion som kan göra mycket. Vi kallar det en prick. En sak som hjälper till när man arbetar med rörliga objekt är att använda vektorer i stället för att skilja x och y-variabler. I dessa kodprover har jag dragit in i tre.js Vector2-klassen. Det är lätt att använda direkt med vector.x och vector.y, men det har också en massa praktiska metoder att arbeta med. Ta en titt på docs för ett djupare dyk.
Kodens exempel blir lite mer komplext eftersom det samverkar med objekt, men det kommer att vara värt det. Kolla in exempelkoden för att se ett nytt scenobjekt som hanterar grunderna att dra till duken. Vår nya Dot- klass kommer att få ett handtag till den här scenen för att få tillgång till alla variabler som dukkontexten som den behöver.
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;}
Till att börja med ställer konstruktören för pricken upp konfigurationen av sitt beteende och anger vissa variabler att använda. Återigen använder detta tre.js vektorklassen. När du gör 60fps, är det viktigt att du initialiserar dina objekt och inte skapar nya medan du animerar. Detta äter i ditt lediga minne och kan göra din visualisering hakig. Observera också hur pricken passeras en kopia av scenen genom referens. Detta håller saker rena.
Dot.prototype = {update : function() {...},draw : function() {...}}
Hela resten av koden kommer att ställas in på prickens prototypobjekt så att varje ny prick som skapas har tillgång till dessa metoder. Jag kommer att fungera med funktion i förklaringen.
update : function( dt ) {this.updatePosition( dt );this.draw( dt );},
Jag delar ut min teckenkod från min uppdateringskod. Detta gör det mycket lättare att behålla och finjustera objektet, ungefär som MVC-mönstret skiljer ut din kontroll och visar logiken. Dt- variabeln är tidsförändringen i millisekunder sedan det senaste uppdateringssamtalet. Namnet är fint och kort och kommer från (inte rädda) calculusderivat. Vad detta gör skiljer din rörelse från hastigheten på bildhastigheten. På så sätt får du inte NES-stilavbrott när saker blir för komplicerade. Din rörelse kommer att släppa ramar om det fungerar hårt, men det kommer att hålla sig i samma 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
Denna funktion är lite udda i sin struktur, men användbar för visualiseringar. Det är verkligen dyrt att allokera minne i en funktion. Flyttningsvariabeln ställs in en gång och återanvänds när funktionen heter.
Denna vektor används endast för att beräkna den nya positionen, men används inte utanför funktionen. Detta är den första vektormatrisen som används. Just nu multipliceras riktningsvektorn mot tidsförändringen och läggs sedan till i positionen. I slutet finns en liten modulåtgärd som håller på att hålla pricken på skärmen.
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);}
Slutligen de lätta grejerna. Hämta en kopia av sammanhanget från scenobjektet och dra sedan en rektangel (eller vad du vill). Rektanglar är förmodligen det snabbaste du kan dra på skärmen.
Vid denna tidpunkt lägger jag till en ny punkt genom att ringa this.dot = new Dot (x, y, this) i huvudbildskonstruktören och sedan lägger jag till en this.dot.update (dt) i scenuppdateringsmetoden och det finns en punkt zoomar runt skärmen. (Kolla källkoden för hela koden i sammanhanget.)
Nu i scenen, istället för att skapa och uppdatera en prick , skapar och uppdaterar vi DotManager . Vi skapar 5000 prickar för att komma igång.
function Scene() {...this.dotManager = new DotManager(5000, this);...};Scene.prototype = {...update : function( dt ) {this.dotManager.update( dt );}...};
Det är lite förvirrande i en rad, så här är det uppdelat som syndfunktionen från tidigare.
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;
Att bli groovy ...
En enda liten tweak. Monokrom är en liten drab, så låt oss lägga till lite färg.
var hue = this.position.x / this.scene.canvas.width * 360;this.color = Utils.hslToFillStyle(hue, 50, 50, 0.5);
Detta enkla objekt inkapslar logiken för musuppdateringarna från resten av scenen. Det uppdaterar bara positionsvektorn på ett musfönster. Resten av objekten kan sedan prova från musens positionsvektor om de skickas en referens till objektet. En försiktighet som jag ignorerar här är om dukens bredd inte är en till en med DIM-pixeldimensionerna, dvs en måttlig storlek på kanvas eller en högre pixeldensitet (näthinnan) eller om duken inte finns på övre vänstra. Musens koordinater behöver justeras i enlighet därmed.
var Scene = function() {...this.mouse = new Mouse( this );...};
Det enda som lämnades för musen var att skapa musobjektet inne i scenen. Nu när vi har en mus, låt oss attrahera prickarna till den.
function Dot( x, y, scene ) {...this.attractSpeed = 1000 * Math.random() + 500;this.attractDistance = (150 * Math.random()) + 180;...}
Jag lade till några skalärvärden till pricken så att varje uppför sig lite annorlunda i simuleringen för att ge den lite realism. Spela runt med dessa värden för att få en annan känsla. Nu vidare till attraktiva musmetoden. Det är lite länge med kommentarerna.
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);}};}()
Denna metod kan vara lite förvirrande om du inte är uppdaterad på din vektormatris. Vektorer kan vara väldigt visuella och kan hjälpa dig om du drar några skribbar ut på ett kaffefärgat pappersskrot. På vanlig engelska får denna funktion avståndet mellan musen och pricken. Därefter flyttar punkten lite närmare punkten baserat på hur nära det redan är till pricken och hur lång tid det har gått. Det gör det genom att bestämma avståndet att flytta (ett normalt skalärt tal) och sedan multiplicera det med den normaliserade vektorn (en vektor med längd 1) av punkten som pekar mot musen. Ok, den sista meningen var inte nödvändigtvis vanlig engelska, men det är en början.