using System; using System.Collections.Generic; using Uigenx.Animation; using UnityEngine; using UnityEngine.UIElements; namespace Uigenx.Effects { /// /// Sets and animates `style.filter`. Verified against Unity 6000.5: `IStyle.filter` is a /// `StyleList<FilterFunction>` and the built in functions are blur, tint, opacity, invert, /// grayscale, sepia, contrast and hue-rotate, each taking exactly one parameter. /// public sealed class UgxFilterController : IDisposable { /// Animation tick, matching the animator. public const long TickIntervalMs = 16; readonly VisualElement _element; // Reused so pushing the list every tick allocates nothing: StyleList wraps the same List. readonly List _functions = new List(2); readonly List _tweens = new List(2); IVisualElementScheduledItem _ticker; bool _disposed; public UgxFilterController(VisualElement element) => _element = element ?? throw new ArgumentNullException(nameof(element)); public int Count => _functions.Count; public FilterFunction this[int index] => _functions[index]; public int Add(FilterFunctionType type, float value) { var fn = new FilterFunction(type); fn.AddParameter(new FilterParameter(value)); return Add(fn); } public int Add(FilterFunctionType type, Color value) { var fn = new FilterFunction(type); fn.AddParameter(new FilterParameter(value)); return Add(fn); } public int Add(FilterFunction function) { _functions.Add(function); Push(); return _functions.Count - 1; } /// Parses one studio filter spec, for example `blur` with args `["8px"]`. public bool TryAdd(string name, IReadOnlyList args) { if (!TryParseType(name, out var type)) return false; var fn = new FilterFunction(type); var text = args != null && args.Count > 0 ? args[0] : null; if (type == FilterFunctionType.Tint) { if (!UgxValueParser.TryColor(text, out var color)) return false; fn.AddParameter(new FilterParameter(color)); } else { if (!TryParseAmount(type, text, out var value)) return false; fn.AddParameter(new FilterParameter(value)); } Add(fn); return true; } public void SetFloat(int index, float value) { if (index < 0 || index >= _functions.Count) return; var fn = _functions[index]; if (fn.parameterCount == 0) fn.AddParameter(new FilterParameter(value)); else fn.SetParameter(0, new FilterParameter(value)); _functions[index] = fn; Push(); } public void SetColor(int index, Color value) { if (index < 0 || index >= _functions.Count) return; var fn = _functions[index]; if (fn.parameterCount == 0) fn.AddParameter(new FilterParameter(value)); else fn.SetParameter(0, new FilterParameter(value)); _functions[index] = fn; Push(); } /// /// Animates one float parameter. USS transitions can do this too, but only when the whole /// filter list matches in type and order; this path works for a single parameter. /// public void AnimateFloat(int index, float target, float durationMs, UgxEasingCurve easing = default) { if (index < 0 || index >= _functions.Count) return; if (durationMs <= 0f) { SetFloat(index, target); return; } float from = _functions[index].parameterCount > 0 ? _functions[index].GetParameter(0).floatValue : 0f; for (int i = _tweens.Count - 1; i >= 0; i--) if (_tweens[i].Index == index) _tweens.RemoveAt(i); _tweens.Add(new Tween(index, from, target, durationMs, easing)); EnsureTicker(); } public void Clear() { _tweens.Clear(); _functions.Clear(); // Null, not None: None is an inline `filter: none` that would shadow the stylesheet. _element.style.filter = StyleKeyword.Null; _ticker?.Pause(); } public void Dispose() { if (_disposed) return; _disposed = true; Clear(); _ticker = null; } /// Advances the tweens. Exposed so tests can step deterministically. public void Tick(float deltaMs) { if (_tweens.Count == 0) return; for (int i = _tweens.Count - 1; i >= 0; i--) { var tween = _tweens[i]; bool done = tween.Advance(deltaMs, out float value); _tweens[i] = tween; ApplyFloat(tween.Index, value); if (done) _tweens.RemoveAt(i); } Push(); if (_tweens.Count == 0) _ticker?.Pause(); } void ApplyFloat(int index, float value) { if (index < 0 || index >= _functions.Count) return; var fn = _functions[index]; if (fn.parameterCount == 0) fn.AddParameter(new FilterParameter(value)); else fn.SetParameter(0, new FilterParameter(value)); _functions[index] = fn; } void EnsureTicker() { if (_ticker == null) { _ticker = _element.schedule.Execute(OnTick).Every(TickIntervalMs); return; } if (!_ticker.isActive) _ticker.Resume(); } void OnTick(TimerState state) => Tick(state.deltaTime); void Push() => _element.style.filter = new StyleList(_functions); public static bool TryParseType(string name, out FilterFunctionType type) { switch (name) { case "blur": type = FilterFunctionType.Blur; return true; case "grayscale": type = FilterFunctionType.Grayscale; return true; case "invert": type = FilterFunctionType.Invert; return true; case "opacity": type = FilterFunctionType.Opacity; return true; case "sepia": type = FilterFunctionType.Sepia; return true; case "tint": type = FilterFunctionType.Tint; return true; case "hue-rotate": type = FilterFunctionType.HueRotate; return true; case "contrast": type = FilterFunctionType.Contrast; return true; default: type = FilterFunctionType.None; return false; } } static bool TryParseAmount(FilterFunctionType type, string text, out float value) { if (string.IsNullOrEmpty(text)) { value = 0f; return false; } if (type == FilterFunctionType.Blur) return UgxValueParser.TryLength(text, out value, out _); if (type == FilterFunctionType.HueRotate) return UgxValueParser.TryAngle(text, out value, out _); var s = text.Trim(); if (s.EndsWith("%", StringComparison.Ordinal)) { if (!UgxValueParser.TryFloat(s.Substring(0, s.Length - 1), out value)) return false; value /= 100f; return true; } return UgxValueParser.TryFloat(s, out value); } struct Tween { public readonly int Index; readonly float _from; readonly float _to; readonly float _durationMs; readonly UgxEasingCurve _easing; float _elapsedMs; public Tween(int index, float from, float to, float durationMs, UgxEasingCurve easing) { Index = index; _from = from; _to = to; _durationMs = durationMs; _easing = easing; _elapsedMs = 0f; } /// Returns true when the tween reached its target. public bool Advance(float deltaMs, out float value) { _elapsedMs += deltaMs; float t = Mathf.Clamp01(_elapsedMs / _durationMs); value = Mathf.LerpUnclamped(_from, _to, _easing.Evaluate(t)); return t >= 1f; } } } }