Browse Source

Implement text that can slide in

stateless
Mikael Finstad 6 years ago
parent
commit
71091b88f0
  1. 9
      examples/slideInText.json5
  2. 2
      index.js
  3. 80
      sources/fabric/fabricFrameSources.js
  4. 3
      sources/frameSource.js
  5. 31
      util.js

9
examples/slideInText.json5

@ -0,0 +1,9 @@
{
outPath: './slideInText.mp4',
clips: [
{ duration: 3, layers: [
{ type: 'image', path: 'assets/img2.jpg' },
{ type: 'slide-in-text', text: 'Text that slides in', top: 0.93, left: 0.04, color: '#fff', originY: 'bottom', fontSize: 0.05 },
] },
],
}

2
index.js

@ -110,7 +110,7 @@ module.exports = async (config = {}) => {
return outLayers;
}
if (['title', 'subtitle', 'news-title'].includes(type)) {
if (['title', 'subtitle', 'news-title', 'slide-in-text'].includes(type)) {
assert(layer.text, 'Please specify a text');
let { fontFamily } = layer;

80
sources/fabric/fabricFrameSources.js

@ -2,12 +2,14 @@ const { fabric } = require('fabric');
const fileUrl = require('file-url');
const { getRandomGradient, getRandomColors } = require('../../colors');
const { easeOutExpo } = require('../../transitions');
const { getPositionProps } = require('../../util');
const { easeOutExpo, easeInOutCubic } = require('../../transitions');
const { getPositionProps, getFrameByKeyFrames } = require('../../util');
// http://fabricjs.com/kitchensink
const defaultFontFamily = 'sans-serif';
const loadImage = async (path) => new Promise((resolve) => fabric.util.loadImage(fileUrl(path), resolve));
function getZoomParams({ progress, zoomDirection, zoomAmount }) {
@ -155,7 +157,7 @@ async function linearGradientFrameSource({ width, height, params }) {
}
async function subtitleFrameSource({ width, height, params }) {
const { text, textColor = '#ffffff', backgroundColor = 'rgba(0,0,0,0.3)', fontFamily = 'sans-serif', delay = 0, speed = 1 } = params;
const { text, textColor = '#ffffff', backgroundColor = 'rgba(0,0,0,0.3)', fontFamily = defaultFontFamily, delay = 0, speed = 1 } = params;
async function onRender(progress, canvas) {
const easedProgress = easeOutExpo(Math.max(0, Math.min((progress - delay) * speed, 1)));
@ -227,7 +229,7 @@ async function imageOverlayFrameSource({ params, width, height }) {
}
async function titleFrameSource({ width, height, params }) {
const { text, textColor = '#ffffff', fontFamily = 'sans-serif', position = 'center', zoomDirection = 'in', zoomAmount = 0.2 } = params;
const { text, textColor = '#ffffff', fontFamily = defaultFontFamily, position = 'center', zoomDirection = 'in', zoomAmount = 0.2 } = params;
async function onRender(progress, canvas) {
// console.log('progress', progress);
@ -266,7 +268,7 @@ async function titleFrameSource({ width, height, params }) {
}
async function newsTitleFrameSource({ width, height, params }) {
const { text, textColor = '#ffffff', backgroundColor = '#d02a42', fontFamily = 'sans-serif', delay = 0, speed = 1 } = params;
const { text, textColor = '#ffffff', backgroundColor = '#d02a42', fontFamily = defaultFontFamily, delay = 0, speed = 1 } = params;
async function onRender(progress, canvas) {
const min = Math.min(width, height);
@ -308,6 +310,73 @@ async function newsTitleFrameSource({ width, height, params }) {
return { onRender };
}
async function getFadedObject({ object, progress }) {
const rect = new fabric.Rect({
left: 0,
width: object.width,
height: object.height,
top: 0,
});
rect.set('fill', new fabric.Gradient({
coords: {
x1: 0,
y1: 0,
x2: object.width,
y2: 0,
},
colorStops: [
{ offset: Math.max(0, (progress * (1 + 0.2)) - 0.2), color: 'rgba(255,255,255,1)' },
{ offset: Math.min(1, (progress * (1 + 0.2))), color: 'rgba(255,255,255,0)' },
],
}));
const gradientMaskImg = await new Promise((r) => rect.cloneAsImage(r));
const fadedImage = await new Promise((r) => object.cloneAsImage(r));
fadedImage.filters.push(new fabric.Image.filters.BlendImage({
image: gradientMaskImg,
mode: 'multiply',
}));
fadedImage.applyFilters();
return fadedImage;
}
async function slideInTextFrameSource({ width, height, params: { text, top = 0.05, left = 0.05, originX = 'left', originY = 'top', fontSize = 0.05, color = '#ffffff', fontFamily = defaultFontFamily } = {} }) {
async function onRender(progress, canvas) {
const fontSizeAbs = Math.round(width * fontSize);
const textBox = new fabric.Text(text, {
fill: color,
fontFamily,
fontSize: fontSizeAbs,
charSpacing: width * 0.1,
});
const { opacity, textSlide } = getFrameByKeyFrames([
{ t: 0.1, props: { opacity: 1, textSlide: 0 } },
{ t: 0.3, props: { opacity: 1, textSlide: 1 } },
{ t: 0.8, props: { opacity: 1, textSlide: 1 } },
{ t: 0.9, props: { opacity: 0, textSlide: 1 } },
], progress);
const fadedObject = await getFadedObject({ object: textBox, progress: easeInOutCubic(textSlide) });
fadedObject.setOptions({
originX,
originY,
top: top * height,
left: left * width,
opacity,
});
canvas.add(fadedObject);
}
return { onRender };
}
async function customFabricFrameSource({ canvas, width, height, params }) {
return params.func(({ width, height, fabric, canvas, params }));
}
@ -322,4 +391,5 @@ module.exports = {
linearGradientFrameSource,
imageFrameSource,
imageOverlayFrameSource,
slideInTextFrameSource,
};

3
sources/frameSource.js

@ -3,7 +3,7 @@ const pMap = require('p-map');
const { rgbaToFabricImage, createCustomCanvasFrameSource, createFabricFrameSource, createFabricCanvas, renderFabricCanvas } = require('./fabric');
const { customFabricFrameSource, subtitleFrameSource, titleFrameSource, newsTitleFrameSource, fillColorFrameSource, radialGradientFrameSource, linearGradientFrameSource, imageFrameSource, imageOverlayFrameSource } = require('./fabric/fabricFrameSources');
const { customFabricFrameSource, subtitleFrameSource, titleFrameSource, newsTitleFrameSource, fillColorFrameSource, radialGradientFrameSource, linearGradientFrameSource, imageFrameSource, imageOverlayFrameSource, slideInTextFrameSource } = require('./fabric/fabricFrameSources');
const createVideoFrameSource = require('./videoFrameSource');
const { createGlFrameSource } = require('./glFrameSource');
@ -18,6 +18,7 @@ const fabricFrameSources = {
'radial-gradient': radialGradientFrameSource,
'fill-color': fillColorFrameSource,
'news-title': newsTitleFrameSource,
'slide-in-text': slideInTextFrameSource,
};
async function createFrameSource({ clip, clipIndex, width, height, channels, verbose, ffmpegPath, ffprobePath, enableFfmpegLog, framerateStr }) {

31
util.js

@ -1,5 +1,7 @@
const execa = require('execa');
const assert = require('assert');
const sortBy = require('lodash/sortBy');
function parseFps(fps) {
const match = typeof fps === 'string' && fps.match(/^([0-9]+)\/([0-9]+)$/);
@ -92,6 +94,34 @@ function getPositionProps({ position, width, height }) {
return { originX, originY, top, left };
}
function getFrameByKeyFrames(keyframes, progress) {
if (keyframes.length < 2) throw new Error('Keyframes must be at least 2');
const sortedKeyframes = sortBy(keyframes, 't');
// TODO check that max is 1
// TODO check that all keyframes have all props
// TODO make smarter so user doesn't need to replicate non-changing props
const invalidKeyframe = sortedKeyframes.find((k, i) => {
if (i === 0) return false;
return k.t === sortedKeyframes[i - 1].t;
});
if (invalidKeyframe) throw new Error('Invalid keyframe');
let prevKeyframe = [...sortedKeyframes].reverse().find((k) => k.t < progress);
// eslint-disable-next-line prefer-destructuring
if (!prevKeyframe) prevKeyframe = sortedKeyframes[0];
let nextKeyframe = sortedKeyframes.find((k) => k.t >= progress);
if (!nextKeyframe) nextKeyframe = sortedKeyframes[sortedKeyframes.length - 1];
if (nextKeyframe.t === prevKeyframe.t) return prevKeyframe.props;
const interProgress = (progress - prevKeyframe.t) / (nextKeyframe.t - prevKeyframe.t);
return Object.fromEntries(Object.entries(prevKeyframe.props).map(([propName, prevVal]) => ([propName, prevVal + ((nextKeyframe.props[propName] - prevVal) * interProgress)])));
}
module.exports = {
parseFps,
readVideoFileInfo,
@ -100,4 +130,5 @@ module.exports = {
toArrayInteger,
readFileStreams,
getPositionProps,
getFrameByKeyFrames,
};
Loading…
Cancel
Save