feat: 帧动画编辑器美化

This commit is contained in:
2026-07-20 22:04:32 +08:00
parent 4ec5e0b442
commit 982a1eedbe
10 changed files with 2369 additions and 273 deletions
@@ -12,10 +12,12 @@ namespace AibisDream.FrameAnimation.Editor
{
private readonly Label subtitle;
private readonly Label behavior;
private readonly Label flowBadge;
private readonly Label issueBadge;
private readonly Label entryBadge;
private readonly VisualElement badgeContainer;
private readonly FrameAnimationPreviewElement preview;
private readonly Action<FrameAnimationClipNodeView> selectedCallback;
private FrameAnimationFlowColorPalette? entryPalette;
public AnimationNode Data { get; }
public Port InputPort { get; }
@@ -29,30 +31,40 @@ namespace AibisDream.FrameAnimation.Editor
this.selectedCallback = selectedCallback;
viewDataKey = data.InternalId;
AddToClassList("fa-node");
style.minWidth = 245f;
style.maxWidth = 285f;
InputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi,
typeof(AnimationNode));
InputPort.portName = "In";
InputPort.portName = string.Empty;
inputContainer.Add(InputPort);
OutputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single,
typeof(AnimationNode));
OutputPort.portName = "Next";
OutputPort.portName = string.Empty;
outputContainer.Add(OutputPort);
subtitle = new Label();
subtitle.AddToClassList("fa-node__clip-id");
behavior = new Label();
flowBadge = new Label();
issueBadge = new Label();
issueBadge.style.unityFontStyleAndWeight = FontStyle.Bold;
extensionContainer.Add(subtitle);
extensionContainer.Add(behavior);
extensionContainer.Add(flowBadge);
extensionContainer.Add(issueBadge);
behavior.AddToClassList("fa-node__behavior");
var meta = new VisualElement();
meta.AddToClassList("fa-node__meta");
meta.Add(subtitle);
meta.Add(behavior);
extensionContainer.Add(meta);
preview = new FrameAnimationPreviewElement();
preview.AddToClassList("fa-node__preview");
extensionContainer.Add(preview);
badgeContainer = new VisualElement();
badgeContainer.AddToClassList("fa-node__badges");
entryBadge = new Label("E") { tooltip = "Flow Entry" };
entryBadge.AddToClassList("fa-node__entry");
badgeContainer.Add(entryBadge);
issueBadge = new Label();
issueBadge.AddToClassList("fa-badge");
issueBadge.style.unityFontStyleAndWeight = FontStyle.Bold;
badgeContainer.Add(issueBadge);
extensionContainer.Add(badgeContainer);
RefreshExpandedState();
}
@@ -65,28 +77,67 @@ namespace AibisDream.FrameAnimation.Editor
internal void Refresh(
FrameAnimationGraph graph,
IReadOnlyList<FrameAnimationEditorIssue> issues,
IReadOnlyList<AnimationFlow> flows,
IReadOnlyCollection<string> entryFlowIds)
IReadOnlyList<AnimationFlow> flows)
{
var clip = graph.Clips.FirstOrDefault(item => item != null && item.Id == Data.ClipId);
title = string.IsNullOrWhiteSpace(Data.DisplayName) ? Data.ClipId : Data.DisplayName;
subtitle.text = clip != null ? $"Clip: {Data.ClipId}" : $"Clip: <Missing {Data.ClipId}>";
subtitle.text = clip != null ? Data.ClipId : $"Missing: {Data.ClipId}";
var speed = Data.SpeedOverride.HasValue
? $"Speed {Data.SpeedOverride.Value:0.###} (Node)"
: clip != null ? $"Speed {clip.Speed:0.###} (Clip)" : "Speed ?";
var end = Data.EndBehaviorOverride?.ToString() ?? "Continue / terminal fallback";
behavior.text = $"{speed} · {end}";
var used = flows.Select(flow => flow.Id).ToArray();
var entries = used.Where(entryFlowIds.Contains).ToArray();
flowBadge.text = used.Length == 0
? "Unused"
: "Flows: " + string.Join(", ", used.Take(3)) + (used.Length > 3 ? $" +{used.Length - 3}" : string.Empty) +
(entries.Length > 0 ? " [ENTRY]" : string.Empty);
var used = flows.Where(flow => flow != null).ToArray();
var entryFlows = used.Where(flow => flow.EntryNodeId == Data.InternalId).ToArray();
entryPalette = entryFlows.Length > 0
? FrameAnimationFlowColorUtility.CreatePalette(
FrameAnimationFlowColorUtility.ResolveRaw(graph, entryFlows[0]))
: (FrameAnimationFlowColorPalette?)null;
entryBadge.style.display = entryPalette.HasValue ? DisplayStyle.Flex : DisplayStyle.None;
if (entryPalette.HasValue)
{
entryBadge.style.backgroundColor = entryPalette.Value.Stroke;
entryBadge.style.color = entryPalette.Value.Text;
entryBadge.tooltip = "Flow Entry: " + string.Join(", ", entryFlows.Select(flow => flow.Id));
}
foreach (var label in badgeContainer.Query<Label>(className: "fa-flow-badge").ToList())
{
label.RemoveFromHierarchy();
}
if (used.Length == 0)
{
var unused = new Label("UNUSED");
unused.AddToClassList("fa-badge");
unused.AddToClassList("fa-badge--yellow");
unused.AddToClassList("fa-flow-badge");
badgeContainer.Insert(entryPalette.HasValue ? 1 : 0, unused);
}
else
{
var insertIndex = entryPalette.HasValue ? 1 : 0;
foreach (var flow in used.Take(3))
{
var palette = FrameAnimationFlowColorUtility.CreatePalette(
FrameAnimationFlowColorUtility.ResolveRaw(graph, flow));
var badge = new Label(flow.Id) { tooltip = FlowTooltip(flow) };
badge.AddToClassList("fa-badge");
badge.AddToClassList("fa-flow-badge");
badge.style.backgroundColor = palette.Muted;
badge.style.color = palette.Text;
badgeContainer.Insert(insertIndex++, badge);
}
}
var matching = issues.Where(issue => issue.Selection.Kind == FrameAnimationEditorSelectionKind.Node &&
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
issueBadge.text = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
? "ERROR"
: matching.Length > 0 ? "WARNING" : string.Empty;
issueBadge.style.display = matching.Length > 0 ? DisplayStyle.Flex : DisplayStyle.None;
issueBadge.EnableInClassList("fa-badge--red",
matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error));
issueBadge.EnableInClassList("fa-badge--yellow",
matching.Length > 0 && matching.All(issue => issue.Severity != FrameAnimationValidationSeverity.Error));
issueBadge.style.color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
? new StyleColor(new Color(1f, 0.35f, 0.3f))
: new StyleColor(new Color(1f, 0.72f, 0.25f));
@@ -96,6 +147,30 @@ namespace AibisDream.FrameAnimation.Editor
: "创建唯一的顺序后继";
}
private static string FlowTooltip(AnimationFlow flow) =>
$"Flow: {flow.DisplayName} ({flow.Id})";
internal void SetFocusedFlow(FrameAnimationGraph graph, AnimationFlow flow, bool belongs)
{
EnableInClassList("fa-node--focused", flow != null && belongs);
if (flow != null && belongs)
{
var palette = FrameAnimationFlowColorUtility.CreatePalette(
FrameAnimationFlowColorUtility.ResolveRaw(graph, flow));
titleContainer.style.backgroundColor = palette.Header;
var titleLabel = titleContainer.Q<Label>(className: "title-label");
if (titleLabel != null) titleLabel.style.color = palette.Text;
style.borderLeftColor = palette.Stroke;
}
else
{
titleContainer.style.backgroundColor = StyleKeyword.Null;
var titleLabel = titleContainer.Q<Label>(className: "title-label");
if (titleLabel != null) titleLabel.style.color = StyleKeyword.Null;
style.borderLeftColor = StyleKeyword.Null;
}
}
internal void SetDimmed(bool dimmed)
{
style.opacity = dimmed ? 0.45f : 1f;
@@ -119,7 +194,10 @@ namespace AibisDream.FrameAnimation.Editor
internal sealed class FrameAnimationEdgeView : Edge
{
private readonly Action<FrameAnimationEdgeView> selectedCallback;
private Color baseColor = Color.white;
private Color baseColor = new Color32(119, 127, 135, 255);
private bool hasError;
private bool hasWarning;
private FrameAnimationFlowElementState previewState;
public AnimationEdge Data { get; }
internal FrameAnimationEdgeView(AnimationEdge data, Action<FrameAnimationEdgeView> selectedCallback)
@@ -128,6 +206,7 @@ namespace AibisDream.FrameAnimation.Editor
this.selectedCallback = selectedCallback;
viewDataKey = data.InternalId;
userData = data;
RegisterCallback<AttachToPanelEvent>(_ => ApplyVisualState());
}
public override void OnSelected()
@@ -146,12 +225,9 @@ namespace AibisDream.FrameAnimation.Editor
var matching = (issues ?? Array.Empty<FrameAnimationEditorIssue>()).Where(issue =>
issue.Selection.Kind == FrameAnimationEditorSelectionKind.Edge &&
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
var color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
? new Color(1f, 0.3f, 0.25f)
: matching.Length > 0 ? new Color(1f, 0.72f, 0.2f) : Color.white;
baseColor = color;
edgeControl.inputColor = color;
edgeControl.outputColor = color;
hasError = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error);
hasWarning = !hasError && matching.Length > 0;
ApplyVisualState();
tooltip = matching.Length == 0
? $"{Data.ExitName} / {Data.Condition}"
: string.Join("\n", matching.Select(issue => $"[{issue.Code}] {issue.Message}"));
@@ -159,25 +235,47 @@ namespace AibisDream.FrameAnimation.Editor
internal void SetPreviewState(FrameAnimationFlowElementState state)
{
if (state == FrameAnimationFlowElementState.Current)
previewState = state;
ApplyVisualState();
}
internal void SetFocusedFlow(Color? flowColor, bool belongs)
{
baseColor = flowColor.HasValue && belongs
? FrameAnimationFlowColorUtility.CreatePalette(flowColor.Value).Stroke
: new Color32(119, 127, 135, 255);
ApplyVisualState();
}
private void ApplyVisualState()
{
var color = hasError
? new Color32(223, 106, 106, 255)
: hasWarning ? new Color32(224, 184, 88, 255) : baseColor;
var width = 3f;
if (!hasError && !hasWarning)
{
edgeControl.inputColor = new Color(0.25f, 0.75f, 1f);
edgeControl.outputColor = new Color(0.25f, 0.75f, 1f);
if (previewState == FrameAnimationFlowElementState.Current)
{
color = Color.Lerp(color, Color.white, 0.28f);
width = 5f;
}
else if (previewState == FrameAnimationFlowElementState.Passed)
{
color = Color.Lerp(color, new Color32(86, 185, 123, 255), 0.22f);
width = 3.5f;
}
else if (previewState == FrameAnimationFlowElementState.Upcoming)
{
color = Color.Lerp(color, new Color32(43, 47, 52, 255), 0.42f);
width = 2f;
}
}
else if (state == FrameAnimationFlowElementState.Passed)
edgeControl.inputColor = color;
edgeControl.outputColor = color;
if (panel != null)
{
edgeControl.inputColor = new Color(0.35f, 0.68f, 0.46f);
edgeControl.outputColor = new Color(0.35f, 0.68f, 0.46f);
}
else if (state == FrameAnimationFlowElementState.Upcoming)
{
edgeControl.inputColor = new Color(0.55f, 0.55f, 0.55f);
edgeControl.outputColor = new Color(0.55f, 0.55f, 0.55f);
}
else
{
edgeControl.inputColor = baseColor;
edgeControl.outputColor = baseColor;
edgeControl.edgeWidth = Mathf.RoundToInt(width);
}
}
}
@@ -285,14 +383,12 @@ namespace AibisDream.FrameAnimation.Editor
if (graph != null)
{
var topology = new FrameAnimationGraphTopology(graph);
var entries = new HashSet<string>(graph.Flows.Where(flow => flow != null)
.Select(flow => flow.EntryNodeId));
foreach (var node in graph.Nodes.Where(node => node != null))
{
var view = new FrameAnimationClipNodeView(node, OnNodeSelected);
view.SetPosition(new Rect(FrameAnimationGraphMutationService.GetNodePosition(graph, node),
new Vector2(255f, 150f)));
view.Refresh(graph, issues, topology.FindFlowsUsingNode(node.InternalId), entries);
new Vector2(224f, 164f)));
view.Refresh(graph, issues, topology.FindFlowsUsingNode(node.InternalId));
nodeViews[node.InternalId] = view;
AddElement(view);
}
@@ -372,8 +468,16 @@ namespace AibisDream.FrameAnimation.Editor
focusedFlowId = flowId ?? string.Empty;
if (graph == null || string.IsNullOrEmpty(flowId))
{
foreach (var view in nodeViews.Values) view.SetDimmed(false);
foreach (var view in edgeViews.Values) view.SetDimmed(false);
foreach (var view in nodeViews.Values)
{
view.SetDimmed(false);
view.SetFocusedFlow(graph, null, false);
}
foreach (var view in edgeViews.Values)
{
view.SetDimmed(false);
view.SetFocusedFlow(null, false);
}
return;
}
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId);
@@ -383,8 +487,19 @@ namespace AibisDream.FrameAnimation.Editor
: new FrameAnimationReachability(Array.Empty<AnimationNode>(), Array.Empty<AnimationEdge>());
var nodeIds = new HashSet<string>(reachable.Nodes.Select(node => node.InternalId));
var edgeIds = new HashSet<string>(reachable.Edges.Select(edge => edge.InternalId));
foreach (var pair in nodeViews) pair.Value.SetDimmed(!nodeIds.Contains(pair.Key));
foreach (var pair in edgeViews) pair.Value.SetDimmed(!edgeIds.Contains(pair.Key));
var flowColor = flow != null ? FrameAnimationFlowColorUtility.ResolveRaw(graph, flow) : (Color?)null;
foreach (var pair in nodeViews)
{
var belongs = nodeIds.Contains(pair.Key);
pair.Value.SetDimmed(!belongs);
pair.Value.SetFocusedFlow(graph, flow, belongs);
}
foreach (var pair in edgeViews)
{
var belongs = edgeIds.Contains(pair.Key);
pair.Value.SetDimmed(!belongs);
pair.Value.SetFocusedFlow(flowColor, belongs);
}
}
internal void SelectAndFrame(FrameAnimationEditorSelection target)