Święta, święta i po świętach. Jako, że zrobiłem sobie trochę wolnego od pisania to miałem więcej czasu na przygotowanie czegoś ciekawszego niż zwykle. Mając na uwadze, że wszyscy mamy już jakieś pojęcie o elemencie canvas, gdyż poświęciłem mu mój poprzedni artykuł, wybór padł na zegar cyfrowy, a wykorzystałem do tego KineticJS. KIneticJS to „HTML5 Canvas JavaScript framework”, w praktyce oznacza to tyle, że praca z elementem canvas przestaje być, powiedzmy wprost, tak nudna.
Wszyscy, którzy uczciwie przebrnęli przez mój poprzedni wpis i zobaczyli jak „przyjemna” jest praca z canvas szybko przekonają się do KineticJS. Nie jest to oczywiście jedyne narzędzie w swojej klasie. Bardzo ciekawy jest też jCanvas (szczególnie dla fanów jQuery), jednak miałem obawy o wydajność naszego zegara (sekundnik i milisekundnik), więc możliwość wsadowego przerysowywania warstwy w KineticJS przesądziła o jego wyborze do tego projektu, ale o tym jeszcze później.
HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="css/app.css"> </head> <body> <div id="container"></div> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> <script src="http://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v4.7.4.min.js"></script> <script src="js/app.js"></script> </body> </html> |
JS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | ;(function ($, window, document, undefined) { "use strict"; /************* #1 *************/ var stage = new Kinetic.Stage({ container: 'container', width: $(document).width(), height: $(document).height() }); var staticClockLayer = new Kinetic.Layer(); var hoursLayer = new Kinetic.Layer(); var minutesLayer = new Kinetic.Layer(); var secAndMsecLayer = new Kinetic.Layer(); /************* #2 *************/ var setup = { refreshTime: 0, //not more than max 1000, if 0 then use startX: 10, startY: 10, ledWidth: 50, ledThickness: 10, ledX: 30, ledY: 30, digitSpace: 15, secondsSpace: 60, ledSecRadius: 10, ledDisabled: '#262626', ledEndabled: '#D30000', clock: '#000000' } /************* #3 *************/ var clock_bg = new Kinetic.Rect({ x: setup.startX, y: setup.startY, width: setup.ledX*2+setup.ledWidth*9+setup.ledThickness*17+setup.digitSpace*5+setup.secondsSpace*3, height: setup.ledY*2+setup.ledWidth*2+setup.ledThickness*2, fill: setup.clock }); /************* #4 *************/ function drawLed(peakX, peakY, rotDeg){ if ( rotDeg == undefined ) rotDeg = 0; var peakXabsolute = peakX + setup.startX; var peakYabsolute = peakY + setup.startX; return new Kinetic.Polygon({ x: peakXabsolute, y: peakYabsolute, points : [ 0, 0, setup.ledThickness/2, -setup.ledThickness/2, setup.ledWidth+setup.ledThickness/2, -setup.ledThickness/2, setup.ledWidth+setup.ledThickness, 0, setup.ledWidth+setup.ledThickness/2, +setup.ledThickness/2, setup.ledThickness/2, +setup.ledThickness/2 ], fill: setup.ledDisabled, rotationDeg: rotDeg, strokeWidth: 0 }); } /************* #5 *************/ function drawDigit(startX, startY){ return { top : drawLed(startX, startY), middle : drawLed(startX, startY+setup.ledWidth+setup.ledThickness), bottom : drawLed(startX, startY+(setup.ledWidth+setup.ledThickness)*2), top_left : drawLed(startX, startY, 90), bottom_left : drawLed(startX, startY+setup.ledWidth+setup.ledThickness, 90), top_right : drawLed(startX+setup.ledWidth+setup.ledThickness, startY, 90), bottom_right : drawLed(startX+setup.ledWidth+setup.ledThickness, startY+setup.ledWidth+setup.ledThickness, 90) } } /************* #6 *************/ var led_h1 = drawDigit(setup.ledX,setup.ledY); var led_h2 = drawDigit(setup.ledX+setup.ledWidth+setup.ledThickness*2+setup.digitSpace,setup.ledY); var led_sd1 = new Kinetic.Circle({ x: setup.startX+(setup.ledWidth+setup.ledThickness*2)*2+setup.digitSpace+setup.secondsSpace/2+setup.ledX-setup.ledThickness/2, y: setup.startY+setup.ledY+setup.ledWidth/2+setup.ledThickness, fill: setup.ledEndabled, radius: setup.ledSecRadius }); var led_sd2 = new Kinetic.Circle({ x: setup.startX+(setup.ledWidth+setup.ledThickness*2)*2+setup.digitSpace+setup.secondsSpace/2+setup.ledX-setup.ledThickness/2, y: setup.startY+setup.ledY+setup.ledWidth*1.5+setup.ledThickness, fill: setup.ledEndabled, radius: setup.ledSecRadius }); var led_m1 = drawDigit(setup.ledX+(setup.ledWidth+setup.ledThickness*2)*2+setup.digitSpace+setup.secondsSpace,setup.ledY); var led_m2 = drawDigit(setup.ledX+(setup.ledWidth+setup.ledThickness*2)*3+setup.digitSpace*2+setup.secondsSpace,setup.ledY); var led_sd3 = new Kinetic.Circle({ x: setup.startX+(setup.ledWidth+setup.ledThickness*2)*4+setup.digitSpace*4+setup.secondsSpace+setup.ledX-setup.ledThickness/2, y: setup.startY+setup.ledY+setup.ledWidth/2+setup.ledThickness, fill: setup.ledEndabled, radius: setup.ledSecRadius }); var led_sd4 = new Kinetic.Circle({ x: setup.startX+(setup.ledWidth+setup.ledThickness*2)*4+setup.digitSpace*4+setup.secondsSpace+setup.ledX-setup.ledThickness/2, y: setup.startY+setup.ledY+setup.ledWidth*1.5+setup.ledThickness, fill: setup.ledEndabled, radius: setup.ledSecRadius }); var led_s1 = drawDigit(setup.ledX+(setup.ledWidth+setup.ledThickness*2)*4+setup.digitSpace*2+setup.secondsSpace*2,setup.ledY); var led_s2 = drawDigit(setup.ledX+(setup.ledWidth+setup.ledThickness*2)*5+setup.digitSpace*3+setup.secondsSpace*2,setup.ledY); var led_sd5 = new Kinetic.Circle({ x: setup.startX+(setup.ledWidth+setup.ledThickness*2)*6+setup.digitSpace*5+setup.secondsSpace*2+setup.ledX-setup.ledThickness/2, y: setup.startY+setup.ledY+setup.ledWidth/2+setup.ledThickness, fill: setup.ledEndabled, radius: setup.ledSecRadius }); var led_sd6 = new Kinetic.Circle({ x: setup.startX+(setup.ledWidth+setup.ledThickness*2)*6+setup.digitSpace*5+setup.secondsSpace*2+setup.ledX-setup.ledThickness/2, y: setup.startY+setup.ledY+setup.ledWidth*1.5+setup.ledThickness, fill: setup.ledEndabled, radius: setup.ledSecRadius }); var led_ms1 = drawDigit(setup.ledX+(setup.ledWidth+setup.ledThickness*2)*6+setup.digitSpace*3+setup.secondsSpace*3,setup.ledY); var led_ms2 = drawDigit(setup.ledX+(setup.ledWidth+setup.ledThickness*2)*7+setup.digitSpace*4+setup.secondsSpace*3,setup.ledY); var led_ms3 = drawDigit(setup.ledX+(setup.ledWidth+setup.ledThickness*2)*8+setup.digitSpace*5+setup.secondsSpace*3,setup.ledY); /************* #7 *************/ staticClockLayer .add(clock_bg) .add(led_sd1) .add(led_sd2) .add(led_sd3) .add(led_sd4) .add(led_sd5) .add(led_sd6) ; hoursLayer .add(led_h1.top) .add(led_h1.middle) .add(led_h1.bottom) .add(led_h1.top_left) .add(led_h1.bottom_left) .add(led_h1.top_right) .add(led_h1.bottom_right) .add(led_h2.top) .add(led_h2.middle) .add(led_h2.bottom) .add(led_h2.top_left) .add(led_h2.bottom_left) .add(led_h2.top_right) .add(led_h2.bottom_right) ; minutesLayer .add(led_m1.top) .add(led_m1.middle) .add(led_m1.bottom) .add(led_m1.top_left) .add(led_m1.bottom_left) .add(led_m1.top_right) .add(led_m1.bottom_right) .add(led_m2.top) .add(led_m2.middle) .add(led_m2.bottom) .add(led_m2.top_left) .add(led_m2.bottom_left) .add(led_m2.top_right) .add(led_m2.bottom_right) ; secAndMsecLayer .add(led_s1.top) .add(led_s1.middle) .add(led_s1.bottom) .add(led_s1.top_left) .add(led_s1.bottom_left) .add(led_s1.top_right) .add(led_s1.bottom_right) .add(led_s2.top) .add(led_s2.middle) .add(led_s2.bottom) .add(led_s2.top_left) .add(led_s2.bottom_left) .add(led_s2.top_right) .add(led_s2.bottom_right) .add(led_ms1.top) .add(led_ms1.middle) .add(led_ms1.bottom) .add(led_ms1.top_left) .add(led_ms1.bottom_left) .add(led_ms1.top_right) .add(led_ms1.bottom_right) .add(led_ms2.top) .add(led_ms2.middle) .add(led_ms2.bottom) .add(led_ms2.top_left) .add(led_ms2.bottom_left) .add(led_ms2.top_right) .add(led_ms2.bottom_right) .add(led_ms3.top) .add(led_ms3.middle) .add(led_ms3.bottom) .add(led_ms3.top_left) .add(led_ms3.bottom_left) .add(led_ms3.top_right) .add(led_ms3.bottom_right) ; stage .add(staticClockLayer) .add(hoursLayer) .add(minutesLayer) .add(secAndMsecLayer); /************* #8 *************/ function setDigit(digit, value){ var digitSettings = [ {'top' : true, 'middle' : false, 'bottom' : true, 'top_right' : true, 'bottom_right' : true, 'bottom_left' : true, 'top_left' : true}, //0 {'top' : false, 'middle' : false, 'bottom' : false, 'top_right' : true, 'bottom_right' : true, 'bottom_left' : false, 'top_left' : false}, //1 {'top' : true, 'middle' : true, 'bottom' : true, 'top_right' : true, 'bottom_right' : false, 'bottom_left' : true, 'top_left' : false}, //2 {'top' : true, 'middle' : true, 'bottom' : true, 'top_right' : true, 'bottom_right' : true, 'bottom_left' : false, 'top_left' : false}, //3 {'top' : false, 'middle' : true, 'bottom' : false, 'top_right' : true, 'bottom_right' : true, 'bottom_left' : false, 'top_left' : true}, //4 {'top' : true, 'middle' : true, 'bottom' : true, 'top_right' : false, 'bottom_right' : true, 'bottom_left' : false, 'top_left' : true}, //5 {'top' : true, 'middle' : true, 'bottom' : true, 'top_right' : false, 'bottom_right' : true, 'bottom_left' : true, 'top_left' : true}, //6 {'top' : true, 'middle' : false, 'bottom' : false, 'top_right' : true, 'bottom_right' : true, 'bottom_left' : false, 'top_left' : false}, //7 {'top' : true, 'middle' : true, 'bottom' : true, 'top_right' : true, 'bottom_right' : true, 'bottom_left' : true, 'top_left' : true}, //8 {'top' : true, 'middle' : true, 'bottom' : true, 'top_right' : true, 'bottom_right' : true, 'bottom_left' : false, 'top_left' : true}, //9 ] if ( digitSettings[value].top == true ) digit.top.setFill(setup.ledEndabled) else digit.top.setFill(setup.ledDisabled); if ( digitSettings[value].middle == true ) digit.middle.setFill(setup.ledEndabled); else digit.middle.setFill(setup.ledDisabled); if ( digitSettings[value].bottom == true ) digit.bottom.setFill(setup.ledEndabled); else digit.bottom.setFill(setup.ledDisabled); if ( digitSettings[value].top_right == true ) digit.top_right.setFill(setup.ledEndabled); else digit.top_right.setFill(setup.ledDisabled); if ( digitSettings[value].bottom_right == true ) digit.bottom_right.setFill(setup.ledEndabled); else digit.bottom_right.setFill(setup.ledDisabled); if ( digitSettings[value].bottom_left == true ) digit.bottom_left.setFill(setup.ledEndabled); else digit.bottom_left.setFill(setup.ledDisabled); if ( digitSettings[value].top_left == true ) digit.top_left.setFill(setup.ledEndabled); else digit.top_left.setFill(setup.ledDisabled); } /************* #9 *************/ function setTime(){ var date = new Date(); var milliseconds = date.getMilliseconds(); var seconds = date.getSeconds(); var minutes = date.getMinutes(); var hours = date.getHours(); var h2 = hours % 10; var h1 = (hours - h2) / 10; var m2 = minutes % 10; var m1 = (minutes - m2) / 10; var s2 = seconds % 10; var s1 = (seconds - s2) / 10; var ms3 = milliseconds % 10; var ms2 = ((milliseconds - ms3) / 10) % 10; var ms1 = (milliseconds - ms3 - ms2 * 10) / 100; setDigit(led_h1,h1); setDigit(led_h2,h2); setDigit(led_m1,m1); setDigit(led_m2,m2); setDigit(led_s1,s1); setDigit(led_s2,s2); setDigit(led_ms1,ms1); setDigit(led_ms2,ms2); setDigit(led_ms3,ms3); if(s1+s2 == 0) minutesLayer.draw(); if(m1+m2 == 0) hoursLayer.draw(); secAndMsecLayer.batchDraw(); if( setup.refreshTime == 0 ) requestAnimationFrame(setTime); } /************* #10 *************/ setTime(); secAndMsecLayer.draw(); minutesLayer.draw(); hoursLayer.draw(); if( setup.refreshTime != 0 ) setInterval(setTime, setup.refreshTime); }(jQuery, this, this.document)); |
#1
Na początku musimy utworzyć obiekt Stage do którego będziemy dodawać poszczególne warstwy, oraz stworzyć same warstwy, kolejno dla: zegara (rysujemy ją tylko raz), cyfry oznaczające godzinę (przerysowujemy raz na minutę), cyfry oznaczające minuty (przerysowujemy raz na sekundę) i milisekundnik (przerysowywany wsadowo).
#2
Pozostało jeszcze stworzyć obiekt z ustawieniami, tak aby dostosowanie wyglądu i działania zegara było później jak najprostsze.
#3
Prosty prostokąt posłuży nam jako tło dla zegara. Widać na tym prostym przykładzie, że obliczanie poszczególnych współrzędnych jest dość złożone, ale dzięki temu bardzo łatwo przystosować zegar według własnych preferencji, korzystając z obiektu ustawień.
#4
Funkcja drawLed zwraca pojedynczą diodę (wielokąt) o początku w punkcie (peakX, peakY) i pozwala na jej obrót o rotDeg stopni.
#5
Funkcja drawDigit zwraca już pojedynczą cyfrę (zestaw 7 wielokątów), jeszcze bez włączonych odpowiednich diod.
#6
Kolejno definiujemy odpowiednie zmienne, które odpowiadać będą cyfrom zegara i kropkom, które oddzielają godziny od minut, minuty od sekund, sekundy od milisekund.
#7
Przypisanie wszystkich obiektów do jednej warstwy spowodowało by, że niepotrzebnie przerysowywalibyśmy kształty, które się nie zmieniły. By zadbać o przyzwoitą wydajność zegara rozmieścimy wszystkie kształty na odpowiednich warstwach. Po dodaniu elementów do warstw należy dodać same warstwy do obiektu Stage, który wskazuje na konkretną kanwę.
#8
Mamy już warstwy ze wszystkimi potrzebnymi elementami, ale narysowanie zegara w tym momencie da nam taki rezultat:
Czegoś brakuje, prawda?
Funkcja setDigit, na podstawie tablicy ustawień wyświetlacza siedmiosegmentowego i przekazanej wartości, zapala odpowiednie diody na wskazanej cyfrze.
#9
Funkcja setTime wykonuje trzy zadania. Pierwsze, pobiera z obiektu Date aktualny czas i ustala wartość dla każdej z cyfr. Drugie, ustawia już wartości dla konkretnych cyfr. Trzecia, to przerysowywanie określonych warstw gdy zajdzie taka potrzeba. Tu na chwile się zatrzymamy.
Sama metoda draw warstwy powoduje jej natychmiastowe przerysowanie, nie ma w tym nic niezwykłego. Metoda batchDraw jest o wiele bardziej interesująca. Silnik animacji w KineticJS automatycznie ogranicza ilość przerysowań, bez znaczenia ile razy w danym czasie wywołamy metodę batchDraw. Dzięki temu warstwa nie jest przerysowywana częściej niż ilość klatek na sekundę jaka może być wyświetlona.
#10
Na koniec czysta formalność w postaci narysowania zegara i ustawienia odświeżania.
Wydajność
Aby zobrazować ile przerysowań zaoszczędzimy wykorzystując metodę batchDraw posłużę się prostymi obliczeniami:
Jeżeli wciągu 1s, przy częstotliwości odświeżania co 1ms, przerysujemy warstwę 1000 razy. Ludzkie oko nie potrafi dostrzec więcej niż 75-85 klatek na sekundę, a częstotliwość odświeżania przeciętnego monitora ciekłokrystalicznego to 60Hz. Wynika z tego, że nie zauważymy ok. 920-940, czyli 92-94% przerysowań. Robi wrażenie, prawda?
setTime vs requestAnimationFrame
W przypadku ustawienia refreshTime na 0, zamiast setTimeout wykorzystywana będzie metoda requestAnimationFrame. W przypadku np. zwykłego stopera gdzie sami odmierzalibyśmy czas skorzystanie z requestAnimationFrame komplikowałoby trochę kwestię inkrementacji. Z powodu, że za każdym razem tworzymy nowy obiekt Date ten problem nas nie dotyczy. Ile zyskamy na skorzystaniu z requestAnimationFrame? IE11 prawdę Ci powie:
setTimeout(setTime,1)
requestAnimationFrame
setTimeout(setTime,1) – bez rysowania wsadowego
Wbrew pozorom nie miałem w tym czasie włączonego Crysis 3.
requestAnimationFrame – bez rysowania wsadowego
Jak widać na żadnym z tych testów mój komputer nie zszedł poniżej 60FPS, więc nie widziałem różnic, ale powyższe wyniki dają do myślenia i pokazują jak bardzo niewielka zmiana w kodzie rzutuje na jego wydajność.
Podsumowanie
Jak widać za pomocą elementu canvas można stworzyć w prosty sposób coś więcej niż kółko czy kwadrat, a za pomocą np. Three.js i WebGL wejść również w świat grafiki 3D. Zachęcam do eksperymentowania i pochwalenia się swoimi dziełami. Cały kod zegara dostępny jest na moim GitHubie.
Przydatne linki
Tagi: promowany