diff --git a/.gitignore b/.gitignore index 047b6d10..ed01eebd 100755 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ examples/output # testing coverage/** dwt* + +*.local +*.local.* diff --git a/examples/filter.js b/examples/filter.js new file mode 100644 index 00000000..0ae9ad73 --- /dev/null +++ b/examples/filter.js @@ -0,0 +1,125 @@ +const { FFCreator, FFScene, FFFilter, FFText, FilterManager } = require('../index'); +const path = require('path'); +const { cwd } = require('process'); + +// register custom filter. +FilterManager.register({ + name: 'Glitch', + paramsTypes: { + offset: 'float', + speed: 'float', + }, + defaultParams: { + offset: 0.1, + speed: 0.15, + }, + glsl: `precision highp float; + uniform float offset; + uniform float speed; + + float random (vec2 st) { + return fract(sin(dot(st.xy, vec2(12.9898,78.233)))* 43758.5453123); + } + float random (float x, float y) { + return random(vec2(x, y)); + } + float random (vec2 st ,float min, float max) { + return min + random(st) * (max - min); + } + float random (float x, float y, float min, float max) { + return random(vec2(x, y), min, max); + } + + vec4 entry_func (vec2 uv) { + vec3 color = getTextureColor(uv).rgb; + float maxOffset = offset / 6.0; + float flag = floor(currentFrame * speed * 50.0); + float maxSplitOffset = offset / 2.0; + + for (float i = 0.0; i < 10.0; i += 1.0) { + float flagOffset = flag + offset; + float sliceY = random(flagOffset, 1999.0 + float(i)); + float sliceH = random(flagOffset, 9999.0 + float(i)) * 0.25; + float hOffset = random(flagOffset, 9625.0 + float(i), -maxSplitOffset, maxSplitOffset); + vec2 splitOff = uv; + splitOff.x += hOffset; + splitOff = fract(splitOff); + if (uv.y > sliceY && uv.y < fract(sliceY+sliceH)) { + color = getTextureColor(splitOff).rgb; + } + } + + vec2 textureOffset = vec2(random(flag + maxOffset, 9999.0, -maxOffset, maxOffset), random(flag, 9999.0, -maxOffset, maxOffset)); + vec2 uvOff = fract(uv + textureOffset); + + float rnd = random(flag, 9999.0); + if (rnd < 0.33) { + color.r = texture2D(templateTexture, uvOff).r; + } else if (rnd < 0.66) { + color.g = texture2D(templateTexture, uvOff).g; + } else { + color.b = texture2D(templateTexture, uvOff).b; + } + + return vec4(color, 1.0); + }`, +}); + +const instance = new FFCreator({ + width: 400, + height: 360, + debug: true, +}); + +instance.setOutput(path.resolve(cwd(), `./Glitch.mp4`)); + +const scene = new FFScene(); +scene.setDuration(2); +scene.setFilter( + new FFFilter({ + name: 'Glitch', + }), +); +scene.setTransition('fade', 1); +scene.addChild( + new FFText({ + text: 'text', + style: { + fontSize: 32, + }, + color: '#0AF0FF', + x: 40, + y: 40, + }), +); +scene.addChild( + new FFText({ + text: 'Hello World!', + style: { + fontSize: 40, + background: '#040404', + }, + color: '#AE00DF', + x: 100, + y: 200, + }), +); +instance.addChild(scene); + +const scene2 = new FFScene(); +scene2.setDuration(2); +scene2.setBgColor('#000000'); +scene2.addChild( + new FFText({ + text: 'All Right!', + style: { + fontSize: 48, + }, + color: '#FFFFFF', + x: 120, + y: 120, + }), +); +instance.addChild(scene2); + +instance.start(); diff --git a/lib/animate/filter.js b/lib/animate/filter.js new file mode 100644 index 00000000..bb8e8615 --- /dev/null +++ b/lib/animate/filter.js @@ -0,0 +1,205 @@ +'use strict'; + +const createTexture = require('gl-texture2d'); +const createShader = require('gl-shader'); + +const FFShader = require('./shader'); +const GLUtil = require('../utils/gl'); +const { FilterManager } = require('../filter'); + +/** + * @typedef { import("./shader").GLFactory } GLFactory + * @typedef { import("./shader").GL } GL + */ + +/** + * here code was modify from pkg: gl-transition. + */ + +const vertexSource = + '\ +attribute vec2 _p;\ +varying vec2 _uv;\ +void main() {\ + gl_Position = vec4(_p, 0.0, 1.0);\ + _uv = vec2(0.5, 0.5) * (_p + vec2(1.0, 1.0));\ +}\ +'; + +/** @type { Record } */ +const resizeModes = { + cover: r => `.5 + (uv - .5) * vec2(min(ratio / ${r}, 1.), min(${r} / ratio, 1.))`, + contain: r => `.5 + (uv - .5) * vec2(max(ratio / ${r}, 1.), max(${r} / ratio, 1.))`, + stretch: () => 'uv', +}; +function concatFragmentCode(source, resizeMode) { + const r = resizeModes[resizeMode]; + if (!r) throw new Error('invalid resizeMode=' + resizeMode); + + return `\ +precision highp float; +varying vec2 _uv; +uniform sampler2D templateTexture; +uniform float progress, ratio, _textureR; +uniform float currentFrame, totalFrame, startFrame, endFrame; + +vec4 getTextureColor(vec2 uv) { + return texture2D(templateTexture, ${r('_textureR')}); +} + +${source} + +void main() { + gl_FragColor = entry_func(_uv); +}\ +`; +} + +class FFFilter extends FFShader { + /** + * + * @param {{ + * name: string; + * params?: Record; + * }} conf + */ + constructor(conf) { + super({ type: 'filter', ...conf }); + + const { name, params = {}, resizeMode = 'stretch' } = this.conf; + /** @type { string } */ + this.name = name; + /** @type { Record } */ + this.params = params; + /** @type { string } */ + this.resizeMode = resizeMode; + } + + /** + * Binding webgl context + * @param { GL } gl - webgl context + * @public + */ + bindGL(gl) { + super.bindGL(gl); + this.initializeFilterSource(); + this.initializeFilterShader(); + } + + initializeFilterSource() { + if (!this.source) { + this.source = FilterManager.getFilterByName(this.name); + this.defer(() => { + this.source = null; + }); + } + } + + initializeFilterShader() { + if (!this.shader) { + this.shader = createShader( + this.gl, + vertexSource, + concatFragmentCode(this.source.glsl, this.resizeMode), + ); + this.createBuffer(this.gl); + this.defer(() => { + this.shader.dispose(); + this.shader = null; + }); + } + } + + /** + * Rendering function + * @private + * @param {{ + * type: any; + * buffer: Buffer; + * progress: number; + * currentFrame: number; + * totalFrame: number; + * startFrame: number; + * endFrame: number; + * }} props + */ + async render(props) { + const { type, buffer: data, progress, currentFrame, totalFrame, startFrame, endFrame } = props; + + if (!data) { + return; + } + + const { gl, buffer, params } = this; + const width = gl.drawingBufferWidth; + const height = gl.drawingBufferHeight; + + gl.clear(gl.COLOR_BUFFER_BIT); + const pixels = await GLUtil.getPixels({ type, data, width, height }); + + const texture = createTexture(gl, pixels); + texture.minFilter = gl.LINEAR; + texture.magFilter = gl.LINEAR; + + buffer.bind(); + this.draw( + { progress, texture, width, height, currentFrame, totalFrame, startFrame, endFrame }, + params, + ); + + texture.dispose(); + } + + /** + * here code was modify from pkg: gl-transition. + * @param {{ + * progress: number; + * texture: ReturnType; + * width: number; + * height: number; + * currentFrame: number; + * totalFrame: number; + * startFrame: number; + * endFrame: number; + * }} data + * @param { Record } params + */ + async draw( + { progress, texture, width, height, currentFrame, totalFrame, startFrame, endFrame }, + params, + ) { + const { shader, source, gl } = this; + shader.bind(); + shader.attributes._p.pointer(); + + shader.uniforms.ratio = width / height; + shader.uniforms.progress = progress; + shader.uniforms.templateTexture = texture.bind(0); + shader.uniforms._textureR = texture.shape[0] / texture.shape[1]; + shader.uniforms.totalFrame = totalFrame; + shader.uniforms.currentFrame = currentFrame; + shader.uniforms.startFrame = startFrame; + shader.uniforms.endFrame = endFrame; + + let unit = 1; + for (let key in source.paramsTypes) { + const value = key in params ? params[key] : source.defaultParams[key]; + if (source.paramsTypes[key] === 'sampler2D') { + if (!value) { + console.warn( + 'uniform[' + key + ']: A texture MUST be defined for uniform sampler2D of a texture', + ); + } else if (typeof value.bind !== 'function') { + throw new Error('uniform[' + key + ']: A gl-texture2d API-like object was expected'); + } else { + shader.uniforms[key] = value.bind(unit++); + } + } else { + shader.uniforms[key] = value; + } + } + gl.drawArrays(gl.TRIANGLES, 0, 3); + } +} + +module.exports = FFFilter; diff --git a/lib/animate/shader.js b/lib/animate/shader.js new file mode 100644 index 00000000..7dedc980 --- /dev/null +++ b/lib/animate/shader.js @@ -0,0 +1,70 @@ +'use strict'; + +/** + * FFTransition - Class used to handle scene transition animation + * + * ####Example: + * + * const transition = new FFTransition({ name, duration, params }); + * + * @object + */ +const createBuffer = require('gl-buffer'); + +const FFBase = require('../core/base'); + +/** + * @typedef { import("gl") } GLFactory + * @typedef { ReturnType } GL + */ + +class FFShader extends FFBase { + constructor(conf) { + super({ type: 'shader', ...conf }); + } + + /** + * Binding webgl context + * @param { GL } gl - webgl context + * @public + */ + bindGL(gl) { + this.gl = gl; + this.defer(() => (this.gl = null)); + } + + /** + * Create VBO + * @private + * @param { GL } gl + */ + createBuffer(gl) { + if (this.buffer) return; + const data = [-1, -1, -1, 4, 4, -1]; + this.buffer = createBuffer(gl, data, gl.ARRAY_BUFFER, gl.STATIC_DRAW); + this.defer(() => (this.buffer = null)); + } + + /** + * Rendering function + * @private + * @abstract + * @param { Record } props + */ + async render(props) { + props; + throw new Error('not implement function: render'); + } + + dispose() { + this.buffer && this.buffer.dispose(); + this.transition && this.transition.dispose(); + } + + destroy() { + this.dispose(); + super.destroy(); + } +} + +module.exports = FFShader; diff --git a/lib/animate/transition.js b/lib/animate/transition.js old mode 100755 new mode 100644 index 0480cda9..aa23e743 --- a/lib/animate/transition.js +++ b/lib/animate/transition.js @@ -9,15 +9,19 @@ * * @object */ -const createBuffer = require('gl-buffer'); const createTexture = require('gl-texture2d'); const createTransition = require('gl-transition').default; -const FFBase = require('../core/base'); +const FFShader = require('./shader'); const GLUtil = require('../utils/gl'); const ShaderManager = require('../shader/shader'); -class FFTransition extends FFBase { +/** + * @typedef { import("./shader").GLFactory } GLFactory + * @typedef { import("./shader").GL } GL + */ + +class FFTransition extends FFShader { constructor(conf) { super({ type: 'transition', ...conf }); @@ -26,16 +30,15 @@ class FFTransition extends FFBase { this.params = params; this.resizeMode = resizeMode; this.duration = duration; - // this.duration = DateUtil.toSeconds(duration); } /** * Binding webgl context - * @param {GL} gl - webgl context + * @param { GL } gl - webgl context * @public */ bindGL(gl) { - this.gl = gl; + super.bindGL(gl); this.createTransitionSource(this.name); this.createTransition(gl); } @@ -43,16 +46,21 @@ class FFTransition extends FFBase { /** * Create glsl source file for transition * @private + * @param { string } name */ createTransitionSource(name) { const source = ShaderManager.getShaderByName(name); - this.source = source; + if (!this.source && source) { + this.source = source; + this.defer(() => (this.source = null)); + } return source; } /** * Create VBO code * @private + * @param { GL } gl */ createTransition(gl) { if (this.transition) return; @@ -60,24 +68,26 @@ class FFTransition extends FFBase { const { resizeMode } = this; this.createBuffer(gl); this.transition = createTransition(gl, this.source, { resizeMode }); + this.defer(() => { + this.transition.dispose(); + this.transition = null; + }); return this.transition; } - /** - * Create VBO - * @private - */ - createBuffer(gl) { - if (this.buffer) return; - const data = [-1, -1, -1, 4, 4, -1]; - this.buffer = createBuffer(gl, data, gl.ARRAY_BUFFER, gl.STATIC_DRAW); - } - /** * Rendering function * @private + * @param {{ + * type: any; + * fromBuff: Buffer; + * toBuff: Buffer; + * progress: number; + * }} props */ - async render({ type, fromBuff, toBuff, progress }) { + async render(props) { + const { type, fromBuff, toBuff, progress } = props; + if (!fromBuff || !toBuff) return; const { gl, buffer, transition, params } = this; @@ -104,19 +114,6 @@ class FFTransition extends FFBase { textureFrom.dispose(); textureTo.dispose(); } - - dispose() { - this.buffer && this.buffer.dispose(); - this.transition && this.transition.dispose(); - } - - destroy() { - this.dispose(); - this.gl = null; - this.source = null; - this.buffer = null; - this.transition = null; - } } module.exports = FFTransition; diff --git a/lib/core/base.js b/lib/core/base.js index 7b675f76..279ada41 100755 --- a/lib/core/base.js +++ b/lib/core/base.js @@ -14,6 +14,9 @@ const Utils = require('../utils/utils'); const FFEventer = require('../event/eventer'); class FFBase extends FFEventer { + /** + * @param { Record } conf + */ constructor(conf) { super(); @@ -72,6 +75,23 @@ class FFBase extends FFEventer { super.destroy(); this.conf = null; this.parent = null; + + if (this._defer && this._defer.length) { + let cb; + while ((cb = this._defer.pop())) { + cb(); + } + } + } + + /** + * @param { function } cb + */ + defer(cb) { + if (!this._defer) { + this._defer = []; + } + this._defer.push(cb); } } diff --git a/lib/core/renderer.js b/lib/core/renderer.js index 51617fb3..3c6003e6 100755 --- a/lib/core/renderer.js +++ b/lib/core/renderer.js @@ -34,11 +34,34 @@ const FFLogger = require('../utils/logger'); const CanvasUtil = require('../utils/canvas'); const Timeline = require('../timeline/timeline'); +/** + * @typedef { import("../node/scene") } FFScene + * @typedef { import("../creator") } FFCreator + * @typedef { import("gl") } GLFactory + * @typedef { ReturnType } GL + * @typedef { Buffer | Uint8Array } RenderedData + * @typedef { (data: RenderedData | null | undefined) => RenderedData | Promise } RenderTransformer + * @typedef { import("../timeline/frame") } FrameData + */ + class Renderer extends FFBase { - constructor({ creator }) { + /** + * @param {{ + * creator: FFCreator; + * transformers?: RenderTransformer[]; + * }} options + */ + constructor(options) { super({ type: 'renderer' }); + + const { creator, transformers } = options; + + /** @type { boolean } */ this.stop = false; + /** @type { FFCreator } */ this.parent = creator; + /** @type { RenderTransformer[] } */ + this.transformers = transformers; } /** @@ -53,6 +76,7 @@ class Renderer extends FFBase { Perf.start(); this.createGL(); this.transBindGL(); + this.filterBindGL(); this.configCache(); this.createStream(); this.createTimeline(); @@ -111,6 +135,7 @@ class Renderer extends FFBase { createGL() { const width = this.rootConf('width'); const height = this.rootConf('height'); + /** @type { GL } */ this.gl = gl(width, height); } @@ -122,6 +147,10 @@ class Renderer extends FFBase { forEach(this.getScenes(), scene => scene.transition.bindGL(this.gl)); } + filterBindGL() { + forEach(this.getScenes(), scene => scene.filter && scene.filter.bindGL(this.gl)); + } + /** * Create a stream pipeline for data transmission * @private @@ -152,49 +181,62 @@ class Renderer extends FFBase { /** * Render a single frame, They are normal clips and transition animation clips. * @private + * @param { undefined | (data: RenderedData | null | undefined) => void } callback */ async renderFrame(callback) { const { timeline, gl } = this; const frameData = timeline.getFrameData(); if (timeline.isOver()) { - callback && callback(null); + if (callback) { + callback(null); + } return null; } - const { type, progress, sceneStart, sceneOver, isLast, scenesIndex } = frameData; + const { frame, type, progress, isSceneStart, isSceneOver, isLast, scenesIndex } = frameData; const creator = this.getCreator(); let scene, data; // Various scene states // scene frameData start event - if (sceneStart) { - const cindex = frameData.getLastSceneIndex(); - scene = this.getSceneByIndex(cindex); + if (isSceneStart) { + scene = this.getCurrentScene(); creator.addDisplayChild(scene); scene.start(); } // scene frame end event - if (sceneOver && !isLast) { - const pindex = frameData.getPrevSceneIndex(); - scene = this.getSceneByIndex(pindex); + if (isSceneOver && !isLast) { + scene = this.getPrevScene(); creator.removeDisplayChild(scene); } FFLogger.info({ pos: 'renderFrame', msg: `current frame - ${timeline.frame}` }); timeline.nextFrame(); + const packConf = scene => { + return { + ...frameData, + scene, + progress, + currentFrame: frame, + totalFrame: timeline.framesNum, + }; + }; + // Rendering logic part if (type === 'normal') { - data = this.snapshotToBuffer(); + // normal part + data = await this.snapshotToBuffer(packConf(this.getCurrentScene())); } else { + // transition part const currScene = this.getSceneByIndex(scenesIndex[0]); const nextScene = this.getSceneByIndex(scenesIndex[1]); creator.addOnlyDisplayChild(currScene); - const fromBuff = this.snapshotToBuffer(); + const fromBuff = await this.snapshotToBuffer(packConf(currScene)); creator.addOnlyDisplayChild(nextScene); - const toBuff = this.snapshotToBuffer(); + const toBuff = await this.snapshotToBuffer(packConf(nextScene)); const { transition } = currScene; const conf = this.rootConf(); @@ -212,24 +254,68 @@ class Renderer extends FFBase { } } - callback && callback(data); + // if has transformers, call them to transform data. + if (this.transformers && this.transformers.length) { + for (const transformer of this.transformers) { + data = await transformer(data); + } + } + + if (callback) { + callback(data); + } + return data; } /** * Take a screenshot of node-canvas and convert it to buffer. * @private + * @param { ({ scene?: FFScene; currentFrame: number; totalFrame: number; } & Partial) | undefined } params */ - snapshotToBuffer() { + async snapshotToBuffer(params) { const conf = this.rootConf(); const creator = this.getCreator(); + creator.render(); const cacheFormat = conf.getVal('cacheFormat'); const quality = conf.getVal('cacheQuality'); const canvas = creator.app.view; - const buffer = CanvasUtil.toBuffer({ type: cacheFormat, canvas, quality }); - return buffer; + + let buffer = CanvasUtil.toBuffer({ type: cacheFormat, canvas, quality }); + + if (!params) { + return buffer; + } else { + const { scene, progress, totalFrame, currentFrame, startFrame, endFrame } = params; + + if (!scene || !scene.filter) { + return buffer; + } else { + const width = conf.getVal('width'); + const height = conf.getVal('height'); + + await scene.filter.render({ + type: cacheFormat, + progress, + buffer, + currentFrame, + totalFrame, + startFrame, + endFrame, + }); + const pixels = GLUtil.getPixelsByteArray({ + gl: this.gl, + width, + height, + }); + + // re-draw buffer. + const canvas = CanvasUtil.draw({ width, height, data: pixels }); + return CanvasUtil.toBuffer({ type: cacheFormat, canvas, quality }); + } + } } /** @@ -263,6 +349,10 @@ class Renderer extends FFBase { this.synthesis = synthesis; } + /** + * + * @returns { FFScene[] } + */ getScenes() { const creator = this.getCreator(); return creator.children; @@ -271,12 +361,34 @@ class Renderer extends FFBase { /** * Get the scene through index * @private + * @param { number } index + * @returns { FFScene } */ getSceneByIndex(index) { const scenes = this.getScenes(); return scenes[index]; } + /** + * Get current scene from `timeline.frameData`. + * @returns { FFScene } + */ + getCurrentScene() { + const frameData = this.timeline.getFrameData(); + const index = frameData.getLastSceneIndex(); + return this.getSceneByIndex(index); + } + + /** + * Get previou scene from `timeline.frameData`. + * @returns { FFScene } + */ + getPrevScene() { + const frameData = this.timeline.getFrameData(); + const index = frameData.getPrevSceneIndex(); + return this.getSceneByIndex(index); + } + /** * Get parent creator * @private diff --git a/lib/creator.js b/lib/creator.js index 876f2cc4..3608c8d8 100755 --- a/lib/creator.js +++ b/lib/creator.js @@ -34,13 +34,15 @@ class FFCreator extends FFCon { constructor(conf = {}) { super({ type: 'creator', ...conf }); + const { transformers, ...rest } = conf; + this.audios = []; this.inCenter = false; - this.conf = new Conf(conf); + this.conf = new Conf(rest); this.loader = new Loader(); this.createApp(); - this.createRenderer(); + this.createRenderer({ transformers }); this.addAudio(this.getConf('audio')); this.switchLog(this.getConf('log')); } @@ -77,9 +79,12 @@ class FFCreator extends FFCon { /** * Create Renderer instance - Core classes for rendering animations and videos. * @private + * @param {{ + * transformers?: import("./core/renderer").RenderTransformer[]; + * }} props */ - createRenderer() { - this.renderer = new Renderer({ creator: this }); + createRenderer(props = {}) { + this.renderer = new Renderer({ creator: this, ...props }); } /** diff --git a/lib/filter/index.js b/lib/filter/index.js new file mode 100644 index 00000000..f3baeaf0 --- /dev/null +++ b/lib/filter/index.js @@ -0,0 +1,47 @@ +const sample = require('lodash/sample'); +const find = require('lodash/find'); + +/** + * @typedef {{ + * name: string; + * paramsTypes: Record; + * defaultParams: Record; + * glsl: string; + * }} ShaderConfig + */ + +/** @type { ShaderConfig[] } */ +const filters = []; + +const FilterManager = { + /** + * + * @param { string } name + * @returns + */ + getFilterByName(name) { + return name.toLowerCase() === 'random' + ? this.getRandomShader() + : find(filters, filter => filter.name.toLowerCase() === name.toLowerCase()); + }, + + getRandomShader() { + return sample(filters); + }, + + /** + * @param { ShaderConfig } filter + * @returns + */ + register(filter) { + if (filters.find(temp => temp.name.toLowerCase() === filter.name.toLowerCase())) { + throw new Error(`duplicated filter: ${filter.name}`); + } + filters.push(filter); + return this; + }, +}; + +module.exports = { + FilterManager, +}; diff --git a/lib/index.js b/lib/index.js index 330325b4..a5a8b434 100755 --- a/lib/index.js +++ b/lib/index.js @@ -27,10 +27,15 @@ const FFVtuber = require('./node/vtuber'); const FFSubtitle = require('./node/subtitle'); const FFVideoAlbum = require('./node/videos'); const FFAudio = require('./audio/audio'); +const FFShader = require('./animate/shader'); +const FFFilter = require('./animate/filter'); + const FFTween = require('./animate/tween'); const FFLogger = require('./utils/logger'); const FFCreatorCenter = require('./center/center'); +const { FilterManager } = require('./filter'); + module.exports = { echarts, FFCreator, @@ -49,7 +54,12 @@ module.exports = { FFVtuber, FFSubtitle, FFVideoAlbum, + FFShader, + FFFilter, + FFTween, FFLogger, FFCreatorCenter, + + FilterManager, }; diff --git a/lib/node/cons.js b/lib/node/cons.js index 7e130beb..e2e22f5e 100755 --- a/lib/node/cons.js +++ b/lib/node/cons.js @@ -18,6 +18,7 @@ const { Container, ProxyObj } = require('inkpaint'); class FFCon extends FFBase { constructor(conf) { super({ type: 'con', ...conf }); + /** @type { FFCon[] | import("../node/node")[] } */ this.children = []; this.createDisplay(); } @@ -34,7 +35,7 @@ class FFCon extends FFBase { /** * Add child elements - * @param {FFNode} child - node object + * @param { import("../node/node") } child - node object * @public */ addChild(child) { @@ -45,7 +46,7 @@ class FFCon extends FFBase { /** * Remove child elements - * @param {FFNode} child - node object + * @param { import("../node/node") } child - node object * @public */ removeChild(child) { @@ -56,7 +57,7 @@ class FFCon extends FFBase { /** * Clear all child elements and add unique elements - * @param {FFNode} children - any node object + * @param { import("../node/node")[] } children - any node object * @public */ addOnlyDisplayChild(...children) { @@ -66,7 +67,7 @@ class FFCon extends FFBase { /** * Show only display child object - * @param {FFNode} child - node object + * @param { import("../node/node") } scene - node object * @public */ showOnlyDisplayChild(scene) { diff --git a/lib/node/scene.js b/lib/node/scene.js index c8e141e1..cf6b2327 100755 --- a/lib/node/scene.js +++ b/lib/node/scene.js @@ -34,6 +34,7 @@ class FFScene extends FFCon { this.setTransition('fade', 0); this.setBgColor(conf.color || conf.bgcolor || conf.background); + this.setFilter(conf.filter); } /** @@ -58,6 +59,12 @@ class FFScene extends FFCon { } this.transition = new FFTransition({ name, duration, params }); + this.defer(() => { + if (this.transition) { + this.transition.destroy(); + this.transition = null; + } + }); } /** @@ -79,6 +86,14 @@ class FFScene extends FFCon { this.duration = duration; } + /** + * + * @param { import("../animate/filter") } filter + */ + setFilter(filter) { + this.filter = filter; + } + /** * Set background color * @param {string} bgcolor - background color @@ -225,10 +240,14 @@ class FFScene extends FFCon { destroy() { this.destroyAudios(); - this.transition.destroy(); - this.transition = null; this.background = null; this.bgCanvas = null; + + if (this.filter) { + this.filter.destroy(); + this.filter = null; + } + super.destroy(); } } diff --git a/lib/timeline/frame.js b/lib/timeline/frame.js index a76c3a9c..53bffaa7 100755 --- a/lib/timeline/frame.js +++ b/lib/timeline/frame.js @@ -12,11 +12,13 @@ class FrameData { this.type = type; this.scenesIndex = []; this.progress = 0; - this.sceneStart = false; - this.sceneEnd = false; - this.sceneOver = false; + this.isSceneStart = false; + this.isSceneEnd = false; + this.isSceneOver = false; this.isFirst = false; this.isLast = false; + this.startFrame = 0; + this.endFrame = 0; } getLastSceneIndex() { diff --git a/lib/timeline/timeline.js b/lib/timeline/timeline.js index 4bfd83c4..f4f937cf 100755 --- a/lib/timeline/timeline.js +++ b/lib/timeline/timeline.js @@ -16,14 +16,33 @@ const FrameData = require('./frame'); const forEach = require('lodash/forEach'); const TimelineUpdate = require('./update'); +/** + * @typedef { import("../node/scene") } FFScene + * @typedef { { + * type: "normal" | "transition"; + * start: number; + * end: number; + * total: number; + * scenesIndex: number[]; + * } } Segment + */ + class Timeline { constructor(fps) { + /** @type { number } */ this.fps = fps; + /** @type { number } */ this.frame = 0; + /** @type { number } */ this.duration = 0; + /** @type { number } */ this.framesNum = 0; this.frameData = new FrameData(); + /** + * @type { Segment[] } + */ this.segments = []; + /** @type { { sceneStart: number; sceneEnd: number; }[] } */ this.scenes = []; } @@ -34,7 +53,7 @@ class Timeline { * |---|-----|===| (s2) * |---|-------| (s3) * - * @param {array} ffscenes - An array of scenes + * @param { FFScene[] } ffscenes - An array of scenes * @public */ annotate(ffscenes) { @@ -99,7 +118,7 @@ class Timeline { /** * Get the data of the current frame. - * @return {FrameData} the data of the current frame + * @return { FrameData & { frame: number; } } the data of the current frame * @public */ getFrameData() { @@ -110,26 +129,28 @@ class Timeline { frameData.frame = frame; frameData.isFirst = isFirst; frameData.isLast = isLast; - frameData.sceneStart = false; - frameData.sceneEnd = false; - frameData.sceneOver = false; + frameData.isSceneStart = false; + frameData.isSceneEnd = false; + frameData.isSceneOver = false; + frameData.startFrame = 0; + frameData.endFrame = 0; // set the scene to start and end for (let i = 0; i < scenes.length; i++) { const { sceneStart, sceneEnd } = scenes[i]; if (sceneStart === frame) { - frameData.sceneStart = true; + frameData.isSceneStart = true; // break; } if (sceneEnd === frame) { - frameData.sceneEnd = true; + frameData.isSceneEnd = true; // break; } if (sceneEnd + 1 === frame) { - frameData.sceneOver = true; + frameData.isSceneOver = true; // break; } } @@ -142,6 +163,8 @@ class Timeline { frameData.type = type; frameData.scenesIndex = scenesIndex; frameData.progress = type === 'transition' ? (frame - start) / total : 0; + frameData.startFrame = start; + frameData.endFrame = end; return frameData; } } diff --git a/lib/timeline/update.js b/lib/timeline/update.js index f4d2083c..1998bdf9 100755 --- a/lib/timeline/update.js +++ b/lib/timeline/update.js @@ -17,6 +17,7 @@ TWEEN.now = () => new Date().getTime(); const TimelineUpdate = { delta: 0, time: 0, + /** @type { Array<(time: number, delta: number) => void | Promise> } */ cbs: [], /** diff --git a/lib/utils/canvas.js b/lib/utils/canvas.js index 0ce7bf79..0c73994f 100755 --- a/lib/utils/canvas.js +++ b/lib/utils/canvas.js @@ -13,14 +13,19 @@ const FS = require('./fs'); const Utils = require('./utils'); const { createCanvas, createImageData, registerFont } = require('inkpaint'); +/** + * @typedef { ReturnType } Canvas + */ + const CanvasUtil = { + /** @type { Canvas } */ canvas: null, /** * Get a unique canvas instance - * @param {number} width - canvas width - * @param {number} height - canvas height - * @return {Canvas} a unique canvas instance + * @param { number } width - canvas width + * @param { number } height - canvas height + * @return { Canvas } a unique canvas instance * @public */ getCanvas({ width, height }) { @@ -33,18 +38,25 @@ const CanvasUtil = { /** * Utility function to get buffer - * @param {string} type - picture format type - * @param {Canvas} canvas - Container canvas - * @param {number} quality - picture quality - * @return {buffer} The returned buffer value + * @param { string } type - picture format type + * @param { Canvas } canvas - Container canvas + * @param { number } quality - picture quality + * @param {{ + * type: string; + * canvas: Canvas; + * quality: number; + * }} option + * @return { Buffer } The returned buffer value * @public */ - toBuffer({ type, canvas, quality = 70 }) { + toBuffer(option) { + let { type, canvas, quality = 70 } = option; quality = quality >> 0; quality = Math.min(quality, 100); let compressionLevel = 10 - 0.1 * quality; compressionLevel = compressionLevel >> 0; + /** @type { Buffer } */ let buffer; switch (type) { case 'png': @@ -69,13 +81,24 @@ const CanvasUtil = { }, /** - * Draw buffer or array data onto the canvas object - * @param {number} width - canvas width + * @param {number} width * @param {number} height - canvas height * @param {array|buffer} data - buffer or array data * @return {Canvas} a unique canvas instance * @public */ + /** + * Draw buffer or array data onto the canvas object + * @param {{ + * width: number; + * height: number; + * data: Uint8Array | Buffer | Array; + * }} props + * width - canvas width\ + * height - canvas height\ + * data - buffer or array data + * @returns + */ draw({ width, height, data }) { const canvas = this.getCanvas({ width, height }); const ctx = canvas.getContext('2d'); diff --git a/lib/utils/stream.js b/lib/utils/stream.js index 6d5121e7..7114b0a4 100755 --- a/lib/utils/stream.js +++ b/lib/utils/stream.js @@ -24,19 +24,33 @@ const siz = require('siz'); const Readable = require('stream').Readable; +/** + * @typedef { (...args: unknown[]) => Buffer | Uint8Array | Promise | Promise } PullFunc + */ + class FFStream extends Readable { constructor({ size = 1024, parallel }) { super(); + /** @type { number } */ this.size = siz(size); + /** @type { number } */ this.highWaterMark = this.size; // default is 64k + /** @type { number } */ this.parallel = parallel; + /** @type { Buffer | Uint8Array } */ this.data = null; + /** @type { PullFunc } */ this.pullFunc = null; + /** @type { number } */ this.cursor = 0; + /** @type { number } */ this.index = 0; } + /** + * @param { PullFunc } func + */ addPullFunc(func) { this.pullFunc = func; } diff --git a/types/index.d.ts b/types/index.d.ts index ab662c01..efdcf1d6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3,7 +3,6 @@ // Definitions by: javaswing // TypeScript Version: 4.2 - /// /// /// @@ -24,6 +23,8 @@ /// /// /// +/// +/// export = FFCreatorSpace; diff --git a/types/lib/FFBase.d.ts b/types/lib/FFBase.d.ts index d507844d..5881ee6c 100644 --- a/types/lib/FFBase.d.ts +++ b/types/lib/FFBase.d.ts @@ -43,5 +43,7 @@ declare namespace FFCreatorSpace { * @return {object|any} root node */ rootConf(key: string, val: any): any | object; + + defer(cb: Function): void; } } diff --git a/types/lib/FFCreator.d.ts b/types/lib/FFCreator.d.ts index d21cbc7f..8c8f32d0 100644 --- a/types/lib/FFCreator.d.ts +++ b/types/lib/FFCreator.d.ts @@ -3,9 +3,15 @@ /// declare namespace FFCreatorSpace { + type RenderedData = Buffer | Uint8Array; + type DataTransformer = ( + data: RenderedData | null | undefined, + ) => RenderedData | Promise; + interface FFCreatorConf extends ConfOptions { log?: boolean; audio?: FFAudioConf | string; + transformers?: DataTransformer[]; } interface FFCreatorEventMap { diff --git a/types/lib/FFFilter.d.ts b/types/lib/FFFilter.d.ts new file mode 100644 index 00000000..4c42fde3 --- /dev/null +++ b/types/lib/FFFilter.d.ts @@ -0,0 +1,17 @@ +declare namespace FFCreatorSpace { + interface FFFilterConf extends FFNodeCommonConf { + name: string; + params?: Record; + } + + /** + * FFFilter - Custom filter for FFScene. + * @example + * + * const glitch = new FFFilter({ name: "Glitch" }); + * scene.addChild(glitch); + */ + class FFFilter extends FFNode { + constructor(conf: FFFilterConf); + } +} diff --git a/types/lib/FFScene.d.ts b/types/lib/FFScene.d.ts index 68d5b06d..69f265a8 100644 --- a/types/lib/FFScene.d.ts +++ b/types/lib/FFScene.d.ts @@ -9,6 +9,8 @@ declare namespace FFCreatorSpace { setDuration(num: number): void; + setFilter(filter: FFFilter): void; + addAudio(args: FFAudioConf): void; /** diff --git a/types/lib/FilterManager.d.ts b/types/lib/FilterManager.d.ts new file mode 100644 index 00000000..a77ee1bd --- /dev/null +++ b/types/lib/FilterManager.d.ts @@ -0,0 +1,19 @@ +declare namespace FFCreatorSpace { + export type ShaderConfig = { + name: string; + paramsTypes: Record; + defaultParams: Record; + glsl: string; + }; + + export interface FilterManager { + /** */ + getFilterByName(name: string): ShaderConfig | undefined; + + getRandomShader(): ShaderConfig; + + register(filter: ShaderConfig): this; + } + + export const FilterManager: FilterManager; +}