const { createCanvas, loadImage } = require("canvas"); const fs = require("fs"); const tempDir = "./temp"; const imagesDir = "./images"; // load images let initialized = false; let images = []; let imgPromises = []; fs.promises.readdir(imagesDir).then(filenames => { for (var filename of filenames) imgPromises.push(loadImage(path.join(imagesDir, filename)).then(data => images.push(data))); Promise.all(imgPromises).then(() => initialized = true); }); async function generateFrames (sourceImagePath, duration) { if (!initialized) return null; let outputPath = path.join(tempDir, "frames_" + Math.floor(Math.random() * 10000).toString().padStart(4, "0")); let sourceImage; await Promise.all([ fs.promises.mkdir(outputPath), loadImage(sourceImagePath).then(data => sourceImage = data) ]); // settings const fps = 20; const res = 256; const totalFrames = fps * duration; // init timeline let objects = []; // define classes class Object { constructor(arg = {}) { this.imageId = arg.imageId || -1; this.spawnFrame = arg.spawnFrame || 0; this.lifetime = fps * duration; this.pos = arg.pos || [0.5, 0.5]; this.scale = arg.scale || [1, 1]; this.pivot = arg.pivot || [0.5, 0.5]; this.rot = arg.rot || 0; this.blink = false; this.sineDur = 0; this.actions = arg.actions || []; objects.push(this); } } class Action { constructor(arg = {}) { this.type = arg.type; this.delta = arg.delta; this.start = arg.start || 0; } } // generate timeline const base = new Object(); if (randomChance(80)) base.actions.push(new Action({ type: "scale", delta: [randomRange(0, 0.1), randomRange(0, 0.05)] })); for (i = 0; i < randomRange(2, 12); i++) { const obj = new Object(); obj.pos = [Math.random(), Math.random()]; const uniformScale = randomRange(0.1, 0.7); obj.scale = [uniformScale + Math.random() * 0.01, uniformScale + Math.random() * 0.01]; obj.imageId = Math.floor(Math.random() * images.length); obj.spawnFrame = Math.floor(Math.random() * totalFrames); obj.lifetime = randomRange(fps * 0.25, fps * 3); obj.pivot = [Math.random(), Math.random()]; if (randomChance(25)) obj.actions.push(new Action({ type: "rotate", delta: randomRange(-8, 8) })) if (randomChance(25)) obj.actions.push(new Action({ type: "scale", delta: [randomRange(0, 0.05), 0] })) if (randomChance(25)) obj.blink = true; else if (randomChance(25)) obj.actions.push(new Action({ type: "move", delta: [randomRange(-0.05, 0.05), randomRange(-0.05, 0.05)] })) if (randomChance(75)) obj.sineDur = randomRange(1, 5); } // sort by spawnframe objects.sort((a, b) => (a.spawnFrame > b.spawnFrame) ? 1 : -1); // render frames let framePromises = []; for (var _f = 0; _f < totalFrames; _f++) { const f = _f; framePromises.push(async () => { const canvas = createCanvas(res, res); const ctx = canvas.getContext("2d"); // draw objects for (var object of objects) { var life = f - object.spawnFrame; // frames passed since init if (life < 0) continue; if (life > object.lifetime) continue; if (object.blink && life % 2 == 1) continue; var pos = object.pos; var rot = object.rot; var scale = object.scale; var image = object.imageId == -1 ? sourceImage : images[object.imageId]; // process actions for (var action of object.actions) { if (f < object.spawnFrame + action.start + 1 || ((action.dur > 0) && (f > object.spawnFrame + action.start + action.dur))) continue; // ignore const progress = object.sineDur > 0 ? 10 * Math.sin(life * object.sineDur) : life; switch (action.type) { case "move": pos = [object.pos[0] + action.delta[0] * progress, object.pos[1] + action.delta[1] * progress]; break; case "scale": scale = [object.scale[0] + action.delta[0] * progress, object.scale[1] + action.delta[1] * progress]; break; case "rotate": rot = object.rot + action.delta * life; break; } } var size = [scale[0] * res, scale[1] * res]; // size in pixels var cpos = [pos[0] * res, pos[1] * res]; // center pos var blpos = [cpos[0] - size[0] * object.pivot[0], cpos[1] - size[1] * object.pivot[1]] // bottom left pos // save base canvas transform ctx.save(); // rotate canvas to match object rotation ctx.translate(cpos[0], cpos[1]); ctx.rotate(rot * Math.PI / 180); ctx.translate(-cpos[0], -cpos[1]); // draw ctx.drawImage(image, blpos[0], blpos[1], size[0], size[1]) // restore transform ctx.restore(); // save the frame const buffer = canvas.toBuffer("image/png"); await fs.promises.writeFile(path.join(outputPath, `${f}.png"`), buffer); } }); } await Promise.all(framePromises.map(fn => fn())); return outputPath; } function randomRange (x, y) { return Math.random() * (y - x) + x; } function randomChance (percent) { return (Math.random() * 100) <= percent; } module.exports = { generateFrames };