variabletime/bin/web/js/record.js

545 lines
21 KiB
JavaScript

import {
clone,
sequencialPromises,
toCssClass,
flattenObject,
deFlattenObject,
} from './utils.js';
const LiveBuffer = function() {
// private
//
// constants
const NO_TIME = -1;
// variables
/// @brief valueBuffer stores all values.
/// it is an object with layerIDs/artboard as key
const valueBuffer = {};
// functions
const register = (id) => {
if (!valueBuffer.hasOwnProperty(id)) {
valueBuffer[id] = new Map();
}
};
const deregister = (id) => {
if (valueBuffer.hasOwnProperty(id)) {
delete valueBuffer[id];
}
};
/// @brief addValues
// values are expected to be
// {
// x: 42.0,
// fontSize_px: 24,
// whatever: "something",
// }
const addValues = (id, values, time_s, start_time_s) => {
const subValueBuffer = valueBuffer[id];
// delete between start_time_s and time_s
if (start_time_s !== NO_TIME) {
subValueBuffer.forEach((value, value_time_s) => {
if (value_time_s > start_time_s && value_time_s <= time_s) {
const keys = Object.keys(values);
for (let k = 0; k < keys.length; k++) {
delete subValueBuffer.get(value_time_s)[keys[k]];
if (Object.keys(subValueBuffer.get(value_time_s)).length === 0) {
subValueBuffer.delete(value_time_s);
break;
}
}
}
});
}
if (subValueBuffer.has(time_s)) {
subValueBuffer.set(time_s, {...subValueBuffer.get(time_s), ...values});
} else {
subValueBuffer.set(time_s, clone(values));
}
};
// get values and merge with previous
// 0.42s: { x: 4 }
// 0.64s: { y: 7 }
// 0.82s: { y: 12, z: 24 }
//
// return for 0.82s = { x: 4, y: 12, x: 24 }
const getValues = (id, time_s) => {
if (valueBuffer[id].size === 0) {
return {};
} else {
valueBuffer[id] = new Map([...valueBuffer[id].entries()].sort());
let mergedValues = {};
let didMergeValues = {};
valueBuffer[id].forEach((value, value_time_s) => {
if (value_time_s <= time_s) {
mergedValues = {...mergedValues, ...value};
} else {
if (Object.keys(didMergeValues).length === 0) {
didMergeValues = clone(mergedValues);
}
Object.keys(value).forEach((key) => {
if(!didMergeValues.hasOwnProperty(key)) {
mergedValues[key] = value[key];
}
});
}
});
return mergedValues;
}
};
// public
this.NO_TIME = NO_TIME;
this.addValues = addValues;
this.getValues = getValues;
this.register = register;
this.deregister = deregister;
};
const LiveUpdater = function(tp, buffy) {
const toUpdate = [];
const update = () => {
const time_s = tp.sheet.sequence.position;
}
this.add = (layer) => {
if (toUpdate.indexOf(layer) < 0) {
toUpdate.push(layer);
}
};
this.remove = (layer) => {
const index = toUpdate.indexOf(layer);
if (index >= 0) {
toUpdate.push(layer);
}
};
this.immediateUpdate = (layer, values) => {
const cv = clone(values);
if (cv.hasOwnProperty('color.r')) {
cv['color'] = {
r: cv['color.r'],
g: cv['color.g'],
b: cv['color.b'],
a: cv['color.a'],
};
delete cv['color.r'];
delete cv['color.g'];
delete cv['color.b'];
delete cv['color.a'];
}
const v = {...layer.theatreObject.value, ...cv};
const p = layer.values2cppProps(v);
if (p !== false) {
const id = layer.id();
if (id !== 'artboard') {
Module.setProps(p, layer.id());
} else {
Module.setArtboardProps(p, layer.id());
}
}
};
};
const Record = function(tp) {
const NOT_RECORDING = 0;
const STARTING_RECORDING = 1;
const RECORDING = 2;
const STOPPING_RECORDING = 3;
const hot = {};
let isRecording = NOT_RECORDING;
const buffy = new LiveBuffer();
const liveUpdater = new LiveUpdater(tp, buffy);
let isInitialized = false;
const init = () => {
if (!isInitialized) {
tp.core.onChange(tp.sheet.sequence.pointer.playing, (playing) => {
if (isRecording === RECORDING && !playing) {
stopRecording();
}
});
isInitialized = true;
}
};
const isHot = (layerID, propTitle) => {
return hot.hasOwnProperty(layerID)
&& hot[layerID].hasOwnProperty(propTitle);
};
const addHot = (layerID, propTitle) => {
if (!isHot(layerID, propTitle)) {
if (!hot.hasOwnProperty(layerID)) {
hot[layerID] = {};
}
hot[layerID][propTitle] = {
recording: [],
};
}
buffy.register(layerID);
// handle UI only if layer is selected
if (getLayer(layerID).isSelected()) {
let cPropTitle = clone(propTitle);
// if colors are separate, there is still just one button
if (cPropTitle.indexOf('color.') === 0) {
cPropTitle = 'color';
}
const button = tp
.getPanelPropTitle(cPropTitle)
.parentNode.parentNode
.querySelector('.recordButton');
if (button !== null) {
button.classList.add('active');
}
}
};
const removeHot = (layerID, propTitle) => {
if (isHot(layerID, propTitle)) {
delete hot[layerID][propTitle];
}
// what if it is the last prop in the layer
if (hot.hasOwnProperty(layerID)) {
if (Object.keys(hot[layerID]).length === 0) {
delete hot[layerID];
buffy.deregister(layerID);
}
}
// handle UI only if layer is selected
if (getLayer(layerID).isSelected()) {
let cPropTitle = clone(propTitle);
// if colors are separate, there is still just one button
if (cPropTitle.indexOf('color.') === 0) {
cPropTitle = 'color';
}
const button = tp
.getPanelPropTitle(cPropTitle)
.parentNode.parentNode
.querySelector('.recordButton');
if (button !== null) {
button.classList.remove('active');
}
}
};
//const makeNotHot = (layerID, propTitle) => {
//if (isHot(layerID, propTitle)) {
//// whatever
//}
//};
const addRecordButton = (layer, propTitle) => {
const panel = tp.getPanel();
const panelPropTitle = tp.getPanelPropTitle(propTitle);
if (panelPropTitle !== null) {
const container = panelPropTitle.parentNode.parentNode;
if (container === null) {
console.log("Record::addRecordButton",
`impossible! cannot find panelPropContainer for ${propTitle}`);
} else if (container.querySelector('.recordButton') !== null) {
// this is super verbose, let's not log by default
//console.log("Record::addRecordButton",
//`already added an record button for ${propTitle}`);
} else {
const button = document.createElement('div');
button.classList.add('recordButton');
button.classList.add(toCssClass(`recordButton${propTitle}`));
button.innerHTML = `<img src="/web/assets/record.svg" alt="record" />`;
container.append(button);
button.addEventListener('click', () => {
if(isRecording === RECORDING) {
stopRecording();
} else {
if (config.record.recordMapped) {
// make all mapped props hot and
Object.keys(audio.mapping)
.forEach((layerID) => {
//if (getLayer(layerID).isSelected()) { // NOTE: multilayer recording
Object.keys(audio.mapping[layerID])
.forEach((propTitle) => {
addHot(layerID, propTitle);
});
//}
});
} else {
// only make this propTitle hot and
// register its layer for recording
addHot(layer.id(), propTitle);
}
startRecording();
}
});
//console.log("Record::addRecordButton",
//`added a record button for ${propTitle}`);
}
} else {
console.log("Record::addRecordButton",
`cannot find panelPropTitle for ${propTitle}`);
}
};
const cloneInput = (layer, propTitle) => {
const panel = tp.getPanel();
const panelPropTitle = tp.getPanelPropTitle(propTitle);
if (panelPropTitle !== null) {
const container = panelPropTitle.parentNode.parentNode;
if (container === null) {
console.log("Record::cloneInput",
`impossible! cannot find panelPropContainer for ${propTitle}`);
} else if (container.querySelector('input.recording') !== null) {
console.log("Record::cloneInput",
`already cloned input for ${propTitle}`);
} else {
const input = container.querySelector('input');
if (input === null) {
console.log("Record::cloneInput",
`uuh.. seems there is no input to clone for ${propTitle}`);
} else {
const input_clone = input.cloneNode();
input_clone.classList.value = '';
input_clone.classList.add('recording');
input.parentNode.after(input_clone);
input.setAttribute('data-previousDisplay', input.style.display);
input.style.display = 'none';
return input_clone;
}
}
}
return null;
};
const uncloneInput = (layer, propTitle) => {
const panel = tp.getPanel();
const panelPropTitle = tp.getPanelPropTitle(propTitle);
if (panelPropTitle !== null) {
const container = panelPropTitle.parentNode.parentNode;
if (container === null) {
console.log("Record::uncloneInput",
`impossible! cannot find panelPropContainer for ${propTitle}`);
} else if (container.querySelector('input.recording') === null) {
console.log("Record::uncloneInput",
`already uncloned input for ${propTitle}`);
} else {
const input = container.querySelector('input:not(.recording)');
const input_clone = container.querySelector('input.recording');
if (input === null ) {
console.log("Record::uncloneInput",
`uuh.. seems there is no input for ${propTitle}`);
} else if (input_clone === null ) {
console.log("Record::uncloneInput",
`uuh.. seems there is no input_clone for ${propTitle}`);
} else {
input_clone.remove();
input.removeAttribute('data-previousDisplay');
const previousInputDisplay = input.getAttribute('data-previousDisplay');
input.style.display = previousInputDisplay;
}
}
}
};
const injectPanel = (layer) => {
const flatValues = clone(layer.theatreObject.value);
flattenObject(flatValues, ['color']);
const props = Object.keys(flatValues);
props.forEach((propTitle) => {
if (config.record.ignoreProps.indexOf(propTitle) < 0) {
addRecordButton(layer, propTitle);
}
});
};
let lastPositions = {};
const addValue = (layerID,
propTitle,
value,
position = tp.sheet.sequence.position,
lastPosition = buffy.NO_TIME) => {
// NOTE: multilayer recording
if (!hot.hasOwnProperty(layerID) || !hot[layerID].hasOwnProperty(propTitle)) {
return;
}
hot[layerID][propTitle].recording.push({
position,
value,
});
const recording = {
[propTitle]: value,
};
if (!lastPositions.hasOwnProperty(layerID)) {
lastPositions[layerID] = {};
}
if (lastPosition === buffy.NO_TIME) {
if (!lastPositions[layerID].hasOwnProperty(propTitle)) {
lastPositions[layerID][propTitle] = position;
}
} else {
lastPositions[layerID][propTitle] = lastPosition;
}
buffy.addValues(layerID, recording, position, lastPositions[layerID][propTitle]);
};
const getValue = (layerID, position) => {
const merged = clone(buffy.getValues(layerID, position));
deFlattenObject(merged);
return merged;
};
const liveUpdate = (layer, position = tp.sheet.sequence.position) => {
const merged = getValue(layer.id(), position);
liveUpdater.immediateUpdate(layer, merged);
};
const startRecording = () => {
isRecording = STARTING_RECORDING;
console.log('Record::startRecording');
document.querySelector('#notice_recording')
.classList.add('visible');
document.querySelector('#notice_recording')
.classList.remove('imprenetrable');
document.querySelector('#notice_recording .what p').innerHTML = 'recording';
document.querySelector('#notice_recording .details p').innerHTML = '';
if (!isInitialized) {
init();
}
lastPositions = {};
tp.sheet.sequence.pause();
const layerKeys = Object.keys(hot);
layerKeys.forEach((layerID) => {
const layer = getLayer(layerID);
layer.updateValuesViaTheatre(false);
const propTitles = Object.keys(hot[layerID]);
propTitles.forEach((propTitle) => {
// NOTE: layerID is not actually used atm
// and should be the layer anyways
const input_clone = cloneInput(layerID, propTitle);
let lastPosition = buffy.NO_TIME;
if (input_clone !== null) {
//input_clone.addEventListener('change', () => {
//const position = tp.sheet.sequence.position;
//const value = parseFloat(input_clone.value);
//addValue(layerID, propTitle, value, position, lastPosition);
//const merged = getValue(layerID, position);
//liveUpdater.immediateUpdate(layer, merged);
//lastPosition = position;
//});
} else {
console.log('Record::startRecording', `whoops input_clone for ${propTitle} is null`);
}
});
tp.sheet.sequence.position = 0;
tp.sheet.sequence.play();
});
isRecording = RECORDING;
};
const stopRecording = () => {
document.querySelector('#notice_recording')
.classList.add('visible');
document.querySelector('#notice_recording')
.classList.add('imprenetrable');
document.querySelector('#notice_recording .what p').innerHTML = 'digesting recording';
document.querySelector('#notice_recording .details p').innerHTML = 'please wait';
isRecording = STOPPING_RECORDING;
return new Promise((resolve) => {
const layerKeys = Object.keys(hot);
const promises = [];
promises.push(() => {
return new Promise((subResolve) => {
const audioOptionsButtons = tp.getPanel()
.querySelectorAll(`.audioButton.active`);
if (audioOptionsButtons !== null) {
audioOptionsButtons.forEach((audioOptionsButton) => {
audioOptionsButton.click();
});
}
subResolve();
});
});
layerKeys.forEach((layerID) => {
const layer = getLayer(layerID);
const propTitles = Object.keys(hot[layerID]);
const keyframes = [];
propTitles.forEach((propTitle) => {
// NOTE: layerID is not actually used atm
// and should be the layer anyways
uncloneInput(layerID, propTitle);
// special treatment if we have sperate RGBA for color
if (propTitle.indexOf('color.') === 0) {
if (propTitle === 'color.r') {
const recording = [];
hot[layerID]['color.r'].recording.forEach((rr, ri) => {
if (ri < hot[layerID]['color.r'].recording.length &&
ri < hot[layerID]['color.g'].recording.length &&
ri < hot[layerID]['color.b'].recording.length &&
ri < hot[layerID]['color.a'].recording.length) {
const r = clone(rr);
r.value = {
r: hot[layerID]['color.r'].recording[ri].value,
g: hot[layerID]['color.g'].recording[ri].value,
b: hot[layerID]['color.b'].recording[ri].value,
a: hot[layerID]['color.a'].recording[ri].value,
};
recording.push(r);
}
});
keyframes.push({
path: ['color'],
keyframes: recording,
});
}
} else {
keyframes.push({
path: propTitle.split('.'),
keyframes: hot[layerID][propTitle].recording,
});
}
});
//setTimeout(() => {
const kf = clone(keyframes);
promises.push(() => {
return new Promise((subResolve) => {
tp.setKeyframes(layer, keyframes).then(() => {
layer.updateValuesViaTheatre(true);
subResolve();
});
})
});
//}, 2000);
});
sequencialPromises(promises, () => {
Object.keys(hot).forEach((layerID) => {
Object.keys(hot[layerID]).forEach((propTitle) => {
removeHot(layerID, propTitle);
});
buffy.deregister(layerID);
});
document.querySelector('#notice_recording')
.classList.remove('visible');
console.log('Record::stopRecording', 'stopped recording');
isRecording = NOT_RECORDING;
resolve();
});
});
};
// public
this.addValue = addValue;
this.getValue = getValue;
this.liveUpdate = liveUpdate;
this.liveUpdater = liveUpdater;
this.addRecordButton = addRecordButton;
this.addHot = addHot;
this.removeHot = removeHot;
this.getHot = () => {
return hot;
};
this.isRecording = () => {
return isRecording != NOT_RECORDING;
};
this.injectPanel = injectPanel;
this.startRecording = startRecording;
this.stopRecording = stopRecording;
};
export {
Record
}