Piękny kod to radość pisania, ale trudno jest dzielić się tą radością z innymi programistami, nie mówiąc o nie-programistach. W czasie wolnym od pracy i czasu rodzinnego bawiłam się pomysłem na wiersz programistyczny, wykorzystując element canvas do rysowania w przeglądarce. Istnieje wiele terminów opisujących eksperymenty wizualne na komputerze, takich jak grafika, szkic kodu, demo i sztuka interaktywna, ale ostatecznie zdecydowałem się na programowanie wiersza, aby opisać ten proces. Ideą wiersza jest dopracowany kawałek prozy, który łatwo się udostępnia, jest zwięzły i estetyczny. To nie jest na wpół ukończony pomysł w szkicowniku, ale spójny utwór przedstawiony widzowi dla przyjemności. Wiersz nie jest narzędziem, ale istnieje, by wywołać emocje.
Dla własnej przyjemności czytałem książki o matematyce, obliczeniach, fizyce i biologii. Nauczyłem się bardzo szybko, że kiedy wdycham pomysł, ludzie szybko nudzą. Wizualnie mogę wziąć niektóre z tych pomysłów, które uważam za fascynujące i szybko dać każdemu poczucie cudowności, nawet jeśli nie rozumieją teorii stojącej za kodem i koncepcjami, które go napędzają. Nie potrzebujesz rękojeści z żadnej twardej filozofii czy matematyki, żeby napisać wiersz programistyczny, tylko chęć zobaczenia czegoś żywego i oddychania na ekranie.
Kod i przykłady, które zestawię poniżej, pomogą w zrozumieniu, jak właściwie zrealizować ten szybki i wysoce satysfakcjonujący proces. Jeśli chcesz podążać za kodem, możesz pobierz tutaj pliki źródłowe.
Podstawową sztuczką podczas tworzenia wiersza jest zachowanie lekkości i prostoty. Nie spędzaj trzech miesięcy na budowaniu naprawdę fajnego demo. Zamiast tego utwórz 10 wierszy, które rozwijają ideę. Napisz eksperymentalny kod, który jest ekscytujący i nie bój się zawieść.
Aby uzyskać szybki przegląd, płótno jest zasadniczo 2-bitowym elementem obrazu bitmapowym, który żyje w DOM, który można narysować. Rysowanie można wykonać za pomocą kontekstu 2d lub kontekstu WebGL. Kontekst jest obiektem JavaScript, którego używasz, aby uzyskać dostęp do narzędzi do rysowania. Zdarzenia JavaScript dostępne na płótnie są bardzo proste, w przeciwieństwie do tych dostępnych dla SVG. Każde wywołane zdarzenie dotyczy elementu jako całości, a nie niczego rysowanego na płótnie, tak jak normalnego elementu obrazu. Oto podstawowy przykład na płótnie:
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);
Rozpoczęcie pracy jest całkiem proste. Jedyną rzeczą, która może być trochę zagmatwana, jest to, że kontekst musi być skonfigurowany z ustawieniami, takimi jak fillStyle, lineWidth, font i strokeStyle, zanim zostanie użyte faktyczne wywołanie losowania. Łatwo zapomnieć o aktualizacji lub zresetowaniu tych ustawień i uzyskać niezamierzone wyniki.
Pierwszy przykład wykonano tylko raz i narysowano statyczny obraz na płótnie. To jest w porządku, ale kiedy naprawdę robi się fajnie, to jest aktualizowane z prędkością 60 klatek na sekundę. Nowoczesne przeglądarki mają wbudowaną funkcję requestAnimationFrame, która synchronizuje niestandardowy kod rysunkowy z cyklami rysowania w przeglądarce. Pomaga to pod względem wydajności i gładkości. Celem wizualizacji powinien być kod, który szumi wzdłuż 60 klatek na sekundę.
(Uwaga na temat wsparcia: dostępne są niektóre proste wieloramienne, jeśli potrzebujesz obsługi starszych przeglądarek).
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();
Teraz przepisuję moją formułę z poprzedniego przykładu kodu jako bardziej podzieloną wersję, która jest łatwiejsza do odczytania.
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;
Jeśli chcesz bawić się z kodem do tej pory, sugerowałbym dodanie ruchu w kierunku y. Spróbuj zmienić wartości w funkcji grzechu lub przełącz się na inny rodzaj funkcji do obejrzenia i zobacz, co się stanie.
Poza poruszaniem ruchu z matematyką, poświęć chwilę, aby wyobrazić sobie, co możesz zrobić z różnymi urządzeniami wejściowymi użytkownika, aby przesunąć kwadrat wokół strony. W przeglądarce dostępne są różne opcje, w tym mikrofon, kamera internetowa, mysz, klawiatura i gamepad. Dodatkowe opcje oparte na wtyczkach są dostępne z czymś takim jak Leap Motion lub Kinect. Korzystając z WebSockets i serwera możesz podłączyć wizualizację do domowego sprzętu. Podłącz mikrofon do interfejsu Web Audio API i steruj pikselami dźwiękiem. Możesz nawet zbudować czujnik ruchu z kamery internetowej i przestraszyć szkołę wirtualnych ryb (ok, zrobiłem to ostatnie w Flashu pięć lat temu).
Teraz, gdy masz już swój wielki pomysł, wróćmy do kilku przykładów. Jeden kwadrat jest nudny, weźmy ante. Po pierwsze, stwórzmy kwadratową funkcję, która może wiele zrobić. Nazwiemy to kropką. Jedną z rzeczy, która pomaga w pracy z ruchomymi obiektami, jest użycie wektorów zamiast oddzielnych zmiennych x i y. W tych próbkach kodu wciągnąłem klasę Vector2.js. Jest łatwy do użycia od razu w wektorach i wektorach.j, ale ma też wiele przydatnych metod pracy z nimi. Spojrzeć na doktorzy dla głębszego nurkowania.
Kod tego przykładu staje się nieco bardziej złożony, ponieważ wchodzi w interakcję z obiektami, ale będzie tego wart. Sprawdź przykładowy kod, aby zobaczyć nowy obiekt Sceny, który zarządza podstawami rysowania na kanwie. Nasza nowa klasa Dot otrzyma uchwyt do tej sceny, aby uzyskać dostęp do zmiennych, takich jak kontekst canvas, którego będzie potrzebować.
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;}
Na początek konstruktor dla Dot ustawia konfigurację swojego zachowania i ustawia niektóre zmienne do użycia. Ponownie używa się klasy wektorowej three.js. Podczas renderowania z szybkością 60 klatek na sekundę ważne jest wstępne zainicjowanie obiektów, a nie tworzenie nowych podczas animowania. To zjada twoją dostępną pamięć i może sprawić, że twoja wizualizacja będzie wzburzona. Zwróć też uwagę na to, jak kropka jest przekazywana kopii sceny przez odniesienie. Dzięki temu rzeczy są czyste.
Dot.prototype = {update : function() {...},draw : function() {...}}
Cała reszta kodu zostanie ustawiona na obiekcie prototypowym Dot, aby każda nowa kropka, która zostanie utworzona, miała dostęp do tych metod. Przejdę do funkcji według funkcji w wyjaśnieniu.
update : function( dt ) {this.updatePosition( dt );this.draw( dt );},
Oddzielam mój kod rysowania od kodu aktualizacji. Ułatwia to konserwację i modyfikację obiektu, podobnie jak wzór MVC oddziela sterowanie i logikę widoku. Zmienna dt to zmiana czasu w milisekundach od ostatniego wywołania aktualizacji. Nazwa jest dobra i krótka i pochodzi z (nie bój się) pochodnych rachunku. To, co to robi, oddziela twój ruch od prędkości klatek na sekundę. W ten sposób nie dostaniesz spowolnień stylu NES, gdy sprawy staną się zbyt skomplikowane. Twój ruch spadnie klatek, jeśli działa ciężko, ale pozostanie z tą samą prędkością.
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
Ta funkcja jest nieco dziwna w swojej strukturze, ale przydatna do wizualizacji. Przydzielenie pamięci w funkcji jest bardzo kosztowne. Zmienna moveDistance jest ustawiona raz i ponownie używana za każdym razem, gdy wywoływana jest funkcja.
Ten wektor służy tylko do obliczenia nowej pozycji, ale nie jest używany poza funkcją. Jest to pierwsza używana matematyka wektorowa. W tej chwili wektor kierunkowy jest mnożony w stosunku do zmiany czasu, a następnie dodawany do pozycji. Na końcu pojawia się mała akcja modulo, która utrzymuje kropkę na ekranie.
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);}
Nareszcie proste rzeczy. Uzyskaj kopię kontekstu z obiektu sceny, a następnie narysuj prostokąt (lub cokolwiek chcesz). Prostokąty to prawdopodobnie najszybsza rzecz, jaką możesz narysować na ekranie.
W tym momencie dodaję nową kropkę , wywołując this.dot = new Dot (x, y, this) w głównym konstruktorze scen, a następnie w metodzie aktualizacji sceny dodaję this.dot.update (dt) i tam jest kropka powiększająca ekran. (Sprawdź kod źródłowy pełnego kodu w kontekście).
Teraz w scenie, zamiast tworzyć i aktualizować Dot , tworzymy i aktualizujemy DotManager . Stworzymy 5000 punktów, aby rozpocząć.
function Scene() {...this.dotManager = new DotManager(5000, this);...};Scene.prototype = {...update : function( dt ) {this.dotManager.update( dt );}...};
Jest trochę mylące w jednym wierszu, więc tutaj jest on rozbity jak funkcja grzechu wcześniej.
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;
Zdobywanie groovy ...
Jeszcze jedna drobna korekta. Monochromatyczny jest trochę szaro, więc dodajmy trochę koloru.
var hue = this.position.x / this.scene.canvas.width * 360;this.color = Utils.hslToFillStyle(hue, 50, 50, 0.5);
Ten prosty obiekt zawiera logikę aktualizacji myszy od reszty sceny. Aktualizuje on tylko wektor pozycji w ruchu myszy. Reszta obiektów może następnie pobierać próbki z wektora pozycji myszy, jeśli zostaną przekazane odniesienie do obiektu. Jedyne zastrzeżenie, które tutaj ignoruję, to to, że szerokość płótna nie jest równa jeden do jednego z wymiarami pikselowymi DOM, tj. Płótno o zmiennej rozdzielczości lub płótno o wyższej gęstości pikseli (siatkówka) lub jeśli płótno nie znajduje się w lewy górny. Współrzędne myszy muszą być odpowiednio dostosowane.
var Scene = function() {...this.mouse = new Mouse( this );...};
Jedyne, co pozostało myszy, to stworzenie obiektu myszy wewnątrz sceny. Teraz, gdy mamy mysz, przyciągnijmy do niej kropki.
function Dot( x, y, scene ) {...this.attractSpeed = 1000 * Math.random() + 500;this.attractDistance = (150 * Math.random()) + 180;...}
Dodałem kilka wartości skalarnych do kropki, aby każdy z nich zachowywał się nieco inaczej w symulacji, aby nadać mu nieco realizmu. Baw się z tymi wartościami, aby uzyskać inne odczucie. Teraz przejdź do metody przyciągania myszy. Trochę długo trwa z komentarzami.
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);}};}()
Ta metoda może być nieco myląca, jeśli nie jesteś na bieżąco z matematyki wektorowej. Wektory mogą być bardzo wizualne i mogą pomóc, jeśli wyciągniesz trochę literki na poplamionym kawą skrawku papieru. W prostym języku angielskim ta funkcja uzyskuje odległość między myszą a kropką. Następnie przesuwa kropkę nieco bliżej kropki, w zależności od tego, jak blisko jest już kropka i czas, który upłynął. Robi to, określając odległość do poruszania się (normalna liczba skalarna), a następnie mnoży ją przez znormalizowany wektor (wektor o długości 1) kropki wskazującej na mysz. Ok, to ostatnie zdanie niekoniecznie było po angielsku, ale to dopiero początek.