413 lines
15 KiB
JavaScript
413 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 = () => {
|
|
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);
|
|
liveUpdater.immediateUpdate(layer, recording);
|
|
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.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
|
|
}
|