Schöner Code ist eine Freude zu schreiben, aber es ist schwierig, diese Freude mit anderen Programmierern zu teilen, ganz zu schweigen von Nicht-Programmierern. In meiner Freizeit zwischen meinem Beruf und meiner Familie habe ich mit der Idee eines Programmiergedichts gespielt, indem ich das Canvas-Element zum Zeichnen im Browser verwende. Es gibt eine Vielzahl von Begriffen, um visuelle Experimente am Computer zu beschreiben, wie zum Beispiel Dev-Art, Code-Skizze, Demo und interaktive Kunst, aber letztendlich habe ich mich darauf festgelegt , Gedichte zu programmieren , um diesen Prozess zu beschreiben. Die Idee hinter einem Gedicht ist eine polierte Prosa, die leicht zu teilen, prägnant und ästhetisch ist. Es ist keine halbfertige Idee in einem Skizzenbuch, sondern ein zusammenhängendes Stück, das dem Betrachter zum Vergnügen präsentiert wird. Ein Gedicht ist kein Werkzeug, sondern existiert, um eine Emotion zu evozieren.
Zu meinem eigenen Vergnügen lese ich Bücher über Mathematik, Rechnen, Physik und Biologie. Ich habe sehr schnell gelernt, dass wenn ich über eine Idee spreche, die Leute ziemlich schnell langweilt. Visuell kann ich einige dieser Ideen, die ich faszinierend finde, aufgreifen und jedem schnell ein Gefühl der Verwunderung vermitteln, auch wenn sie die Theorie hinter dem Code und den Konzepten, die ihn steuern, nicht verstehen. Man braucht keine harte Philosophie oder Mathematik, um ein Programmiergedicht zu schreiben, sondern nur den Wunsch, etwas live zu sehen und auf dem Bildschirm zu atmen.
Der Code und die Beispiele, die ich unten zusammengestellt habe, werden ein Verständnis dafür schaffen, wie man diesen schnellen und höchst befriedigenden Prozess tatsächlich abwickeln kann. Wenn Sie dem Code folgen möchten, können Sie das tun Laden Sie die Quelldateien hier herunter.
Der Haupttrick bei der Erstellung eines Gedichtes besteht darin, es leicht und einfach zu halten. Verbring nicht drei Monate damit, eine wirklich coole Demo zu erstellen. Erstellen Sie stattdessen 10 Gedichte, die eine Idee entwickeln. Schreiben Sie experimentellen Code, der aufregend ist, und haben Sie keine Angst zu versagen.
Für einen schnellen Überblick ist die Zeichenfläche im Wesentlichen ein 2D-Bitmap-Bildelement, das sich im DOM befindet, auf das gezeichnet werden kann. Das Zeichnen kann entweder mit einem 2d-Kontext oder einem WebGL-Kontext erfolgen. Der Kontext ist das JavaScript-Objekt, mit dem Sie auf die Zeichenwerkzeuge zugreifen können. Die JavaScript-Ereignisse, die für Canvas verfügbar sind, sind im Gegensatz zu denen, die für SVG verfügbar sind, sehr Barebones. Jedes Ereignis, das ausgelöst wird, gilt für das Element als Ganzes, nicht für alles, was wie ein normales Bildelement auf die Leinwand gezeichnet wird. Hier ist ein einfaches Leinwandbeispiel:
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);
Es ist ziemlich einfach, anzufangen. Das einzige, was ein wenig verwirrend sein kann, ist, dass der Kontext mit den Einstellungen wie fillStyle, lineWidth, font und strokeStyle konfiguriert werden muss, bevor der eigentliche Zeichenaufruf verwendet wird. Es ist leicht zu vergessen, diese Einstellungen zu aktualisieren oder zurückzusetzen und einige unbeabsichtigte Ergebnisse zu erhalten.
Das erste Beispiel wurde nur einmal ausgeführt und zeichnete ein statisches Bild auf die Arbeitsfläche. Das ist in Ordnung, aber wenn es wirklich Spaß macht, ist es bei 60 Bildern pro Sekunde aktualisiert. Moderne Browser haben die integrierte Funktion requestAnimationFrame , die benutzerdefinierten Zeichnungscode mit den Zeichenzyklen des Browsers synchronisiert. Dies hilft in Bezug auf Effizienz und Laufruhe. Das Ziel einer Visualisierung sollte Code sein, der mit 60 Bildern pro Sekunde summt.
(Ein Hinweis zur Unterstützung: Es gibt einige einfache Polyfills, wenn Sie ältere Browser unterstützen müssen.)
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();
Jetzt schreibe ich meine Formel aus dem vorherigen Codebeispiel als eine mehr heruntergekommene Version, die einfacher zu lesen ist.
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;
Wenn Sie mit dem Code so weit herumspielen möchten, würde ich vorschlagen, etwas Bewegung in der y-Richtung hinzuzufügen. Versuchen Sie, die Werte in der sin-Funktion zu ändern, oder wechseln Sie zu einer anderen Art von Funktion, um herumzuspielen und zu sehen, was passiert.
Denken Sie nicht nur an Bewegung mit Mathematik, sondern nehmen Sie sich einen Moment Zeit, um sich vorzustellen, was Sie mit verschiedenen Benutzereingabegeräten tun können, um ein Quadrat um eine Seite zu bewegen. Im Browser sind alle möglichen Optionen wie Mikrofon, Webcam, Maus, Tastatur und Gamepad verfügbar. Zusätzliche Plugin-gesteuerte Optionen sind mit etwas wie dem Leap Motion oder Kinect verfügbar. Mit WebSockets und einem Server können Sie eine Visualisierung an selbst erstellte Hardware anschließen. Schließen Sie ein Mikrofon an die Web Audio API an und steuern Sie Ihre Pixel mit Ton. Sie können sogar einen Bewegungssensor aus einer Webcam bauen und eine Schule virtueller Fische erschrecken (ok, ich habe das letzte in Flash vor etwa fünf Jahren gemacht.)
Nun, da Sie Ihre große Idee haben, lassen Sie uns noch einmal auf einige Beispiele zurückkommen. Ein Platz ist langweilig, lass uns den Ante auffrischen. Als erstes erstellen wir eine quadratische Funktion, die viel kann. Wir werden es einen Punkt nennen. Eine Sache, die beim Arbeiten mit sich bewegenden Objekten hilft, ist die Verwendung von Vektoren anstelle von separaten x- und y-Variablen. In diesen Codebeispielen habe ich die three.js Vector2-Klasse eingefügt. Es ist einfach zu verwenden mit vector.x und vector.y, aber es hat auch eine Reihe von praktischen Methoden, um mit ihnen zu arbeiten. Sieh dir das an die Dokumente für einen tieferen Tauchgang.
Der Code dieses Beispiels wird ein wenig komplexer, weil er mit Objekten interagiert, aber es wird sich lohnen. Sehen Sie sich den Beispielcode an, um ein neues Scene- Objekt zu sehen, das die Grundlagen des Zeichnens auf der Zeichenfläche verwaltet. Unsere neue Dot- Klasse erhält ein Handle für diese Szene, um auf alle Variablen wie den Canvas-Kontext zuzugreifen, den sie benötigt.
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;}
Zunächst legt der Konstruktor für den Punkt die Konfiguration seines Verhaltens fest und legt einige zu verwendende Variablen fest. Auch hier wird die Vektorklasse three.js verwendet. Beim Rendern mit 60 fps ist es wichtig, dass Sie Ihre Objekte vorinitialisieren und keine neuen erstellen, während Sie animieren. Dies frisst sich in Ihrem verfügbaren Speicher und kann Ihre Visualisierung abgehackt machen. Beachten Sie auch, dass dem Punkt eine Kopie der Szene als Referenz übergeben wird. Das hält die Dinge sauber.
Dot.prototype = {update : function() {...},draw : function() {...}}
Der gesamte übrige Code wird auf das Prototypobjekt des Dot gesetzt, so dass jeder neue Punkt, der erstellt wird, Zugriff auf diese Methoden hat. Ich werde in der Erklärung Funktion für Funktion gehen.
update : function( dt ) {this.updatePosition( dt );this.draw( dt );},
Ich teile meinen Zeichencode von meinem Update-Code ab. Dies macht es viel einfacher, Ihr Objekt zu pflegen und zu optimieren, ähnlich wie das MVC-Muster Ihre Kontroll- und Ansichtslogik trennt. Die Variable dt ist die Änderung der Zeit in Millisekunden seit dem letzten Aktualisierungsaufruf. Der Name ist schön und kurz und kommt von (Kalkül-Derivaten). Was dies tut, trennt Ihre Bewegung von der Geschwindigkeit der Bildrate. Auf diese Weise erhalten Sie keine NES-Stil-Verlangsamungen, wenn die Dinge zu kompliziert werden. Ihre Bewegung wird Frames löschen, wenn es hart arbeitet, aber es wird bei der gleichen Geschwindigkeit bleiben.
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
Diese Funktion ist in ihrer Struktur ein wenig seltsam, aber praktisch für Visualisierungen. Es ist sehr teuer, Speicher in einer Funktion zu reservieren. Die Variable moveDistance wird einmal festgelegt und bei jedem Aufruf der Funktion erneut verwendet.
Dieser Vektor wird nur zur Berechnung der neuen Position verwendet, aber nicht außerhalb der Funktion verwendet. Dies ist die erste Vektormathematik, die verwendet wird. Gerade jetzt wird der Richtungsvektor gegen die Änderung der Zeit multipliziert und dann zu der Position addiert. Am Ende gibt es eine kleine Modulo-Aktion, um den Punkt auf dem Bildschirm zu halten.
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);}
Endlich das einfache Zeug. Holen Sie eine Kopie des Kontexts aus dem Szenenobjekt und zeichnen Sie dann ein Rechteck (oder was immer Sie wollen). Rechtecke sind wahrscheinlich die Schnellste, die Sie auf dem Bildschirm zeichnen können.
An dieser Stelle füge ich einen neuen Punkt hinzu, indem ich im Hauptszenenkonstruktor this.dot = new Dot (x, y, this) aufrufe , und dann füge ich in der Methode scene update ein this.dot.update (dt) hinzu ein Punkt zoomt um den Bildschirm. (Überprüfen Sie den Quellcode für den vollständigen Code im Kontext.)
Anstatt einen Punkt zu erstellen und zu aktualisieren, erstellen und aktualisieren wir jetzt den DotManager . Wir werden 5000 Punkte erstellen, um loszulegen.
function Scene() {...this.dotManager = new DotManager(5000, this);...};Scene.prototype = {...update : function( dt ) {this.dotManager.update( dt );}...};
Es ist ein wenig verwirrend in einer Zeile, daher ist es hier wie die Sündenfunktion von früher aufgeteilt.
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 werden ...
Noch ein kleiner Tipp. Monochrom ist ein wenig eintönig, also fügen wir etwas Farbe hinzu.
var hue = this.position.x / this.scene.canvas.width * 360;this.color = Utils.hslToFillStyle(hue, 50, 50, 0.5);
Dieses einfache Objekt kapselt die Logik der Mausaktualisierungen vom Rest der Szene ein. Es aktualisiert nur den Positionsvektor bei einer Mausbewegung. Die restlichen Objekte können dann vom Positionsvektor der Maus abtasten, wenn ihnen ein Verweis auf das Objekt übergeben wird. Eine Einschränkung, die ich hier ignoriere, ist, wenn die Breite der Leinwand nicht eins zu eins mit den Pixel-Dimensionen des DOM ist, dh eine ansprechend skalierte Leinwand oder eine höhere Pixeldichte (Retina) Leinwand oder wenn die Leinwand nicht an der oben links. Die Koordinaten der Maus müssen entsprechend angepasst werden.
var Scene = function() {...this.mouse = new Mouse( this );...};
Das einzige, was für die Maus übrig blieb, war das Mausobjekt in der Szene zu erstellen. Jetzt, da wir eine Maus haben, locken wir die Punkte dazu.
function Dot( x, y, scene ) {...this.attractSpeed = 1000 * Math.random() + 500;this.attractDistance = (150 * Math.random()) + 180;...}
Ich fügte dem Punkt einige skalare Werte hinzu, so dass sich jeder in der Simulation ein wenig anders verhält, um ihm ein wenig Realismus zu geben. Spielen Sie mit diesen Werten herum, um ein anderes Gefühl zu bekommen. Nun zu der Attract-Maus-Methode. Es ist ein bisschen lang mit den Kommentaren.
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);}};}()
Diese Methode könnte etwas verwirrend sein, wenn Sie nicht auf dem Laufenden sind. Vektoren können sehr visuell sein und können helfen, wenn Sie ein paar Skizzen auf einem mit Kaffee verschmutzten Stück Papier malen. Im einfachen Englisch erhält diese Funktion den Abstand zwischen der Maus und dem Punkt. Dann bewegt sich der Punkt etwas näher zum Punkt, je nachdem, wie nah es bereits dem Punkt und der verstrichenen Zeit ist. Dazu berechnet man die zu bewegende Entfernung (eine normale Skalarzahl) und multipliziert diese mit dem normalisierten Vektor (einem Vektor mit der Länge 1) des auf die Maus zeigenden Punkts. Ok, dieser letzte Satz war nicht unbedingt einfach Englisch, aber es ist ein Anfang.