block by harrystevens 32966d31139ae61534d8d267e74f19f6

Self-Rotating Analog Clock

Full Screen

This block uses D3.js to draw a clock that tells the time by rotating itself. No need for hands! It also uses chroma.js to color the background according to the time of day.

I learned how to calculate the points around a circle’s circumference from this stackoverflow thread. Mathematicians call it the parametric equation for a circle.

index.html

<html>
  <head>
    <style>
    body {
      margin: 0;
      font-family: "Helvetica Neue", sans-serif;
    }
    .border {
      fill: #fff;
      stroke: #3a403d;
      stroke-width: 20px;
    }
    .tick {
      stroke: #3a403d;
    }
    .tick.hour {
      stroke-width: 4px;
    }
    .tick-minute {
      stroke-width: 1px;
    }
    .inner-circle {
      fill: #fff;
    }
    .text {
      fill: #fff;
      font-size: 10px;
    }
    .track {
      fill: #e74c3c;
    }
    .half {
      fill: #3a403d;
    }
    </style>
  </head>
  <body>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/1.2.1/chroma.min.js"></script>
    <script>
    // declare a whole bunch of variable to draw with
    var borderWidth = 20,
      every = 144;
      svg = d3.select("body").append("svg"),
      g = d3.select("svg").append("g"),
      circle = g.append("circle"), // lch mode creates more saturated colors
      color = chroma.scale(["#281324", "#ffff00", "#281324"]).mode('lch'), // a color scale for the background
      ticks = [],
      text = [];
    for (let i = 1; i <= every; i++){
      ticks[i] = g.append("line");
      text[i] = g.append("text").attr("text-anchor", "middle");
    }
    var innerCircle = g.append("circle"),
      half = svg.append("text").attr("text-anchor", "middle"),
      track = svg.append("polygon");


    function draw(){
      var width = window.innerWidth,
        height = window.innerHeight,
        radius = height < width ? height / 2 - borderWidth / 2 : width / 2 - borderWidth / 2; // the radius of the circle will change depending on the window orientation

      // the big svg wrapper
      svg
          .attr("width", width)
          .attr("height", height);

      // a square-shaped g element that can be rotated around its center
      g
          .attr("width", width > height ? height : width)
          .attr("height", width > height ? height : width);

      // clock border
      circle
          .attr("class", "border")
          .attr("cx", width / 2)
          .attr("cy", height / 2)
          .attr("r", radius);

      // calculate ticks
      for (let i = 1; i <= every; i++){

        // The parametric equation for a circle is
        // x = cx + r * cos(a)
        // y = cy + r * sin(a)
        // Where r is the radius, cx,cy the origin, and a the angle in radians.

        // A radian is 57.2958 degrees

        var x = radius * Math.cos((360 * (i / every)) / 57.2958) + width / 2,
          y = radius * Math.sin((360 * (i / every)) / 57.2958) + height / 2;

        // draw ticks, with thicker ticks for the hours
        ticks[i]
            .attr("class", "tick " + (i / 12 % 1 == 0 ? "hour" : "minute"))
            .attr("x1", width / 2)
            .attr("y1", height / 2)
            .attr("x2", x)
            .attr("y2", y);

        // first, only select the hours, and then calculate the hours
        // (knowing that the 12 defaults to where the 3 should be)
        var t = i / 12 % 1 == 0 ? i / 12 >= 10 ? i / 12 - 9 : i / 12 + 3 : null;
        text[i]
            .attr("class", "text")
            .attr("x", x)
            .attr("y", y)
            .attr("dy", borderWidth / 5)
            .text(t);

      }

      // plop a white circle in the center to hide some of the tick lines
      innerCircle
          .attr("class", "inner-circle")
          .attr("cx", width / 2)
          .attr("cy", height / 2)
          .attr("r", radius / 1.2);

      // use d3 timer to handle the gradual rotation
      d3.timer(function(elapsed){

        // calculate rotation based on the current time
        var date = new Date(),
          hrs = date.getHours(),
          hrs = hrs > 12 ? hrs - 12 : hrs,
          min = date.getMinutes(),
          sec = date.getSeconds(),
          time = hrs * 3600 + min * 60 + sec,
          tot = 12 * 3600,
          rot = -((time * 360) / tot);

        // AM or PM?
        half
            .attr("class", "half")
            .attr("x", width / 2)
            .attr("y", height / 2)
            .attr("dy", radius / 12)
            .style("font-size", radius / 3)
            .text(new Date().getHours() > 11 ? "PM" : "AM"); // update the AM/PM situation

        // calculate the background color (0 and 1 are beginning and end of day, .5 is noon)
        var c = (date.getHours() * 3600 + min * 60 + sec) / (3600 * 24);

        // svg background
        svg
            .style("background", color(c));

        g.attr("transform", "rotate(" + rot + " " + width / 2 + " " + height / 2 + ")"); // rotate the clock

        // rotate the text (backwards)
        for (let i = 1; i <= every; i++){
          var x = radius * Math.cos((360 * (i / every)) / 57.2958) + width / 2,
            y = radius * Math.sin((360 * (i / every)) / 57.2958) + height / 2;

          text[i].attr("transform", "rotate(" + (-rot) + " " + x + " " + y + ")")
        };
      });

      // now draw the track on top of the whole thing
      var topX = width / 2,
        topY = height < width ? 0 : height / 2 - radius - borderWidth / 2,
        a = (topX - 5) + "," + topY,
        b = (topX + 5) + "," + topY,
        c = topX + "," + (topY + radius / 6),
        tri = a + " " + b + " " + c;

      // draw the track triangle
      track
          .attr("class", "track")
          .attr("points", tri);

    } // end draw()

    // allow for resizing
    window.onload = draw, window.onresize = draw;
    </script>
  </body>
</html>