Skip to content

Instantly share code, notes, and snippets.

@jspillers
Last active June 5, 2020 05:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jspillers/3d9dac3c222ba85cbf475fd49c451b73 to your computer and use it in GitHub Desktop.
Save jspillers/3d9dac3c222ba85cbf475fd49c451b73 to your computer and use it in GitHub Desktop.
data-grid-gif
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<link rel='stylesheet' href="https://unpkg.com/@finos/perspective-viewer@0.4.6/dist/umd/material-dense.dark.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://unpkg.com/@finos/perspective-workspace"></script>
<script src="https://unpkg.com/@finos/perspective-viewer-datagrid"></script>
<script src="https://unpkg.com/@finos/perspective-viewer-d3fc"></script>
<script src="https://unpkg.com/@finos/perspective"></script>
<script type="module">
import { loadGifFrames, playGif } from './load-gif.js';
//let gifUrl = 'https://i.giphy.com/oPu2IgQHwb3Qk.gif';
//let gifUrl = 'https://i.giphy.com/l4KibK3JwaVo0CjDO.gif';
let gifUrl = 'https://i.giphy.com/fZk0FD0wxQpb2.gif';
let imageSizePercent = 20;
// haha, global vars... fight me.
window.isPlaying = true;
window.currentFrame = 0;
window.targetPlaybackSpeed = 15;
window.addEventListener('DOMContentLoaded', async () => {
const viewer = document.getElementsByTagName('perspective-viewer')[0];
let frames;
const resetTable = async () => {
if (window.table) {
window.isPlaying = false;
await window.table.clear();
await viewer.view.delete();
await window.table.delete();
window.table = undefined;
}
};
const imageUrlLabel = document.querySelector('#ImageUrlContainer label span');
const imageUrlInput = document.querySelector('#ImageUrl');
imageUrlInput.value = gifUrl;
imageUrlInput.addEventListener('change', async (event) => {
gifUrl = event.target.value;
});
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
const imageSizeSliderLabel = document.querySelector('#ImageSizeSliderContainer label span');
const imageSizeSlider = document.querySelector('#ImageSizeSlider');
imageSizeSlider.value = clamp(imageSizePercent, 1, 100);
imageSizeSliderLabel.innerHTML = imageSizeSlider.value + '%';
imageSizeSlider.addEventListener('change', (event) => {
imageSizePercent = event.target.value;
imageSizeSliderLabel.innerHTML = imageSizePercent + '%';
});
const speedSliderLabel = document.querySelector('#PlaybackSpeedSliderContainer label span');
const playbackSpeedSlider = document.querySelector('#PlaybackSpeedSlider');
playbackSpeedSlider.value = clamp(window.targetPlaybackSpeed, 1, 24);
speedSliderLabel.innerHTML = playbackSpeedSlider.value + ' FPS';
playbackSpeedSlider.addEventListener('change', (event) => {
window.targetPlaybackSpeed = event.target.value;
speedSliderLabel.innerHTML = window.targetPlaybackSpeed + ' FPS';
});
const loadButton = document.querySelector('button#Load');
loadButton.addEventListener('click', async (event) => {
event.preventDefault();
await resetTable();
frames = await loadGifFrames(gifUrl);
console.log('load')
window.isPlaying = true;
playGif(frames, viewer, gifUrl, imageSizePercent);
});
const playPauseButton = document.querySelector('button#PlayPause');
const playPauseTextSpan = playPauseButton.querySelector('span');
const playPauseIcon = playPauseButton.querySelector('i');
playPauseButton.addEventListener('click', (event) => {
event.preventDefault();
if (window.isPlaying) {
console.log('pause')
// is now paused
playPauseTextSpan.innerHTML = 'Play ';
playPauseIcon.classList.remove('fa-pause');
playPauseIcon.classList.add('fa-play');
window.isPlaying = false;
} else {
// is now playing
console.log('play')
playPauseTextSpan.innerHTML = 'Pause ';
playPauseIcon.classList.remove('fa-play');
playPauseIcon.classList.add('fa-pause');
window.isPlaying = true;
playGif(frames, viewer, gifUrl, imageSizePercent);
}
});
viewer.addEventListener("perspective-datagrid-after-update", event => {
const datagrid = event.detail;
for (const td of datagrid.get_tds()) {
const metadata = datagrid.get_meta(td);
td.style.backgroundColor = metadata.value;
}
});
frames = await loadGifFrames(gifUrl);
playGif(frames, viewer, gifUrl, imageSizePercent);
});
</script>
</head>
<body>
<div class="main container">
<form class="container" id="ImageControls">
<div id="ImageUrlContainer">
<label for="ImageUrl">Gif Url</label>
<input type="text" value="" id="ImageUrl" />
<button id="Load">Load</button>
</div>
<div id="ImageSizeSliderContainer">
<label for="ImageSizeSlider">Image size, percentage of original: <span>25%</span></label>
<input type="range" min="1" max="100" class="slider" id="ImageSizeSlider" />
</div>
<div id="PlaybackSpeedSliderContainer">
<label for="PlaybackSpeedSlider"> Target playback speed: <span>15 FPS</span></label>
<input type="range" min="1" max="24" class="slider" id="PlaybackSpeedSlider" />
</div>
<button id="PlayPause" style=""><span>Pause </span><i class="fa fa-pause"></i></button>
</form>
<div class="container" id="PerspectiveViewer">
</div>
</div>
<perspective-viewer plugin="datagrid" />
<style>
.hidden {
display: none;
}
#ImageControls {
padding: 5px 15px;
color: #efefef;
}
#PlayPause {
font-size:14px
}
perspective-viewer td, perspective-viewer th {
color: rbga(0,0,0,0) !important;
font-size: 1px;
padding: 0;
overflow: hidden;
height: 15px !important;
width: 15px !important;
max-height: 15px !important;
max-width: 15px !important;
}
perspective-viewer {
flex: 1;
}
body {
background-color: rgb(46,49,54);
display: flex;
flex-direction: column;
position: absolute;
font-family: sans-serif;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
}
@media (max-width: 600px) {
html {
overflow: hidden;
}
body {
position: fixed;
height: 100%;
width: 100%;
margin: 0;
overflow: hidden;
touch-action: none;
}
}
</style>
</body>
import { Decoder } from 'https://unpkg.com/fastgif@0.1.1/fastgif.js';
const decoder = new Decoder();
const getColorIndicesForCoord = (x, y, width) => {
var red = y * (width * 4) + x * 4;
return [red, red + 1, red + 2, red + 3];
};
const arrAvg = arr => arr.reduce((a,b) => a + b, 0) / arr.length;
export const loadGifFrames = async (gifUrl) => {
return window.fetch(gifUrl)
.then((response) => response.arrayBuffer())
.then((buffer) => decoder.decode(buffer));
};
export const playGif = async (frames, viewer, gifUrl, imageSizePercent) => {
const origWidth = frames[0].imageData.width;
const origHeight = frames[0].imageData.height;
const newWidth = Math.floor(origWidth * (imageSizePercent / 100.0));
const newHeight = Math.floor(origHeight * (imageSizePercent / 100.0));
console.log({imageSizePercent, newWidth, newHeight})
const gridSize = Math.floor(origWidth / newWidth);
const processedFrames = frames.map((frame, frameNum) => {
let indices;
let framePixels = [];
let rowPixels;
let averageGroup = {};
for (let y = 0; y < newHeight; y++) {
rowPixels = { id: y };
for (let x = 0; x < newWidth; x++) {
averageGroup = { r: [], g: [], b: [], a: [] };
// super elite rockstar ninja level algorithm begins here:
// step one: gather all pixel data for a given grid size into an object
// where each key is an array of integers representing color or alpha values
for (let ya = 0; ya < gridSize; ya++) {
for (let xa = 0; xa < gridSize; xa++) {
indices = getColorIndicesForCoord((x * gridSize) + xa, (y * gridSize) + ya, origWidth)
averageGroup.r.push(frame.imageData.data[indices[0]]);
averageGroup.g.push(frame.imageData.data[indices[1]]);
averageGroup.b.push(frame.imageData.data[indices[2]]);
averageGroup.a.push(frame.imageData.data[indices[3]]);
}
}
// next, average each channel and turn it into a CSS rgba string
rowPixels[`x${x}`] = 'rgba('
+ arrAvg(averageGroup.r) + ','
+ arrAvg(averageGroup.g) + ','
+ arrAvg(averageGroup.b) + ','
+ arrAvg(averageGroup.a) + ')';
}
// smoosh the row array into a frame array
framePixels.push(rowPixels);
}
return framePixels;
});
const createOrUpdateTable = (pixels) => {
if (window.table !== undefined) {
window.table.update(pixels);
} else {
console.log("---create table---")
const worker = perspective.worker();
window.table = worker.table(pixels, { index: 'id' });
viewer.load(window.table);
}
};
// recursively play frames until stopped
const playFrames = () => {
console.log('playFrames, current frame: ' + window.currentFrame);
if (window.isPlaying) {
createOrUpdateTable(processedFrames[window.currentFrame]);
window.currentFrame++;
if (window.currentFrame === processedFrames.length) {
window.currentFrame = 0;
}
setTimeout(() => {
playFrames();
}, 1000 / window.targetPlaybackSpeed);
}
};
playFrames();
};
.hidden {
display: none;
}
#ImageControls {
padding: 5px 15px;
color: #efefef;
}
#PlayPause {
font-size:14px
}
perspective-viewer td, perspective-viewer th {
color: rbga(0,0,0,0) !important;
font-size: 1px;
padding: 0;
overflow: hidden;
height: 15px !important;
width: 15px !important;
max-height: 15px !important;
max-width: 15px !important;
}
perspective-viewer {
flex: 1;
}
body {
background-color: rgb(46,49,54);
display: flex;
flex-direction: column;
position: absolute;
font-family: sans-serif;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
}
@media (max-width: 600px) {
html {
overflow: hidden;
}
body {
position: fixed;
height: 100%;
width: 100%;
margin: 0;
overflow: hidden;
touch-action: none;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment