variabletime/bin/web/js/record.js

416 lines
15 KiB
JavaScript

import {
clone,
sequencialPromises,
} 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 v = {...layer.theatreObject.value, ...values};
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 hot = {};
let isRecording = false;
const buffy = new LiveBuffer();
const liveUpdater = new LiveUpdater(tp, buffy);
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()) {
const button = tp
.getPanelPropContainer(propTitle)
.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()) {
const button = tp
.getPanelPropContainer(propTitle)
.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 = tp.getPanelPropContainer(panelPropTitle);
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(`recordButton${propTitle}`);
button.innerHTML = `<img src="/web/assets/record.svg" alt="record" />`;
container.append(button);
button.addEventListener('click', () => {
if(isRecording) {
stopRecording();
} else {
if (config.record.recordMapped) {
// make all mapped props hot and
Object.keys(audio.mapping)
.forEach((layerID) => {
if (getLayer(layerID).isSelected()) {
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 = tp.getPanelPropContainer(panelPropTitle);
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 = tp.getPanelPropContainer(panelPropTitle);
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 props = Object.keys(layer.theatreObject.value);
props.forEach((propTitle) => {
if (config.record.ignoreProps.indexOf(propTitle) < 0) {
addRecordButton(layer, propTitle);
}
});
};
const startRecording = () => {
console.log('Record::startRecording');
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', (e) => {
const position = tp.sheet.sequence.position;
const value = parseFloat(input_clone.value);
hot[layerID][propTitle].recording.push({
position,
value,
});
const recording = {
[propTitle]: value,
};
buffy.addValues(layerID, recording, position, lastPosition);
const merged = buffy.getValues(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 = true;
};
const stopRecording = () => {
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);
keyframes.push({
path: [propTitle],
keyframes: hot[layerID][propTitle].recording,
});
});
//setTimeout(() => {
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);
});
console.log('Record::stopRecording', 'stopped recording');
isRecording = false;
resolve();
});
});
};
// public
this.liveUpdater = liveUpdater;
this.addRecordButton = addRecordButton;
this.addHot = addHot;
this.removeHot = removeHot;
this.getHot = () => {
return hot;
};
this.isRecording = () => {
return isRecording;
};
this.injectPanel = injectPanel;
this.startRecording = startRecording;
this.stopRecording = stopRecording;
};
export {
Record
}