From b40ae98ef8d1c0f7d4113df10d45489aeed11c03 Mon Sep 17 00:00:00 2001 From: Charles Forman Date: Fri, 2 Jun 2017 13:36:22 -0400 Subject: [PATCH] Animated GIF exporting! --- package.json | 1 + src/js/main.js | 4 ++ src/js/menu.js | 10 ++++ src/js/window/exporter.js | 102 +++++++++++++++++++++++++++++++++ src/js/window/main-window.js | 35 ++++++++++- src/js/window/notifications.js | 2 + 6 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/js/window/exporter.js diff --git a/package.json b/package.json index c338c32855..81731d022f 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "electron-is-dev": "0.1.2", "electron-localshortcut": "^1.0.0", "express": "^4.15.2", + "gifencoder": "^1.0.6", "gl-vec2": "^1.0.0", "moment": "^2.18.1", "socket.io": "^1.7.3", diff --git a/src/js/main.js b/src/js/main.js index 5a92c21005..896648f0b5 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -616,4 +616,8 @@ ipcMain.on('toggleNewShot', (event, arg) => { ipcMain.on('showTip', (event, arg) => { mainWindow.webContents.send('showTip', arg) +}) + +ipcMain.on('exportAnimatedGif', (event, arg) => { + mainWindow.webContents.send('exportAnimatedGif', arg) }) \ No newline at end of file diff --git a/src/js/menu.js b/src/js/menu.js index 38577eabd7..264b6de648 100644 --- a/src/js/menu.js +++ b/src/js/menu.js @@ -51,6 +51,16 @@ const template = [ { type: 'separator' }, + { + label: 'Export Animated GIF', + accelerator: 'CmdOrCtrl+E', + click ( item, focusedWindow, event) { + ipcRenderer.send('exportAnimatedGif') + } + }, + { + type: 'separator' + }, { accelerator: 'CmdOrCtrl+P', label: 'Print current scene worksheet...', diff --git a/src/js/window/exporter.js b/src/js/window/exporter.js new file mode 100644 index 0000000000..c47177c6c9 --- /dev/null +++ b/src/js/window/exporter.js @@ -0,0 +1,102 @@ +const EventEmitter = require('events').EventEmitter +const fs = require('fs') +const path = require('path') +const util = require('../utils/index.js') +const GIFEncoder = require('gifencoder') +const moment = require('moment') + +const getImage = (url) => { + return new Promise(function(resolve, reject){ + let img = new Image() + img.onload = () => { + resolve(img) + } + img.onerror = () => { + reject(img) + } + img.src = url + }) +} + +class Exporter extends EventEmitter { + constructor () { + super() + } + + exportAnimatedGif (boards, boardSize, destWidth, boardPath) { + let canvases = [] + + let sequence = Promise.resolve() + boards.forEach((board)=> { + // Chain one computation onto the sequence + let canvas = document.createElement('canvas') + canvas.width = boardSize.width + canvas.height = boardSize.height + let context = canvas.getContext('2d') + sequence = sequence.then(function() { + if (board.layers) { + // get reference layer if exists + if (board.layers['reference']) { + let filepath = path.join(boardPath, 'images', board.layers['reference'].url) + return getImage(filepath) + } + } + }).then(function(result) { + // Draw reference if exists and load main. + if (result) { + context.drawImage(result,0,0) + } + let filepath = path.join(boardPath, 'images', board.url) + return getImage(filepath) + }).then(function(result) { + // draw main and push it to the array of canvases + if (result) { + context.drawImage(result,0,0) + } + canvases.push(canvas) + }) + }) + + sequence.then(()=>{ + let aspect = boardSize.height / boardSize.width + let destSize = {width: destWidth, height: Math.floor(destWidth*aspect)} + let encoder = new GIFEncoder(destSize.width, destSize.height) + // save in the boards directory + let filename = boardPath.split(path.sep) + filename = filename[filename.length-1] + if (!fs.existsSync(path.join(boardPath, 'exports'))) { + fs.mkdirSync(path.join(boardPath, 'exports')) + } + let filepath = path.join(boardPath, 'exports', filename + ' ' + moment().format('YYYY-MM-DD hh.mm.ss') + '.gif') + console.log(filepath) + encoder.createReadStream().pipe(fs.createWriteStream(filepath)) + encoder.start() + encoder.setRepeat(0) // 0 for repeat, -1 for no-repeat + encoder.setDelay(2000) // frame delay in ms + encoder.setQuality(10) // image quality. 10 is default. + let canvas = document.createElement('canvas') + canvas.width = destSize.width + canvas.height = destSize.height + let context = canvas.getContext('2d') + for (var i = 0; i < boards.length; i++) { + context.fillStyle = 'white' + context.fillRect(0,0,destSize.width,destSize.height) + context.drawImage(canvases[i], 0,0,destSize.width,destSize.height) + let duration + if (boards[i].duration) { + duration = boards[i].duration + } else { + duration = 2000 + } + encoder.setDelay(duration) + encoder.addFrame(context) + } + encoder.finish() + // emit a finish event! + this.emit('complete', filepath) + }) + } + +} + +module.exports = new Exporter() \ No newline at end of file diff --git a/src/js/window/main-window.js b/src/js/window/main-window.js index 9c450b1b27..f792aaaa4f 100644 --- a/src/js/window/main-window.js +++ b/src/js/window/main-window.js @@ -24,6 +24,7 @@ const LayersEditor = require('./layers-editor.js') const sfx = require('../wonderunit-sound.js') const keytracker = require('../utils/keytracker.js') const storyTips = new(require('./story-tips'))(sfx, notifications) +const exporter = require('./exporter.js') const pkg = require('../../../package.json') @@ -2665,6 +2666,32 @@ let copyBoards = () => { } } +let exportAnimatedGif = () => { + // load all the images in the selection + if (selections.has(currentBoard)) { + saveImageFile() + } + let boards + if (selections.size == 1) { + boards = util.stringifyClone(boardData.boards) + } else { + boards = [...selections].sort(util.compareNumbers).map(n => util.stringifyClone(boardData.boards[n])) + } + let boardSize = storyboarderSketchPane.sketchPane.getCanvasSize() + + notifications.notify({message: "Exporting " + boards.length + " boards. Please wait...", timing: 5}) + sfx.down() + setTimeout(()=>{ + exporter.exportAnimatedGif(boards, boardSize, 500, boardPath, boardFilename) + }, 1000) +} + +exporter.on('complete', path => { + notifications.notify({message: "I exported your board selection as a GIF. Share it with your friends! Post it to you twitter thing or your slack dingus.", timing: 20}) + sfx.positive() + shell.showItemInFolder(path) +}) + /** * Paste * @@ -3276,4 +3303,10 @@ ipcRenderer.on('toggleSpeaking', (event, args) => { ipcRenderer.on('showTip', (event, args) => { storyTips.show() -}) \ No newline at end of file +}) + +ipcRenderer.on('exportAnimatedGif', (event, args) => { + exportAnimatedGif() +}) + + diff --git a/src/js/window/notifications.js b/src/js/window/notifications.js index c7005a5b0e..75d24f2b86 100644 --- a/src/js/window/notifications.js +++ b/src/js/window/notifications.js @@ -9,6 +9,8 @@ const removeNotification = (index) => { if (notification) { let el = notification.el el.style.opacity = 0 + el.style.height = '0px' + clearTimeout(notification.index) if (el.parentNode) { setTimeout(() => {