import uniq from 'lodash/uniq'; import {addMonkeypatch} from './utils'; import {EASING_OPTIONS, EASING_DEFAULT, getEasingArgDefault, parseEasingArg, GeckolibKeyframe, isArgsEasing} from './easing'; import {GECKOLIB_MODEL_ID} from "./constants"; const easingRegExp = /^ease(InOut|In|Out)?([\w]+)$/; export const loadAnimationUI = () => { Blockbench.on('display_animation_frame', displayAnimationFrameCallback); Blockbench.on('update_keyframe_selection', updateKeyframeSelectionCallback); Blockbench.on('render_frame', renderFrameCallback) addMonkeypatch(window, null, "updateKeyframeEasing", updateKeyframeEasing); addMonkeypatch(window, null, "updateKeyframeEasingArg", updateKeyframeEasingArg); }; export const unloadAnimationUI = () => { Blockbench.removeListener('display_animation_frame', displayAnimationFrameCallback); Blockbench.removeListener('update_keyframe_selection', updateKeyframeSelectionCallback); Blockbench.removeListener('render_frame', renderFrameCallback); }; export const displayAnimationFrameCallback = (/*...args*/) => { // const keyframe = $('#keyframe'); // console.log('displayAnimationFrameCallback:', args, 'keyframe:', keyframe); // keyframe is null here }; export function renderFrameCallback() { if (Format.id !== GECKOLIB_MODEL_ID) return Timeline.keyframes.forEach((kf: GeckolibKeyframe) => { if (kf.interpolation != "linear" && kf.easing != undefined) { kf.easing = undefined kf.easingArgs = undefined window.updateKeyframeSelection(); } if (kf.interpolation === "step") { kf.interpolation = "linear" if (kf.data_points.length == 1) addDataPoint() window.updateKeyframeSelection(); } updateKeyframeIcon(kf) }) const addPrePostButton = document.querySelector('#keyframe_type_label > div'); if (addPrePostButton) addPrePostButton.hidden = true; } export function updateKeyframeEasing(value) { Undo.initEdit({keyframes: Timeline.selected}) // var axis = $(obj).attr('axis'); // const value = $(obj).val(); // console.log('updateKeyframeEasing value:', value, 'obj:', obj); if (value === "-") return; Timeline.selected.forEach((kf: GeckolibKeyframe) => { kf.easing = value; kf.easingArgs = undefined; kf.interpolation = 'linear'; }) window.updateKeyframeSelection(); // Ensure easingArg display is updated // Animator.preview(); Undo.finishEdit('edit keyframe easing') } export function updateKeyframeEasingArg(obj) { Undo.initEdit({keyframes: Timeline.selected}) if ($(obj).val() === "-") return; // console.log('updateKeyframeEasingArg value:', $(obj).val(), 'obj:', obj); Timeline.selected.forEach((kf: GeckolibKeyframe) => { const value = parseEasingArg(kf, ($(obj).val() as string).trim()); kf.easingArgs = [value]; // obj.value = value; }) Undo.finishEdit('edit keyframe easing argument') } export const updateKeyframeSelectionCallback = (/*...args*/) => { $('#keyframe_bar_easing').remove() $('#keyframe_bar_easing_type').remove() $('#keyframe_bar_easing_arg1').remove() let multi_channel = false; //eslint-disable-line @typescript-eslint/no-unused-vars let channel: boolean | string | number = false; Timeline.selected.forEach((kf) => { if (channel === false) { channel = kf.channel } else if (channel !== kf.channel) { multi_channel = true } }) Timeline.keyframes.forEach((kf: GeckolibKeyframe) => { updateKeyframe(kf) }) const getMultiSelectValue = (selector, defaultValue, conflictValue) => { const selectorFunction = typeof selector === 'function' ? selector : x => (x[selector] === undefined ? defaultValue : x[selector]); if (Timeline.selected.length > 1) { const uniqSelected = uniq(Timeline.selected.map(selectorFunction)); if (uniqSelected.length === 1) { return uniqSelected[0]; } else { return conflictValue; } } else { return selectorFunction(Timeline.selected[0]) || defaultValue; } }; const keyframesByChannel = Timeline.keyframes.reduce((acc, kf) => { // Dear god I miss lodash if (!acc.has(kf.animator)) acc.set(kf.animator, {}); const animatorChannels = acc.get(kf.animator); if (!animatorChannels[kf.channel]) animatorChannels[kf.channel] = []; animatorChannels[kf.channel].push(kf); animatorChannels[kf.channel].sort((a, b) => { if (a.time < b.time) return -1; if (a.time > b.time) return 1; return 0; }); return acc; }, new Map()); const isFirstInChannel = (kf: _Keyframe) => keyframesByChannel.get(kf.animator)[kf.channel].indexOf(kf) < 1; if (Timeline.selected.length && Format.id === GECKOLIB_MODEL_ID) { if (Timeline.selected.every(kf => kf.animator instanceof BoneAnimator && !isFirstInChannel(kf))) { const displayedEasing = getMultiSelectValue('easing', EASING_DEFAULT, 'null'); const convertEasingTypeToId = (easing, easingType, inputEasingOrType) => { const easingTypeToTypeId = type => { let finalEasingType = "In"; if (type === "out") { finalEasingType = "Out"; } else if (type === "inout") { finalEasingType = "InOut"; } return finalEasingType; }; let finalEasing = 'ease'; if (inputEasingOrType === "in" || inputEasingOrType === "out" || inputEasingOrType === "inout") { const finalEasingType = easingTypeToTypeId(inputEasingOrType) finalEasing += finalEasingType + easing.substring(0, 1).toUpperCase() + easing.substring(1); } else if (inputEasingOrType === "linear" || inputEasingOrType == "step") { finalEasing = inputEasingOrType; } else { const finalEasingType = easingTypeToTypeId(easingType); finalEasing += finalEasingType + inputEasingOrType.substring(0, 1).toUpperCase() + inputEasingOrType.substring(1); } return finalEasing; }; const addEasingTypeIcons = (bar, easingType, title) => { const div = document.createElement("div"); div.innerHTML = getIcon(easingType); div.id = "kf_easing_type_" + easingType; div.setAttribute("style", "stroke:var(--color-text);margin:0px;padding:3px;width:30px;height:30px"); div.setAttribute("title", title); div.onclick = () => { const selectedEasing = $(".selected_kf_easing"); const selectedEasingType = $(".selected_kf_easing_type"); const keySelectedEasing = selectedEasing.attr("id").substring(15); const keySelectedEasingType = selectedEasingType.length <= 0 ? "in" : selectedEasingType.attr("id").substring(15); const currentEasing = convertEasingTypeToId(keySelectedEasing, keySelectedEasingType, keySelectedEasing); const finalEasing = convertEasingTypeToId(keySelectedEasing, keySelectedEasingType, easingType); if (finalEasing != currentEasing) { // console.log("Changed from " + currentEasing + " to " + finalEasing); updateKeyframeEasing(finalEasing); } }; bar.appendChild(div); }; const keyframePanel = document.getElementById('panel_keyframe'); if (keyframePanel) { let easingBar: HTMLElement = document.createElement('div'); keyframePanel.appendChild(easingBar); easingBar.outerHTML = `
`; easingBar = document.getElementById('keyframe_bar_easing'); addEasingTypeIcons(easingBar, "linear", "Switch to Linear easing"); addEasingTypeIcons(easingBar, "step", "Switch to Step easing"); addEasingTypeIcons(easingBar, "sine", "Switch to Sine easing"); addEasingTypeIcons(easingBar, "quad", "Switch to Quadratic easing"); addEasingTypeIcons(easingBar, "cubic", "Switch to Cubic easing"); addEasingTypeIcons(easingBar, "quart", "Switch to Quartic easing"); addEasingTypeIcons(easingBar, "quint", "Switch to Quntic easing"); addEasingTypeIcons(easingBar, "expo", "Switch to Exponential easing"); addEasingTypeIcons(easingBar, "circ", "Switch to Circle easing"); addEasingTypeIcons(easingBar, "back", "Switch to Back easing"); addEasingTypeIcons(easingBar, "elastic", "Switch to Elastic easing"); addEasingTypeIcons(easingBar, "bounce", "Switch to Bounce easing"); const keyEasing = getEasingInterpolation(displayedEasing); const keyEasingElement = document.getElementById("kf_easing_type_" + keyEasing); keyEasingElement.style.stroke = "var(--color-accent)"; keyEasingElement.classList.add('selected_kf_easing'); if (!(keyEasing === "linear" || keyEasing == "step")) { let easingTypeBar: HTMLElement = document.createElement('div'); keyframePanel.appendChild(easingTypeBar); easingTypeBar.outerHTML = `
`; easingTypeBar = document.getElementById('keyframe_bar_easing_type'); addEasingTypeIcons(easingTypeBar, "in", "Switch to In easing type"); addEasingTypeIcons(easingTypeBar, "out", "Switch to Out easing type"); addEasingTypeIcons(easingTypeBar, "inout", "Switch to In/Out easing type"); const keyEasingType = getEasingType(displayedEasing); const keyEasingTypeElement = document.getElementById("kf_easing_type_" + keyEasingType); keyEasingTypeElement.style.stroke = "var(--color-accent)"; keyEasingTypeElement.classList.add('selected_kf_easing_type'); } if (keyEasing !== "linear") document.getElementById('panel_keyframe').querySelector('div.bb-select').textContent = "GeckoLib" const getEasingArgLabel = (kf: GeckolibKeyframe) => { switch(kf.easing) { case EASING_OPTIONS.easeInBack: case EASING_OPTIONS.easeOutBack: case EASING_OPTIONS.easeInOutBack: return 'Overshoot'; case EASING_OPTIONS.easeInElastic: case EASING_OPTIONS.easeOutElastic: case EASING_OPTIONS.easeInOutElastic: case EASING_OPTIONS.easeInBounce: case EASING_OPTIONS.easeOutBounce: case EASING_OPTIONS.easeInOutBounce: return 'Bounciness'; case EASING_OPTIONS.step: return 'Steps'; default: return 'N/A'; } }; const easingArgLabel = getMultiSelectValue(getEasingArgLabel, null, null); if (Timeline.selected.every((kf: GeckolibKeyframe) => isArgsEasing(kf.easing)) && easingArgLabel !== null) { const argDefault = getMultiSelectValue(getEasingArgDefault, null, null); const [displayedValue] = getMultiSelectValue('easingArgs', [argDefault], [argDefault]); let scaleBar: HTMLElement = document.createElement('div'); keyframePanel.appendChild(scaleBar); scaleBar.outerHTML = `
`; scaleBar = document.getElementById('keyframe_bar_easing_arg1'); } } } } }; const getEasingInterpolation = (name: string) => { const matches = name.match(easingRegExp); if (matches) { return matches[2].toLowerCase(); } return name; }; const getEasingType = (name: string) => { const matches = name.match(easingRegExp); if (matches) { return matches[1].toLowerCase(); } return "in"; }; const updateKeyframe = (kf: GeckolibKeyframe) => { if (kf.data_points.length != 1 && kf.interpolation !== "linear") { removeLastDataPoint() } } const updateKeyframeIcon = (kf: GeckolibKeyframe) => { const element = document.getElementById(kf.uuid); if (element && element.children && kf.easing) element.children[0].className = 'easing-' + kf.easing.split(/\.?(?=[A-Z])/).join('_').toLowerCase().replace("ease_", "") } const addDataPoint = () => { Undo.initEdit({keyframes: Timeline.selected}) Timeline.selected.forEach(kf => { if (kf.data_points.length < kf.animator.channels[kf.channel].max_data_points) { kf.data_points.push(new KeyframeDataPoint(kf)) kf.data_points.last().extend(kf.data_points[0]) } }) Animator.preview() Undo.finishEdit('Add keyframe data point') } const removeLastDataPoint = () => { Undo.initEdit({keyframes: Timeline.selected}) Timeline.selected.forEach(kf => { if (kf.data_points.length >= 2) { kf.data_points.remove(kf.data_points.last()); } }) Animator.preview() Undo.finishEdit('Remove keyframe data point') } const getIcon = (name: string) => { switch(name) { case "back": return ''; case "bounce": return ''; case "circ": return ''; case "cubic": return ''; case "elastic": return ''; case "expo": case "in": return ''; case "inout": return ''; case "out": return ''; case "quad": return ''; case "quart": return ''; case "quint": return ''; case "sine": return ''; case "step": return ''; default: // linear return ''; } };