/**
* @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}/`);
});