/* * 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 { /// /// A background image which displays a gradient between two different colors using HSV /// interpolation. /// internal sealed class ColorGradient : Image { /// /// The position to use on the track. /// public float Position { get => position; set { position = Mathf.Clamp01(value); SetPosition(); } } /// /// The currently selected color. /// public Color SelectedColor { get => current; set { current = value; EstimatePosition(); } } /// /// The currently selected color. /// private Color current; /// /// Whether the image texture needs to be regenerated. /// private bool dirty; /// /// Gradient unfortunately always uses RGB. Run curves manually using HSV. /// private Vector2 hue; /// /// The position to use on the track. /// private float position; /// /// The texture used when drawing the background. /// 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; } /// /// Estimates a position based on the selected color. /// 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(); } } /// /// Cleans up the preview when the component is disposed. /// protected override void OnDestroy() { if (preview != null) { Destroy(preview); preview = null; } if (sprite != null) { Destroy(sprite); sprite = null; } base.OnDestroy(); } /// /// Called when the image needs to be resized. /// protected override void OnRectTransformDimensionsChange() { base.OnRectTransformDimensionsChange(); dirty = true; } /// /// Updates the currently selected color with the interpolated value based on the /// current position. /// 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); } /// /// Sets the range of colors to be displayed. /// /// The minimum hue. /// The maximum hue. /// The minimum saturation. /// The maximum saturation. /// The minimum value. /// The maximum value. 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; } /// /// Called by Unity when the component is created. /// protected override void Start() { base.Start(); dirty = true; } /// /// Regenerates the color gradient image if necessary. /// 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; } } } }