OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / IFSEngine / Model / ColorPalette.cs

126 lines · csharp ref:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;

namespace IFSEngine.Model;

public class ColorPalette
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string Name { get; set; } = "Unnamed palette";
    /// <summary>
    /// Background color associated with this palette (RGB). 
    /// <c>null</c> means fully transparent background.
    /// </summary>
    public Vector3? BackgroundColor { get; set; } = null;
    public SortedList<double, Vector4> KeyColors { get; set; } = [];
    public InterpolationMode InterpolationMode { get; set; } = InterpolationMode.LinearRGB;
    /// <summary>
    /// Author information, using the existing <see cref="Author"/> class.
    /// </summary>
    public Author Author { get; set; }
    public List<string> Tags { get; set; } = [];

    public static ColorPalette Default { get; } = new ColorPalette
    {
        Name = "Default palette",
        BackgroundColor = new Vector3(0, 0, 0),
        InterpolationMode = InterpolationMode.LinearRGB,
        KeyColors =
        {
            [0.0] = new Vector4(1, 1, 1, 1),
            [1.0] = new Vector4(1, 1, 1, 1)
        }
    };

    /// <summary>
    /// Sample the gradient at the given position (0.0–1.0) using the palette's interpolation mode.
    /// </summary>
    public Vector4 SampleGradient(double position)
    {
        if (KeyColors.Count == 0)
            return new Vector4(BackgroundColor ?? Vector3.Zero, 1.0f);

        if (position <= KeyColors.Keys[0])
            return KeyColors.Values[0];
        if (position >= KeyColors.Keys[^1])
            return KeyColors.Values[^1];

        int i = Array.BinarySearch(KeyColors.Keys.ToArray(), position);
        if (i < 0)
            i = ~i; // no exact match -> get the closest match using bitwise complement

        var pos1 = KeyColors.Keys[i - 1];
        var pos2 = KeyColors.Keys[i];
        var t = (float)((position - pos1) / (pos2 - pos1));

        var c1 = KeyColors.Values[i - 1];
        var c2 = KeyColors.Values[i];

        return InterpolationMode switch
        {
            InterpolationMode.LinearRGB => Vector4.Lerp(c1, c2, t),
            InterpolationMode.Srgb => SrgbLerp(c1, c2, t),
            InterpolationMode.Mixbox => MixboxLerp(c1, c2, t),
            _ => Vector4.Lerp(c1, c2, t)
        };
    }

    /// <summary>
    /// Decode sRGB to linear, interpolate, re-encode to sRGB.
    /// </summary>
    private static Vector4 SrgbLerp(Vector4 c1, Vector4 c2, float t)
    {
        var l1 = SrgbToLinear(c1);
        var l2 = SrgbToLinear(c2);
        var lerp = Vector4.Lerp(l1, l2, t);
        return LinearToSrgb(lerp);
    }

    /// <summary>
    /// sRGB to linear (piecewise approximation of the sRGB gamma curve).
    /// </summary>
    private static Vector4 SrgbToLinear(Vector4 c)
    {
        return new Vector4(
            SrgbChannelToLinear(c.X),
            SrgbChannelToLinear(c.Y),
            SrgbChannelToLinear(c.Z),
            c.W);
    }

    private static float SrgbChannelToLinear(float c)
    {
        return c <= 0.04045f ? c / 12.92f : (float)Math.Pow((c + 0.055f) / 1.055f, 2.4);
    }

    /// <summary>
    /// Linear to sRGB.
    /// </summary>
    private static Vector4 LinearToSrgb(Vector4 c)
    {
        return new Vector4(
            LinearChannelToSrgb(c.X),
            LinearChannelToSrgb(c.Y),
            LinearChannelToSrgb(c.Z),
            c.W);
    }

    private static float LinearChannelToSrgb(float c)
    {
        return c <= 0.0031308f ? c * 12.92f : 1.055f * (float)Math.Pow(c, 1.0 / 2.4) - 0.055f;
    }

    /// <summary>
    /// Pigment-based mixing using the Mixbox library.
    /// </summary>
    private static Vector4 MixboxLerp(Vector4 c1, Vector4 c2, float t)
    {
        var src = new[] { c1.X, c1.Y, c1.Z, c1.W };
        var dst = new[] { c2.X, c2.Y, c2.Z, c2.W };
        Scrtwpns.Mixbox.Mixbox.LerpFloat(src, dst, t);
        return new Vector4(src[0], src[1], src[2], 1.0f);
    }
}