using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace RimWorldAnimationStudio { public class KeyframeSlider : Slider, IPointerClickHandler, IBeginDragHandler, IEndDragHandler { public AnimationTimeline timeline; public Transform ghostSliders; public Slider ghostSliderPrefab; public Image handleImage; public GameObject soundIcon; public int maxGhosts = 4; public int actorID; public int keyframeID; private PawnAnimationClip clip; private PawnKeyframe keyframe; private float dragTimeStart = -1f; private int dragTickStart = -1; public KeyframeSlider linkedSlider; public Keyframe pivotKeyframe; public int linkedOffset; public void Initialize(AnimationTimeline timeline, int actorID, int keyframeID) { this.timeline = timeline; this.clip = Workspace.Instance.GetPawnAnimationClip(actorID); this.keyframe = Workspace.Instance.GetPawnKeyframe(actorID, keyframeID); this.actorID = actorID; this.keyframeID = keyframeID; PawnKeyframe keyframe = Workspace.Instance.GetPawnKeyframe(actorID, keyframeID); maxValue = Workspace.StageWindowSize; value = keyframe.atTick.Value; OnValueChanged(); onValueChanged.AddListener(delegate (float value) { OnValueChanged(); }); } public void OnValueChanged() { keyframe.atTick = (int)value; clip.BuildSimpleCurves(); timeline.InitiateUpdateOfGhostFrames(); } // Ghost sliders are non-interactable slider handle public void UpdateGhostFrames() { if (maxGhosts == 0) { return; } int requiredGhosts = GetGhostFramesRequired(); int currentGhostCount = ghostSliders.childCount; for (int i = 0; i < Mathf.Max(requiredGhosts, currentGhostCount); i++) { int targetTick = (int)(i * clip.duration + keyframe.atTick); if (ghostSliders.childCount <= i) { Instantiate(ghostSliderPrefab, ghostSliders); } GameObject ghostSliderObject = ghostSliders.GetChild(i).gameObject; ghostSliderObject.SetActive(i < requiredGhosts); Slider ghostSlider = ghostSliderObject.GetComponent(); ghostSlider.maxValue = Workspace.StageWindowSize; ghostSlider.value = targetTick; if (targetTick > ghostSlider.maxValue) { ghostSlider.gameObject.SetActive(false); } } } public int GetGhostFramesRequired() { if (Workspace.animationDef.animationStages[Workspace.stageID].isLooping == false) { return 0; } if (clip.duration <= 1) { return 0; } return Math.Min(Mathf.CeilToInt((float)Workspace.StageWindowSize / clip.duration), maxGhosts); } public void OnPointerClick(PointerEventData eventData) { Workspace.actorID = actorID; if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.LeftCommand)) { Workspace.keyframeID.Add(keyframeID); } else if (Workspace.keyframeID.NullOrEmpty() || Workspace.keyframeID.Contains(keyframeID) == false) { Workspace.keyframeID = new List { keyframeID }; } if (eventData.clickCount >= 2) { AnimationController.Instance.stageTick = keyframe.atTick.Value; } } public void OnBeginDrag(PointerEventData eventData) { Workspace.actorID = actorID; dragTimeStart = Time.unscaledTime; dragTickStart = keyframe.atTick.Value; if (Workspace.keyframeID.NullOrEmpty() || Workspace.keyframeID.Contains(keyframeID) == false) { Workspace.keyframeID = new List { keyframeID }; } List selectedKeyframes = Workspace.Instance.GetPawnKeyframes(Workspace.keyframeID).Except(new List() { keyframe })?.ToList(); // Link other slected keyframes to the movement of this one if (selectedKeyframes.NotNullOrEmpty()) { foreach (PawnKeyframe selectedKeyframe in selectedKeyframes) { KeyframeSlider unlinkedSlider = selectedKeyframe.GetKeyframeSlider(); if (unlinkedSlider != null) { unlinkedSlider.linkedSlider = this; unlinkedSlider.linkedOffset = unlinkedSlider.keyframe.atTick.Value - keyframe.atTick.Value; } } pivotKeyframe = keyframe.atTick < selectedKeyframes[0].atTick ? selectedKeyframes.Last() : selectedKeyframes.First(); } } public override void OnDrag(PointerEventData eventData) { Workspace.actorID = actorID; // The first keyframe can't be moved if (keyframe.atTick == Constants.minTick) { value = Constants.minTick; return; } // Sticky drag if (Time.unscaledTime - dragTimeStart < 0.05f) return; interactable = true; base.OnDrag(eventData); // Snap to nearest keyframe (on another timeline) int targetTick = Workspace.FindClosestKeyFrameAtTick(keyframe.atTick.Value, Mathf.CeilToInt(Workspace.StageWindowSize * 0.01f), actorID); if (Input.GetKey(KeyCode.LeftShift) && Workspace.Instance.DoesPawnKeyframeExistAtTick(Workspace.stageID, actorID, targetTick) == false) { value = (float)targetTick; } // Prevent other frames from being moved to the first keyframe if (value == Constants.minTick) { value = Constants.minTick + 1; } } public void OnEndDrag(PointerEventData eventData) { if (keyframe.atTick == Constants.minTick) { value = Constants.minTick; return; } foreach (Selectable otherSlider in Selectable.allSelectablesArray) { if (otherSlider is KeyframeSlider) { Debug.Log("unlinked keyframes"); (otherSlider as KeyframeSlider).linkedSlider = null; } } interactable = false; Workspace.Instance.RecordEvent("Keyframe move"); } protected override void Update() { base.Update(); // Update outdated values if (Workspace.keyframeID.NullOrEmpty() || Workspace.keyframeID.Contains(keyframeID) == false) { linkedSlider = null; } else if (AnimationController.Instance.stretchKeyframesToggle.isOn && linkedSlider != null && linkedSlider.IsPivotKeyframe(keyframe) == false) { value = Mathf.CeilToInt(linkedSlider.keyframe.atTick.Value + linkedOffset * linkedSlider.ScaledOffsetFromPivot()); } else if (AnimationController.Instance.stretchKeyframesToggle.isOn == false && linkedSlider != null) { value = Mathf.Clamp(linkedSlider.keyframe.atTick.Value + linkedOffset, Constants.minTick + 1, Workspace.StageWindowSize); } else if (keyframe.atTick.Value != value) { value = keyframe.atTick.Value; } // Update key color if (keyframe.atTick.HasValue && Workspace.keyframeID.Contains(keyframeID) && AnimationController.Instance.stageTick == keyframe.atTick.Value) { handleImage.color = Constants.ColorPurple; } else if (Workspace.keyframeID.Contains(keyframeID)) { handleImage.color = Constants.ColorCyan; } else if (AnimationController.Instance.stageTick == keyframe.atTick.Value) { handleImage.color = Constants.ColorPink; } else { handleImage.color = Constants.ColorGrey; } // Show sound symbol string soundDef = Workspace.Instance.GetPawnKeyframe(actorID, keyframeID)?.soundEffect; soundIcon.SetActive(soundDef != null && soundDef != "" && soundDef != "None"); } public float ScaledOffsetFromPivot() { if (dragTickStart == pivotKeyframe.atTick.Value) return 0f; return (float)(keyframe.atTick.Value - pivotKeyframe.atTick.Value) / (dragTickStart - pivotKeyframe.atTick.Value); } public bool IsPivotKeyframe(Keyframe otherKeyframe) { return pivotKeyframe == otherKeyframe; } } }