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;
}
}
}
}