Skip to content

Commit

Permalink
Merge branch 'master' of github.com:uber/hubble.gl into modal-refining
Browse files Browse the repository at this point in the history
  • Loading branch information
RaymondDashWu committed Aug 5, 2021
2 parents 9710fc7 + d8d7e3e commit 59883bd
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 43 deletions.
2 changes: 1 addition & 1 deletion examples/kepler-integration/src/app.js
Expand Up @@ -84,7 +84,7 @@ const GlobalStyle = createGlobalStyle`
}
.load-data-modal__tab__item.active {
color: #fff
color: #fff;
border-bottom: 3px solid #fff;
}
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
@@ -1,5 +1,5 @@
{
"version": "1.3.0-alpha.4",
"version": "1.3.0-alpha.5",
"npmClient": "yarn",
"useWorkspaces": true,
"packages": [
Expand Down
2 changes: 1 addition & 1 deletion modules/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@hubble.gl/core",
"version": "1.3.0-alpha.4",
"version": "1.3.0-alpha.5",
"description": "hubble.gl core library classes",
"license": "MIT",
"repository": {
Expand Down
45 changes: 31 additions & 14 deletions modules/core/src/keyframes/kepler-filter-keyframes.js
Expand Up @@ -43,7 +43,18 @@ import {hold, linear} from './easings';
* Current time is a point. An array of sorted time steps need to be provided.
* animation controller calls next animation at a interval when the point jumps to the next step
*/
function timeRangeKeyframes({filter, timings}) {
function getKeyFramesFree(filter) {
const delta = filter.value[1] - filter.value[0];
return {
keyframes: [
{value: [filter.domain[0], filter.domain[0] + delta]},
{value: [filter.domain[1] - delta, filter.domain[1]]}
],
easings: linear
};
}

export function timeRangeKeyframes({filter, timings}) {
if (filter.type !== 'timeRange') {
throw new Error("filter type must be 'timeRange'.'");
}
Expand All @@ -53,14 +64,7 @@ function timeRangeKeyframes({filter, timings}) {
switch (filter.animationWindow) {
default:
case 'free': {
const delta = filter.value[1] - filter.value[0];
return {
keyframes: [
{value: [filter.domain[0], filter.domain[0] + delta]},
{value: [filter.domain[1] - delta, filter.domain[1]]}
],
easings: linear
};
return getKeyFramesFree(filter);
}
case 'incremental': {
return {
Expand All @@ -78,13 +82,26 @@ function timeRangeKeyframes({filter, timings}) {
};
}
case 'interval': {
const delta = Math.round(duration / filter.steps.length);
const {bins, plotType} = filter;
const {interval} = plotType;
if (
!interval ||
!bins ||
Object.keys(bins).length === 0 ||
!Object.values(bins)[0][interval]
) {
// shouldn't happen return
return getKeyFramesFree(filter);
}
const intervalBins = Object.values(bins)[0][interval];
const delta = Math.round(duration / intervalBins.length);

// const delta = Math.round(duration / filter.steps.length);
return {
timings: filter.steps.map((_, idx) => timings[0] + delta * idx),
keyframes: filter.steps.map((step, idx) => {
const nextIdx = idx >= filter.steps.length - 1 ? 0 : idx + 1;
timings: intervalBins.map((_, idx) => timings[0] + delta * idx),
keyframes: intervalBins.map(bin => {
return {
value: [step, nextIdx]
value: [bin.x0, bin.x1]
};
}),
easings: hold
Expand Down
1 change: 1 addition & 0 deletions modules/core/test/index.js
Expand Up @@ -18,4 +18,5 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import './keyframes/camera-keyframes.spec';
import './keyframes/kepler-filter-keyframes.spec';
import './keyframes/utils.spec';
83 changes: 83 additions & 0 deletions modules/core/test/keyframes/kepler-filter-keyframes.spec.js
@@ -0,0 +1,83 @@
// Copyright (c) 2021 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// 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 test from 'tape-catch';

import {
timeRangeKeyframes
// @ts-ignore
// eslint-disable-next-line import/no-unresolved
} from '@hubble.gl/core/keyframes/kepler-filter-keyframes';

const intervalFilter = {
type: 'timeRange',
id: 'test-test',
animationWindow: 'interval',
value: [1421315219000, 1421315400000],
domain: [1421315219000, 1421348744000],
plotType: {
type: 'histogram',
interval: '5-minute'
},
bins: {
a543fcbd: {
'5-minute': [
{
x0: 1421315100000,
x1: 1421315400000
},
{
x0: 1421315400000,
x1: 1421315700000
},
{
x0: 1421315700000,
x1: 1421316000000
}
]
}
}
};
const TEST_CASES = [
{
args: {
filter: intervalFilter,
timings: [0, 1000]
},
expected: {
keyframes: [
{value: [1421315100000, 1421315400000]},
{value: [1421315400000, 1421315700000]},
{value: [1421315700000, 1421316000000]}
],
timings: [0, 333, 666]
},
message: 'keyframes for interval filter should be correct'
}
];

test('CameraKeyframes#flyToInterpolator', t => {
TEST_CASES.forEach(testCase => {
const result = timeRangeKeyframes(testCase.args);
t.deepEqual(result.keyframes, testCase.expected.keyframes, testCase.message);
t.deepEqual(result.timings, testCase.expected.timings, testCase.message);
});

t.end();
});
6 changes: 3 additions & 3 deletions modules/main/package.json
@@ -1,6 +1,6 @@
{
"name": "hubble.gl",
"version": "1.3.0-alpha.4",
"version": "1.3.0-alpha.5",
"description": "hubble.gl is an animation and video capture library for vis.gl in the browser",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -30,7 +30,7 @@
"build-bundle": "webpack --display=minimal --config ../../scripts/bundle.config.js"
},
"dependencies": {
"@hubble.gl/core": "1.3.0-alpha.4",
"@hubble.gl/react": "1.3.0-alpha.4"
"@hubble.gl/core": "1.3.0-alpha.5",
"@hubble.gl/react": "1.3.0-alpha.5"
}
}
2 changes: 1 addition & 1 deletion modules/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@hubble.gl/react",
"version": "1.3.0-alpha.4",
"version": "1.3.0-alpha.5",
"description": "React components for hubble.gl",
"license": "MIT",
"repository": {
Expand Down
Expand Up @@ -70,9 +70,12 @@ export class ExportVideoPanelContainer extends Component {
resolution: '1280x720',
durationMs: 1000,
rendering: false, // Will set a spinner overlay if true
previewing: false,
...(initialState || {})
};
this.state.viewState = scaleToVideoExport(mapState, this._getContainer());
const viewState = scaleToVideoExport(mapState, this._getContainer());
this.state.viewState = viewState;
this.state.memo = {viewState};
this.state.adapter = new DeckAdapter({glContext});
}

Expand All @@ -97,7 +100,8 @@ export class ExportVideoPanelContainer extends Component {

getCanvasSize() {
const {resolution} = this.state;
return getResolutionSetting(resolution);
const {width, height} = getResolutionSetting(resolution);
return {width, height};
}

_getContainer() {
Expand Down Expand Up @@ -249,8 +253,9 @@ export class ExportVideoPanelContainer extends Component {
const filename = this.getFileName();
const formatConfigs = this.getFormatConfigs();
const timecode = this.getTimecode();
this.setState({previewing: true, memo: {viewState: {...this.state.viewState}}});
const onStop = () => {
this.forceUpdate();
this.setState({previewing: false, viewState: {...this.state.memo.viewState}});
};
adapter.animationManager.setKeyframes('kepler', {
...this.getFilterKeyframes(),
Expand All @@ -273,10 +278,12 @@ export class ExportVideoPanelContainer extends Component {
const formatConfigs = this.getFormatConfigs();
const timecode = this.getTimecode();

this.setState({rendering: true}); // Enables overlay after user clicks "Render"
// Enables overlay after user clicks "Render"
this.setState({rendering: true, memo: {viewState: {...this.state.viewState}}});
const onStop = () => {
this.setState({rendering: false});
}; // Disables overlay once export is done saving (generates file to download)
// Disables overlay once export is done saving (generates file to download)
this.setState({rendering: false, viewState: {...this.state.memo.viewState}});
};
adapter.animationManager.setKeyframes('kepler', {
...this.getFilterKeyframes(),
...this.getTripKeyframes(),
Expand Down Expand Up @@ -313,7 +320,8 @@ export class ExportVideoPanelContainer extends Component {
fileName,
resolution,
viewState,
rendering
rendering,
previewing
} = this.state;

const timecode = this.getTimecode();
Expand All @@ -331,7 +339,11 @@ export class ExportVideoPanelContainer extends Component {
frameRate: timecode.framerate
};

const {width, height} = this.getCanvasSize();
let canvasSize = this.getCanvasSize();
if (previewing) {
// set resolution to be 1:1 with container when previewing to improve performance.
canvasSize = this._getContainer();
}

return (
<ExportVideoPanel
Expand All @@ -352,8 +364,9 @@ export class ExportVideoPanelContainer extends Component {
adapter={adapter}
handlePreviewVideo={this.onPreviewVideo}
handleRenderVideo={this.onRenderVideo}
resolution={[width, height]}
resolution={[canvasSize.width, canvasSize.height]}
rendering={rendering}
previewing={previewing}
/>
);
}
Expand Down
Expand Up @@ -122,6 +122,7 @@ const ExportVideoPanel = ({
resolution,
viewState,
rendering,
previewing,
deckProps,
staticMapProps
}) => {
Expand All @@ -147,6 +148,7 @@ const ExportVideoPanel = ({
mapboxLayerBeforeId={mapboxLayerBeforeId}
handlePreviewVideo={handlePreviewVideo}
handleRenderVideo={handleRenderVideo}
rendering={rendering || previewing}
/>
</Panel>
);
Expand Down
4 changes: 0 additions & 4 deletions modules/react/src/components/export-video/modal-tab-edit.js
Expand Up @@ -46,10 +46,6 @@ function EditTab({settings}) {
'Orbit (90º)',
'Orbit (180º)',
'Orbit (360º)',
'North to South',
'South to North',
'East to West',
'West to East',
'Zoom Out',
'Zoom In'
]}
Expand Down
34 changes: 28 additions & 6 deletions modules/react/src/components/export-video/utils.js
Expand Up @@ -114,23 +114,45 @@ const COMPRESSION_RATIO = 0.8;
const BIT_DEPTH = 6;

/**
* Estimates file size of resulting animation
* Estimates file size of resulting animation. All formulas are approximations
* created with small sample of animations on default NY taxi trips data and
* based off hubble's default framerate and the bitrate/depth of 3rd party encoders
* @param {number} frameRate frame rate of animation (set by developer)
* @param {array} resolution [width, height] of animation
* @param {number} durationMs duration of animation (set by developer)
* @param {string} mediaType 'GIF', 'WEBM', etc.
* @returns {string} size in MB
*/
export function estimateFileSize(frameRate, resolution, durationMs, mediaType) {
// Based off of https://www.youtube.com/watch?v=DDcYvesZsnw for uncompressed video
// Formula: ((horizontal * vertical * bit depth) / (8 * 1024 * 1024 [convert to megabyte MB])) * (frame rate * time in seconds) * compression ratio
// Additional resource https://stackoverflow.com/questions/27559103/video-size-calculation
// TODO Read resource from Imgur dev https://stackoverflow.com/questions/23920098/how-to-estimate-gif-file-size
const seconds = Math.floor(durationMs / 1000);
if (mediaType === 'gif') {
const seconds = Math.floor(durationMs / 1000);
// Based off of https://www.youtube.com/watch?v=DDcYvesZsnw for uncompressed video
// Formula: ((horizontal * vertical * bit depth) / (8 * 1024 * 1024 [convert to megabyte MB])) * (frame rate * time in seconds) * compression ratio
// Additional resource https://stackoverflow.com/questions/27559103/video-size-calculation
return `${Math.floor(
((resolution[0] * resolution[1] * BIT_DEPTH) / MB) * (frameRate * seconds) * COMPRESSION_RATIO
)} MB`;
}
if (mediaType === 'webm') {
// Formula: multiplier (arbitrary unit created by analyzing bitrate of outputs) * Mb (megabit) * seconds * .125 (conversion from bit to byte)
// frameRate determines Mb/s in animations with a lot of movement at 720p. Multiply by seconds and convert to MB (megabyte)
return `${Math.ceil((resolution[0] / 1280) * frameRate * seconds * 0.125)} MB`;
}
if (mediaType === 'png') {
// Note: Adds one frame to size to account for an extra frame when exporting to pictures.
return `${Math.floor(
((resolution[0] * resolution[1] * BIT_DEPTH) / MB) *
((frameRate + 1) * seconds) *
COMPRESSION_RATIO
)} MB`;
}
if (mediaType === 'jpeg') {
// Note: Adds one frame to size to account for an extra frame when exporting to pictures.
return `${Math.floor(
((resolution[0] * resolution[1] * BIT_DEPTH) / MB) *
((frameRate + 1) * seconds) *
(COMPRESSION_RATIO - 0.4)
)} MB`;
}
return 'Size estimation unavailable';
}
6 changes: 3 additions & 3 deletions yarn.lock
Expand Up @@ -12734,9 +12734,9 @@ tar-stream@^2.1.4:
readable-stream "^3.1.1"

tar@^4.4.10, tar@^4.4.12, tar@^4.4.8:
version "4.4.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
version "4.4.15"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.15.tgz#3caced4f39ebd46ddda4d6203d48493a919697f8"
integrity sha512-ItbufpujXkry7bHH9NpQyTXPbJ72iTlXgkBAYsAjDXk3Ds8t/3NfO5P4xZGy7u+sYuQUbimgzswX4uQIEeNVOA==
dependencies:
chownr "^1.1.1"
fs-minipass "^1.2.5"
Expand Down

0 comments on commit 59883bd

Please sign in to comment.