Source: clock.js

/**
 * @file
 *
 * Summary.
 * <p>Draw a clock using the canvas API from node-canvas.</p>
 *
 *  Description. 
 *  <p>A simple method consists in mapping hours, minutes and seconds into angles,
 *   and then map polar coordinates to cartesian coordinates, considering zero degrees
 *   at three o'clock.
 *  </p>
 * 
 *  <pre>
 *  Documentation:
 *  - Ubuntu:
 *     - sudo apt install jsdoc-toolkit
 *     - sudo apt install npm
 *  - MacOS:
 *     - sudo port install npm5 (or npm6)
 *     - sudo npm install -g jsdoc
 *  - jsdoc -d doc-clock clock.js
 *
 *  Server:
 *  - npm init
 *  - npm install canvas
 *  - node clock.js &
 *  </pre>
 *
 *  @see http://localhost:3000
 *  @author Paulo Roma Cavalcanti
 *  @since 14/11/2020
 */

const fs = require('fs')
const http = require('http');
const {Canvas,Image} = require('canvas');
Canvas.Image = Image;
var canvas = new Canvas(512,512);

/// Fluminense logo.
var flu = "fluminense.png";

/// Image with the logo.
var img = null;

/** Image width.
 *  @type {number} 
 */
var imgw = 0;

/** Image height.
 *  @type {number} 
 */
var imgh = 0;

/** Canvas context.
 *  @type {HTMLElement} 
 */
var context = canvas.getContext("2d");

const twoPi = 2 * Math.PI;  

/** Canvas radius.
 *  @type {number} 
 */
var clockRadius = canvas.width/2;

/** Canvas center.
 *  @type {point} 
 */
var center = [clockRadius,clockRadius];

/** Set the image dimension.
 *  
 *   @param {number} w image width.
 *   @param {number} w image height.
 *   @param {number} r clock radius.
 *   @return {number[]} scale to fit the image in the clock without distortion.
 */
function _imgSize (w,h,r) {
    let d = 2*r*0.8;
    return [d*w/h, d];
}

// Size of the logo.
var imgSize = _imgSize(imgw,imgh,clockRadius);

/**
 * A 2D point.
 *
 * @typedef {Object} point.
 * @property {number} x coordinate.
 * @property {number} y coordinate.
 */  

/** Convert from polar to cartesian coordinates.
 *  
 *  @param {number} radius vector length.
 *  @param {number} angle vector angle.
 *  @return {point} a 2D point.
 */
function polar2Cartesian(radius, angle) {
    angle -= twoPi / 4; // 0 degrees is at three o'clock.
    return {
        x: radius * Math.cos(angle),
        y: radius * Math.sin(angle)
    };
}

/** Translate a point.
 *
 *  @param {point} pos given point.
 *  @param {number[]} vec translation vector.
 *  @return {point} a new translated point.
 */
function translate(pos,vec) {
    return {
        x: pos.x + vec[0],
        y: pos.y + vec[1]
    };
}

/** Scale a vector.
 *
 *  @param {point} pos given vector (pos-[0,0]).
 *  @param {number[]} vec scale factor.
 *  @return {point} a new scaled vector.
 */
function scale(pos,vec) {
    return {
        x: pos[0] * vec[0],
        y: pos[1] * vec[1]
    };
}

/** Draw a circle.
 *
 *  @param {point} center of the circle.
 *  @param {number} radius of the circle.
 *  @param {boolean} fill draws a solid or hollow circle.
 *  @see https://riptutorial.com/html5-canvas/example/11126/beginpath--a-path-command-
 */
function circle(center,radius,fill=true) {
    context.beginPath();
    context.arc(center[0], center[1], radius, 0, twoPi);
    if (fill) 
        context.fill();
    else 
        context.stroke();
}

/**
 * Redraw the clock: a logo, circle, three handles, ticks.
 *
 * @author: Paulo Roma.
 * @date 02/11/2020.
 */
function setTime() {

    // Clear screen.
    context.clearRect(0, 0, canvas.width, canvas.height);

    // Canvas background.
    context.fillStyle = "#ffebcc";
    context.fillRect(0, 0, canvas.width, canvas.height);

    // Translate the center of the logo
    // to the center of the canvas.
    var coord = translate(scale(imgSize,[-1/2,-1/2]),center);

    if (img) {
        // Draw the logo.
        context.drawImage(img, coord.x, coord.y, imgSize[0], imgSize[1]);
    }

    // context.globalAlpha = 0.3; // set global alpha

    // Draw clock border.
    context.strokeStyle = "#8B2439"; // grenĂ¡
    context.lineWidth = 3;
    circle(center, clockRadius-8, false);

    // Draw the tick numbers.
    context.fillStyle = "#446127";   // dark green
    context.font = clockRadius / 10 + "px arial";
    context.textAlign = "center";
    context.textBaseline = "middle";

    var fiveMin = twoPi / 12; // each 5 min is 30 degrees
    var oneMin  = twoPi / 60; // each 1 min is 6 degrees
    for (var i = 0; i < 12; i++) {
        // polar to cartesian coordinates
        coord = polar2Cartesian(0.9*clockRadius, i*fiveMin);
        // translate to the center of the canvas
        coord = translate(coord,center);
        context.fillText( i == 0 ? 12 : i, coord.x, coord.y );
    }

    var date = new Date();
    var hours   = date.getHours();    // (from 0-23)
    var minutes = date.getMinutes();  // (from 0-59)
    var seconds = date.getSeconds();  // (from 0-59)

    // 12 hours format: AM / PM
    hours = hours % 12 || 12;

    // Draw the handles.
    context.strokeStyle = "orange";

    let time2Angle = [hours   * fiveMin,
                      minutes * oneMin,
                      seconds * oneMin];

    let handleLength = [0.7,0.8,0.9];
    let handleWidth  = [6,4,2];

    for (let i = 0; i < 3; ++i) {
        context.beginPath();
        context.moveTo(center[0], center[1]);
        coord = polar2Cartesian(handleLength[i]*clockRadius, time2Angle[i]);
        coord = translate(coord,center);
        context.lineTo(coord.x, coord.y);
        context.lineWidth = handleWidth[i];
        context.stroke();    
    }

    // Handle origin.
    circle(center, 5);
}

/** Server hostname. <br>
 *
 *  0.0.0.0 means all IPv4 addresses on the local machine.
 *
 *  @type {string}
 */
const hostname = '0.0.0.0';  

/** Server port.
 *  
 *  @type {number}
 */
const port = process.env.PORT || 3000;

const server = http.createServer(function (req, res) {
    /** Asynchronously reads the entire contents of a file.
     *  The callback is passed two arguments (err, data):
     *
     *  @param {Error} err error object.
     *  @param {string | Buffer} data the contents of the file.
     */
    fs.readFile(__dirname + '/' + flu, function(err, data) {
        if (err) throw err;
        img = new Image; // Create a new Image
        img.src = data;
        imgw = img.width; 
        imgh = img.height;
        // Scale for the logo.
        imgSize = _imgSize(imgw,imgh,clockRadius);

        res.write('<html><body>');
            setTime();
            // grab the contents of an HTML5 canvas using the canvas toDataURL().
            // The data returned from the toDataURL() function is a string, 
            // which represents an encoded URL containing the grabbed graphical data. 
            res.write('<img id="clock" src="' + canvas.toDataURL() + '" />');
        res.write('</body></html>');
        res.end();
    });

});

server.listen(port, hostname, () => {
      console.log(`Server running at http://${hostname}:${port}/`);
});