545 lines
21 KiB
JavaScript
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
|
|
}
|