Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom filter for FFScene, global transformer hook for FFCreator. #290

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ examples/output
# testing
coverage/**
dwt*

*.local
*.local.*
125 changes: 125 additions & 0 deletions examples/filter.js
Original file line number Diff line number Diff line change
@@ -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();
205 changes: 205 additions & 0 deletions lib/animate/filter.js
Original file line number Diff line number Diff line change
@@ -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<string, function> } */
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<string, unknown>;
* }} conf
*/
constructor(conf) {
super({ type: 'filter', ...conf });

const { name, params = {}, resizeMode = 'stretch' } = this.conf;
/** @type { string } */
this.name = name;
/** @type { Record<string, unknown> } */
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<import("gl-texture2d")>;
* width: number;
* height: number;
* currentFrame: number;
* totalFrame: number;
* startFrame: number;
* endFrame: number;
* }} data
* @param { Record<string, unknown> } 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;
Loading