From 386dcb46d6c96df256c058b420bfbc3bbd3df427 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sat, 15 Oct 2022 11:06:50 -0700 Subject: [PATCH] api(core) breaking interface change ideas for hubble v2 --- examples/workshop/hello-world/app-v2.js | 206 ++++++++++++++++++ modules/core/src/animations/deck-animation.js | 4 +- .../core/src/animations/kepler-animation.js | 4 +- .../deck-animator.js} | 42 ++-- .../core/src/{adapters => animators}/index.js | 2 +- modules/core/src/capture/video-capture.js | 8 +- modules/core/src/encoders/frame-encoder.js | 5 + modules/core/src/encoders/index.js | 6 +- .../core/src/encoders/video/gif-encoder.js | 26 +-- .../encoders/video/jpeg-sequence-encoder.js | 26 +-- .../encoders/video/png-sequence-encoder.js | 22 +- ...{stream-encoder.js => realtime-encoder.js} | 24 +- .../core/src/encoders/video/webm-encoder.js | 11 +- modules/core/src/index.js | 9 +- modules/core/src/keyframes/index.js | 2 +- ...era-keyframes.js => map-view-keyframes.js} | 2 +- modules/core/src/types.d.ts | 47 ++-- modules/core/test/index.js | 2 +- ...mes.spec.js => map-view-keyframes.spec.js} | 10 +- modules/main/src/index.js | 8 +- modules/react/src/components/encoders.js | 8 +- .../export-video-panel-container.js | 12 +- .../react/src/components/quick-animation.js | 4 +- modules/react/src/hooks.js | 8 +- modules/react/src/index.js | 2 +- yarn.lock | 6 +- 26 files changed, 367 insertions(+), 139 deletions(-) create mode 100644 examples/workshop/hello-world/app-v2.js rename modules/core/src/{adapters/deck-adapter.js => animators/deck-animator.js} (84%) rename modules/core/src/{adapters => animators}/index.js (95%) rename modules/core/src/encoders/video/{stream-encoder.js => realtime-encoder.js} (78%) rename modules/core/src/keyframes/{camera-keyframes.js => map-view-keyframes.js} (98%) rename modules/core/test/keyframes/{camera-keyframes.spec.js => map-view-keyframes.spec.js} (94%) diff --git a/examples/workshop/hello-world/app-v2.js b/examples/workshop/hello-world/app-v2.js new file mode 100644 index 00000000..f462a0e1 --- /dev/null +++ b/examples/workshop/hello-world/app-v2.js @@ -0,0 +1,206 @@ +/* eslint-disable import/first */ +/* global hubble, deck, popmotion */ + +// RENDER SETTINGS +const timecode = { + start: 0, + end: 3000, + framerate: 30 +}; + +const resolution = { + width: 1920, + height: 1080 +}; + +const previewEncoder = hubble.PreviewEncoder(); +// const webmEncoder = hubble.WebMEncoder({quality: 0.99}); +// const gifEncoder = hubble.GifEncoder({width: 480, height: 270}); + +// -- RENDER SETTINGS END + +// ANIMATION SETTINGS +/** +import { + DeckAnimation, + DeckAdapter, + AnimationManager +} from "@hubble.gl/core"; +import { anticipate, reverseEasing, easeIn } from "popmotion"; +import { ScatterplotLayer, TextLayer } from "@deck.gl/layers"; +*/ + +const BLUE = [37, 80, 129]; + +const VIEW_STATE = { + longitude: -122.402, + latitude: 37.79, + zoom: 14, + bearing: 0, + pitch: 0 +}; + +const animation = new hubble.DeckAnimation({ + getLayers: a => { + /* const frame = a.layerKeyframes['circle'].getFrame(); + console.log(frame); */ + + return a.applyLayerKeyframes([ + new deck.ScatterplotLayer({ + id: 'circle', + data: [ + { + position: [-122.402, 37.79], + color: BLUE, + radius: 1000 + } + ], + getFillColor: d => d.color, + getRadius: d => d.radius, + opacity: 1, // 0.1 + radiusScale: 1 // 0.01 + }), + new deck.TextLayer({ + id: 'text', + data: [ + { + position: [-122.402, 37.79], + text: 'Hola Mundo' + } + ], + opacity: 1, // 0.1 + getAngle: 0, + getPixelOffset: [0, -32], + getColor: [255, 255, 255], + getSize: 64 + }) + ]); + }, + layerKeyframes: [ + { + id: 'circle', + keyframes: [ + { + opacity: 0.1, + radiusScale: 0.01 + }, + { + opacity: 1, + radiusScale: 1 + } + ], + timings: [0, 1000], + easings: popmotion.cubicBezier(0.75, 0.25, 0.25, 0.75) + }, + { + id: 'text', + keyframes: [ + { + opacity: 0, + getPixelOffset: [0, -64] + }, + { + opacity: 1, + getPixelOffset: [0, 0] + } + ], + timings: [0, 1000] + } + ] +}); +// -- ANIMATION SETTINGS END + +// VISUALIZATION +/* import { Deck } from "@deck.gl/core"; */ + +const BACKGROUND = [30 / 255, 30 / 255, 30 / 255, 1]; + +export const deckgl = new deck.Deck({ + canvas: 'deck-canvas', + // Resolution Props + width: resolution.width, + height: resolution.height, + useDevicePixels: 1, // Otherwise retina displays will double resolution. + // Camera Props + initialViewState: VIEW_STATE, + controller: true, + // Visualization Props + parameters: { + // Background color. Most video formats don't fully support transparency + clearColor: BACKGROUND + } +}); +// -- VISUALIZATION END + +// RENDERER SETUP +const animator = new hubble.DeckAnimator({ + deck: deckgl, + animations: [animation] + // drawOverride: (animator, deckgl) => { + // deckgl.setProps(animator.getProps({ + // onNextFrame: animator.setProps // draw loop + // })); + // } +}); + +// In v2, draw loop goes inside DeckAnimator +function setProps() { + deckgl.setProps( + animator.getProps({ + onNextFrame: setProps // draw loop + }) + ); +} + +const embedVideo = blob => { + if (blob && blob.type === 'image/gif') { + const gifElement = document.getElementById('gif-render'); + gifElement.style.display = 'block'; + gifElement.src = URL.createObjectURL(blob); + } + if (blob && blob.type === 'video/webm') { + const videoElement = document.getElementById('video-render'); + videoElement.style.display = 'block'; + videoElement.setAttribute('controls', true); + videoElement.setAttribute('autoplay', true); + videoElement.src = URL.createObjectURL(blob); + videoElement.addEventListener('canplaythrough', () => { + videoElement.play(); + }); + } +}; + +const render = () => { + animator.render({ + encoder: previewEncoder, + timecode, + onComplete: setProps, + onSave: embedVideo // display the rendered video in the UI + }); + deckgl.redraw(true); +}; +// -- RENDERER SETUP END + +// RENDER RUNTIME +setProps(); + +animation.setOnLayersUpdate(layers => { + deckgl.setProps({ + layers + }); +}); + +// -- RENDER RUNTIME END + +// ANIMATION SETTINGS UI +const scrubber = document.getElementById('scrubber'); +scrubber.onchange = e => + animator.seek({ + timeMs: e.target.value + }); +scrubber.setAttribute('max', timecode.end); + +document.body.style.margin = '0px'; +const reRenderElement = document.getElementById('re-render'); +reRenderElement.onclick = render; +// -- ANIMATION UI END diff --git a/modules/core/src/animations/deck-animation.js b/modules/core/src/animations/deck-animation.js index 154a1cbd..1c851104 100644 --- a/modules/core/src/animations/deck-animation.js +++ b/modules/core/src/animations/deck-animation.js @@ -17,7 +17,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {CameraKeyframes, DeckLayerKeyframes} from '../keyframes'; +import {MapViewKeyframes, DeckLayerKeyframes} from '../keyframes'; import Animation from './animation'; function noop() {} @@ -60,7 +60,7 @@ export default class DeckAnimation extends Animation { if (this.cameraKeyframe && cameraKeyframe) { this.cameraKeyframe.set(cameraKeyframe); } else if (cameraKeyframe) { - this.cameraKeyframe = new CameraKeyframes(cameraKeyframe); + this.cameraKeyframe = new MapViewKeyframes(cameraKeyframe); this.unattachedKeyframes.push(this.cameraKeyframe); } diff --git a/modules/core/src/animations/kepler-animation.js b/modules/core/src/animations/kepler-animation.js index 0d1c6bb4..61c0537e 100644 --- a/modules/core/src/animations/kepler-animation.js +++ b/modules/core/src/animations/kepler-animation.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import { - CameraKeyframes, + MapViewKeyframes, KeplerFilterKeyframes, KeplerLayerKeyframes, KeplerTripKeyframes @@ -104,7 +104,7 @@ export default class KeplerAnimation extends Animation { if (this.cameraKeyframe && cameraKeyframe) { this.cameraKeyframe.set(cameraKeyframe); } else if (cameraKeyframe) { - this.cameraKeyframe = new CameraKeyframes(cameraKeyframe); + this.cameraKeyframe = new MapViewKeyframes(cameraKeyframe); this.unattachedKeyframes.push(this.cameraKeyframe); } diff --git a/modules/core/src/adapters/deck-adapter.js b/modules/core/src/animators/deck-animator.js similarity index 84% rename from modules/core/src/adapters/deck-adapter.js rename to modules/core/src/animators/deck-animator.js index 1d23540a..d80c0746 100644 --- a/modules/core/src/adapters/deck-adapter.js +++ b/modules/core/src/animators/deck-animator.js @@ -23,7 +23,7 @@ import {PreviewEncoder} from '../encoders'; import {AnimationManager} from '../animations'; import {VideoCapture} from '../capture/video-capture'; -export default class DeckAdapter { +export default class DeckAnimator { /** @type {any} */ deck; /** @type {AnimationManager} */ @@ -37,28 +37,34 @@ export default class DeckAdapter { /** * @param {Object} params - * @param {AnimationManager} params.animationManager + * @param {any} params.deck + * @param {any[]} params.animations * @param {WebGL2RenderingContext} params.glContext */ - constructor({animationManager = undefined, glContext = undefined}) { - this.animationManager = animationManager || new AnimationManager({}); + constructor({deck = undefined, animations = undefined, glContext = undefined}) { + this.animationManager = new AnimationManager({animations}); this.glContext = glContext; this.videoCapture = new VideoCapture(); this.shouldAnimate = false; this.enabled = false; this.getProps = this.getProps.bind(this); + this.setDeckProps = this.setDeckProps.bind(this); + this.setDeck = this.setDeck.bind(this); this.render = this.render.bind(this); this.stop = this.stop.bind(this); this.seek = this.seek.bind(this); + this.onAfterRender = this.onAfterRender.bind(this); + this.setDeck(deck); } setDeck(deck) { this.deck = deck; + this.setDeckProps(); } /** * @param {Object} params - * @param {any} params.deck + * @param {any?} params.deck * @param {(nextTimeMs: number) => void} params.onNextFrame * @param {Object} params.extraProps */ @@ -86,10 +92,17 @@ export default class DeckAdapter { return {...extraProps, ...props}; } + setDeckProps() { + this.deck.setProps( + this.getProps({ + onNextFrame: this.setDeckProps // draw loop + }) + ); + } + /** * @param {Object} params - * @param {typeof import('../encoders').FrameEncoder} params.Encoder - * @param {Partial} params.formatConfigs + * @param {import('../encoders').FrameEncoder} params.encoder * @param {string} params.filename * @param {{start: number, end: number, framerate: number}} params.timecode * @param {() => void} params.onStopped @@ -97,8 +110,7 @@ export default class DeckAdapter { * @param {() => void} params.onComplete */ render({ - Encoder = PreviewEncoder, - formatConfigs = {}, + encoder = new PreviewEncoder(), filename = undefined, timecode = {start: 0, end: 0, framerate: 30}, onStopped = undefined, @@ -107,14 +119,13 @@ export default class DeckAdapter { }) { this.shouldAnimate = true; this.videoCapture.render({ - Encoder, - formatConfigs, + encoder, timecode, filename, onStop: () => this.stop({onStopped, onSave, onComplete}) }); this.enabled = true; - this.seek({timeMs: timecode.start}); + this.seek(timecode.start); } /** @@ -131,10 +142,9 @@ export default class DeckAdapter { } /** - * @param {Object} params - * @param {number} params.timeMs + * @param {number} timeMs */ - seek({timeMs}) { + seek(timeMs) { this.animationManager.timeline.setTime(timeMs); this.animationManager.draw(); } @@ -147,7 +157,7 @@ export default class DeckAdapter { const areAllLayersLoaded = this.deck && this.deck.props.layers.every(layer => layer.isLoaded); if (this.videoCapture.isRecording() && areAllLayersLoaded && readyToCapture) { this.videoCapture.capture(this.deck.canvas, nextTimeMs => { - this.seek({timeMs: nextTimeMs}); + this.seek(nextTimeMs); proceedToNextFrame(nextTimeMs); }); } diff --git a/modules/core/src/adapters/index.js b/modules/core/src/animators/index.js similarity index 95% rename from modules/core/src/adapters/index.js rename to modules/core/src/animators/index.js index 5285ff99..3966ad55 100644 --- a/modules/core/src/adapters/index.js +++ b/modules/core/src/animators/index.js @@ -17,4 +17,4 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -export {default as DeckAdapter} from './deck-adapter'; +export {default as DeckAnimator} from './deck-animator'; diff --git a/modules/core/src/capture/video-capture.js b/modules/core/src/capture/video-capture.js index 0089365d..aff38a85 100644 --- a/modules/core/src/capture/video-capture.js +++ b/modules/core/src/capture/video-capture.js @@ -62,19 +62,19 @@ export class VideoCapture { /** * Start recording. * @param {Object} params - * @param {typeof FrameEncoder} params.Encoder - * @param {import('types').FormatConfigs} params.formatConfigs + * @param {FrameEncoder} params.encoder * @param {{start: number, end: number, framerate: number, duration?: number}} params.timecode * @param {() => void} params.onStop */ - render({Encoder, formatConfigs, timecode, filename = undefined, onStop = undefined}) { + render({encoder, timecode, filename = undefined, onStop = undefined}) { if (!this.isRecording()) { console.time('render'); this.filename = this._sanitizeFilename(filename); this.timecode = this._sanatizeTimecode(timecode); console.log(`Starting recording for ${this.timecode.duration}ms.`); this.onStop = onStop; - this.encoder = new Encoder({...formatConfigs, framerate: this.timecode.framerate}); + this.encoder = encoder; + encoder.setFramerate(this.timecode.framerate); this.recording = true; this.encoder.start(); } diff --git a/modules/core/src/encoders/frame-encoder.js b/modules/core/src/encoders/frame-encoder.js index 5ec3cdf9..201029bb 100644 --- a/modules/core/src/encoders/frame-encoder.js +++ b/modules/core/src/encoders/frame-encoder.js @@ -48,4 +48,9 @@ export default class FrameEncoder { async save() { throw new Error('Encoder: Implement a save function'); } + + /** @type {(framerate: number) => void} */ + setFramerate(framerate) { + this.framerate = framerate; + } } diff --git a/modules/core/src/encoders/index.js b/modules/core/src/encoders/index.js index db61365c..932c26df 100644 --- a/modules/core/src/encoders/index.js +++ b/modules/core/src/encoders/index.js @@ -22,8 +22,8 @@ export {default as PNGSequenceEncoder} from './video/png-sequence-encoder'; export {default as JPEGSequenceEncoder} from './video/jpeg-sequence-encoder'; export {default as JPEGEncoder} from './photo/jpeg-encoder'; export {default as PNGEncoder} from './photo/png-encoder'; -export {default as WebMEncoder} from './video/webm-encoder'; -export {default as StreamEncoder} from './video/stream-encoder'; +export {default as WEBMEncoder} from './video/webm-encoder'; +export {default as RealtimeEncoder} from './video/realtime-encoder'; export {default as FrameEncoder} from './frame-encoder'; export {default as PreviewEncoder} from './utils/preview-encoder'; -export {default as GifEncoder} from './video/gif-encoder'; +export {default as GIFEncoder} from './video/gif-encoder'; diff --git a/modules/core/src/encoders/video/gif-encoder.js b/modules/core/src/encoders/video/gif-encoder.js index ff25c7b2..acb84bb9 100644 --- a/modules/core/src/encoders/video/gif-encoder.js +++ b/modules/core/src/encoders/video/gif-encoder.js @@ -1,29 +1,29 @@ import {GIFBuilder} from '@loaders.gl/video'; import FrameEncoder from '../frame-encoder'; -export default class GifEncoder extends FrameEncoder { +export default class GIFEncoder extends FrameEncoder { /** * @type {{width: number, height: number, numWorkers: number, sampleInterval: number, jpegQuality: number}} */ - options; + settings; - /** @param {import('types').FrameEncoderSettings} settings */ + /** @param {import('types').GIFSettings} settings */ constructor(settings) { super(settings); this.mimeType = 'image/gif'; this.extension = '.gif'; this.gifBuilder = null; - this.options = {}; + this.settings = {}; - if (settings.gif) { - this.options = {...settings.gif}; + if (settings) { + this.settings = {...settings}; } - this.options.width = this.options.width || 720; - this.options.height = this.options.height || 480; - this.options.numWorkers = this.options.numWorkers || 4; - this.options.sampleInterval = this.options.sampleInterval || 10; - this.options.jpegQuality = this.options.jpegQuality || 1.0; + this.settings.width = this.settings.width || 720; + this.settings.height = this.settings.height || 480; + this.settings.numWorkers = this.settings.numWorkers || 4; + this.settings.sampleInterval = this.settings.sampleInterval || 10; + this.settings.jpegQuality = this.settings.jpegQuality || 1.0; // this.source = settings.source this.source = 'images'; @@ -36,7 +36,7 @@ export default class GifEncoder extends FrameEncoder { start() { this.gifBuilder = new GIFBuilder({ source: this.source, - ...this.options, + ...this.settings, interval: 1 / this.framerate }); } @@ -44,7 +44,7 @@ export default class GifEncoder extends FrameEncoder { /** @param {HTMLCanvasElement} canvas */ async add(canvas) { if (this.source === 'images') { - const dataUrl = canvas.toDataURL('image/jpeg', this.options.jpegQuality); + const dataUrl = canvas.toDataURL('image/jpeg', this.settings.jpegQuality); await this.gifBuilder.add(dataUrl); } } diff --git a/modules/core/src/encoders/video/jpeg-sequence-encoder.js b/modules/core/src/encoders/video/jpeg-sequence-encoder.js index 73b96f43..507e9e7f 100644 --- a/modules/core/src/encoders/video/jpeg-sequence-encoder.js +++ b/modules/core/src/encoders/video/jpeg-sequence-encoder.js @@ -33,21 +33,21 @@ class JPEGSequenceEncoder extends FrameEncoder { /** @type {{filename: ArrayBuffer}} */ filemap; - /** @param {import('types').FrameEncoderSettings} settings */ + /** @param {import('types').JPEGSettings} settings */ constructor(settings) { super(settings); this.tarBuilder = null; this.filemap = {}; - this.options = {}; + this.settings = {}; - if (settings.jpeg) { - this.options = {...settings.jpeg}; + if (settings) { + this.settings = {...settings}; } - this.options.quality = this.options.quality || 1.0; - this.options.archive = this.options.archive || TAR; + this.settings.quality = this.settings.quality || 1.0; + this.settings.archive = this.settings.archive || TAR; - switch (this.options.archive) { + switch (this.settings.archive) { case TAR: { this.mimeType = TARBuilder.properties.mimeType; this.extension = `.${TARBuilder.properties.extensions[0]}`; @@ -59,7 +59,7 @@ class JPEGSequenceEncoder extends FrameEncoder { break; } default: { - throw new Error(`Unsupported archive type [zip, tar]: ${this.options.archive}`); + throw new Error(`Unsupported archive type [zip, tar]: ${this.settings.archive}`); } } } @@ -73,8 +73,8 @@ class JPEGSequenceEncoder extends FrameEncoder { async add(canvas) { const mimeType = 'image/jpeg'; const extension = '.jpg'; - const buffer = await canvasToArrayBuffer(canvas, mimeType, this.options.quality); - switch (this.options.archive) { + const buffer = await canvasToArrayBuffer(canvas, mimeType, this.settings.quality); + switch (this.settings.archive) { case TAR: { const filename = pad(this.tarBuilder.count) + extension; this.tarBuilder.addFile(buffer, filename); @@ -86,7 +86,7 @@ class JPEGSequenceEncoder extends FrameEncoder { break; } default: { - throw new Error(`Unsupported archive type [zip, tar]: ${this.options.archive}`); + throw new Error(`Unsupported archive type [zip, tar]: ${this.settings.archive}`); } } } @@ -95,7 +95,7 @@ class JPEGSequenceEncoder extends FrameEncoder { * @return {Promise} */ async save() { - switch (this.options.archive) { + switch (this.settings.archive) { case TAR: { const arrayBuffer = await this.tarBuilder.build(); return new Blob([arrayBuffer], {type: TARBuilder.properties.mimeType}); @@ -105,7 +105,7 @@ class JPEGSequenceEncoder extends FrameEncoder { return new Blob([arrayBuffer], {type: ZipWriter.mimeTypes[0]}); } default: { - throw new Error(`Unsupported archive type [zip, tar]: ${this.options.archive}`); + throw new Error(`Unsupported archive type [zip, tar]: ${this.settings.archive}`); } } } diff --git a/modules/core/src/encoders/video/png-sequence-encoder.js b/modules/core/src/encoders/video/png-sequence-encoder.js index 03c5222d..3b1192e4 100644 --- a/modules/core/src/encoders/video/png-sequence-encoder.js +++ b/modules/core/src/encoders/video/png-sequence-encoder.js @@ -33,20 +33,20 @@ class PNGSequenceEncoder extends FrameEncoder { /** @type {{filename: ArrayBuffer}} */ filemap; - /** @param {import('types').FrameEncoderSettings} settings */ + /** @param {import('types').PNGSettings} settings */ constructor(settings) { super(settings); this.tarBuilder = null; this.filemap = {}; - this.options = {}; + this.settings = {}; - if (settings.png) { - this.options = {...settings.png}; + if (settings) { + this.settings = {...settings}; } - this.options.archive = this.options.archive || TAR; + this.settings.archive = this.settings.archive || TAR; - switch (this.options.archive) { + switch (this.settings.archive) { case TAR: { this.mimeType = TARBuilder.properties.mimeType; this.extension = `.${TARBuilder.properties.extensions[0]}`; @@ -58,7 +58,7 @@ class PNGSequenceEncoder extends FrameEncoder { break; } default: { - throw new Error(`Unsupported archive type [zip, tar]: ${this.options.archive}`); + throw new Error(`Unsupported archive type [zip, tar]: ${this.settings.archive}`); } } } @@ -73,7 +73,7 @@ class PNGSequenceEncoder extends FrameEncoder { const mimeType = 'image/png'; const extension = '.png'; const buffer = await canvasToArrayBuffer(canvas, mimeType); - switch (this.options.archive) { + switch (this.settings.archive) { case TAR: { const filename = pad(this.tarBuilder.count) + extension; this.tarBuilder.addFile(buffer, filename); @@ -85,7 +85,7 @@ class PNGSequenceEncoder extends FrameEncoder { break; } default: { - throw new Error(`Unsupported archive type [zip, tar]: ${this.options.archive}`); + throw new Error(`Unsupported archive type [zip, tar]: ${this.settings.archive}`); } } } @@ -94,7 +94,7 @@ class PNGSequenceEncoder extends FrameEncoder { * @return {Promise} */ async save() { - switch (this.options.archive) { + switch (this.settings.archive) { case TAR: { const arrayBuffer = await this.tarBuilder.build(); return new Blob([arrayBuffer], {type: TARBuilder.properties.mimeType}); @@ -104,7 +104,7 @@ class PNGSequenceEncoder extends FrameEncoder { return new Blob([arrayBuffer], {type: ZipWriter.mimeTypes[0]}); } default: { - throw new Error(`Unsupported archive type [zip, tar]: ${this.options.archive}`); + throw new Error(`Unsupported archive type [zip, tar]: ${this.settings.archive}`); } } } diff --git a/modules/core/src/encoders/video/stream-encoder.js b/modules/core/src/encoders/video/realtime-encoder.js similarity index 78% rename from modules/core/src/encoders/video/stream-encoder.js rename to modules/core/src/encoders/video/realtime-encoder.js index ef3f42a3..b2e5584d 100644 --- a/modules/core/src/encoders/video/stream-encoder.js +++ b/modules/core/src/encoders/video/realtime-encoder.js @@ -20,10 +20,10 @@ import FrameEncoder from '../frame-encoder'; -/* - HTMLCanvasElement.captureStream() +/** + A frame dropping, high performance video recorder that captures a raw canvas media stream in realtime. */ -class StreamEncoder extends FrameEncoder { +class RealtimeEncoder extends FrameEncoder { /** @type {MediaStream} */ stream; /** @type {MediaRecorder} */ @@ -32,12 +32,18 @@ class StreamEncoder extends FrameEncoder { chunks; /** - * @param {import('types').FrameEncoderSettings} settings + * @param {import('types').RealtimeSettings} settings */ constructor(settings) { super(settings); - this.mimeType = 'video/webm'; - this.extension = '.webm'; + if (settings.video === 'webm') { + this.mimeType = 'video/webm'; + this.extension = '.webm'; + } else if (settings.video === 'mp4') { + this.mimeType = 'video/mp4'; + this.extension = '.mp4'; + } + this.stream = null; this.mediaRecorder = null; this.chunks = []; @@ -58,7 +64,7 @@ class StreamEncoder extends FrameEncoder { async add(canvas) { if (!this.stream) { this.stream = canvas.captureStream(this.framerate); - this.mediaRecorder = new MediaRecorder(this.stream); + this.mediaRecorder = new MediaRecorder(this.stream, {mimeType: this.mimeType}); this.mediaRecorder.start(); this.mediaRecorder.ondataavailable = e => { @@ -75,7 +81,7 @@ class StreamEncoder extends FrameEncoder { /** @type Promise */ const waiting = new Promise(resolve => { this.mediaRecorder.onstop = () => { - const blob = new Blob(this.chunks, {type: 'video/webm'}); + const blob = new Blob(this.chunks, {type: this.mimeType}); this.chunks = []; resolve(blob); }; @@ -86,4 +92,4 @@ class StreamEncoder extends FrameEncoder { } } -export default StreamEncoder; +export default RealtimeEncoder; diff --git a/modules/core/src/encoders/video/webm-encoder.js b/modules/core/src/encoders/video/webm-encoder.js index b8e73ae6..dc624a45 100644 --- a/modules/core/src/encoders/video/webm-encoder.js +++ b/modules/core/src/encoders/video/webm-encoder.js @@ -25,17 +25,14 @@ import FrameEncoder from '../frame-encoder'; /** * WebM Encoder */ -class WebMEncoder extends FrameEncoder { +class WEBMEncoder extends FrameEncoder { /** @type {WebMWriter} */ videoWriter; - /** @param {import('types').FrameEncoderSettings} settings */ + /** @param {import('types').WEBMSettings} settings */ constructor(settings) { super(settings); - this.quality = 0.8; - if (settings.webm && settings.webm.quality) { - this.quality = settings.webm.quality; - } + this.quality = settings.quality || 0.8; const canvas = document.createElement('canvas'); if (canvas.toDataURL('image/webp').substr(5, 10) !== 'image/webp') { @@ -74,4 +71,4 @@ class WebMEncoder extends FrameEncoder { } } -export default WebMEncoder; +export default WEBMEncoder; diff --git a/modules/core/src/index.js b/modules/core/src/index.js index 9fe9a266..8b5b3057 100644 --- a/modules/core/src/index.js +++ b/modules/core/src/index.js @@ -21,22 +21,23 @@ // Intialize globals, check version import './lib/init'; -export {DeckAdapter} from './adapters'; +export {DeckAnimator} from './animators'; export { PNGSequenceEncoder, JPEGSequenceEncoder, JPEGEncoder, PNGEncoder, - WebMEncoder, + WEBMEncoder, FrameEncoder, PreviewEncoder, - GifEncoder + GIFEncoder, + RealtimeEncoder } from './encoders'; export { Keyframes, - CameraKeyframes, + MapViewKeyframes, hold, linear, DeckLayerKeyframes, diff --git a/modules/core/src/keyframes/index.js b/modules/core/src/keyframes/index.js index ecf9f01e..9ee496e4 100644 --- a/modules/core/src/keyframes/index.js +++ b/modules/core/src/keyframes/index.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. export {default as Keyframes} from './keyframes'; -export {default as CameraKeyframes} from './camera-keyframes'; +export {default as MapViewKeyframes} from './map-view-keyframes'; export {default as DeckLayerKeyframes} from './deck-layer-keyframes'; export {default as KeplerLayerKeyframes} from './kepler-layer-keyframes'; export {default as KeplerFilterKeyframes} from './kepler-filter-keyframes'; diff --git a/modules/core/src/keyframes/camera-keyframes.js b/modules/core/src/keyframes/map-view-keyframes.js similarity index 98% rename from modules/core/src/keyframes/camera-keyframes.js rename to modules/core/src/keyframes/map-view-keyframes.js index 9f944132..ed99c378 100644 --- a/modules/core/src/keyframes/camera-keyframes.js +++ b/modules/core/src/keyframes/map-view-keyframes.js @@ -46,7 +46,7 @@ export function flyToInterpolator(start, end, factor, options) { return viewport; } -export default class CameraKeyFrames extends Keyframes { +export default class MapViewKeyframes extends Keyframes { constructor({timings, keyframes, easings, interpolators, width, height}) { super({ timings, diff --git a/modules/core/src/types.d.ts b/modules/core/src/types.d.ts index 673e3f48..4221852a 100644 --- a/modules/core/src/types.d.ts +++ b/modules/core/src/types.d.ts @@ -1,4 +1,4 @@ -import { Keyframes, CameraKeyframes } from "./keyframes"; +import { Keyframes, MapViewKeyframes } from "./keyframes"; import { FrameEncoder } from "./encoders"; @@ -26,28 +26,31 @@ type DeckGl = { }; canvas: any; } -type FrameEncoderSettings = Partial - -interface EncoderSettings extends FormatConfigs { +interface EncoderSettings { framerate: number } -interface FormatConfigs { - png: { - archive: 'tar' | 'zip' - } - jpeg: { - archive: 'tar' | 'zip' - quality: number - }, - webm: { - quality: number - } - gif: { - numWorkers: number, - sampleInterval: number, - width: number, - height: number - jpegQuality: number - } +interface GIFSettings extends EncoderSettings { + numWorkers: number + sampleInterval: number + width: number + height: number + jpegQuality: number +} + +interface WEBMSettings extends EncoderSettings { + quality: number +} + +interface PNGSettings extends EncoderSettings { + archive: 'tar' | 'zip' +} + +interface JPEGSettings extends EncoderSettings { + archive: 'tar' | 'zip' + quality: number } + +interface RealtimeSettings extends EncoderSettings { + video: 'webm' | 'mp4' +} \ No newline at end of file diff --git a/modules/core/test/index.js b/modules/core/test/index.js index be4b5aee..3492d9e8 100644 --- a/modules/core/test/index.js +++ b/modules/core/test/index.js @@ -17,7 +17,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import './keyframes/camera-keyframes.spec'; +import './keyframes/map-view-keyframes.spec'; import './keyframes/kepler-filter-keyframes.spec'; import './keyframes/utils.spec'; import './animations/kepler-animation.spec'; diff --git a/modules/core/test/keyframes/camera-keyframes.spec.js b/modules/core/test/keyframes/map-view-keyframes.spec.js similarity index 94% rename from modules/core/test/keyframes/camera-keyframes.spec.js rename to modules/core/test/keyframes/map-view-keyframes.spec.js index f12a07c4..f96b4db5 100644 --- a/modules/core/test/keyframes/camera-keyframes.spec.js +++ b/modules/core/test/keyframes/map-view-keyframes.spec.js @@ -18,14 +18,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import test from 'tape-catch'; -import {CameraKeyframes, hold} from '@hubble.gl/core'; +import {MapViewKeyframes, hold} from '@hubble.gl/core'; import {easeInOut} from 'popmotion'; import {toLowPrecision} from '@deck.gl/test-utils'; import { flyToInterpolator // @ts-ignore // eslint-disable-next-line import/no-unresolved -} from '@hubble.gl/core/keyframes/camera-keyframes'; +} from '@hubble.gl/core/keyframes/map-view-keyframes'; /* eslint-disable max-len */ const TEST_CASES = [ @@ -70,7 +70,7 @@ const TEST_CASES = [ ]; /* eslint-enable max-len */ -test('CameraKeyframes#flyToInterpolator', t => { +test('MapViewKeyframes#flyToInterpolator', t => { TEST_CASES.filter(testCase => testCase.transition).forEach(testCase => { Object.keys(testCase.transition).forEach(time => { const propsInTransition = flyToInterpolator( @@ -85,8 +85,8 @@ test('CameraKeyframes#flyToInterpolator', t => { t.end(); }); -test('Keyframes#CameraKeyframes', t => { - const camera = new CameraKeyframes({ +test('Keyframes#MapViewKeyframes', t => { + const camera = new MapViewKeyframes({ timings: [0, 5000, 20000, 40000, 42000, 47000], keyframes: [ { diff --git a/modules/main/src/index.js b/modules/main/src/index.js index ed824981..71d2e7c5 100644 --- a/modules/main/src/index.js +++ b/modules/main/src/index.js @@ -19,19 +19,19 @@ // THE SOFTWARE. export { - // Adapter - DeckAdapter, + // Animator + DeckAnimator, // Encoders PNGSequenceEncoder, JPEGSequenceEncoder, JPEGEncoder, PNGEncoder, - WebMEncoder, + WEBMEncoder, FrameEncoder, PreviewEncoder, // Keyframes Keyframes, - CameraKeyframes, + MapViewKeyframes, DeckLayerKeyframes, KeplerFilterKeyframes, KeplerLayerKeyframes, diff --git a/modules/react/src/components/encoders.js b/modules/react/src/components/encoders.js index 662c64f3..0e84131b 100644 --- a/modules/react/src/components/encoders.js +++ b/modules/react/src/components/encoders.js @@ -1,9 +1,9 @@ import { - WebMEncoder, + WEBMEncoder, JPEGSequenceEncoder, PNGSequenceEncoder, PreviewEncoder, - GifEncoder + GIFEncoder } from '@hubble.gl/core'; export const PREVIEW = 'Preview'; @@ -16,8 +16,8 @@ export const ENCODER_LIST = [PREVIEW, WEBM, JPEG, PNG, GIF]; export const ENCODERS = { [PREVIEW]: PreviewEncoder, - [GIF]: GifEncoder, - [WEBM]: WebMEncoder, + [GIF]: GIFEncoder, + [WEBM]: WEBMEncoder, [JPEG]: JPEGSequenceEncoder, [PNG]: PNGSequenceEncoder }; diff --git a/modules/react/src/components/export-video/export-video-panel-container.js b/modules/react/src/components/export-video/export-video-panel-container.js index 69ce55f4..240af144 100644 --- a/modules/react/src/components/export-video/export-video-panel-container.js +++ b/modules/react/src/components/export-video/export-video-panel-container.js @@ -21,13 +21,13 @@ import React, {Component} from 'react'; import {easeInOut} from 'popmotion'; import { - DeckAdapter, + DeckAnimator, KeplerAnimation, - WebMEncoder, + WEBMEncoder, JPEGSequenceEncoder, PNGSequenceEncoder, PreviewEncoder, - GifEncoder + GIFEncoder } from '@hubble.gl/core'; import ExportVideoPanel from './export-video-panel'; @@ -35,8 +35,8 @@ import {parseSetCameraType, scaleToVideoExport} from './utils'; import {DEFAULT_FILENAME, getResolutionSetting} from './constants'; const ENCODERS = { - gif: GifEncoder, - webm: WebMEncoder, + gif: GIFEncoder, + webm: WEBMEncoder, jpeg: JPEGSequenceEncoder, png: PNGSequenceEncoder }; @@ -78,7 +78,7 @@ export class ExportVideoPanelContainer extends Component { const viewState = scaleToVideoExport(mapState, this._getContainer()); this.state.viewState = viewState; this.state.memo = {viewState}; - this.state.adapter = new DeckAdapter({glContext}); + this.state.adapter = new DeckAnimator({glContext}); } componentDidMount() { diff --git a/modules/react/src/components/quick-animation.js b/modules/react/src/components/quick-animation.js index 3fa83f21..e829b71f 100644 --- a/modules/react/src/components/quick-animation.js +++ b/modules/react/src/components/quick-animation.js @@ -1,7 +1,7 @@ import React, {useState, useRef, useMemo} from 'react'; import DeckGL from '@deck.gl/react'; import BasicControls from './basic-controls'; -import {useDeckAdapter, useNextFrame} from '../hooks'; +import {useDeckAnimator, useNextFrame} from '../hooks'; export const QuickAnimation = ({ initialViewState, @@ -15,7 +15,7 @@ export const QuickAnimation = ({ const deck = useMemo(() => deckRef.current && deckRef.current.deck, [deckRef.current]); const [busy, setBusy] = useState(false); const onNextFrame = useNextFrame(); - const {adapter, layers, cameraFrame, setCameraFrame} = useDeckAdapter( + const {adapter, layers, cameraFrame, setCameraFrame} = useDeckAnimator( animation, initialViewState ); diff --git a/modules/react/src/hooks.js b/modules/react/src/hooks.js index 083cab62..5ec20ba5 100644 --- a/modules/react/src/hooks.js +++ b/modules/react/src/hooks.js @@ -19,7 +19,7 @@ // THE SOFTWARE. import {useState, useCallback, useMemo} from 'react'; -import {DeckAdapter, DeckAnimation} from '@hubble.gl/core'; +import {DeckAnimator, DeckAnimation} from '@hubble.gl/core'; import {MapboxLayer} from '@deck.gl/mapbox'; export function useNextFrame() { @@ -27,11 +27,11 @@ export function useNextFrame() { return useCallback(() => updateState({}), []); } -export function useDeckAdapter(deckAnimation, initialViewState = undefined) { +export function useDeckAnimator(deckAnimation, initialViewState = undefined) { const [layers, setLayers] = useState([]); const [cameraFrame, setCameraFrame] = useState(initialViewState); const adapter = useMemo(() => { - const a = new DeckAdapter({}); + const a = new DeckAnimator({}); deckAnimation.setOnLayersUpdate(setLayers); if (initialViewState) { deckAnimation.setOnCameraUpdate(setCameraFrame); @@ -55,7 +55,7 @@ export function useHubbleGl({ }) { const deck = useMemo(() => deckRef.current && deckRef.current.deck, [deckRef.current]); const nextFrame = useNextFrame(); - const {adapter, layers, cameraFrame, setCameraFrame} = useDeckAdapter( + const {adapter, layers, cameraFrame, setCameraFrame} = useDeckAnimator( deckAnimation, initialViewState ); diff --git a/modules/react/src/index.js b/modules/react/src/index.js index d03f5628..f20b0bc9 100644 --- a/modules/react/src/index.js +++ b/modules/react/src/index.js @@ -31,5 +31,5 @@ export { QuickAnimation, RenderPlayer } from './components'; -export {useNextFrame, useDeckAdapter, useDeckAnimation, useHubbleGl} from './hooks'; +export {useNextFrame, useDeckAnimator, useDeckAnimation, useHubbleGl} from './hooks'; export {createKeplerLayers} from './kepler-layers'; diff --git a/yarn.lock b/yarn.lock index 3fba25a9..21c1979f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3995,9 +3995,9 @@ camelize@^1.0.0: integrity sha512-W2lPwkBkMZwFlPCXhIlYgxu+7gC/NUlCtdK652DAJ1JdgV0sTrvuPFshNPrFa1TY2JOkLhgdeEBplB4ezEa+xg== caniuse-lite@^1.0.30001370: - version "1.0.30001374" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz" - integrity sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw== + version "1.0.30001419" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001419.tgz" + integrity sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw== cartocolor@^4.0.2: version "4.0.2"