using System; using System.Collections.Generic; using DG.Tweening; using UnityEngine; using UnityEngine.UI; using VMdemo.Simulation; namespace VMdemo.UI { public class StepFlowAnimator : MonoBehaviour { [Serializable] public class StepNodeView { public TranslationStep step; public RectTransform target; public CanvasGroup canvasGroup; public Image background; } [Header("Step Nodes")] [SerializeField] private List stepNodes = new List(); [Header("Colors")] [SerializeField] private Color idleColor = new Color(0.18f, 0.22f, 0.28f, 1f); [SerializeField] private Color activeColor = new Color(0.19f, 0.62f, 0.98f, 1f); [SerializeField] private Color faultColor = new Color(0.97f, 0.31f, 0.27f, 1f); [Header("Animation")] [SerializeField] private float highlightDuration = 0.2f; [SerializeField] private float normalScale = 1f; [SerializeField] private float activeScale = 1.08f; [SerializeField] private float pulseScale = 1.04f; [SerializeField] private RectTransform accessPulseTarget; private readonly Dictionary _nodeMap = new Dictionary(); private TranslationStep? _lastStep; private void Awake() { BuildNodeMap(); ResetVisual(); } public void ResetVisual() { DOTween.Kill(this); _lastStep = null; foreach (var node in stepNodes) { if (node == null) { continue; } if (node.target != null) { node.target.localScale = Vector3.one * normalScale; } if (node.canvasGroup != null) { node.canvasGroup.alpha = 0.72f; } if (node.background != null) { node.background.color = idleColor; } } if (accessPulseTarget != null) { accessPulseTarget.localScale = Vector3.one; } } public void AnimateStep(TranslationStep currentStep, bool isPageFault, bool accessCompleted) { BuildNodeMap(); if (_lastStep.HasValue && _nodeMap.TryGetValue(_lastStep.Value, out var lastNode)) { AnimateToIdle(lastNode); } if (_nodeMap.TryGetValue(currentStep, out var currentNode)) { AnimateToActive(currentNode, isPageFault && currentStep == TranslationStep.HandlePageFault); } if (accessCompleted) { AnimateAccessCompletedPulse(); } _lastStep = currentStep; } private void BuildNodeMap() { _nodeMap.Clear(); foreach (var node in stepNodes) { if (node == null) { continue; } if (!_nodeMap.ContainsKey(node.step)) { _nodeMap.Add(node.step, node); } } } private void AnimateToIdle(StepNodeView node) { if (node.target != null) { node.target.DOKill(); node.target.DOScale(normalScale, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this); } if (node.canvasGroup != null) { node.canvasGroup.DOKill(); node.canvasGroup.DOFade(0.72f, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this); } if (node.background != null) { node.background.DOKill(); node.background.DOColor(idleColor, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this); } } private void AnimateToActive(StepNodeView node, bool emphasizeFault) { if (node.target != null) { node.target.DOKill(); node.target.DOScale(activeScale, highlightDuration).SetEase(Ease.OutBack).SetTarget(this); } if (node.canvasGroup != null) { node.canvasGroup.DOKill(); node.canvasGroup.DOFade(1f, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this); } if (node.background != null) { node.background.DOKill(); var targetColor = emphasizeFault ? faultColor : activeColor; node.background.DOColor(targetColor, highlightDuration).SetEase(Ease.OutQuad).SetTarget(this); } if (emphasizeFault && node.target != null) { node.target.DOPunchScale(new Vector3(0.07f, 0.07f, 0f), 0.28f, 6, 0.75f).SetTarget(this); } } private void AnimateAccessCompletedPulse() { if (accessPulseTarget == null) { return; } accessPulseTarget.DOKill(); accessPulseTarget .DOScale(pulseScale, 0.12f) .SetEase(Ease.OutQuad) .SetLoops(2, LoopType.Yoyo) .SetTarget(this); } } }