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 = `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 }