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); const ctv = clone(layer.theatreObject.value); 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']; } flattenObject(cv, ['color']); flattenObject(ctv, ['color']); const v = {...ctv, ...cv}; deFlattenObject(v, ['color']); 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 = `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 }