JavaScript

Zegar cyfrowy z wykorzystaniem KineticJS

Ś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

<!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="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        <script src="https://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v4.7.4.min.js"></script>
        <script src="js/app.js"></script>
    </body>
</html>

JS

;(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

Brak komentarzy

Napisz komentarz jako pierwszy!

Zostaw odpowiedź