diff --git a/index.js b/index.js index 92322f8..258580c 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ const JSON5 = require('json5'); const fs = require('fs-extra'); const { parseFps, readVideoFileInfo, readAudioFileInfo, multipleOf2 } = require('./util'); -const { registerFont } = require('./sources/fabricFrameSource'); +const { registerFont } = require('./sources/fabric'); const { createFrameSource } = require('./sources/frameSource'); const { calcTransition } = require('./transitions'); diff --git a/sources/fabric.js b/sources/fabric.js new file mode 100644 index 0000000..e55d1a9 --- /dev/null +++ b/sources/fabric.js @@ -0,0 +1,87 @@ +const { fabric } = require('fabric'); +const nodeCanvas = require('canvas'); + +const { canvasToRgba } = require('./shared'); + + +// Fabric is used as a fundament for compositing layers in editly + +function fabricCanvasToRgba(canvas) { + // https://github.com/fabricjs/fabric.js/blob/26e1a5b55cbeeffb59845337ced3f3f91d533d7d/src/static_canvas.class.js + // https://github.com/fabricjs/fabric.js/issues/3885 + const internalCanvas = fabric.util.getNodeCanvas(canvas.lowerCanvasEl); + const ctx = internalCanvas.getContext('2d'); + + // require('fs').writeFileSync(`${Math.floor(Math.random() * 1e12)}.png`, internalCanvas.toBuffer('image/png')); + // throw new Error('abort'); + + return canvasToRgba(ctx); +} + +function createFabricCanvas({ width, height }) { + return new fabric.StaticCanvas(null, { width, height }); +} + +async function renderFabricCanvas(canvas) { + canvas.renderAll(); + const rgba = fabricCanvasToRgba(canvas); + canvas.clear(); + canvas.dispose(); + return rgba; +} + +async function rgbaToFabricImage({ width, height, rgba }) { + const canvas = nodeCanvas.createCanvas(width, height); + const ctx = canvas.getContext('2d'); + // https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/putImageData + ctx.putImageData(new nodeCanvas.ImageData(Uint8ClampedArray.from(rgba), width, height), 0, 0); + // https://stackoverflow.com/questions/58209996/unable-to-render-tiff-images-and-add-it-as-a-fabric-object + return new fabric.Image(canvas); +} + +async function createFabricFrameSource(func, { width, height, ...rest }) { + const onInit = async () => func(({ width, height, fabric, ...rest })); + + const { onRender = () => {}, onClose = () => {} } = await onInit() || {}; + + return { + readNextFrame: onRender, + close: onClose, + }; +} + +async function createCustomCanvasFrameSource({ width, height, params }) { + const canvas = nodeCanvas.createCanvas(width, height); + const context = canvas.getContext('2d'); + + const { onClose, onRender } = await params.func(({ width, height, canvas })); + + async function readNextFrame(progress) { + context.clearRect(0, 0, canvas.width, canvas.height); + await onRender(progress); + // require('fs').writeFileSync(`${new Date().getTime()}.png`, canvas.toBuffer('image/png')); + // I don't know any way to draw a node-canvas as a layer on a fabric.js canvas, other than converting to rgba first: + return canvasToRgba(context); + } + + return { + readNextFrame, + // Node canvas needs no cleanup https://github.com/Automattic/node-canvas/issues/1216#issuecomment-412390668 + close: onClose, + }; +} + +function registerFont(...args) { + fabric.nodeCanvas.registerFont(...args); +} + +module.exports = { + registerFont, + createFabricFrameSource, + createCustomCanvasFrameSource, + + createFabricCanvas, + renderFabricCanvas, + rgbaToFabricImage, +}; diff --git a/sources/fabricFrameSource.js b/sources/fabric/fabricFrameSources.js similarity index 74% rename from sources/fabricFrameSource.js rename to sources/fabric/fabricFrameSources.js index ec5f18a..a43c690 100644 --- a/sources/fabricFrameSource.js +++ b/sources/fabric/fabricFrameSources.js @@ -1,62 +1,13 @@ const { fabric } = require('fabric'); const fileUrl = require('file-url'); -const nodeCanvas = require('canvas'); -const { createCanvas } = nodeCanvas; - -const { canvasToRgba } = require('./shared'); -const { getRandomGradient, getRandomColors } = require('../colors'); -const { easeOutExpo } = require('../transitions'); -const { getPositionProps } = require('../util'); +const { getRandomGradient, getRandomColors } = require('../../colors'); +const { easeOutExpo } = require('../../transitions'); +const { getPositionProps } = require('../../util'); // http://fabricjs.com/kitchensink -function fabricCanvasToRgba(canvas) { - // https://github.com/fabricjs/fabric.js/blob/26e1a5b55cbeeffb59845337ced3f3f91d533d7d/src/static_canvas.class.js - // https://github.com/fabricjs/fabric.js/issues/3885 - const internalCanvas = fabric.util.getNodeCanvas(canvas.lowerCanvasEl); - const ctx = internalCanvas.getContext('2d'); - - // require('fs').writeFileSync(`${Math.floor(Math.random() * 1e12)}.png`, internalCanvas.toBuffer('image/png')); - // throw new Error('abort'); - - return canvasToRgba(ctx); -} - -function createFabricCanvas({ width, height }) { - return new fabric.StaticCanvas(null, { width, height }); -} - -async function renderFabricCanvas(canvas) { - canvas.renderAll(); - const rgba = fabricCanvasToRgba(canvas); - canvas.clear(); - canvas.dispose(); - return rgba; -} - -async function rgbaToFabricImage({ width, height, rgba }) { - const canvas = createCanvas(width, height); - const ctx = canvas.getContext('2d'); - // https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData - // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/putImageData - ctx.putImageData(new nodeCanvas.ImageData(Uint8ClampedArray.from(rgba), width, height), 0, 0); - // https://stackoverflow.com/questions/58209996/unable-to-render-tiff-images-and-add-it-as-a-fabric-object - return new fabric.Image(canvas); -} - -async function createFabricFrameSource(func, { width, height, ...rest }) { - const onInit = async () => func(({ width, height, fabric, ...rest })); - - const { onRender = () => {}, onClose = () => {} } = await onInit() || {}; - - return { - readNextFrame: onRender, - close: onClose, - }; -} - const loadImage = async (path) => new Promise((resolve) => fabric.util.loadImage(fileUrl(path), resolve)); function getZoomParams({ progress, zoomDirection, zoomAmount }) { @@ -357,40 +308,11 @@ async function newsTitleFrameSource({ width, height, params }) { return { onRender }; } -async function createCustomCanvasFrameSource({ width, height, params }) { - const canvas = createCanvas(width, height); - const context = canvas.getContext('2d'); - - const { onClose, onRender } = await params.func(({ width, height, canvas })); - - async function readNextFrame(progress) { - context.clearRect(0, 0, canvas.width, canvas.height); - await onRender(progress); - // require('fs').writeFileSync(`${new Date().getTime()}.png`, canvas.toBuffer('image/png')); - // I don't know any way to draw a node-canvas as a layer on a fabric.js canvas, other than converting to rgba first: - return canvasToRgba(context); - } - - return { - readNextFrame, - // Node canvas needs no cleanup https://github.com/Automattic/node-canvas/issues/1216#issuecomment-412390668 - close: onClose, - }; -} - async function customFabricFrameSource({ canvas, width, height, params }) { return params.func(({ width, height, fabric, canvas })); } -function registerFont(...args) { - fabric.nodeCanvas.registerFont(...args); -} - module.exports = { - registerFont, - createFabricFrameSource, - createCustomCanvasFrameSource, - customFabricFrameSource, subtitleFrameSource, titleFrameSource, @@ -400,8 +322,4 @@ module.exports = { linearGradientFrameSource, imageFrameSource, imageOverlayFrameSource, - - createFabricCanvas, - renderFabricCanvas, - rgbaToFabricImage, }; diff --git a/sources/frameSource.js b/sources/frameSource.js index dd1dc96..0a8a534 100644 --- a/sources/frameSource.js +++ b/sources/frameSource.js @@ -1,10 +1,25 @@ const assert = require('assert'); const pMap = require('p-map'); -const { rgbaToFabricImage, customFabricFrameSource, createCustomCanvasFrameSource, titleFrameSource, subtitleFrameSource, imageFrameSource, imageOverlayFrameSource, linearGradientFrameSource, radialGradientFrameSource, fillColorFrameSource, createFabricFrameSource, newsTitleFrameSource, createFabricCanvas, renderFabricCanvas } = require('./fabricFrameSource'); +const { rgbaToFabricImage, createCustomCanvasFrameSource, createFabricFrameSource, createFabricCanvas, renderFabricCanvas } = require('./fabric'); + +const { customFabricFrameSource, subtitleFrameSource, titleFrameSource, newsTitleFrameSource, fillColorFrameSource, radialGradientFrameSource, linearGradientFrameSource, imageFrameSource, imageOverlayFrameSource } = require('./fabric/fabricFrameSources'); + const createVideoFrameSource = require('./videoFrameSource'); const { createGlFrameSource } = require('./glFrameSource'); +const fabricFrameSources = { + fabric: customFabricFrameSource, + image: imageFrameSource, + 'image-overlay': imageOverlayFrameSource, + title: titleFrameSource, + subtitle: subtitleFrameSource, + 'linear-gradient': linearGradientFrameSource, + 'radial-gradient': radialGradientFrameSource, + 'fill-color': fillColorFrameSource, + 'news-title': newsTitleFrameSource, +}; + async function createFrameSource({ clip, clipIndex, width, height, channels, verbose, ffmpegPath, ffprobePath, enableFfmpegLog, framerateStr }) { const { layers, duration } = clip; @@ -14,21 +29,17 @@ async function createFrameSource({ clip, clipIndex, width, height, channels, ver const { type, ...params } = layer; console.log('createFrameSource', type, 'clip', clipIndex, 'layer', layerIndex); - const frameSourceFuncs = { - video: createVideoFrameSource, - gl: createGlFrameSource, - canvas: createCustomCanvasFrameSource, - fabric: async (opts) => createFabricFrameSource(customFabricFrameSource, opts), - image: async (opts) => createFabricFrameSource(imageFrameSource, opts), - 'image-overlay': async (opts) => createFabricFrameSource(imageOverlayFrameSource, opts), - title: async (opts) => createFabricFrameSource(titleFrameSource, opts), - subtitle: async (opts) => createFabricFrameSource(subtitleFrameSource, opts), - 'linear-gradient': async (opts) => createFabricFrameSource(linearGradientFrameSource, opts), - 'radial-gradient': async (opts) => createFabricFrameSource(radialGradientFrameSource, opts), - 'fill-color': async (opts) => createFabricFrameSource(fillColorFrameSource, opts), - 'news-title': async (opts) => createFabricFrameSource(newsTitleFrameSource, opts), - }; - const createFrameSourceFunc = frameSourceFuncs[type]; + let createFrameSourceFunc; + if (fabricFrameSources[type]) { + createFrameSourceFunc = async (opts) => createFabricFrameSource(fabricFrameSources[type], opts); + } else { + createFrameSourceFunc = { + video: createVideoFrameSource, + gl: createGlFrameSource, + canvas: createCustomCanvasFrameSource, + }[type]; + } + assert(createFrameSourceFunc, `Invalid type ${type}`); const frameSource = await createFrameSourceFunc({ ffmpegPath, ffprobePath, width, height, duration, channels, verbose, enableFfmpegLog, framerateStr, params });