Browse Source

- Implemet image overlay

- add zoomDirection and zoomAmount to title
stateless
Mikael Finstad 6 years ago
parent
commit
720768efe1
  1. 42
      README.md
  2. 2
      examples/subtitle.json5
  3. 4
      index.js
  4. 82
      sources/fabricFrameSource.js
  5. 3
      sources/frameSource.js
  6. 31
      util.js

42
README.md

@ -114,7 +114,7 @@ Edit specs are JavaScript / JSON objects describing the whole edit operation wit
layerType: { layerType: {
'fill-color': { 'fill-color': {
color: '#ff6666', color: '#ff6666',
}
}
// ...more per-layer-type defaults // ...more per-layer-type defaults
}, },
}, },
@ -158,7 +158,7 @@ Edit specs are JavaScript / JSON objects describing the whole edit operation wit
| `defaults.duration` | `--clip-duration` | Set default clip duration for clips that don't have an own duration | `4` | sec | | `defaults.duration` | `--clip-duration` | Set default clip duration for clips that don't have an own duration | `4` | sec |
| `defaults.transition` | | An object `{ name, duration }` describing the default transition. Set to **null** to disable transitions | | | | `defaults.transition` | | An object `{ name, duration }` describing the default transition. Set to **null** to disable transitions | | |
| `defaults.transition.duration` | `--transition-duration` | Default transition duration | `0.5` | sec | | `defaults.transition.duration` | `--transition-duration` | Default transition duration | `0.5` | sec |
| `defaults.transition.name` | `--transition-name` | Default transition type. See **Transition types** | `random` | |
| `defaults.transition.name` | `--transition-name` | Default transition type. See [Transition types](#transition-types) | `random` | |
| `clips[]` | | List of clip objects that will be played in sequence. Each clip can have one or more layers. | | | | `clips[]` | | List of clip objects that will be played in sequence. Each clip can have one or more layers. | | |
| `clips[].duration` | | Clip duration. See `defaults.duration`. If unset, the clip duration will be that of the **first video layer**. | `defaults.duration` | | | `clips[].duration` | | Clip duration. See `defaults.duration`. If unset, the clip duration will be that of the **first video layer**. | `defaults.duration` | |
| `clips[].transition` | | Specify transition at the **end** of this clip. See `defaults.transition` | `defaults.transition` | | | `clips[].transition` | | Specify transition at the **end** of this clip. See `defaults.transition` | `defaults.transition` | |
@ -201,19 +201,32 @@ Audio layers will be mixed together. If `cutFrom`/`cutTo` is set, the resulting
#### Layer type 'image' #### Layer type 'image'
Full screen image (auto letterboxed)
| Parameter | Description | Default | | | Parameter | Description | Default | |
|-|-|-|-| |-|-|-|-|
| `path` | Path to image file | | | | `path` | Path to image file | | |
| `zoomDirection` | Zoom direction for Ken Burns effect: `in`, `out` or `null` to disable | `in` | |
| `zoomAmount` | Zoom amount for Ken Burns effect | `0.1` | |
See also See [Ken Burns parameters](#ken-burns-parameters).
#### Layer type 'image-overlay'
Image overlay with a custom position on the screen.
| Parameter | Description | Default | |
|-|-|-|-|
| `path` | Path to image file | | |
| `position` | See [Position parameter](#position-parameter) | | |
See also [Ken Burns parameters](#ken-burns-parameters).
#### Layer type 'title' #### Layer type 'title'
- `fontPath` - See `defaults.layer.fontPath` - `fontPath` - See `defaults.layer.fontPath`
- `text` - Title text to show, keep it short - `text` - Title text to show, keep it short
- `textColor` - default `#ffffff` - `textColor` - default `#ffffff`
- `position` - One of either:
- `top`, `bottom` or `center` - vertical position
- An object `{ x, y, originX = 'left', originY = 'top' }`, where `{ x: 0, y: 0 }` is the upper left corner of the screen, and `{ x: 1, y: 1 }` is the lower right corner. `originX` and `originY` are optional, and specify the position origin of the text object.
- `position` - See **Positions**
See also [Ken Burns parameters](#ken-burns-parameters)
#### Layer type 'subtitle' #### Layer type 'subtitle'
- `fontPath` - See `defaults.layer.fontPath` - `fontPath` - See `defaults.layer.fontPath`
@ -261,6 +274,21 @@ Loads a GLSL shader. See [gl.json5](https://github.com/mifi/editly/blob/master/e
- `fragmentPath` - `fragmentPath`
- `vertexPath` (optional) - `vertexPath` (optional)
### Position parameter
Certain layers support the position parameter
`position` can be one of either:
- `top`, `bottom` or `center` - vertical position (horizontally centered)
- An object `{ x, y, originX = 'left', originY = 'top' }`, where `{ x: 0, y: 0 }` is the upper left corner of the screen, and `{ x: 1, y: 1 }` is the lower right corner, `x` is relative to video width, `y` to height. `originX` and `originY` are optional, and specify the position's origin (anchor position) of the object.
### Ken Burns parameters
| Parameter | Description | Default | |
|-|-|-|-|
| `zoomDirection` | Zoom direction for Ken Burns effect: `in`, `out` or `null` to disable | | |
| `zoomAmount` | Zoom amount for Ken Burns effect | `0.1` | |
## Troubleshooting ## Troubleshooting
- If you get `Error: The specified module could not be found.`, try: `npm un -g editly && npm i -g --build-from-source editly` (see [#15](https://github.com/mifi/editly/issues/15)) - If you get `Error: The specified module could not be found.`, try: `npm un -g editly && npm i -g --build-from-source editly` (see [#15](https://github.com/mifi/editly/issues/15))

2
examples/subtitle.json5

@ -12,7 +12,7 @@
] }, ] },
{ duration: 2, layers: [ { duration: 2, layers: [
{ type: 'fill-color' }, { type: 'fill-color' },
{ type: 'title', position: { x: 0, y: 1, originY: 'bottom' }, text: 'Custom position' },
{ type: 'title', position: { x: 0, y: 1, originY: 'bottom' }, text: 'Custom position', zoomDirection: null },
] }, ] },
], ],
} }

4
index.js

@ -70,7 +70,7 @@ module.exports = async (config = {}) => {
const { type, ...restLayer } = layer; const { type, ...restLayer } = layer;
// https://github.com/mifi/editly/issues/39 // https://github.com/mifi/editly/issues/39
if (type === 'image') {
if (['image', 'image-overlay'].includes(type)) {
await assertFileExists(restLayer.path); await assertFileExists(restLayer.path);
} else if (type === 'gl') { } else if (type === 'gl') {
await assertFileExists(restLayer.fragmentPath); await assertFileExists(restLayer.fragmentPath);
@ -78,7 +78,7 @@ module.exports = async (config = {}) => {
if (['fabric', 'canvas'].includes(type)) assert(typeof layer.func === 'function', '"func" must be a function'); if (['fabric', 'canvas'].includes(type)) assert(typeof layer.func === 'function', '"func" must be a function');
if (['image', 'fabric', 'canvas', 'gl', 'radial-gradient', 'linear-gradient', 'fill-color'].includes(type)) return layer;
if (['image', 'image-overlay', 'fabric', 'canvas', 'gl', 'radial-gradient', 'linear-gradient', 'fill-color'].includes(type)) return layer;
// TODO if random-background radial-gradient linear etc // TODO if random-background radial-gradient linear etc
if (type === 'pause') return handleLayer({ ...restLayer, type: 'fill-color' }); if (type === 'pause') return handleLayer({ ...restLayer, type: 'fill-color' });

82
sources/fabricFrameSource.js

@ -7,6 +7,7 @@ const { createCanvas } = nodeCanvas;
const { canvasToRgba } = require('./shared'); const { canvasToRgba } = require('./shared');
const { getRandomGradient, getRandomColors } = require('../colors'); const { getRandomGradient, getRandomColors } = require('../colors');
const { easeOutExpo } = require('../transitions'); const { easeOutExpo } = require('../transitions');
const { getPositionProps } = require('../util');
// http://fabricjs.com/kitchensink // http://fabricjs.com/kitchensink
@ -56,10 +57,21 @@ async function createFabricFrameSource(func, { width, height, ...rest }) {
}; };
} }
const loadImage = async (path) => new Promise((resolve) => fabric.util.loadImage(fileUrl(path), resolve));
function getZoomParams({ progress, zoomDirection, zoomAmount }) {
let scaleFactor = 1;
if (zoomDirection === 'in') scaleFactor = (1 + zoomAmount * progress);
else if (zoomDirection === 'out') scaleFactor = (1 + zoomAmount * (1 - progress));
return scaleFactor;
}
async function imageFrameSource({ verbose, params, width, height }) { async function imageFrameSource({ verbose, params, width, height }) {
if (verbose) console.log('Loading', params.path);
const { path, zoomDirection = 'in', zoomAmount = 0.1 } = params;
const imgData = await new Promise((resolve) => fabric.util.loadImage(fileUrl(params.path), resolve));
if (verbose) console.log('Loading', path);
const imgData = await loadImage(path);
const getImg = () => new fabric.Image(imgData, { const getImg = () => new fabric.Image(imgData, {
originX: 'center', originX: 'center',
@ -76,15 +88,10 @@ async function imageFrameSource({ verbose, params, width, height }) {
if (blurredImg.height > blurredImg.width) blurredImg.scaleToWidth(width); if (blurredImg.height > blurredImg.width) blurredImg.scaleToWidth(width);
else blurredImg.scaleToHeight(height); else blurredImg.scaleToHeight(height);
async function onRender(progress, canvas) { async function onRender(progress, canvas) {
const { zoomDirection = 'in', zoomAmount = 0.1 } = params;
const img = getImg(); const img = getImg();
let scaleFactor = 1;
if (zoomDirection === 'in') scaleFactor = (1 + progress * zoomAmount);
else if (zoomDirection === 'out') scaleFactor = (1 + zoomAmount * (1 - progress));
const scaleFactor = getZoomParams({ progress, zoomDirection, zoomAmount });
if (img.height > img.width) img.scaleToHeight(height * scaleFactor); if (img.height > img.width) img.scaleToHeight(height * scaleFactor);
else img.scaleToWidth(width * scaleFactor); else img.scaleToWidth(width * scaleFactor);
@ -232,34 +239,40 @@ async function subtitleFrameSource({ width, height, params }) {
return { onRender }; return { onRender };
} }
function getPositionProps({ position, width, height, objHeight }) {
let originY = 'center';
let originX = 'center';
let top = height / 2;
let left = width / 2;
if (position === 'top') {
originY = 'top';
top = height * objHeight;
} else if (position === 'bottom') {
originY = 'bottom';
top = height;
}
async function imageOverlayFrameSource({ params, width, height }) {
const { path, position, width: relWidth, height: relHeight, zoomDirection, zoomAmount = 0.1 } = params;
if (position.x != null) {
originX = position.originX || 'left';
left = width * position.x;
}
if (position.y != null) {
originY = position.originY || 'top';
top = height * position.y;
const imgData = await loadImage(path);
const { left, top, originX, originY } = getPositionProps({ position, width, height });
const img = new fabric.Image(imgData, {
originX,
originY,
left,
top,
});
async function onRender(progress, canvas) {
const scaleFactor = getZoomParams({ progress, zoomDirection, zoomAmount });
if (relWidth != null) {
img.scaleToWidth(relWidth * width * scaleFactor);
} else if (relHeight != null) {
img.scaleToHeight(relHeight * height * scaleFactor);
} else {
// Default to screen width
img.scaleToWidth(width * scaleFactor);
}
canvas.add(img);
} }
return { originX, originY, top, left };
return { onRender };
} }
async function titleFrameSource({ width, height, params }) { async function titleFrameSource({ width, height, params }) {
const { text, textColor = '#ffffff', fontFamily = 'sans-serif', position = 'center' } = params;
const { text, textColor = '#ffffff', fontFamily = 'sans-serif', position = 'center', zoomDirection = 'in', zoomAmount = 0.2 } = params;
async function onRender(progress, canvas) { async function onRender(progress, canvas) {
// console.log('progress', progress); // console.log('progress', progress);
@ -268,7 +281,7 @@ async function titleFrameSource({ width, height, params }) {
const fontSize = Math.round(min * 0.1); const fontSize = Math.round(min * 0.1);
const scale = (1 + progress * 0.2).toFixed(4);
const scaleFactor = getZoomParams({ progress, zoomDirection, zoomAmount });
const textBox = new fabric.Textbox(text, { const textBox = new fabric.Textbox(text, {
fill: textColor, fill: textColor,
@ -281,15 +294,15 @@ async function titleFrameSource({ width, height, params }) {
// We need the text as an image in order to scale it // We need the text as an image in order to scale it
const textImage = await new Promise((r) => textBox.cloneAsImage(r)); const textImage = await new Promise((r) => textBox.cloneAsImage(r));
const { left, top, originX, originY } = getPositionProps({ position, width, height, objHeight: 0.05 });
const { left, top, originX, originY } = getPositionProps({ position, width, height });
textImage.set({ textImage.set({
originX, originX,
originY, originY,
left, left,
top, top,
scaleX: scale,
scaleY: scale,
scaleX: scaleFactor,
scaleY: scaleFactor,
}); });
canvas.add(textImage); canvas.add(textImage);
} }
@ -382,6 +395,7 @@ module.exports = {
radialGradientFrameSource, radialGradientFrameSource,
linearGradientFrameSource, linearGradientFrameSource,
imageFrameSource, imageFrameSource,
imageOverlayFrameSource,
createFabricCanvas, createFabricCanvas,
renderFabricCanvas, renderFabricCanvas,

3
sources/frameSource.js

@ -1,7 +1,7 @@
const assert = require('assert'); const assert = require('assert');
const pMap = require('p-map'); const pMap = require('p-map');
const { rgbaToFabricImage, customFabricFrameSource, createCustomCanvasFrameSource, titleFrameSource, subtitleFrameSource, imageFrameSource, linearGradientFrameSource, radialGradientFrameSource, fillColorFrameSource, createFabricFrameSource, newsTitleFrameSource, createFabricCanvas, renderFabricCanvas } = require('./fabricFrameSource');
const { rgbaToFabricImage, customFabricFrameSource, createCustomCanvasFrameSource, titleFrameSource, subtitleFrameSource, imageFrameSource, imageOverlayFrameSource, linearGradientFrameSource, radialGradientFrameSource, fillColorFrameSource, createFabricFrameSource, newsTitleFrameSource, createFabricCanvas, renderFabricCanvas } = require('./fabricFrameSource');
const createVideoFrameSource = require('./videoFrameSource'); const createVideoFrameSource = require('./videoFrameSource');
const { createGlFrameSource } = require('./glFrameSource'); const { createGlFrameSource } = require('./glFrameSource');
@ -20,6 +20,7 @@ async function createFrameSource({ clip, clipIndex, width, height, channels, ver
canvas: createCustomCanvasFrameSource, canvas: createCustomCanvasFrameSource,
fabric: async (opts) => createFabricFrameSource(customFabricFrameSource, opts), fabric: async (opts) => createFabricFrameSource(customFabricFrameSource, opts),
image: async (opts) => createFabricFrameSource(imageFrameSource, opts), image: async (opts) => createFabricFrameSource(imageFrameSource, opts),
'image-overlay': async (opts) => createFabricFrameSource(imageOverlayFrameSource, opts),
title: async (opts) => createFabricFrameSource(titleFrameSource, opts), title: async (opts) => createFabricFrameSource(titleFrameSource, opts),
subtitle: async (opts) => createFabricFrameSource(subtitleFrameSource, opts), subtitle: async (opts) => createFabricFrameSource(subtitleFrameSource, opts),
'linear-gradient': async (opts) => createFabricFrameSource(linearGradientFrameSource, opts), 'linear-gradient': async (opts) => createFabricFrameSource(linearGradientFrameSource, opts),

31
util.js

@ -62,6 +62,36 @@ function toArrayInteger(buffer) {
const multipleOf2 = (x) => (x + (x % 2)); const multipleOf2 = (x) => (x + (x % 2));
function getPositionProps({ position, width, height }) {
let originY = 'center';
let originX = 'center';
let top = height / 2;
let left = width / 2;
const margin = 0.05;
if (position === 'top') {
originY = 'top';
top = height * margin;
} else if (position === 'bottom') {
originY = 'bottom';
top = height * (1 - margin);
} else if (position === 'center') {
originY = 'center';
top = height / 2;
}
if (position && position.x != null) {
originX = position.originX || 'left';
left = width * position.x;
}
if (position && position.y != null) {
originY = position.originY || 'top';
top = height * position.y;
}
return { originX, originY, top, left };
}
module.exports = { module.exports = {
parseFps, parseFps,
readVideoFileInfo, readVideoFileInfo,
@ -69,4 +99,5 @@ module.exports = {
multipleOf2, multipleOf2,
toArrayInteger, toArrayInteger,
readFileStreams, readFileStreams,
getPositionProps,
}; };
Loading…
Cancel
Save