oni-priority-ux/mod/PLibOptions/ColorGradient.cs

228 lines
6.4 KiB
C#

/*
* Copyright 2022 Peter Han
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using UnityEngine;
using UnityEngine.UI;
namespace PeterHan.PLib.Options {
/// <summary>
/// A background image which displays a gradient between two different colors using HSV
/// interpolation.
/// </summary>
internal sealed class ColorGradient : Image {
/// <summary>
/// The position to use on the track.
/// </summary>
public float Position {
get => position;
set {
position = Mathf.Clamp01(value);
SetPosition();
}
}
/// <summary>
/// The currently selected color.
/// </summary>
public Color SelectedColor {
get => current;
set {
current = value;
EstimatePosition();
}
}
/// <summary>
/// The currently selected color.
/// </summary>
private Color current;
/// <summary>
/// Whether the image texture needs to be regenerated.
/// </summary>
private bool dirty;
/// <summary>
/// Gradient unfortunately always uses RGB. Run curves manually using HSV.
/// </summary>
private Vector2 hue;
/// <summary>
/// The position to use on the track.
/// </summary>
private float position;
/// <summary>
/// The texture used when drawing the background.
/// </summary>
private Texture2D preview;
private Vector2 sat;
private Vector2 val;
public ColorGradient() {
current = Color.black;
position = 0.0f;
preview = null;
dirty = true;
hue = Vector2.right;
sat = Vector2.right;
val = Vector2.right;
}
/// <summary>
/// Estimates a position based on the selected color.
/// </summary>
internal void EstimatePosition() {
Color.RGBToHSV(current, out float h, out float s, out float v);
float newPos = 0.0f, divider = 0.0f;
if (!Mathf.Approximately(hue.x, hue.y)) {
newPos += Mathf.Clamp01(Mathf.InverseLerp(hue.x, hue.y, h));
divider += 1.0f;
}
if (!Mathf.Approximately(sat.x, sat.y)) {
newPos += Mathf.Clamp01(Mathf.InverseLerp(sat.x, sat.y, s));
divider += 1.0f;
}
if (!Mathf.Approximately(val.x, val.y)) {
newPos += Mathf.Clamp01(Mathf.InverseLerp(val.x, val.y, v));
divider += 1.0f;
}
if (divider > 0.0f) {
position = newPos / divider;
SetPosition();
}
}
/// <summary>
/// Cleans up the preview when the component is disposed.
/// </summary>
protected override void OnDestroy() {
if (preview != null) {
Destroy(preview);
preview = null;
}
if (sprite != null) {
Destroy(sprite);
sprite = null;
}
base.OnDestroy();
}
/// <summary>
/// Called when the image needs to be resized.
/// </summary>
protected override void OnRectTransformDimensionsChange() {
base.OnRectTransformDimensionsChange();
dirty = true;
}
/// <summary>
/// Updates the currently selected color with the interpolated value based on the
/// current position.
/// </summary>
internal void SetPosition() {
float h = Mathf.Clamp01(Mathf.Lerp(hue.x, hue.y, position));
float s = Mathf.Clamp01(Mathf.Lerp(sat.x, sat.y, position));
float v = Mathf.Clamp01(Mathf.Lerp(val.x, val.y, position));
current = Color.HSVToRGB(h, s, v);
}
/// <summary>
/// Sets the range of colors to be displayed.
/// </summary>
/// <param name="hMin">The minimum hue.</param>
/// <param name="hMax">The maximum hue.</param>
/// <param name="sMin">The minimum saturation.</param>
/// <param name="sMax">The maximum saturation.</param>
/// <param name="vMin">The minimum value.</param>
/// <param name="vMax">The maximum value.</param>
public void SetRange(float hMin, float hMax, float sMin, float sMax, float vMin,
float vMax) {
// Ensure correct order
if (hMin > hMax) {
hue.x = hMax; hue.y = hMin;
} else {
hue.x = hMin; hue.y = hMax;
}
if (sMin > sMax) {
sat.x = sMax; sat.y = sMin;
} else {
sat.x = sMin; sat.y = sMax;
}
if (vMin > vMax) {
val.x = vMax; val.y = vMin;
} else {
val.x = vMin; val.y = vMax;
}
EstimatePosition();
dirty = true;
}
/// <summary>
/// Called by Unity when the component is created.
/// </summary>
protected override void Start() {
base.Start();
dirty = true;
}
/// <summary>
/// Regenerates the color gradient image if necessary.
/// </summary>
internal void Update() {
if (dirty || preview == null || sprite == null) {
var rt = rectTransform.rect;
int w = Mathf.RoundToInt(rt.width), h = Mathf.RoundToInt(rt.height);
if (w > 0 && h > 0) {
var newData = new Color[w * h];
float hMin = hue.x, hMax = hue.y, sMin = sat.x, sMax = sat.y, vMin = val.x,
vMax = val.y;
var oldSprite = sprite;
bool rebuild = preview == null || oldSprite == null || preview.width !=
w || preview.height != h;
if (rebuild) {
if (preview != null)
Destroy(preview);
if (oldSprite != null)
Destroy(oldSprite);
preview = new Texture2D(w, h);
}
// Generate one row of colors
for (int i = 0; i < w; i++) {
float t = (float)i / w;
newData[i] = Color.HSVToRGB(Mathf.Lerp(hMin, hMax, t), Mathf.Lerp(sMin,
sMax, t), Mathf.Lerp(vMin, vMax, t));
}
// Native copy row to all others
for (int i = 1; i < h; i++)
Array.Copy(newData, 0, newData, i * w, w);
preview.SetPixels(newData);
preview.Apply();
if (rebuild)
sprite = Sprite.Create(preview, new Rect(0.0f, 0.0f, w, h),
new Vector2(0.5f, 0.5f));
}
dirty = false;
}
}
}
}