diff --git a/app.js b/app.js new file mode 100644 index 0000000000000000000000000000000000000000..7db080ed32d28c1f7b1c8ba7f1ac6dc4a151f1b7 --- /dev/null +++ b/app.js @@ -0,0 +1,54 @@ +'use strict'; + +// server +const express = require('express'); +const app = express(); +const path = require('path'); + +const tmpDir = __dirname + '/tmp/'; +const publicDir = __dirname + '/public/'; + +// canvas generator +const CountdownGenerator = require('./countdown-generator'); + +app.use(express.static(publicDir)); +app.use(express.static(tmpDir)); + +// root +app.get('/', function(req, res) { + res.sendFile(publicDir + 'index.html'); +}); + +// generate and download the gif +app.get('/generate', function(req, res) { + let { time, width, height, color, bg, name, frames } = req.query; + + if (!time) { + throw Error('Time parameter is required.'); + } + + CountdownGenerator.init(time, width, height, color, bg, name, frames, () => { + let filePath = tmpDir + name + '.gif'; + res.download(filePath); + }); +}); + +// serve the gif to a browser +app.get('/serve', function(req, res) { + let { time, width, height, color, bg, name='default', frames } = req.query; + + if (!time) { + throw Error('Time parameter is required.'); + } + + CountdownGenerator.init(time, width, height, color, bg, name, frames, () => { + let filePath = tmpDir + name + '.gif'; + res.sendFile(filePath); + }); +}); + +app.listen(process.env.PORT || 2025, function() { + console.log("Express server listening on port %d in %s mode", this.address().port, app.settings.env); +}); + +module.exports = app; \ No newline at end of file diff --git a/countdown-generator/index.js b/countdown-generator/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f2ac263d8be080a325e10e03eae6f13da8806310 --- /dev/null +++ b/countdown-generator/index.js @@ -0,0 +1,167 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const GIFEncoder = require('gifencoder'); +const Canvas = require('canvas'); +const moment = require('moment'); + +module.exports = { + /** + * Initialise the GIF generation + * @param {string} time + * @param {number} width + * @param {number} height + * @param {string} color + * @param {string} bg + * @param {string} name + * @param {number} frames + * @param {requestCallback} cb - The callback that is run once complete. + */ + init: function(time, width = 200, height = 200, color = 'ffffff', bg = '000000', name = 'default', frames = 30, cb) { + // Set some sensible upper / lower bounds + this.width = this.clamp(width, 150, 500); + this.height = this.clamp(height, 150, 500); + this.frames = this.clamp(frames, 1, 90); + + this.bg = '#' + bg; + this.textColor = '#' + color; + this.name = name; + + // loop optimisations + this.halfWidth = Number(this.width / 2); + this.halfHeight = Number(this.height / 2); + + this.encoder = new GIFEncoder(this.width, this.height); + this.canvas = Canvas.createCanvas(this.width, this.height); + this.ctx = this.canvas.getContext('2d'); + + // calculate the time difference (if any) + let timeResult = this.time(time); + + // start the gif encoder + this.encode(timeResult, cb); + }, + /** + * Limit a value between a min / max + * @link http://stackoverflow.com/questions/11409895/whats-the-most-elegant-way-to-cap-a-number-to-a-segment + * @param number - input number + * @param min - minimum value number can have + * @param max - maximum value number can have + * @returns {number} + */ + clamp: function(number, min, max) { + return Math.max(min, Math.min(number, max)); + }, + /** + * Calculate the diffeence between timeString and current time + * @param {string} timeString + * @returns {string|Object} - return either the date passed string, or a valid moment duration object + */ + time: function(timeString) { + // grab the current and target time + let target = moment(timeString); + let current = moment(); + + // difference between the 2 (in ms) + let difference = target.diff(current); + + // either the date has passed, or we have a difference + if (difference <= 0) { + return 'Date has passed!'; + } else { + // duration of the difference + return moment.duration(difference); + } + }, + /** + * Encode the GIF with the information provided by the time function + * @param {string|Object} timeResult - either the date passed string, or a valid moment duration object + * @param {requestCallback} cb - the callback to be run once complete + */ + encode: function(timeResult, cb) { + let enc = this.encoder; + let ctx = this.ctx; + let tmpDir = process.cwd() + '/tmp/'; + + // create the tmp directory if it doesn't exist + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir); + } + + let filePath = tmpDir + this.name + '.gif'; + + // pipe the image to the filesystem to be written + let imageStream = enc + .createReadStream() + .pipe(fs.createWriteStream(filePath)); + // once finised, generate or serve + imageStream.on('finish', () => { + // only execute callback if it is a function + typeof cb === 'function' && cb(); + }); + + // estimate the font size based on the provided width + let fontSize = Math.floor(this.width / 12) + 'px'; + let fontFamily = 'Courier New'; // monospace works slightly better + + // set the font style + ctx.font = [fontSize, fontFamily].join(' '); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // start encoding gif with following settings + enc.start(); + enc.setRepeat(0); + enc.setDelay(1000); + enc.setQuality(10); + + // if we have a moment duration object + if (typeof timeResult === 'object') { + for (let i = 0; i < this.frames; i++) { + // extract the information we need from the duration + let days = Math.floor(timeResult.asDays()); + let hours = Math.floor(timeResult.asHours() - (days * 24)); + let minutes = Math.floor(timeResult.asMinutes()) - (days * 24 * 60) - (hours * 60); + let seconds = Math.floor(timeResult.asSeconds()) - (days * 24 * 60 * 60) - (hours * 60 * 60) - (minutes * 60); + + // make sure we have at least 2 characters in the string + days = (days.toString().length == 1) ? '0' + days : days; + hours = (hours.toString().length == 1) ? '0' + hours : hours; + minutes = (minutes.toString().length == 1) ? '0' + minutes : minutes; + seconds = (seconds.toString().length == 1) ? '0' + seconds : seconds; + + // build the date string + let string = [days, 'd ', hours, 'h ', minutes, 'm ', seconds, 's'].join(''); + + // paint BG + ctx.fillStyle = this.bg; + ctx.fillRect(0, 0, this.width, this.height); + + // paint text + ctx.fillStyle = this.textColor; + ctx.fillText(string, this.halfWidth, this.halfHeight); + + // add finalised frame to the gif + enc.addFrame(ctx); + + // remove a second for the next loop + timeResult.subtract(1, 'seconds'); + } + } else { + // Date has passed so only using a string + + // BG + ctx.fillStyle = this.bg; + ctx.fillRect(0, 0, this.width, this.height); + + // Text + ctx.fillStyle = this.textColor; + ctx.fillText(timeResult, this.halfWidth, this.halfHeight); + enc.addFrame(ctx); + } + + // finish the gif + enc.finish(); + } +}; \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 2d7e6834fb6366b3120c7a37cc5f637bc4a33928..0000000000000000000000000000000000000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -console.log("欢迎来到 InsCode"); \ No newline at end of file diff --git a/package.json b/package.json index 72caa1750a1c44c18460a496d258fbd3c51c673a..f58f386504ce84e49427a04c6f66db7909a94d1d 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,21 @@ { - "name": "nodejs", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "dev": "node index.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@types/node": "^18.0.6", - "node-fetch": "^3.2.6" - } + "name": "date-gif", + "version": "1.0.0", + "description": "Use Node to generate an animated countdown gif.", + "main": "app.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "node app.js" + }, + "author": "yma16", + "license": "ISC", + "dependencies": { + "canvas": "^3.0.0", + "express": "^5.0.0", + "gifencoder": "^2.0.0", + "moment": "^2.30.0" + }, + "engines": { + "node": ">=6.0.0" } - \ No newline at end of file +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000000000000000000000000000000000000..842e4ca1b4242702e3acabfb5657d77dd37c0f66 --- /dev/null +++ b/public/index.html @@ -0,0 +1,83 @@ + + + +
+ + + +The very simple app I have created allows you to generate a countdown timer animated gif depending on the URL parameters you provide. View the code.
+ +* required.
+ +Simple example setting only the date + time.
+Example setting a custom set of colours.
+Example setting a custom set of dimensions.
+Under the hood the app uses:
+