diff --git a/examples/minimal/app.js b/examples/minimal/app.js index 7ce167f3..fe7e9681 100644 --- a/examples/minimal/app.js +++ b/examples/minimal/app.js @@ -22,6 +22,9 @@ const encoderSettings = { }, jpeg: { quality: 0.8 + }, + gif: { + sampleInterval: 1000 } }; diff --git a/examples/terrain/app.js b/examples/terrain/app.js index 39986098..20944bc7 100644 --- a/examples/terrain/app.js +++ b/examples/terrain/app.js @@ -23,6 +23,9 @@ const encoderSettings = { }, jpeg: { quality: 0.8 + }, + gif: { + sampleInterval: 1000 } }; diff --git a/modules/core/package.json b/modules/core/package.json index 91442e6a..e39a1d19 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -30,6 +30,8 @@ "build-bundle": "webpack --display=minimal --config ../../scripts/bundle.config.js" }, "dependencies": { + "@loaders.gl/core": "^2.1.6", + "@loaders.gl/video": "2.2.0-alpha.1", "@luma.gl/engine": "^8.1.2", "downloadjs": "^1.4.7", "popmotion": "^8.6.10", diff --git a/modules/core/src/capture/video-capture.js b/modules/core/src/capture/video-capture.js index 1ad5ec01..219efdbe 100644 --- a/modules/core/src/capture/video-capture.js +++ b/modules/core/src/capture/video-capture.js @@ -109,6 +109,7 @@ export class VideoCapture { */ render(Encoder, encoderSettings, sceneLengthMs, onStop = undefined) { if (!this.isRecording()) { + console.time('render'); encoderSettings = this.parseEncoderSettings(encoderSettings, sceneLengthMs); console.log(`Starting recording for ${this.durationMs}ms.`); @@ -169,17 +170,20 @@ export class VideoCapture { * @param {{ (blob: Blob): boolean }} [callback] */ save(callback) { + console.timeEnd('render'); if (!callback) { /** * @param {Blob} blob */ callback = blob => { + console.timeEnd('save'); if (blob) { download(blob, this.filename + this.encoder.extension, this.encoder.mimeType); } return false; }; } + console.time('save'); this.encoder.save().then(callback); } diff --git a/modules/core/src/encoders/index.js b/modules/core/src/encoders/index.js index 9da0c2bb..17ba7a31 100644 --- a/modules/core/src/encoders/index.js +++ b/modules/core/src/encoders/index.js @@ -26,3 +26,4 @@ export {default as WebMEncoder} from './video/webm-encoder'; export {default as StreamEncoder} from './video/stream-encoder'; export {default as FrameEncoder} from './frame-encoder'; export {default as PreviewEncoder} from './utils/preview-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 new file mode 100644 index 00000000..a9ed31ed --- /dev/null +++ b/modules/core/src/encoders/video/gif-encoder.js @@ -0,0 +1,49 @@ +import {GIFBuilder} from '@loaders.gl/video'; +import FrameEncoder from '../frame-encoder'; + +export default class GifEncoder extends FrameEncoder { + /** @param {import('types').FrameEncoderSettings} settings */ + constructor(settings) { + super(settings); + this.mimeType = 'image/gif'; + this.extension = '.gif'; + this.gifBuilder = null; + this.options = {}; + + if (settings.gif) { + this.options = settings.gif; + } + + 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.source = settings.source + this.source = 'images'; + + this.start = this.start.bind(this); + this.add = this.add.bind(this); + this.save = this.save.bind(this); + } + + start() { + this.gifBuilder = new GIFBuilder({ + source: this.source, + ...this.options, + interval: 1 / this.framerate + }); + } + + /** @param {HTMLCanvasElement} canvas */ + async add(canvas) { + if (this.source === 'images') { + const dataUrl = canvas.toDataURL('image/jpeg', 0.8); + await this.gifBuilder.add(dataUrl); + } + } + + async save() { + return this.gifBuilder.build(); + } +} diff --git a/modules/core/src/index.js b/modules/core/src/index.js index 70d37ced..ed379da2 100644 --- a/modules/core/src/index.js +++ b/modules/core/src/index.js @@ -27,7 +27,8 @@ export { PNGEncoder, WebMEncoder, FrameEncoder, - PreviewEncoder + PreviewEncoder, + GifEncoder } from './encoders'; export { diff --git a/modules/core/src/types.d.ts b/modules/core/src/types.d.ts index 7ee65490..361798dc 100644 --- a/modules/core/src/types.d.ts +++ b/modules/core/src/types.d.ts @@ -34,14 +34,12 @@ interface EncoderSettings { webm: { quality: number } - // gif: { - // numWorkers: number, - // sampleInterval: number, - // resize: { - // width: number, - // height: number - // } - // }, + gif: { + numWorkers: number, + sampleInterval: number, + width: number, + height: number + }, } interface DeckSceneParams { diff --git a/modules/react/src/components/basic-controls.js b/modules/react/src/components/basic-controls.js index a8f8e481..7a8d079f 100644 --- a/modules/react/src/components/basic-controls.js +++ b/modules/react/src/components/basic-controls.js @@ -23,12 +23,13 @@ import { WebMEncoder, JPEGSequenceEncoder, PNGSequenceEncoder, - PreviewEncoder + PreviewEncoder, + GifEncoder } from '@hubble.gl/core'; import EncoderDropdown from './encoder-dropdown'; export default function BasicControls({adapter, busy, setBusy, encoderSettings}) { - const [encoder, setEncoder] = useState('webm'); + const [encoder, setEncoder] = useState('gif'); const onRender = () => { if (encoder === 'preview') { @@ -39,6 +40,8 @@ export default function BasicControls({adapter, busy, setBusy, encoderSettings}) adapter.render(JPEGSequenceEncoder, encoderSettings, () => setBusy(false)); } else if (encoder === 'png') { adapter.render(PNGSequenceEncoder, encoderSettings, () => setBusy(false)); + } else if (encoder === 'gif') { + adapter.render(GifEncoder, encoderSettings, () => setBusy(false)); } setBusy(true); diff --git a/modules/react/src/components/encoder-dropdown.js b/modules/react/src/components/encoder-dropdown.js index b02f7df3..fa32f8dc 100644 --- a/modules/react/src/components/encoder-dropdown.js +++ b/modules/react/src/components/encoder-dropdown.js @@ -28,6 +28,7 @@ export default function EncoderDropdown({disabled, encoder, setEncoder}) { + ); } diff --git a/yarn.lock b/yarn.lock index a3d9fe6b..9b70ea77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -853,6 +853,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.3.1": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" + integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -1645,6 +1652,30 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" +"@loaders.gl/core@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@loaders.gl/core/-/core-2.1.6.tgz#9c3e0cf5a1aebb672c50876ab75fd35033b8672c" + integrity sha512-1uxlSZ3dKbraWmvsOsBeSfJxR/+mXjDCsDLkl8N9QzXeN1nIer3siDPLqckox4MMsPxPo5uo1Be8iwORAUo3VQ== + dependencies: + "@babel/runtime" "^7.3.1" + "@loaders.gl/loader-utils" "2.1.6" + +"@loaders.gl/loader-utils@2.1.6", "@loaders.gl/loader-utils@^2.1.3": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@loaders.gl/loader-utils/-/loader-utils-2.1.6.tgz#8eb5a9a2215b3c94c3bb548767cbc276e3701a5c" + integrity sha512-V9ZJH60HwSmSIw2jqNDMFpDqZVfy02WZ1/1e2ENw3PODE8ubNqs1bQ8nFj9omqwVMPF7fBVLvzoeoGrpeW0gfQ== + dependencies: + "@babel/runtime" "^7.3.1" + "@probe.gl/stats" "^3.2.1" + +"@loaders.gl/video@2.2.0-alpha.1": + version "2.2.0-alpha.1" + resolved "https://registry.yarnpkg.com/@loaders.gl/video/-/video-2.2.0-alpha.1.tgz#1bb09e9e507605a72c6e8a13b34926b69c717191" + integrity sha512-4NDe3fNPOTS4MvPxlxrbqr/kKgkB9tOVY2in/s/oYBo0uiDK+WSU0AztnyKxF1b/EsHzTEIzhOEhjvSwbgRQgQ== + dependencies: + "@loaders.gl/loader-utils" "^2.1.3" + gifshot "^0.4.5" + "@luma.gl/constants@8.1.2": version "8.1.2" resolved "https://registry.yarnpkg.com/@luma.gl/constants/-/constants-8.1.2.tgz#729fb10ba6dc6946ccb2745f4b4d22c2f4b021d7" @@ -1837,7 +1868,7 @@ "@babel/runtime" "^7.0.0" probe.gl "3.2.1" -"@probe.gl/stats@3.2.1": +"@probe.gl/stats@3.2.1", "@probe.gl/stats@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@probe.gl/stats/-/stats-3.2.1.tgz#4e95c75513229ebb01384045e89524a466c022eb" integrity sha512-tXe5krgbodxtVdUVWG4oIicMoCHNGp7QYkaHSDrUeTfJVqYuZz99T6r7qmn0bCo4wQlzn936jJ+QiAltHxatig== @@ -4848,6 +4879,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gifshot@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/gifshot/-/gifshot-0.4.5.tgz#e3cb570203a3b139ff3069d7578098a29c03b0f8" + integrity sha1-48tXAgOjsTn/MGnXV4CYopwDsPg= + git-raw-commits@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5"