Mooie code is een genot om te schrijven, maar het is moeilijk om die vreugde te delen met andere programmeurs, en niet te vergeten met niet-programmeurs. In mijn vrije tijd tussen mijn dagtaak en mijn familie heb ik gespeeld met het idee van een programmeerdicht met behulp van het canvaselement om in de browser te tekenen. Er zijn veel verschillende termen om visuele experimenten op de computer te beschrijven, zoals dev art, code sketch, demo en interactieve kunst, maar uiteindelijk besloot ik om gedichten te programmeren om dit proces te beschrijven. Het idee achter een gedicht is een gepolijst stukje proza dat gemakkelijk deelbaar, beknopt en esthetisch is. Het is geen half-afgewerkt idee in een schetsboek, maar een samenhangend stuk gepresenteerd aan de kijker voor hun plezier. Een gedicht is geen hulpmiddel, maar bestaat om een emotie op te roepen.
Voor mijn eigen plezier las ik boeken over wiskunde, rekenen, natuurkunde en biologie. Ik heb heel snel geleerd dat wanneer ik op een idee loop, het mensen vrij snel verveelt. Ik kan visueel een aantal van deze ideeën aannemen, die ik fascinerend vind, en snel iemand een gevoel van verwondering geven, zelfs als ze de theorie achter de code en de concepten die ermee werken niet begrijpen. Je hebt geen handvat nodig voor een harde filosofie of wiskunde om een programmeergedicht te schrijven, gewoon een verlangen om iets live te zien en te ademen op het scherm.
De code en voorbeelden die ik hieronder heb opgesteld, zullen een begin maken met het begrijpen hoe dit snelle en zeer bevredigende proces daadwerkelijk kan worden uitgevoerd. Als u de code wilt volgen die u kunt volgen download hier de bronbestanden.
De belangrijkste truc bij het daadwerkelijk maken van een gedicht is om het licht en eenvoudig te houden. Besteed geen drie maanden aan het bouwen van een echt coole demo. Maak in plaats daarvan 10 gedichten die een idee evolueren. Schrijf experimentele code die opwindend is en wees niet bang om te falen.
Voor een snel overzicht is het canvas in wezen een 2d bitmap-afbeeldingselement dat in de DOM leeft waarop kan worden getekend. Tekenen kan worden gedaan met behulp van een 2D-context of een WebGL-context. De context is het JavaScript-object dat u gebruikt om toegang te krijgen tot de tekenhulpmiddelen. De JavaScript-gebeurtenissen die beschikbaar zijn voor canvas zijn erg barebones, in tegenstelling tot de beschikbare voor SVG. Elke gebeurtenis die wordt geactiveerd, is voor het element als geheel, niet als iets dat op het canvas wordt getekend, net als bij een normaal afbeeldingselement. Hier is een eenvoudig canvasvoorbeeld:
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);
Het is vrij eenvoudig om te beginnen. Het enige dat een beetje verwarrend kan zijn, is dat de context moet worden geconfigureerd met de instellingen zoals fillStyle, lineWidth, font en strokeStyle voordat de daadwerkelijke tekenoproep wordt gebruikt. Het is gemakkelijk om te vergeten deze instellingen bij te werken of opnieuw in te stellen en onbedoelde resultaten te krijgen.
Het eerste voorbeeld is maar één keer uitgevoerd en heeft een statische afbeelding op het canvas getekend. Dat is OK, maar als het echt leuk wordt, is het bijgewerkt met 60 frames per seconde. Moderne browsers hebben het ingebouwde functieverzoekAnimationFrame dat de aangepaste tekencode synchroniseert met de trekkingscycli van de browser. Dit helpt in termen van efficiëntie en zachtheid. Het doelwit van een visualisatie moet een code zijn die meegeeft met 60 frames per seconde.
(Een opmerking over ondersteuning: er zijn enkele eenvoudige polyfills beschikbaar als u oudere browsers wilt ondersteunen.)
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 zal ik mijn formule van het vorige codevoorbeeld herschrijven als een meer afgebroken versie die gemakkelijker te lezen is.
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;
Als je tot nu toe met de code wilt spelen, zou ik willen voorstellen wat beweging in de y-richting toe te voegen. Probeer de waarden in de sin-functie te veranderen of schakel over naar een andere functie om te spelen en te zien wat er gebeurt.
Behalve het rijden met wiskunde, neem even de tijd om je voor te stellen wat je kunt doen met verschillende invoerapparaten van gebruikers om een vierkant rond een pagina te verplaatsen. Er zijn allerlei opties beschikbaar in de browser, waaronder de microfoon, webcam, muis, toetsenbord en gamepad. Extra door plug-ins aangedreven opties zijn beschikbaar met zoiets als de Leap Motion of Kinect. Met behulp van WebSockets en een server kunt u een visualisatie aansluiten op zelfgebouwde hardware. Sluit een microfoon aan op de Web Audio API en rijd uw pixels met geluid. Je kunt zelfs een bewegingssensor uit een webcam bouwen en een school van virtuele vissen bang maken (ok ik deed de laatste een keer in Flash vijf of zo geleden).
Dus nu je je grote idee hebt, laten we teruggaan naar nog enkele voorbeelden. Eén vierkant is saai, laten we de ante opzoeken. Laten we eerst een vierkante functie maken die veel kan. We noemen het een Dot. Een ding dat daarbij helpt, is om bij het werken met bewegende objecten vectoren te gebruiken in plaats van afzonderlijke x- en y-variabelen. In deze codevoorbeelden heb ik de three.js Vector2-klasse ingetrokken. Het is eenvoudig te gebruiken met vector.x en vector.y, maar het heeft ook een aantal handige methoden om met ze te werken. Kijk eens naar de documenten voor een diepere duik.
De code van dit voorbeeld wordt een beetje gecompliceerder omdat er interactie is met objecten, maar het is het waard. Bekijk de voorbeeldcode om een nieuw Scene- object te zien dat de basisbeginselen van het tekenen naar het canvas beheert. Onze nieuwe Dot- klasse krijgt een handvat voor deze scène om toegang te krijgen tot alle variabelen zoals de canvascontext die nodig is.
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;}
Om te beginnen stelt de constructor voor de punt de configuratie van zijn gedrag in en stelt hij enkele variabelen in om te gebruiken. Nogmaals, dit is met behulp van de three.js vectorklasse. Bij rendering met 60 fps is het belangrijk om uw objecten vooraf te initialiseren en geen nieuwe objecten te maken tijdens het animeren. Dit eet in uw beschikbare geheugen en kan uw visualisatie schokkerig maken. Merk ook op hoe de punt door verwijzing een kopie van de scène wordt doorgegeven. Dit houdt dingen schoon.
Dot.prototype = {update : function() {...},draw : function() {...}}
De rest van de code wordt ingesteld op het prototype-object van de Dot, zodat elke nieuwe stip die wordt gemaakt toegang heeft tot deze methoden. Ik ga functie voor functie in de uitleg.
update : function( dt ) {this.updatePosition( dt );this.draw( dt );},
Ik scheid mijn draw-code van mijn update-code. Dit maakt het veel gemakkelijker om je object te onderhouden en bij te stellen, net zoals het MVC-patroon je besturings- en viewlogica scheidt. De dt- variabele is de verandering in tijd in milliseconden sinds de laatste update-oproep. De naam is leuk en kort en komt van (wees niet bang) calculus-afgeleiden. Wat dit doet, scheidt uw beweging van de snelheid van de framesnelheid. Op deze manier krijg je geen vertragingen in de NES-stijl als de dingen te ingewikkeld worden. Je beweging laat frames vallen als het hard werkt, maar het blijft op dezelfde snelheid.
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
Deze functie is een beetje vreemd in zijn structuur, maar handig voor visualisaties. Het is erg duur om geheugen toe te wijzen aan een functie. De variabele moveDistance wordt één keer ingesteld en telkens opnieuw gebruikt wanneer de functie wordt aangeroepen.
Deze vector wordt alleen gebruikt om de nieuwe positie te berekenen, maar wordt niet gebruikt buiten de functie. Dit is de eerste vector wiskunde die wordt gebruikt. Op dit moment wordt de richtingsvector vermenigvuldigd met de verandering in de tijd en vervolgens toegevoegd aan de positie. Aan het einde is er een kleine modulo-actie gaande om de stip op het scherm te houden.
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);}
Eindelijk de makkelijke dingen. Download een kopie van de context uit het scèneobject en teken een rechthoek (of wat je maar wilt). Rechthoeken zijn waarschijnlijk het snelste dat u op het scherm kunt tekenen.
Op dit punt voeg ik een nieuwe Dot toe door deze.dot = new Dot (x, y, this) in de hoofdscène-constructor aan te roepen, en dan in de scene-update-methode voeg ik een this.dot.update (dt) toe en er is een punt die over het scherm zoomt. (Bekijk de broncode voor de volledige code in context.)
Nu in de scène, in plaats van een punt te maken en bij te werken, maken en updaten we de DotManager . We maken 5000 punten om te beginnen.
function Scene() {...this.dotManager = new DotManager(5000, this);...};Scene.prototype = {...update : function( dt ) {this.dotManager.update( dt );}...};
Het is een beetje verwarrend in één regel, dus hier wordt het opgesplitst als de sin-functie van eerder.
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;
Groovy worden ...
Nog een klein beetje tweak. Monochroom is een beetje saai, dus laten we wat kleur toevoegen.
var hue = this.position.x / this.scene.canvas.width * 360;this.color = Utils.hslToFillStyle(hue, 50, 50, 0.5);
Dit eenvoudige object kapselt de logica van de muisupdates van de rest van de scène in. Het update alleen de positievector bij een muisbeweging. De rest van de objecten kan vervolgens de voorbeeldvector van de muis samplen als ze een verwijzing naar het object hebben doorgegeven. Een waarschuwing die ik hier negeer, is of de breedte van het canvas niet één op één is met de pixeldimensies van de DOM, dat wil zeggen een canvas met een responsief aangepaste grootte of een canvas met een hogere pixeldichtheid (retina) of als het canvas niet bij de linksboven. De coördinaten van de muis moeten dienovereenkomstig worden aangepast.
var Scene = function() {...this.mouse = new Mouse( this );...};
Het enige dat overbleef voor de muis was om het muisobject in de scène te maken. Nu we een muis hebben, trekken we er de puntjes naar toe.
function Dot( x, y, scene ) {...this.attractSpeed = 1000 * Math.random() + 500;this.attractDistance = (150 * Math.random()) + 180;...}
Ik heb een aantal scalaire waarden aan de stip toegevoegd zodat elke zich een beetje anders gedraagt in de simulatie om het een beetje realistisch te maken. Speel rond met deze waarden om een ander gevoel te krijgen. Nu verder met de attract muis methode. Het is een beetje lang met de opmerkingen.
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);}};}()
Deze methode kan een beetje verwarrend zijn als u niet op de hoogte bent van uw vector wiskunde. Vectoren kunnen zeer visueel zijn en kunnen helpen als u wat krabbels op een met koffie bevlekt stukje papier tekent. In gewoon Engels krijgt deze functie de afstand tussen de muis en de stip. Het verplaatst de stip dan iets dichter bij de stip op basis van hoe dicht het al is bij de stip en de hoeveelheid tijd die is verstreken. Het doet dit door de te verplaatsen afstand uit te rekenen (een normaal scalair getal) en dat vervolgens te vermenigvuldigen met de genormaliseerde vector (een vector met lengte 1) van de punt die naar de muis wijst. Ok, die laatste zin was niet altijd duidelijk Engels, maar het is een begin.