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 PawnKeyframe pivotKeyframe; public int linkedOffset; public void Initialize(AnimationTimeline timeline, int actorID, int keyframeID) { this.timeline = timeline; this.clip = Workspace.GetPawnAnimationClip(actorID); this.keyframe = Workspace.GetPawnKeyframe(keyframeID); this.actorID = actorID; this.keyframeID = keyframeID; PawnKeyframe keyframe = Workspace.GetPawnKeyframe(keyframeID); maxValue = Workspace.StageWindowSize; value = keyframe.atTick.Value; 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) { Workspace.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.GetPawnKeyframesByID(Workspace.keyframeID).Except(new List() { keyframe })?.ToList(); // Link other slected keyframes to the movement of this one if (selectedKeyframes.NotNullOrEmpty()) { pivotKeyframe = keyframe.atTick <= selectedKeyframes.Min(x => x.atTick) ? selectedKeyframes.FirstOrDefault(x => x.atTick >= selectedKeyframes.Max(y => y.atTick)) : selectedKeyframes.FirstOrDefault(x => x.atTick <= selectedKeyframes.Min(y => y.atTick)); foreach (PawnKeyframe selectedKeyframe in selectedKeyframes) { KeyframeSlider unlinkedSlider = selectedKeyframe.GetKeyframeSlider(); if (unlinkedSlider != null) { if (Workspace.stretchKeyframes && unlinkedSlider.keyframe.atTick == pivotKeyframe.atTick) continue; unlinkedSlider.linkedSlider = this; unlinkedSlider.linkedOffset = unlinkedSlider.keyframe.atTick.Value - keyframe.atTick.Value; } } } } 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.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; } List keyframesToCheck = Workspace.GetAllPawnKeyframesAtTick(actorID, keyframe.atTick.Value); if (keyframesToCheck.NotNullOrEmpty()) { foreach (PawnKeyframe _keyframe in keyframesToCheck) { if (_keyframe != keyframe) { Workspace.GetAnimationClipThatOwnsKeyframe(_keyframe.keyframeID).RemovePawnKeyframe(_keyframe.keyframeID); } } } foreach (Selectable selectable in Selectable.allSelectablesArray) { if (selectable is KeyframeSlider) { KeyframeSlider linkedSlider = selectable.GetComponent(); PawnKeyframe linkedKeyframe = linkedSlider.keyframe; if (linkedSlider.linkedSlider != null) { keyframesToCheck = Workspace.GetAllPawnKeyframesAtTick(actorID, linkedKeyframe.atTick.Value); if (keyframesToCheck.NotNullOrEmpty() && keyframesToCheck.Count > 1) { foreach (PawnKeyframe _keyframe in keyframesToCheck) { if (_keyframe.keyframeID != linkedKeyframe.keyframeID) { Workspace.GetAnimationClipThatOwnsKeyframe(_keyframe.keyframeID).RemovePawnKeyframe(_keyframe.keyframeID); } } } } linkedSlider.linkedSlider = null; linkedSlider.pivotKeyframe = null; } } interactable = false; Workspace.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 (Workspace.stretchKeyframes && linkedSlider != null) { value = Mathf.CeilToInt(linkedSlider.keyframe.atTick.Value + linkedOffset * linkedSlider.ScaledOffsetFromPivot()); } else if (Workspace.stretchKeyframes == 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) && Workspace.StageTick == keyframe.atTick.Value) { handleImage.color = Constants.ColorPurple; } else if (Workspace.keyframeID.Contains(keyframeID)) { handleImage.color = Constants.ColorCyan; } else if (Workspace.StageTick == keyframe.atTick.Value) { handleImage.color = Constants.ColorPink; } else { handleImage.color = Constants.ColorGrey; } // Show sound symbol string soundDef = Workspace.GetPawnKeyframe(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(PawnKeyframe otherKeyframe) { return pivotKeyframe == otherKeyframe; } } }