//--------------------------------------------------------------------------//
// Copyright 2023-2025 Chocolate Dinosaur Ltd. All rights reserved. //
// For full documentation visit https://www.chocolatedinosaur.com //
//--------------------------------------------------------------------------//
using UnityEngine;
using UnityInternal = UnityEngine.Internal;
namespace ChocDino.UIFX
{
public enum GradientWrap
{
Clamp,
Repeat,
Mirror,
}
public enum GradientLerp
{
Step,
Linear,
Smooth,
}
public enum GradientColorSpace
{
Linear,
Perceptual,
}
public enum GradientShape
{
None,
Horizontal,
Vertical,
Diagonal,
Linear,
Radial,
Conic,
}
[UnityInternal.ExcludeFromDocs]
internal class GradientTexture : System.IDisposable
{
private static Color s_color = Color.black;
private int _resolution = 256;
private Texture2D _texture;
public Texture2D Texture { get { return _texture; } }
public GradientTexture(int resolution)
{
_resolution = resolution;
}
public void Update(Gradient gradient)
{
if (_texture == null)
{
_texture = new Texture2D(_resolution, 1, TextureFormat.RGBAHalf, false);
_texture.filterMode = FilterMode.Bilinear;
_texture.wrapMode = TextureWrapMode.Clamp;
}
float inv = 1f / (_texture.width - 1);
for (int x = 0; x < _texture.width; x++)
{
var t = x * inv;
// TODO: convert to premultiplied and linear here?
_texture.SetPixel(x, 0, gradient.Evaluate(t).linear);
}
_texture.Apply(false, false);
}
public void Update(AnimationCurve curve)
{
if (_texture == null)
{
_texture = new Texture2D(_resolution, 1, TextureFormat.RHalf, false);
_texture.filterMode = FilterMode.Bilinear;
_texture.wrapMode = TextureWrapMode.Clamp;
}
float inv = 1f / (_texture.width - 1);
for (int x = 0; x < _texture.width; x++)
{
var t = x * inv;
s_color.r = curve.Evaluate(t);
_texture.SetPixel(x, 0, s_color);
}
_texture.Apply(false, false);
}
public void Dispose()
{
ObjectHelper.Destroy(ref _texture);
}
}
#if false
public enum GradientWrap
{
Clamp,
Repeat,
Mirror,
}
public enum GradientMix
{
Step,
Linear,
Smooth,
}
public enum GradientColorSpace
{
sRGB,
Linear,
Perceptual,
}
#endif
[UnityInternal.ExcludeFromDocs]
public static class GradientUtils
{
#if false
public static Color EvalGradient(float t, Gradient gradient, GradientWrapMode wrapMode, float offset = 0f, float scale = 1f, float scalePivot = 0f)
{
t -= scalePivot;
t *= scale;
t += scalePivot;
t += offset;
if (wrapMode == GradientWrapMode.Wrap)
{
// NOTE: Only wrap if we're outside of the range, otherwise for t=1.0 (which happens often) we'll evaulate 0.0 which in most cases is not what we want
if (t < 0f || t > 1f)
{
t = Mathf.Repeat(t, 1f);
}
}
else if (wrapMode == GradientWrapMode.Mirror)
{
t = Mathf.PingPong(t, 1f);
if (Mathf.Sign(scale) < 0f)
{
t = 1f - t;
}
}
return gradient.Evaluate(t);
}
#endif
///
/// Reverse the gradient
///
public static void Reverse(Gradient gradient)
{
GradientColorKey[] colors = gradient.colorKeys;
GradientAlphaKey[] alphas = gradient.alphaKeys;
for (int i = 0; i < colors.Length; i++)
{
colors[i].time = 1f - colors[i].time;
}
for (int i = 0; i < alphas.Length; i++)
{
alphas[i].time = 1f - alphas[i].time;
}
gradient.SetKeys(colors, alphas);
}
///
/// CSS linear gradients always have the start and end colors at one of the edges/corners.
/// This method calculates the parameters for our shader.
///
public static void GetCssLinearGradientShaderParams(float angle, Rect rect, out Vector2 uvPointOnStartLine, out Vector2 uvStartLineDirection, out float uvGradientLength, out float uvRectRatio)
{
// Definitions:
// Gradient angle 0..360 degrees clock-wise where 0 is upwards.
// Gradient line goes from the center of the rectangle in the direction of the angle.
// Method:
// The gradient line runs in the direction of the angle.
// The gradient goes from a Start line to an End line. These lines are parallel and are perpendicular to the Gradient line.
// From the angle, we pick the closest opposite quadrant corner on the rectangle - this corner point will be a point on the Start line.
// We get the direction of the start line, which is just 90 degree rotation of the gradient direction.
// In the shader it does a dot product to find the closest point on the Start line from the uv coordinate - this distance is used to draw the gradient.
// Get the coordinates of the closest quadrant corner in the opposite direction of our gradient
// this point lies on the starting line
Vector2 startingLineCorner = rect.min;
{
int quadrant = Mathf.FloorToInt((angle % 360f) / 90f);
switch (quadrant)
{
case 0:
break;
case 1:
startingLineCorner = new Vector2(rect.xMin, rect.yMax);
break;
case 2:
startingLineCorner = rect.max;
break;
case 3:
startingLineCorner = new Vector2(rect.xMax, rect.yMin);
break;
default:
// Should never get here
Debug.LogError("Invalid quadarant");
break;
}
}
float angleRad = Mathf.Deg2Rad * angle;
uvRectRatio = rect.width / rect.height;
// Calculate distance between Start and End line in UV-space
{
float uvWidth = uvRectRatio;
float uvHeight = 1f;
Vector2 gradientDirection = new Vector2(Mathf.Sin(angleRad), Mathf.Cos(angleRad));
uvGradientLength = Mathf.Abs(uvWidth * gradientDirection.x) + Mathf.Abs(uvHeight * gradientDirection.y);
}
// Convert pointOnStartLine to UV-space
uvPointOnStartLine.x = (startingLineCorner.x - rect.x) / rect.width;
uvPointOnStartLine.y = (startingLineCorner.y - rect.y) / rect.height;
uvPointOnStartLine.x *= uvRectRatio;
// Calculate direction of the start line
uvStartLineDirection = new Vector2(Mathf.Sin(angleRad+Mathf.PI * 0.5f), Mathf.Cos(angleRad+Mathf.PI * 0.5f));
}
}
#if false
[System.Serializable]
internal class GradientShader
{
internal static class ShaderProp
{
public readonly static int GradientColorCount = Shader.PropertyToID("_GradientColorCount");
public readonly static int GradientAlphaCount = Shader.PropertyToID("_GradientAlphaCount");
public readonly static int GradientColors = Shader.PropertyToID("_GradientColors");
public readonly static int GradientAlphas = Shader.PropertyToID("_GradientAlphas");
public readonly static int GradientTransform = Shader.PropertyToID("_GradientTransform");
public readonly static int GradientRadial = Shader.PropertyToID("_GradientRadial");
public readonly static int GradientDither = Shader.PropertyToID("_GradientDither");
}
internal static class ShaderKeyword
{
public const string GradientMixSmooth = "GRADIENT_MIX_SMOOTH";
public const string GradientMixLinear = "GRADIENT_MIX_LINEAR";
public const string GradientMixStep = "GRADIENT_MIX_STEP";
internal const string GradientColorSpaceSRGB = "GRADIENT_COLORSPACE_SRGB";
internal const string GradientColorSpaceLinear = "GRADIENT_COLORSPACE_LINEAR";
internal const string GradientColorSpacePerceptual = "GRADIENT_COLORSPACE_PERCEPTUAL";
}
[SerializeField] Gradient _gradient;
[SerializeField] GradientMix _mixMode = GradientMix.Smooth;
[SerializeField] GradientColorSpace _colorSpace = GradientColorSpace.Perceptual;
[Range(0f, 1f)]
[SerializeField] float _dither = 0.5f;
/*[Range(-1f, 1f)]
[SerializeField] float _centerX = 0f;
[Range(-1f, 1f)]
[SerializeField] float _centerY = 0f;
[Range(0f, 16f)]
[SerializeField] float _radius = 0.5f;*/
[SerializeField] float _scale = 1f;
[Range(0f, 1f)]
[SerializeField] float _scalePivot = 0.5f;
[SerializeField] float _offset = 0f;
[SerializeField] GradientWrap _wrapMode = GradientWrap.Clamp;
//public float GradientCenterX { get { return _gradientCenterX; } set { _gradientCenterX = value; ForceUpdate(); } }
//public float GradientCenterY { get { return _gradientCenterY; } set { _gradientCenterY = value; ForceUpdate(); } }
//public float GradientRadius { get { return _gradientRadius; } set { _gradientRadius = value; ForceUpdate(); } }
//public Gradient Gradient { get { return _gradient; } set { _gradient = value; ForceUpdate(); } }
private Vector4[] _colorKeys = new Vector4[8];
private Vector4[] _alphaKeys = new Vector4[8];
private void GradientToArrays()
{
int colorKeyCount = _gradient.colorKeys.Length;
for (int i = 0; i < colorKeyCount; i++)
{
Color c = _gradient.colorKeys[i].color;
switch (_colorSpace)
{
default:
case GradientColorSpace.sRGB:
_colorKeys[i] = new Vector4(c.r, c.g, c.b, _gradient.colorKeys[i].time);
break;
case GradientColorSpace.Linear:
c = c.linear;
_colorKeys[i] = new Vector4(c.r, c.g, c.b, _gradient.colorKeys[i].time);
break;
case GradientColorSpace.Perceptual:
{
Vector3 oklab = ColorUtils.LinearToOklab(c.linear);
_colorKeys[i] = new Vector4(oklab.x, oklab.y, oklab.z, _gradient.colorKeys[i].time);
}
break;
}
}
int alphaKeyCount = _gradient.alphaKeys.Length;
for (int i = 0; i < alphaKeyCount; i++)
{
_alphaKeys[i] = new Vector4(_gradient.alphaKeys[i].alpha, 0f, 0f, _gradient.alphaKeys[i].time);
}
}
internal void SetupMaterial(Material material)
{
if (_gradient == null ) { return; }
GradientToArrays();
material.SetInt(ShaderProp.GradientColorCount, _gradient.colorKeys.Length);
material.SetInt(ShaderProp.GradientAlphaCount, _gradient.alphaKeys.Length);
material.SetVectorArray(ShaderProp.GradientColors, _colorKeys);
material.SetVectorArray(ShaderProp.GradientAlphas, _alphaKeys);
material.SetVector(ShaderProp.GradientTransform, new Vector4(_scale, _scalePivot, _offset, (float)_wrapMode));
//material.SetVector(ShaderProp.GradientRadial, new Vector4(_centerX, _centerY, _radius, 0f));
//material.SetFloat(ShaderProp.GradientDither, Mathf.Lerp(0f, 0.05f, _dither));
// Mixing mode
switch (_mixMode)
{
default:
case GradientMix.Smooth:
material.DisableKeyword(ShaderKeyword.GradientMixLinear);
material.DisableKeyword(ShaderKeyword.GradientMixStep);
material.EnableKeyword(ShaderKeyword.GradientMixSmooth);
break;
case GradientMix.Linear:
material.DisableKeyword(ShaderKeyword.GradientMixStep);
material.DisableKeyword(ShaderKeyword.GradientMixSmooth);
material.EnableKeyword(ShaderKeyword.GradientMixLinear);
break;
case GradientMix.Step:
material.DisableKeyword(ShaderKeyword.GradientMixSmooth);
material.DisableKeyword(ShaderKeyword.GradientMixLinear);
material.EnableKeyword(ShaderKeyword.GradientMixStep);
break;
}
// Mixing color space
switch (_colorSpace)
{
default:
case GradientColorSpace.sRGB:
material.DisableKeyword(ShaderKeyword.GradientColorSpaceLinear);
material.DisableKeyword(ShaderKeyword.GradientColorSpacePerceptual);
material.EnableKeyword(ShaderKeyword.GradientColorSpaceSRGB);
break;
case GradientColorSpace.Linear:
material.DisableKeyword(ShaderKeyword.GradientColorSpaceSRGB);
material.DisableKeyword(ShaderKeyword.GradientColorSpacePerceptual);
material.EnableKeyword(ShaderKeyword.GradientColorSpaceLinear);
break;
case GradientColorSpace.Perceptual:
material.DisableKeyword(ShaderKeyword.GradientColorSpaceSRGB);
material.DisableKeyword(ShaderKeyword.GradientColorSpaceLinear);
material.EnableKeyword(ShaderKeyword.GradientColorSpacePerceptual);
break;
}
}
}
#endif
}