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"