//********************************** Banshee Engine (www.banshee4d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using System; using System.Collections.Generic; using bs; namespace bs.Editor { /** @addtogroup Windows * @{ */ /// /// A color gradient editor window that allows the user to add or modify colors from a color gradient. /// public class GradientPicker : ModalWindow { private const int EDITOR_HORZ_PADDING = 10; private const int TEX_WIDTH = 256; private const int TEX_HEIGHT = 4; private ColorGradient gradient; private GradientKeyEditor editor; private GUITexture guiGradientTexture; private GUICanvas overlayCanvas; private GUIPanel editorPanel; private GUIButton guiOK; private GUIButton guiCancel; private Texture texture; private SpriteTexture spriteTexture; private Action closedCallback; /// /// Shows the gradient picker window. /// /// Optional callback to trigger when the user finishes editing the gradient or /// cancels out of the dialog. /// An instance of the gradient picker window. public static GradientPicker Show(Action closedCallback = null) { GradientPicker picker = new GradientPicker(new ColorGradient(), closedCallback); return picker; } /// /// Shows the gradient picker window and sets the initial gradient to show. /// /// Gradient to initially display in the window. /// Optional callback to trigger when the user finishes editing the gradient or /// cancels out of the dialog. /// A. instance of the gradient picker window. public static GradientPicker Show(ColorGradient gradient, Action closedCallback = null) { GradientPicker picker = new GradientPicker(gradient, closedCallback); return picker; } /// /// Constructs a new gradient picker window. /// /// Gradient to initially display in the window. /// Optional callback to trigger when the user finishes editing the gradient or /// cancels out of the dialog. protected GradientPicker(ColorGradient gradient, Action closedCallback = null) : base(false) { Title = new LocEdString("Gradient Picker"); Width = 270; Height = 135; this.gradient = gradient; this.closedCallback = closedCallback; } private void OnInitialize() { guiOK = new GUIButton(new LocEdString("OK")); guiCancel = new GUIButton(new LocEdString("Cancel")); guiOK.OnClick += OnOK; guiCancel.OnClick += OnCancel; GUILayout mainVertLayout = GUI.AddLayoutY(); mainVertLayout.AddSpace(10); GUILayout editorHorzLayout = mainVertLayout.AddLayoutX(); editorHorzLayout.AddSpace(EDITOR_HORZ_PADDING); GUIPanel gradientEditorPanel = editorHorzLayout.AddPanel(); editorHorzLayout.AddSpace(EDITOR_HORZ_PADDING); mainVertLayout.AddSpace(15); GUILayout buttonHorzLayout = mainVertLayout.AddLayoutX(); buttonHorzLayout.AddFlexibleSpace(); buttonHorzLayout.AddElement(guiOK); buttonHorzLayout.AddSpace(10); buttonHorzLayout.AddElement(guiCancel); buttonHorzLayout.AddFlexibleSpace(); mainVertLayout.AddFlexibleSpace(); editorPanel = gradientEditorPanel.AddPanel(0); GUIPanel editorOverlay = gradientEditorPanel.AddPanel(-1); overlayCanvas = new GUICanvas(); editorOverlay.AddElement(overlayCanvas); GUILayout editorVertLayout = editorPanel.AddLayoutY(); GUILayout guiGradientLayout = editorVertLayout.AddLayoutX(); guiGradientLayout.AddSpace(GradientKeyEditor.RECT_WIDTH / 2); texture = Texture.Create2D(TEX_WIDTH, TEX_HEIGHT); spriteTexture = new SpriteTexture(texture); guiGradientTexture = new GUITexture(spriteTexture, GUITextureScaleMode.StretchToFit); guiGradientTexture.SetHeight(30); UpdateTexture(); guiGradientLayout.AddElement(guiGradientTexture); guiGradientLayout.AddSpace(GradientKeyEditor.RECT_WIDTH / 2); editorVertLayout.AddSpace(10); editor = new GradientKeyEditor(editorVertLayout, gradient.GetKeys(), Width - EDITOR_HORZ_PADDING * 2, 20); editor.OnGradientModified += colorGradient => { gradient = colorGradient; UpdateTexture(); UpdateKeyLines(); }; editorVertLayout.AddFlexibleSpace(); GUITexture containerBg = new GUITexture(null, EditorStylesInternal.ContainerBg); Rect2I containerBounds = editor.GetBounds(GUI); containerBounds.x -= 2; containerBounds.y -= 2; containerBounds.width += 4; containerBounds.height += 6; containerBg.Bounds = containerBounds; GUIPanel editorUnderlay = GUI.AddPanel(1); editorUnderlay.AddElement(containerBg); UpdateKeyLines(); EditorInput.OnPointerPressed += OnPointerPressed; EditorInput.OnPointerDoubleClick += OnPointerDoubleClicked; EditorInput.OnPointerMoved += OnPointerMoved; EditorInput.OnPointerReleased += OnPointerReleased; EditorInput.OnButtonUp += OnButtonUp; } private void OnDestroy() { EditorInput.OnPointerPressed -= OnPointerPressed; EditorInput.OnPointerDoubleClick -= OnPointerDoubleClicked; EditorInput.OnPointerMoved -= OnPointerMoved; EditorInput.OnPointerReleased -= OnPointerReleased; EditorInput.OnButtonUp -= OnButtonUp; } /// /// Draws lines connecting the individual key color over the gradient itself. /// private void UpdateKeyLines() { overlayCanvas.Clear(); ColorGradientKey[] keys = gradient.GetKeys(); for (int i = 0; i < keys.Length; i++) { int pixel = editor.TimeToPixel(keys[i].time); overlayCanvas.DrawLine(new Vector2I(pixel, 0), new Vector2I(pixel, 40), Color.DarkGray); } } /// /// Updates the gradient texture. /// public void UpdateTexture() { const int width = 256; const int height = 4; Color[] colors = new Color[width * height]; float halfPixel = 0.5f / width; if (gradient.NumKeys > 0) { for (int x = 0; x < width; x++) { Color color = gradient.Evaluate(x / (float) width + halfPixel); for (int y = 0; y < height; y++) colors[y * width + x] = color; } } else { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) colors[y * width + x] = Color.Black; } } texture.SetPixels(colors); guiGradientTexture.SetTexture(spriteTexture); } /// /// Triggered when the user selects a gradient and closes the dialog. /// void OnOK() { closedCallback?.Invoke(true, gradient); Close(); } /// /// Triggered when the user cancels gradient editing and closes the dialog. /// void OnCancel() { closedCallback?.Invoke(false, gradient); Close(); } #region Input callbacks /// /// Triggered when the user presses a mouse button. /// /// Information about the mouse press event. private void OnPointerPressed(PointerEvent ev) { if (ev.IsUsed) return; editor.OnPointerPressed(ScreenToKeyEditorPos(ev.ScreenPos), ev.Button); } /// /// Triggered when the user double clicks the left mouse button. /// /// Information about the mouse event. private void OnPointerDoubleClicked(PointerEvent ev) { if (ev.IsUsed) return; Vector2I panelPos = ScreenToKeyEditorPos(ev.ScreenPos); Rect2I guiGradientBounds = guiGradientTexture.Bounds; if (guiGradientBounds.Contains(panelPos)) { Vector2I canvasPos = panelPos - new Vector2I(guiGradientBounds.x, guiGradientBounds.y); float time = canvasPos.x / (float) Math.Max(guiGradientBounds.width - 1, 1); if (time >= 0.0f && time <= 1.0f) { List keys = new List(gradient.GetKeys()); keys.Add(new ColorGradientKey(Color.Black, time)); keys.Sort((lhs, rhs) => lhs.time.CompareTo(rhs.time)); gradient = new ColorGradient(keys.ToArray()); UpdateTexture(); editor.Rebuild(keys); UpdateKeyLines(); } } else editor.OnPointerDoubleClicked(panelPos, ev.Button); } /// /// Triggered when the user moves the mouse. /// /// Information about the mouse move event. private void OnPointerMoved(PointerEvent ev) { if (ev.IsUsed) return; editor.OnPointerMoved(ScreenToKeyEditorPos(ev.ScreenPos), ev.Button); } /// /// Triggered when the user releases a mouse button. /// /// Information about the mouse release event. private void OnPointerReleased(PointerEvent ev) { if (ev.IsUsed) return; editor.OnPointerReleased(ScreenToKeyEditorPos(ev.ScreenPos)); } /// /// Triggered when the user releases a keyboard button. /// /// Information about the keyboard release event. private void OnButtonUp(ButtonEvent ev) { editor.OnButtonUp(ev); } /// /// Converts screen coordinates in coordinates relative to the panel containing the key editor control. /// /// Coordinates in screen space. /// Coordinates relative to the key editor control. private Vector2I ScreenToKeyEditorPos(Vector2I screenPos) { Vector2I windowPos = ScreenToWindowPos(screenPos); Rect2I elementBounds = GUIUtility.CalculateBounds(editorPanel, GUI); return windowPos - new Vector2I(elementBounds.x, elementBounds.y); } #endregion /// /// Handles drawing and editing of individual keys present on the color gradient. /// public class GradientKeyEditor { public const int RECT_WIDTH = 14; private static readonly Color SELECTED_COLOR = Color.BansheeOrange; private static readonly Color PLAIN_COLOR = Color.DarkGray; private const int DRAG_START_DISTANCE = 3; private GUICanvas canvas; private List keys = new List(); private int width; private int height; private int selectedIdx = -1; private bool isMousePressedOverKey; private bool isDragInProgress; private Vector2I dragStart; /// /// Triggered whenever keyframe in a gradient is modified (added, removed or edited). /// public Action OnGradientModified; /// /// Constructs a new gradient key editor control. /// /// GUI layout to attach the child GUI controls to. /// Set of keys to initially display on the editor. /// Width of the editor in pixels. /// Height of the editor in pixels. public GradientKeyEditor(GUILayout parent, ColorGradientKey[] keys, int width, int height) { canvas = new GUICanvas(); parent.AddElement(canvas); Rebuild(new List(keys), width, height); } /// /// Rebuilds the editor display using the provided color keys. /// /// Set of keys to initially display on the editor. public void Rebuild(List keys) { this.keys = keys; canvas.Clear(); for (int i = 0; i < keys.Count; i++) DrawColor(keys[i].color, MathEx.Clamp01(keys[i].time), i == selectedIdx); } /// /// Returns the bounds of the GUI element relative to the provided panel. /// public Rect2I GetBounds(GUIPanel relativeTo) { return GUIUtility.CalculateBounds(canvas, relativeTo); } /// /// Rebuilds the editor display using the provided size and color keys. /// /// Set of keys to initially display on the editor. /// Width of the editor in pixels. /// Height of the editor in pixels. private void Rebuild(List keys, int width, int height) { this.width = width; this.height = height; canvas.SetWidth(width); canvas.SetHeight(height); Rebuild(keys); } /// /// Rebuilds the editor display using the currently set properties. /// private void Rebuild() { Rebuild(keys, width, height); } /// /// Attempts to find a color key rectangle at the specified location. /// /// Pixel position to look at, relative to the canvas element. /// Index of the color key, if any was found. /// True if the key was found under the provided position, false otherwise. public bool FindKey(Vector2I pixelCoords, out int keyIdx) { keyIdx = -1; if (pixelCoords.y < 0 || pixelCoords.y >= height) return false; for (int i = 0; i < keys.Count; i++) { float t = MathEx.Clamp01(keys[i].time); int x = (int)(t * Math.Max(width - RECT_WIDTH, 0)); if (pixelCoords.x >= x & pixelCoords.x < (x + RECT_WIDTH)) { keyIdx = i; return true; } } return false; } /// /// Handles input. Should be called by the owning window whenever a pointer is pressed. /// /// Position of the pointer relative to the panel parent to this element. /// Pointer button involved in the event. internal void OnPointerPressed(Vector2I panelPos, PointerButton button) { Rect2I canvasBounds = canvas.Bounds; Vector2I canvasPos = panelPos - new Vector2I(canvasBounds.x, canvasBounds.y); if (button == PointerButton.Left) { int keyIdx; if (FindKey(canvasPos, out keyIdx)) { selectedIdx = keyIdx; isMousePressedOverKey = true; dragStart = canvasPos; } else selectedIdx = -1; Rebuild(); } } /// /// Handles input. Should be called by the owning window whenever a pointer is double-clicked. /// /// Position of the pointer relative to the panel parent to this element. /// Pointer button involved in the event. internal void OnPointerDoubleClicked(Vector2I panelPos, PointerButton button) { Rect2I canvasBounds = canvas.Bounds; if (!canvasBounds.Contains(panelPos)) return; Vector2I canvasPos = panelPos - new Vector2I(canvasBounds.x, canvasBounds.y); int keyIdx; if (FindKey(canvasPos, out keyIdx)) { ColorPicker.Show(keys[keyIdx].color, false, (b, color) => { if (b) { ColorGradientKey key = keys[keyIdx]; key.color = color; keys[keyIdx] = key; OnGradientModified?.Invoke(new ColorGradient(keys.ToArray())); Rebuild(); } }); } else { float time = PixelToTime(canvasPos.x); if (time >= 0.0f && time <= 1.0f) { keys.Add(new ColorGradientKey(Color.Black, time)); keys.Sort((lhs, rhs) => lhs.time.CompareTo(rhs.time)); OnGradientModified?.Invoke(new ColorGradient(keys.ToArray())); } Rebuild(); } } /// /// Handles input. Should be called by the owning window whenever a pointer is moved. /// /// Position of the pointer relative to the panel parent to this element. /// Pointer button involved in the event. internal void OnPointerMoved(Vector2I panelPos, PointerButton button) { if (button != PointerButton.Left) return; if (isMousePressedOverKey) { Rect2I canvasBounds = canvas.Bounds; Vector2I canvasPos = panelPos - new Vector2I(canvasBounds.x, canvasBounds.y); if (!isDragInProgress) { int distance = Vector2I.Distance(canvasPos, dragStart); if (distance >= DRAG_START_DISTANCE) isDragInProgress = true; } if (isDragInProgress) { float time = PixelToTime(canvasPos.x); if (time >= 0.0f && time <= 1.0f) { ColorGradientKey key = keys[selectedIdx]; key.time = time; keys[selectedIdx] = key; OnGradientModified?.Invoke(new ColorGradient(keys.ToArray())); } Rebuild(); } } } /// /// Handles input. Should be called by the owning window whenever a pointer is released. /// /// Position of the pointer relative to the panel parent to this element. internal void OnPointerReleased(Vector2I panelPos) { isDragInProgress = false; isMousePressedOverKey = false; } /// /// Handles input. Should be called by the owning window whenever a button is released. /// /// Object containing button release event information. internal void OnButtonUp(ButtonEvent ev) { if (ev.Button == ButtonCode.Delete && selectedIdx != -1 && !isMousePressedOverKey) { if (selectedIdx < keys.Count) { keys.RemoveAt(selectedIdx); OnGradientModified?.Invoke(new ColorGradient(keys.ToArray())); } selectedIdx = -1; Rebuild(); } } /** Converts a pixel position over the editor (on x axis) to a time in range [0, 1]. */ internal float PixelToTime(int x) { x -= RECT_WIDTH / 2; return x / (float) Math.Max(this.width - RECT_WIDTH, 0); } /** Converts a time in range [0, 1] to a pixel position over the editor (on x axis). */ internal int TimeToPixel(float t) { return (int) Math.Min(t * Math.Max(width - RECT_WIDTH, 0), width - RECT_WIDTH - 1) + RECT_WIDTH / 2; } /// /// Draws a rectangle representing a single color key in the gradient. /// /// Color to display. /// Time at which to draw the rectangle, in range [0, 1]. /// True to draw the rectangle as selected, plain otherwise. private void DrawColor(Color color, float t, bool selected) { int x = TimeToPixel(t); Vector2I a = new Vector2I(x + RECT_WIDTH / 2, 0); Vector2I b = new Vector2I(x + RECT_WIDTH / 2, height - 1); Vector2I c = new Vector2I(x - RECT_WIDTH / 2, 0); Vector2I d = new Vector2I(x - RECT_WIDTH / 2, height - 1); Vector2I[] linePoints = new Vector2I[] { a, b, d, c, a }; Vector2I[] trianglePoints = new Vector2I[] { a, b, c, d }; Color colorNoAlpha = color; colorNoAlpha.a = 1.0f; canvas.DrawTriangleStrip(trianglePoints, colorNoAlpha, 102); canvas.DrawPolyLine(linePoints, selected ? SELECTED_COLOR : PLAIN_COLOR, 100); Vector2I alphaA = new Vector2I(x - 1, height - 1); Vector2I alphaB = new Vector2I(x + RECT_WIDTH / 2, height / 2); Vector2I alphaC = new Vector2I(x + RECT_WIDTH / 2, height - 1); canvas.DrawTriangleList(new [] { alphaA, alphaB, alphaC }, Color.White * color.a, 101); } }; } /** @} */ }