let createCanvas = null; let loadImage = null; let createImageData = null; const isWeb = typeof window != 'undefined'; if (isWeb) { loadImage = function(imageUrl) { return new Promise(function (resolve, reject) { var img = new Image; img.setAttribute('crossOrigin', 'anonymous'); img.src = imageUrl; img.onload = function () { resolve(img); } }); } } else { const canvas = require('canvas'); createCanvas = canvas.createCanvas; loadImage = canvas.loadImage; createImageData = canvas.createImageData; } Date.prototype.Format = function(fmt) { //author: meizz var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小时 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; } class ImageObject { constructor(imageIndex, imageUrl, thumbWidth, thumbHeight, displayEndTime) { this.imageIndex = imageIndex; this.imageUrl = imageUrl; this.imageData = null; this.isAnimationEnd = false; this.displayEndTime = displayEndTime; this.nowStep = 1; this.width = thumbWidth; this.height = thumbHeight; this.canvas = null; this.ctx = null; } async load() { this.image = await loadImage(this.imageUrl); if (typeof document != 'undefined') { if ($('J_thumbImageCanvas_' + this.imageIndex).length == 0) { $('#J_cerateVideoPlayer').append('') } this.canvas = document.getElementById('J_thumbImageCanvas_' + this.imageIndex); this.canvas.width = this.width; this.canvas.height = this.height; } else { this.canvas = createCanvas(this.width, this.height); } this.ctx = this.canvas.getContext('2d'); let x = (this.canvas.width - this.image.width)/2; let y = (this.canvas.height - this.image.height)/2; if (x < y && x < 0) { let scale = this.canvas.width / this.image.width; this.ctx.scale(scale, scale); x = y = 0; } else if (y <= x && y < 0) { let scale = this.canvas.height / this.image.height; this.ctx.scale(scale, scale); x = y = 0; } else { this.ctx.scale(1, 1); } this.ctx.fillStyle = 'white'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(this.image, x, y, this.image.width, this.image.height); this.imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); //console.log('load ', this.imageUrl,this.width, this.height, this.image.width, this.image.height); } } class CanvasVideo { constructor(imageUrlList, videoEffect, musicCode, videoSeconds, width, height, saveFilename, progressCallback) { //canvas this.progressCallbackTimer = null; this.progressCallback = progressCallback; this.videoPlayStatus = 'wait'; this.isWeb = typeof window != 'undefined'; this.width = width; this.height = height; this.fps = 30; this.saveFilename = saveFilename; if (this.isWeb) { this.canvas = document.getElementById('J_videoCanvas'); } else { this.canvas = createCanvas(this.width, this.height); } this.ctx = this.canvas.getContext('2d'); this.ctx.clearRect(0,0, this.canvas.width, this.canvas.height); //video this.videoEffect = videoEffect; this.musicCode = musicCode; this.imageTime = Math.floor(videoSeconds * 1000 / imageUrlList.length); this.videoTotalTime = videoSeconds * 1000; this.currentTime = 0; this.imageObjectList = []; for(let i in imageUrlList) { let displayEndTime = Math.floor(this.imageTime * (parseInt(i)+1)); this.imageObjectList.push(new ImageObject(i, imageUrlList[i], width, height, displayEndTime)); } this.initEncoder(); } initEncoder() { } debug() { //console.log(...arguments); } renderSceneFade() { let imageIndex = this.getCurrentImageIndex(); let imageObj = this.imageObjectList[imageIndex]; let lastImageObj = imageIndex > 0 ? this.imageObjectList[imageIndex-1] : false; if (imageObj.isAnimationEnd) { this.debug('isAnimationEnd skip ', imageIndex); return; } if (!lastImageObj) { this.debug('first .. ', this.currentTime, imageObj.displayEndTime); this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.globalAlpha = 1; if (this.isWeb) { this.ctx.putImageData(imageObj.imageData, 0, 0); } else { this.encoder.setDelay(imageObj.displayEndTime - this.currentTime); this.encoder.addFrame(imageObj.imageData.data); } imageObj.isAnimationEnd = true; return; } var step = 20; let nowStep = imageObj.nowStep; var endImageData = imageObj.imageData var startImageData = lastImageObj.imageData; if (this.isWeb) { var imageData = this.ctx.createImageData(imageObj.width, imageObj.height); } else { var imageData = createImageData(imageObj.width, imageObj.height); } for (var x = 1; x <= imageObj.width; x+=1) { for (var y = 1; y <= imageObj.height; y+= 1) { var index = 4 * ((y - 1) * imageObj.width + (x - 1)); // 变为绿色,色值依次是0, 128, 0, 256 // imagedata.data[index] = 0; // imagedata.data[index + 1] = 128; // imagedata.data[index + 2] = 0; // imagedata.data[index + 3] = 256; for( var i=0;i<3;i++) { imageData.data[index + i] = (endImageData.data[index + i] - startImageData.data[index + i]) / step * nowStep +startImageData.data[index + i]; } imageData.data[index + 3] = 256; } } imageObj.isAnimationEnd = step==nowStep; imageObj.nowStep++; this.debug(imageObj.displayEndTime, imageObj.isAnimationEnd,imageObj.nowStep,imageObj.width,imageObj.height ) if (this.isWeb) { this.ctx.putImageData(imageData, 0, 0); } else { if (imageObj.isAnimationEnd) { this.encoder.setDelay(imageObj.displayEndTime - this.currentTime); } else { this.encoder.setFrameRate(this.fps); } this.encoder.addFrame(imageData.data); } } renderSceneZoom() { let imageIndex = this.getCurrentImageIndex(); let imageObj = this.imageObjectList[imageIndex]; if (typeof imageObj.scale == 'undefined') { imageObj.scale = 0.8; } if (typeof imageObj.alpha == 'undefined') { imageObj.alpha = 0.5; } if (imageObj.isAnimationEnd) { this.debug('isAnimationEnd skip ', imageIndex); return; } // this.ctx.globalAlpha = 1; // this.ctx.fillStyle = 'white'; // this.ctx.fillRect(0, 0, this.width, this.height); let frameCount = Math.ceil(500/1000*this.fps); imageObj.scale += 0.4/frameCount; imageObj.scale = Math.min(1.2, imageObj.scale); imageObj.alpha += 0.5/frameCount; imageObj.alpha = Math.min(1, imageObj.alpha); imageObj.isAnimationEnd = imageObj.scale==1.2; if (imageObj.isAnimationEnd) { imageObj.alpha = 1; !this.isWeb && this.encoder.setDelay(imageObj.displayEndTime - this.currentTime); } else { !this.isWeb && this.encoder.setFrameRate(this.fps); } let newX = (imageObj.width * imageObj.scale - imageObj.width)/-2; let newY = (imageObj.height * imageObj.scale - imageObj.height)/-2; this.ctx.globalAlpha = imageObj.alpha; this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.drawImage(imageObj.canvas, newX, newY, imageObj.width * imageObj.scale, imageObj.height * imageObj.scale); this.debug('add frame ', imageObj.scale, imageObj.alpha); !this.isWeb && this.encoder.addFrame(this.ctx); } renderSceneSlide() { let imageIndex = this.getCurrentImageIndex(); let imageObj = this.imageObjectList[imageIndex]; let direction = imageIndex % 2 == 0 ? 'left': 'right'; let moveDistance = Math.ceil(imageObj.width / 2); let duration = 500; if (typeof imageObj.moveX == 'undefined') { imageObj.moveX = direction == 'left' ? -moveDistance : moveDistance; } if (typeof imageObj.alpha == 'undefined') { imageObj.alpha = 0; } if (imageObj.isAnimationEnd) { this.debug('isAnimationEnd skip ', imageIndex); return; } if (imageIndex == 0) { this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.globalAlpha = 1; this.ctx.fillStyle = 'white'; this.ctx.fillRect(0, 0, this.width, this.height); } let frameCount = Math.ceil(duration/1000*this.fps); let moveStep = Math.ceil(moveDistance / frameCount) * (direction == 'left' ? 1 : -1); imageObj.moveX += moveStep; if (direction == 'left') { imageObj.moveX = Math.min(0, imageObj.moveX); } else { imageObj.moveX = Math.max(0, imageObj.moveX); } imageObj.alpha += 1/frameCount; imageObj.alpha = Math.min(1, imageObj.alpha); imageObj.isAnimationEnd = imageObj.moveX==0; if (imageObj.isAnimationEnd) { imageObj.alpha = 1; !this.isWeb && this.encoder.setDelay(imageObj.displayEndTime - this.currentTime); } else { !this.isWeb && this.encoder.setFrameRate(this.fps); } this.ctx.globalAlpha = imageObj.alpha; this.ctx.drawImage(imageObj.canvas, imageObj.moveX, 0, imageObj.width, imageObj.height); this.debug('add frame ', imageObj.moveX, imageObj.alpha); !this.isWeb && this.encoder.addFrame(this.ctx); } renderShutters() { let imageIndex = this.getCurrentImageIndex(); let imageObj = this.imageObjectList[imageIndex]; let lastImageObj = imageIndex > 0 ? this.imageObjectList[imageIndex-1] : false; if (imageObj.isAnimationEnd) { this.debug('isAnimationEnd skip ', imageIndex); return; } if (!lastImageObj) { this.debug('first .. ', this.currentTime, imageObj.displayEndTime); this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.globalAlpha = 1; if (this.isWeb) { this.ctx.putImageData(imageObj.imageData, 0, 0); } else { this.encoder.setDelay(imageObj.displayEndTime - this.currentTime); this.encoder.addFrame(imageObj.imageData.data); } imageObj.isAnimationEnd = true; return; } var direction = imageIndex % 4; var step = 900 / 1000 * this.fps; var lineWidth = 40; var lineDistance = 30; let nowStep = imageObj.nowStep; var endImageData = imageObj.imageData var startImageData = lastImageObj.imageData; if (this.isWeb) { var imageData = this.ctx.createImageData(imageObj.width, imageObj.height); } else { var imageData = createImageData(imageObj.width, imageObj.height); } for (var x = 1; x <= imageObj.width; x+=1) { for (var y = 1; y <= imageObj.height; y+= 1) { var index = 4 * ((y - 1) * imageObj.width + (x - 1)); if (direction == 0) { let lineTotal = Math.ceil(imageObj.width / lineWidth); let lineIndex = Math.ceil(x /lineWidth); let oldPixMaxY = imageObj.height - imageObj.height / step * nowStep; oldPixMaxY = oldPixMaxY - (lineTotal - lineIndex) * lineWidth / 3 * 2; for (var i = 0; i < 4; i++) { imageData.data[index + i] = y < oldPixMaxY ? startImageData.data[index + i] : endImageData.data[index + i]; } imageData.data[index + 3] = 256; } else if (direction == 1) { let lineTotal = Math.ceil(imageObj.height / lineWidth); let lineIndex = Math.ceil(y /lineWidth); let oldPixMinX = imageObj.width / step * nowStep; oldPixMinX = oldPixMinX + (lineTotal - lineIndex) * lineWidth / 3 * 2; for (var i = 0; i < 4; i++) { imageData.data[index + i] = x > oldPixMinX ? startImageData.data[index + i] : endImageData.data[index + i]; } imageData.data[index + 3] = 256; } else if (direction == 2) { let lineTotal = Math.ceil(imageObj.width / lineWidth); let lineIndex = Math.ceil(x /lineWidth); let oldPixMinY = imageObj.height / step * nowStep; oldPixMinY = oldPixMinY + (lineTotal - lineIndex) * lineWidth / 3 * 2; for (var i = 0; i < 4; i++) { imageData.data[index + i] = y > oldPixMinY ? startImageData.data[index + i] : endImageData.data[index + i]; } imageData.data[index + 3] = 256; } else { let lineTotal = Math.ceil(imageObj.height / lineWidth); let lineIndex = Math.ceil(y /lineWidth); let oldPixMaxX = imageObj.width - imageObj.width / step * nowStep; oldPixMaxX = oldPixMaxX - (lineTotal - lineIndex) * lineWidth / 3 * 2; for (var i = 0; i < 4; i++) { imageData.data[index + i] = x < oldPixMaxX ? startImageData.data[index + i] : endImageData.data[index + i]; } imageData.data[index + 3] = 256; } } } imageObj.isAnimationEnd = step==nowStep; imageObj.nowStep++; this.debug(imageObj.displayEndTime, imageObj.isAnimationEnd,imageObj.nowStep,imageObj.width,imageObj.height ) if (this.isWeb) { this.ctx.putImageData(imageData, 0, 0); } else { if (imageObj.isAnimationEnd) { this.encoder.setDelay(imageObj.displayEndTime - this.currentTime); } else { this.encoder.setFrameRate(this.fps); } this.encoder.addFrame(imageData.data); } } renderRotate() { let imageIndex = this.getCurrentImageIndex(); let imageObj = this.imageObjectList[imageIndex]; let duration = 300; let rotateAngle = 45; if (typeof imageObj.rotate == 'undefined') { imageObj.rotate = -45; } this.ctx.setTransform(1, 0, 0, 1, 0, 0); if (imageObj.isAnimationEnd) { this.debug('isAnimationEnd skip ', imageIndex); return; } if (imageIndex == 0) { this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.globalAlpha = 1; this.ctx.fillStyle = 'white'; this.ctx.fillRect(0, 0, this.width, this.height); } let frameCount = Math.ceil(duration / 1000 * this.fps); let moveStep = Math.ceil(rotateAngle / frameCount); imageObj.rotate += moveStep; imageObj.rotate = Math.min(0, imageObj.rotate); imageObj.isAnimationEnd = imageObj.rotate==0; if (imageObj.isAnimationEnd) { !this.isWeb && this.encoder.setDelay(imageObj.displayEndTime - this.currentTime); } else { !this.isWeb && this.encoder.setFrameRate(this.fps); } this.ctx.rotate(imageObj.rotate * Math.PI / 180); this.ctx.drawImage(imageObj.canvas, 0, 0, imageObj.width, imageObj.height); this.debug('add frame ', imageObj.moveX, imageObj.alpha); !this.isWeb && this.encoder.addFrame(this.ctx); } renderSceneSkew() { let imageIndex = this.getCurrentImageIndex(); let imageObj = this.imageObjectList[imageIndex]; let duration = 400; let angle = 1; if (typeof imageObj.angle == 'undefined') { imageObj.angle = 1; } if (imageObj.isAnimationEnd) { this.debug('isAnimationEnd skip ', imageIndex); return; } let frameCount = Math.ceil(duration / 1000 * this.fps); let moveStep = angle / frameCount; imageObj.angle -= moveStep; imageObj.angle = Math.max(0, imageObj.angle); imageObj.isAnimationEnd = imageObj.angle==0; if (imageObj.isAnimationEnd) { !this.isWeb && this.encoder.setDelay(imageObj.displayEndTime - this.currentTime); } else { !this.isWeb && this.encoder.setFrameRate(this.fps); } this.ctx.setTransform(1, 0, imageObj.angle, 1, 0, 0); this.ctx.drawImage(imageObj.canvas, 0, 0, imageObj.width, imageObj.height); this.debug('add frame ', imageObj.moveX, imageObj.alpha); //fs.writeFileSync('img/'+idx+'.png', this.canvas.toBuffer()); !this.isWeb && this.encoder.addFrame(this.ctx); } getCurrentImageIndex() { return Math.floor(this.currentTime/this.imageTime); } pause() { this.videoPlayStatus = 'pause'; } stop() { this.videoPlayStatus = 'stop'; this.currentTime = 0; } play(oneFrame) { let startTs = (new Date()).getTime(); let timeStep = Math.ceil(1000/this.fps); let self = this; if (this.isWeb) { self.videoPlayStatus = 'playing'; function render() { if (typeof self.progressCallback == 'function') { self.progressCallback({ currentImageIndex: self.getCurrentImageIndex(), videoTotalTime: self.videoTotalTime, currentTime: self.currentTime, videoPlayStatus: self.videoPlayStatus, }); } if (self.videoPlayStatus == 'pause') { self.progressCallbackTimer && clearTimeout(self.progressCallbackTimer); return; } if (self.videoPlayStatus == 'stop') { self.progressCallbackTimer && clearTimeout(self.progressCallbackTimer); self.currentTime = 0; return; } if (self.videoPlayStatus == 'playing') { self.render(); self.currentTime += timeStep; if (oneFrame) { self.videoPlayStatus = 'pause'; } if (self.currentTime >= self.videoTotalTime) { self.videoPlayStatus = 'wait'; self.currentTime = 0; } self.progressCallbackTimer = setTimeout(render, timeStep); } } render(); } else { while(this.currentTime < this.videoTotalTime) { this.render(); this.currentTime += timeStep; } this.encoder.finish(); } let endTs = (new Date()).getTime(); this.makeGifSeconds = Math.ceil((endTs - startTs)/1000); } async loadImages() { let startTs = (new Date()).getTime(); for (let i = 0; i< this.imageObjectList.length;i++) { await this.imageObjectList[i].load(); } this.loadImageSeconds = Math.ceil(((new Date()).getTime() - startTs)/1000); } render() { switch (this.videoEffect) { case 'sceneFade'://淡入当初 this.renderSceneFade(); break; case 'sceneZoom'://图片放大 this.renderSceneZoom(); break; case 'sceneSlide'://左右幻灯 this.renderSceneSlide(); break; case 'sceneShutters'://百叶窗 this.renderShutters(); break; case 'sceneRotate'://旋转飞入 this.renderRotate(); break; case 'sceneSkew': //斜角切换 this.renderSceneSkew() break; } } } if (typeof module !== 'undefined') { module.exports = CanvasVideo; }