index.js (517 lines of code) (raw):
// index.js
'use strict';
// include styles
require('./index.css');
require('./src/Gui/Gui.css');
const deepClone = require('./deep-clone.js');
const randomize = require('./randomize.js');
const buildGradients = require('./gradients.js');
const is = require('./check-layer-type.js');
const drawToCanvas = require('./draw-to-canvas.js');
const JSZip = require('jszip');
const JSZipUtils = require('jszip-utils');
const FileSaver = require('jszip/vendor/FileSaver');
const timing = require('./timing.js');
const isProduction = (process.env.NODE_ENV && (process.env.NODE_ENV == 'production'));
// initialize Elm Application
const App = require('./src/Main.elm');
//const mountNode = document.getElementById('elm-target');
const mountNode = document.getElementById('js-animation');
// The third value on embed are the initial values for incomming ports into Elm
const app = App.Elm.Main.init(
{ node: mountNode,
flags:
{ forcedMode: null,
runningAt: isProduction ? 'production' : 'developement'
}
});
const startGui = require('./gui.js');
const buildFSS = require('./fss.js');
const nativeMetaballs = require('./native-metaballs.js');
const fssScenes = {};
const allNativeMetaballs = {};
const batchPause = 1000;
let savingBatch = false;
const exportScene = (scene) => {
return scene.meshes[0].geometry.vertices.map((vertex) => (
{ v0: vertex.v0,
time: vertex.time,
anchor: vertex.anchor,
gradient: vertex.gradient
}
));
}
const prepareModelForImport = (model) => {
const toSend = deepClone(model);
toSend.layers =
model.layers.map(layerDef => {
const layerDef_ = deepClone(layerDef);
//layerDef_.model = JSON.stringify(layerDef.model);
return layerDef_;
});
return toSend;
}
const import_ = (app, parsedState) => {
const preparedModel = prepareModelForImport(parsedState);
app.ports.import_.send(JSON.stringify(preparedModel));
parsedState.layers.map((layer, index) => {
if (is.fss(layer)) {
const fssScene = buildFSS(parsedState, layer.model, layer.sceneFuzz);
fssScenes[index] = fssScene;
app.ports.rebuildFss.send({ value: fssScene, layer: index });
}
});
app.ports.pause.send(null);
}
const export_ = (app, exportedState) => {
app.ports.pause.send(null);
const stateObj = JSON.parse(exportedState);
stateObj.layers.forEach((layer, index) => {
layer.sceneFuzz = is.fss(layer)
? exportScene(fssScenes[index]) || exportScene(buildFSS(model, layer.model))
: null;
})
return {
source: stateObj,
json: JSON.stringify(stateObj, null, 2)
};
}
const waitForContent = ({ path, name }) => {
return new Promise((resolve, reject) => {
JSZipUtils.getBinaryContent(path, (err, content) => {
if (err) { reject(err); return; }
resolve({ path, name, content });
});
});
};
const exportZip_ = (app, exportedState) => {
const sequenceFiles = function (fileList, handler, filesContent) {
const fileListCopy = fileList.concat([]);
const nextFilePath = fileListCopy.shift();
JSZipUtils.getBinaryContent(
nextFilePath, (err, nextFileContent) => {
if (err) { throw err; }
if (!filesContent) { filesContent = {}; };
filesContent[nextFilePath] = nextFileContent;
if (fileListCopy.length == 0) {
handler(filesContent);
return;
};
sequenceFiles(fileListCopy, handler, filesContent);
}
);
}
const PATHS =
{ 'bundle': './player.bundle.js',
'html': './index.player.html',
'style': './index.css' };
const NAMES =
{ 'bundle': 'player.bundle.js',
'html': 'index.html',
'style': 'index.css',
'scene': 'scene.js' };
sequenceFiles([ PATHS.bundle, PATHS.html, PATHS.style ],
(files) => {
const playerBundle = files[PATHS.bundle];
const playerHtml = files[PATHS.html];
const playerCss = files[PATHS.style];
const { json, source } = export_(app, exportedState);
const zip = new JSZip();
zip.file(NAMES.bundle, playerBundle, { binary: true });
zip.file(NAMES.scene, 'window.jsGenScene = ' + json + ';');
zip.file(NAMES.html, playerHtml, { binary: true });
zip.file(NAMES.style, playerCss, { binary: true });
const assets = zip.folder('assets');
const assetPromises =
[ source.product + '-text', 'jetbrains' ]
.map(fileName => {
return { name: fileName + '.svg'
, path : './assets/' + fileName + '.svg'
};
})
.map(waitForContent);
Promise.all(assetPromises)
.then(files =>
files.map(
({ content, name }) => {
assets.file(name, content, { binary: true }) }
)
)
.then(() => zip.generateAsync({type:"blob"}))
.then(content => new FileSaver(content, source.product + "_html5.zip"));
});
}
const prepareImportExport = () => {
app.ports.export_.subscribe((exportedState) => {
const exportCode = export_(app, exportedState).json;
document.getElementById('export-target').className = 'shown';
document.getElementById('export-code').value = exportCode;
});
app.ports.exportZip_.subscribe((exportedState) => {
try {
// console.log('exportedState', exportedState);
exportZip_(app, exportedState);
} catch(e) {
console.error(e);
alert('Failed to create .zip');
}
});
// document.getElementById('close-export').addEventListener('click', () => {
// document.getElementById('export-target').className = '';
// });
// document.getElementById('close-import').addEventListener('click', () => {
// document.getElementById('import-target').className = '';
// });
// setTimeout(() => {
// document.getElementById('import-button').addEventListener('click', () => {
// document.getElementById('import-target').className = 'shown';
// });
// }, 100);
// document.getElementById('import').addEventListener('click', () => {
// try {
// if (document.getElementById('import-code').value) {
// const importedScene = JSON.parse(document.getElementById('import-code').value);
// import_(app, importedScene);
// } else {
// alert('Nothing to import');
// }
// } catch(e) {
// console.error(e);
// alert('Failed to parse or send, incorrect format?');
// }
// });
}
const savePng = (hiddenLink, { size, product, background, layers }) => {
const trgCanvas = document.querySelector('#js-save-buffer');
const [ width, height ] = size; // [ srcCanvas.width, srcCanvas.height ];
trgCanvas.width = width;
trgCanvas.height = height;
if (!trgCanvas) return;
trgCanvas.style.display = 'block';
trgCanvas.style.backgroundColor = background;
const trgContext = trgCanvas.getContext('2d');
hiddenLink.download = width + 'x'+ height + '-' + product + '.png';
// console.log(layers);
const toFetch = layers.concat([]).reverse().reduce((prev, { index, def, kind, visibility }) => {
// console.log(index, def, kind, visibility);
if (visibility == 'hidden') {
return prev;
}
if (def == 'background') {
return [ ...prev, { selector : '#layer-' + index + ' svg', collect : 'svg' } ];
}
if (def == 'cover') {
return [ ...prev
// , { selector : '#layer-' + index + ' .text-layer--slogan', collect : 'html' }
, { selector : '#layer-' + index, collect : 'html' }
, { selector : '#layer-' + index + ' .product-name-layer', collect : 'stored' }
, { selector : '#layer-' + index + ' .logo-layer', collect : 'stored' }
];
}
if (kind == 'webgl' || kind == 'js') {
return [ ...prev, { selector : '#layer-' + index + ' canvas', collect : 'canvas' } ];
}
if (kind == 'html') {
return [ ...prev, { selector : '#layer-' + index + ' > div', collect : 'html' } ];
}
// `collect` could be equals to 'image', also, but we don't handle it
return prev;
}, []);
const performSave = () => {
trgCanvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
hiddenLink.href = url;
hiddenLink.click();
URL.revokeObjectURL(url);
trgCanvas.style.display = 'none';
});
}
const fetchNext = (toFetch) => {
if (!toFetch.length) {
//console.log('saving');
performSave();
return;
}
const current = toFetch[0];
//console.log(current.selector, current.collect);
const step = () => {
//console.log('applied', current.selector, current.collect);
fetchNext(toFetch.slice(1));
};
if (current.collect == 'html') {
drawToCanvas.html(current.selector, trgCanvas, width, height, step);
} else if (current.collect == 'svg') {
drawToCanvas.svg(current.selector, trgCanvas, width, height, step);
} else if (current.collect == 'stored') {
drawToCanvas.stored(current.selector, trgCanvas, step);
} else if (current.collect == 'image') {
drawToCanvas.image(current.src, () => {}, trgCanvas, 0, 0, width, height, step);
} else if (current.collect == 'canvas') {
drawToCanvas.canvas(current.selector, trgCanvas, step);
// drawToCanvas.selector(current.selector, trgCanvas, step);
} else {
console.error('Unknown collect spec: ', current.collect);
return null;
}
}
requestAnimationFrame(() => { // without that, image buffer will be empty
//const trgContext = trgCanvas.getContext('2d');
trgCanvas.style.display = 'block';
//trgContext.fillStyle = background;
//trgContext.fillRect(0, 0, width, height);
fetchNext(toFetch);
});
}
prepareImportExport();
const updateOrInitNativeMetaballs = (size, layerModel, palette, index) => {
if (!allNativeMetaballs[index]) {
const nativeMetaballsModel = nativeMetaballs.build(size, layerModel, palette, index);
allNativeMetaballs[index] = nativeMetaballsModel;
const debouncedResize = timing.debounce(function(newSize) {
const prev = allNativeMetaballs[index];
if (!prev) return;
// prev.resize(newSize);
// prev.size = newSize;
allNativeMetaballs[index] = nativeMetaballs.update(newSize, prev, prev.stop, index);
}, 300);
if (app.ports.requestWindowResize) {
app.ports.requestWindowResize.subscribe((size) => {
debouncedResize(size);
});
} else console.error('No port `requestWindowResize` was detected');
debouncedResize(size);
} else {
const prev = allNativeMetaballs[index];
prev.model = layerModel || prev.model;
prev.palette = palette || prev.palette;
const nativeMetaballsModel =
nativeMetaballs.update(size, prev, prev.stop, index);
allNativeMetaballs[index] = nativeMetaballsModel;
}
};
const updateNativeMetaballsEffects = (subject, value, index) => {
if (allNativeMetaballs[index]) {
const prev = allNativeMetaballs[index];
if (subject == 'blur') {
prev.model.effects.blur = value;
} else if (subject == 'fat') {
prev.model.effects.fat = value;
} else if (subject == 'ring') {
prev.model.effects.ring = value;
}
allNativeMetaballs[index].updateEffects(prev.model.effects);
//allNativeMetaballs[index] = nativeMetaballs.update(prev.size, prev);
}
}
const pauseNativeMetaballs = (index) => {
if (allNativeMetaballs[index]) allNativeMetaballs[index].pause();
}
const continueNativeMetaballs = (index) => {
if (allNativeMetaballs[index]) allNativeMetaballs[index].play();
}
const resizeNativeMetaballs = (index, size) => {
if (allNativeMetaballs[index]) allNativeMetaballs[index].resize(size);
}
const convertRanges = r =>
{
return {
groups : { min : Math.floor(r.minGroups), max: Math.floor(r.maxGroups) },
balls: { min : Math.floor(r.minBalls), max: Math.floor(r.maxBalls) },
radius: { min : Math.floor(r.minRadius), max: Math.floor(r.maxRadius) },
speed: { min : r.minSpeed, max: r.maxSpeed },
phase: { min : r.minPhase, max: r.maxPhase },
amplitude:
{
x: { min : r.minAmplitudeX, max: r.maxAmplitudeX },
y: { min : r.minAmplitudeY, max: r.maxAmplitudeY }
}
};
}
// document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
const hiddenLink = document.createElement('a');
hiddenLink.download = 'jetbrains-art-v2.png';
if (app.ports.requestFitToWindow) {
app.ports.requestFitToWindow.subscribe((_) => {
app.ports.setCustomSize.send(
{ presetCode: null, viewport: [ window.innerWidth, window.innerHeight ]}
);
});
} else console.error('No port `buildFluidGradientTextures` was detected');
if (app.ports.requestWindowResize) {
app.ports.requestWindowResize.subscribe((size) => {
const [ width, height ] = size;
// console.log(width, height);
window.resizeTo(width, height);
});
} else console.error('No port `requestWindowResize` was detected');
// app.ports.nextBatchStep.subscribe((update) => {
// if (savingBatch) {
// // console.log('saving ', size);
// savePng(hiddenLink, update);
// };
// });
app.ports.triggerSavePng.subscribe((update) => {
savePng(hiddenLink, update);
});
app.ports.requestRandomize.subscribe((model) => {
const toSend = deepClone(model);
randomize((randomizedModel) => {
//const toSend = deepClone(randomizedModel);
// randomizedModel.layers.forEach((layer) => {
// console.log(layer, layer.model);
// });
// prepareModelForImport(randomizedModel).layers.forEach((layer) => {
// console.log(layer, layer.model);
// });
app.ports.applyRandomizer.send(prepareModelForImport(randomizedModel));
}, toSend, null)({})();
});
app.ports.startGui.subscribe(([ model, constants ]) => {
const altGui = document.getElementById('grid-gui');
if (altGui) altGui.focus();
document.body.style.backgroundColor = model.background;
// console.log('startGui', model);
if (!model.mode || (model.mode.substring(0, 4) != 'tron')) {
const { config, update, updateSizeSet } = startGui(
document,
model,
constants,
{ changeLightSpeed : index => value =>
{ app.ports.changeLightSpeed.send({ layer: index, value: Math.round(value) }) }
, changeVignette : index => value =>
{ app.ports.changeVignette.send({ layer: index, value: value }) }
, changeIris : index => value =>
{ app.ports.changeIris.send({ layer: index, value: value }) }
, changeFacesX : index => value =>
{ app.ports.changeFacesX.send({ layer: index, value: Math.round(value) }) }
, changeFacesY : index => value =>
{ app.ports.changeFacesY.send({ layer: index, value: Math.round(value) }) }
, changeRenderMode : index => renderMode =>
{ app.ports.changeFssRenderMode.send({ layer: index, value: renderMode }) }
, changeWGLBlend : (index, blend) =>
{ app.ports.changeWGLBlend.send({ layer: index, value: blend }) }
, changeHtmlBlend : (index, blend) =>
{ app.ports.changeHtmlBlend.send({ layer: index, value: blend }) }
, changeProduct : (id) =>
{ app.ports.changeProduct.send(id) }
, setCustomSize : (value) => {
const size = value.split(',');
const width = parseInt(size[0]);
const height = parseInt(size[1]);
if (width > 0 && height > 0) {
app.ports.setCustomSize.send([ width, height ]);
} else {
app.ports.setCustomSize.send([ window.innerWidth, window.innerHeight ]);
}
}
, savePng : () =>
{ app.ports.savePng.send(null); }
, saveBatch : sizes_ => {
let sizes = sizes_.concat([[0, 0]]);
let sizeIndex = 0;
savingBatch = true;
const nextPng = () => {
if (sizeIndex < sizes.length) {
const [ width, height ] = sizes[sizeIndex];
// console.log('sending', width, height);
app.ports.setCustomSize.send([ width, height ]);
sizeIndex = sizeIndex + 1;
setTimeout(nextPng, batchPause);
} else {
savingBatch = false;
// console.log('done saving batch');
}
};
nextPng();
}
// , store : () =>
// { app.ports.store.send(null); }
, changeAmplitude : index => (x, y, z) =>
{ app.ports.changeAmplitude.send({ layer: index, value: [ x, y, z ]}); }
, shiftColor : index => (h, s, b) =>
{ app.ports.shiftColor.send({ layer: index, value: [ h, s, b ]}); }
, changeOpacity : index => value =>
{ app.ports.changeOpacity.send({ layer: index, value: value }) }
, turnOn : index =>
{ app.ports.turnOn.send(index); }
, turnOff : index =>
{ app.ports.turnOff.send(index); }
, mirrorOn : index =>
{ app.ports.mirrorOn.send(index); }
, mirrorOff : index =>
{ app.ports.mirrorOff.send(index); }
, rotate : value =>
{ app.ports.rotate.send(value); }
, applyRandomizer : value =>
{ app.ports.applyRandomizer.send(prepareModelForImport(value)); }
, iFeelLucky : () =>
{ app.ports.iFeelLucky.send(null); }
, refreshFluid : (index) =>
{ app.ports.refreshFluid.send({ layer: index }); }
, refreshNativeMetaballs : (index) =>
{ app.ports.refreshNativeMetaballs.send({ layer: index }); }
, changeFluidVariety : index => value =>
{ app.ports.changeFluidVariety.send({ layer: index, value }); }
, changeFluidOrbit : index => value =>
{ app.ports.changeFluidOrbit.send({ layer: index, value }); }
, rebuildFluidGradients : (index) =>
{ app.ports.requestRegenerateFluidGradients.send({ layer: index }); }
, changeNativeMetaballsVariety : index => value =>
{ app.ports.changeNativeMetaballsVariety.send({ layer: index, value }); }
, changeNativeMetaballsOrbit : index => value =>
{ app.ports.changeNativeMetaballsOrbit.send({ layer: index, value }); }
, changeNativeMetaballsEffects : (index, subject) => value =>
{ app.ports.changeNativeMetaballsEffects.send({ layer: index, subject, value });}
, switchBackgroundStop : (layerIndex, stopIndex) => value =>
{ app.ports.switchBackgroundStop.send({ layer: layerIndex, stopIndex, value }); }
, switchBackgroundGradientType : layerIndex => isRadial =>
{ const orientation = isRadial ? 'radial' : 'vertical';
app.ports.switchGradientOrientation.send({ layer: layerIndex, orientation });
}
, darkenBackground : layerIndex => darken =>
{ app.ports.darkenBackground.send({ layer: layerIndex, darken });
}
, switchCoverProductVisibility : layerIndex => isProductShown =>
{ app.ports.switchCoverProductVisibility.send(
{ layer: layerIndex, isProductShown });
}
, resize: (presetCode) =>
{ console.log(presetCode);
app.ports.resize.send({
presetCode, viewport: [ window.innerWidth, window.innerHeight ]
});
}
});
// app.ports.pushUpdate.subscribe((data) => {
// // console.log('push update received', data);
// config.product = data.product;
// // TODO: apply the mode change in GUI and so change the size selection
// update();
// });
// TODO: there is the similar code in `gui.js`, merge it somehow
app.ports.informNativeMetaballsUpdate.subscribe(
({ layer : index, size, model : layerModel, palette }) => {
config['variety'+index] = layerModel.variety;
config['orbit'+index] = layerModel.orbit;
config['blur'+index] = layerModel.effects.blur;
config['fat'+index] = layerModel.effects.fat;
config['ring'+index] = layerModel.effects.ring;
update();
});
// TODO: there is the similar code in `gui.js`, merge it somehow
app.ports.informBackgroundUpdate.subscribe(
({ layer : index, model : layerModel }) => {
const stopStates = layerModel.stops || [];
const gradientType = layerModel.orientation || "linear";
config['isRadial'+index] = gradientType == "radial";
config['stop1'+index] = stopStates[0] == "on";
config['stop2'+index] = stopStates[1] == "on";
config['stop3'+index] = stopStates[2] == "on";
config['darken'+index] = layerModel.darken;
update();
});
app.ports.updateLayerStats.subscribe(
({ layer : index, blend, opacity }) => {
// FIXME: also support WebGL Blends
config['layer'+index+'Blend'] = blend[1];
config['opacity'+index] = opacity;
update();
});
app.ports.modeChanged.subscribe((mode) => {
updateSizeSet(mode);
});
}
model.layers.forEach((layer, index) => {
if (is.fss(layer)) {
// console.log('rebuild FSS layer', index);
const fssScene = buildFSS(model, layer.model);
fssScenes[index] = fssScene;
app.ports.rebuildFss.send({ value: fssScene, layer: index });
}
if (is.nativeMetaballs(layer)) {
// update is done at bang now.
// updateOrInitNativeMetaballs(model.size, layer.model, model.palette, index);
// app.ports.rebuildFss.send({ value: fssScene, layer: index });
}
// if (is.fluid(layer)) {
// const gradients = buildGradients(model, layer.model);
// app.ports.loadFluidGradients.send({ value: gradients, layer: index });
// }
});
if (app.ports.requestFssRebuild) {
app.ports.requestFssRebuild.subscribe(({ layer : index, model, value : fssModel }) => {
const layer = model.layers[index];
//console.log(model.layers);
//console.log('requestFssRebuild', index, model.layers[index], is.fss(layer));
if (is.fss(layer)) {
// console.log('forced to rebuild FSS layer', index);
// FIXME: just use layer.model instead of `fssModel`
const fssScene = buildFSS(model, fssModel);
fssScenes[index] = fssScene;
app.ports.rebuildFss.send({ value: fssScene, layer: index });
layer.scene = fssScene;
}
});
} else console.error('No port `requestFssRebuild` was detected');
});
if (app.ports.buildFluidGradientTextures) {
app.ports.buildFluidGradientTextures.subscribe(([ index, layerModel ]) => {
//if (is.fluid(layer)) {
const gradients = buildGradients(layerModel);
app.ports.loadFluidGradientTextures.send({ value: gradients, layer: index });
//}
});
} else console.error('No port `buildFluidGradientTextures` was detected');
if (app.ports.informNativeMetaballsUpdate) {
app.ports.informNativeMetaballsUpdate.subscribe(
({ layer : index, size, model : layerModel, palette }) => {
requestAnimationFrame(() => {
updateOrInitNativeMetaballs(size, layerModel, palette, index);
});
});
} else console.error('No port `informNativeMetaballsUpdate` was detected');
if (app.ports.sendNativeMetaballsEffects) {
app.ports.sendNativeMetaballsEffects.subscribe(({ layer : index, subject, value }) => {
updateNativeMetaballsEffects(subject, value, index);
});
} else console.error('No port `sendNativeMetaballsEffects` was detected');
if (app.ports.pauseNativeMetaballs) {
app.ports.pauseNativeMetaballs.subscribe(({ layer : index }) => {
pauseNativeMetaballs(index);
});
} else console.error('No port `pauseNativeMetaballs` was detected');
if (app.ports.continueNativeMetaballs) {
app.ports.continueNativeMetaballs.subscribe(({ layer : index }) => {
continueNativeMetaballs(index);
});
} else console.error('No port `continueNativeMetaballs` was detected');
if (app.ports.resizeNativeMetaballs) {
app.ports.resizeNativeMetaballs.subscribe(({ layer : index, size }) => {
resizeNativeMetaballs(index, size);
});
} else console.error('No port `resizeNativeMetaballs` was detected');
app.ports.bang.send(null);
let panelsHidden = false;
document.addEventListener('keydown', (event) => {
if (event.keyCode == 32) {
const overlayPanels = document.querySelectorAll('.hide-on-space');
for (let i = 0; i < overlayPanels.length; i++) {
overlayPanels[i].style.display = panelsHidden ? 'block' : 'none';
}
panelsHidden = !panelsHidden;
}
});
}, 100);