//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using System;
using System.Collections.Generic;
using BansheeEngine;
namespace BansheeEditor
{
/** @addtogroup AnimationEditor
* @{
*/
///
/// Draws one or multiple curves over the specified physical area. User can specify horizontal and vertical range to
/// display, as well as physical size of the GUI area.
///
internal class GUICurveDrawing
{
private const int LINE_SPLIT_WIDTH = 2;
private static readonly Color COLOR_MID_GRAY = new Color(90.0f / 255.0f, 90.0f / 255.0f, 90.0f / 255.0f, 1.0f);
private static readonly Color COLOR_DARK_GRAY = new Color(40.0f / 255.0f, 40.0f / 255.0f, 40.0f / 255.0f, 1.0f);
private EdAnimationCurve[] curves;
private bool[][] selectedKeyframes;
private int width;
private int height;
private float xRange = 60.0f;
private float yRange = 20.0f;
private int fps = 1;
private int markedFrameIdx = 0;
private int drawableWidth;
private GUICanvas canvas;
private GUIGraphTicks tickHandler;
///
/// Creates a new curve drawing GUI element.
///
/// Layout into which to add the GUI element.
/// Width of the element in pixels.
/// Height of the element in pixels.
/// Initial set of curves to display.
public GUICurveDrawing(GUILayout layout, int width, int height, EdAnimationCurve[] curves)
{
canvas = new GUICanvas();
layout.AddElement(canvas);
tickHandler = new GUIGraphTicks(GUITickStepType.Time);
this.curves = curves;
SetSize(width, height);
ClearSelectedKeyframes(); // Makes sure the array is initialized
Rebuild();
}
///
/// Change the set of curves to display.
///
/// New set of curves to draw on the GUI element.
public void SetCurves(EdAnimationCurve[] curves)
{
this.curves = curves;
}
///
/// Change the physical size of the GUI element.
///
/// Width of the element in pixels.
/// Height of the element in pixels.
public void SetSize(int width, int height)
{
this.width = width;
this.height = height;
canvas.SetWidth(width);
canvas.SetHeight(height);
drawableWidth = Math.Max(0, width - GUIGraphTime.PADDING * 2);
}
///
/// Changes the visible range that the GUI element displays.
///
/// Range of the horizontal area. Displayed area will range from [0, xRange].
/// Range of the vertical area. Displayed area will range from
/// [-yRange * 0.5, yRange * 0.5]
public void SetRange(float xRange, float yRange)
{
this.xRange = xRange;
this.yRange = yRange;
}
///
/// Number of frames per second, used for frame selection and marking.
///
/// Number of prames per second.
public void SetFPS(int fps)
{
this.fps = Math.Max(1, fps);
}
///
/// Sets the frame at which to display the frame marker.
///
/// Index of the frame to display the marker on, or -1 to clear the marker.
public void SetMarkedFrame(int frameIdx)
{
markedFrameIdx = frameIdx;
}
///
/// Marks the specified key-frame as selected, changing the way it is displayed.
///
/// Index of the curve the keyframe is on.
/// Index of the keyframe.
/// True to select it, false to deselect it.
public void SelectKeyframe(int curveIdx, int keyIdx, bool selected)
{
if (selectedKeyframes == null)
return;
if (curveIdx < 0 || curveIdx >= selectedKeyframes.Length)
return;
if (keyIdx < 0 || keyIdx >= selectedKeyframes[curveIdx].Length)
return;
selectedKeyframes[curveIdx][keyIdx] = selected;
}
///
/// Clears any key-frames that were marked as selected.
///
public void ClearSelectedKeyframes()
{
selectedKeyframes = new bool[curves.Length][];
for (int i = 0; i < curves.Length; i++)
{
KeyFrame[] keyframes = curves[i].Native.KeyFrames;
selectedKeyframes[i] = new bool[keyframes.Length];
}
}
///
/// Returns time for a frame with the specified index. Depends on set range and FPS.
///
/// Index of the frame (not a key-frame) to get the time for.
/// Time of the frame with the provided index.
public float GetTimeForFrame(int frameIdx)
{
float range = GetRange();
int numFrames = (int)range * fps;
float timePerFrame = range / numFrames;
return frameIdx* timePerFrame;
}
///
/// Retrieves information under the provided window coordinates. This involves coordinates of the curve, as well
/// as curve and key-frame indices that were under the coordinates (if any).
///
/// Coordinate relative to the window the GUI element is on.
/// Curve coordinates within the range as specified by . Only
/// Valid when function returns true.
/// Sequential index of the curve that's under the coordinates. -1 if no curve. Index
/// corresponds to the curve index as provided by the curve array in the constructor or
/// .
/// Index of a keyframe that that's under the coordinates, on the curve as referenced by
/// . -1 if no keyframe.
/// True if the window coordinates were within the curve area, false otherwise.
public bool GetCoordInfo(Vector2I windowCoords, out Vector2 curveCoords, out int curveIdx, out int keyIdx)
{
Rect2I bounds = canvas.Bounds;
// Check if outside of curve drawing bounds
if (windowCoords.x < (bounds.x + GUIGraphTime.PADDING) || windowCoords.x >= (bounds.x + bounds.width - GUIGraphTime.PADDING) ||
windowCoords.y < bounds.y || windowCoords.y >= (bounds.y + bounds.height))
{
curveCoords = new Vector2();
curveIdx = -1;
keyIdx = -1;
return false;
}
// Find time and value of the place under the coordinates
Vector2I relativeCoords = windowCoords - new Vector2I(bounds.x + GUIGraphTime.PADDING, bounds.y);
float lengthPerPixel = xRange / drawableWidth;
float heightPerPixel = yRange / height;
float yOffset = yRange/2.0f;
float t = relativeCoords.x*lengthPerPixel;
float value = yOffset - relativeCoords.y*heightPerPixel;
curveCoords = new Vector2();
curveCoords.x = t;
curveCoords.y = value;
// Find nearest keyframe, if any
keyIdx = -1;
curveIdx = -1;
float nearestDistance = float.MaxValue;
for (int i = 0; i < curves.Length; i++)
{
EdAnimationCurve curve = curves[i];
KeyFrame[] keyframes = curve.Native.KeyFrames;
for (int j = 0; j < keyframes.Length; j++)
{
Vector2I keyframeCoords = new Vector2I((int)(keyframes[j].time / lengthPerPixel),
(int)((yOffset - keyframes[j].value) / heightPerPixel));
float distanceToKey = Vector2I.Distance(relativeCoords, keyframeCoords);
if (distanceToKey < nearestDistance)
{
nearestDistance = distanceToKey;
keyIdx = j;
curveIdx = i;
}
}
// We're not near any keyframe
if (nearestDistance > 5.0f)
keyIdx = -1;
}
// Find nearest curve, if any
if (keyIdx == -1)
{
// Note: This will not detect a curve if coordinate is over a step, and in general this works poorly with large slopes
curveIdx = -1;
nearestDistance = float.MaxValue;
for (int i = 0; i < curves.Length; i++)
{
EdAnimationCurve curve = curves[i];
KeyFrame[] keyframes = curve.Native.KeyFrames;
if (keyframes.Length == 0)
continue;
if (t < keyframes[0].time || t > keyframes[keyframes.Length - 1].time)
continue;
float curveValue = curves[i].Native.Evaluate(t);
float distanceToCurve = Math.Abs(curveValue - value);
if (distanceToCurve < nearestDistance)
{
nearestDistance = distanceToCurve;
curveIdx = i;
}
}
// We're not near any curve
float nearestDistancePx = nearestDistance/heightPerPixel;
if (nearestDistancePx > 15.0f)
curveIdx = -1;
}
return true;
}
///
/// Draws a vertical frame marker on the curve area.
///
/// Time at which to draw the marker.
/// Color with which to draw the marker.
private void DrawFrameMarker(float t, Color color)
{
int xPos = (int)((t / GetRange()) * drawableWidth) + GUIGraphTime.PADDING;
Vector2I start = new Vector2I(xPos, 0);
Vector2I end = new Vector2I(xPos, height);
canvas.DrawLine(start, end, color);
}
///
/// Draws a horizontal line representing the line at y = 0.
///
private void DrawCenterLine()
{
int heightOffset = height / 2; // So that y = 0 is at center of canvas
Vector2I start = new Vector2I(0, heightOffset);
Vector2I end = new Vector2I(width, heightOffset);
canvas.DrawLine(start, end, COLOR_DARK_GRAY);
}
///
/// Draws a keyframe a the specified time and value.
///
/// Time to draw the keyframe at.
/// Y value to draw the keyframe at.
/// Determines should the keyframe be drawing using the selected color scheme, or normally.
///
private void DrawKeyframe(float t, float y, bool selected)
{
int heightOffset = height / 2; // So that y = 0 is at center of canvas
int xPos = (int)((t / GetRange()) * drawableWidth) + GUIGraphTime.PADDING;
int yPos = heightOffset - (int)((y/yRange)*height);
Vector2I a = new Vector2I(xPos - 3, yPos);
Vector2I b = new Vector2I(xPos, yPos - 3);
Vector2I c = new Vector2I(xPos + 3, yPos);
Vector2I d = new Vector2I(xPos, yPos + 3);
// Draw diamond shape
Vector2I[] linePoints = new Vector2I[] { a, b, c, d, a };
Vector2I[] trianglePoints = new Vector2I[] { b, c, a, d };
canvas.DrawTriangleStrip(trianglePoints, Color.White, 101);
if (selected)
canvas.DrawPolyLine(linePoints, Color.BansheeOrange, 100);
else
canvas.DrawPolyLine(linePoints, Color.Black, 100);
}
///
/// Returns the range of times displayed by the timeline rounded to the multiple of FPS.
///
/// If true, extra range will be included to cover the right-most padding.
/// Time range rounded to a multiple of FPS.
private float GetRange(bool padding = false)
{
float spf = 1.0f / fps;
float range = xRange;
if (padding)
{
float lengthPerPixel = xRange / drawableWidth;
range += lengthPerPixel * GUIGraphTime.PADDING;
}
return ((int)range / spf) * spf;
}
///
/// Rebuilds the internal GUI elements. Should be called whenever timeline properties change.
///
public void Rebuild()
{
canvas.Clear();
if (curves == null)
return;
tickHandler.SetRange(0.0f, GetRange(true), drawableWidth + GUIGraphTime.PADDING);
// Draw vertical frame markers
int numTickLevels = tickHandler.NumLevels;
for (int i = numTickLevels - 1; i >= 0; i--)
{
float[] ticks = tickHandler.GetTicks(i);
float strength = tickHandler.GetLevelStrength(i);
for (int j = 0; j < ticks.Length; j++)
{
Color color = COLOR_DARK_GRAY;
color.a *= strength;
DrawFrameMarker(ticks[j], color);
}
}
// Draw center line
DrawCenterLine();
// Draw curves
int curveIdx = 0;
foreach (var curve in curves)
{
Color color = GetUniqueColor(curveIdx);
DrawCurve(curve, color);
// Draw keyframes
KeyFrame[] keyframes = curve.Native.KeyFrames;
for (int i = 0; i < keyframes.Length; i++)
DrawKeyframe(keyframes[i].time, keyframes[i].value, IsSelected(curveIdx, i));
curveIdx++;
}
// Draw selected frame marker
if (markedFrameIdx != -1)
DrawFrameMarker(GetTimeForFrame(markedFrameIdx), Color.BansheeOrange);
}
///
/// Generates a unique color based on the provided index.
///
/// Index to use for generating a color. Should be less than 30 in order to guarantee reasonably
/// different colors.
/// Unique color.
private Color GetUniqueColor(int idx)
{
const int COLOR_SPACING = 359 / 15;
float hue = ((idx * COLOR_SPACING) % 359) / 359.0f;
return Color.HSV2RGB(new Color(hue, 175.0f / 255.0f, 175.0f / 255.0f));
}
///
/// Checks is the provided key-frame currently marked as selected.
///
/// Index of the curve the keyframe is on.
/// Index of the keyframe.
/// True if selected, false otherwise.
private bool IsSelected(int curveIdx, int keyIdx)
{
if (selectedKeyframes == null)
return false;
if (curveIdx < 0 || curveIdx >= selectedKeyframes.Length)
return false;
if (keyIdx < 0 || keyIdx >= selectedKeyframes[curveIdx].Length)
return false;
return selectedKeyframes[curveIdx][keyIdx];
}
///
/// Draws the curve using the provided color.
///
/// Curve to draw within the currently set range.
/// Color to draw the curve with.
private void DrawCurve(EdAnimationCurve curve, Color color)
{
float lengthPerPixel = xRange/drawableWidth;
float pixelsPerHeight = height/yRange;
int heightOffset = height/2; // So that y = 0 is at center of canvas
KeyFrame[] keyframes = curve.Native.KeyFrames;
if (keyframes.Length < 0)
return;
// Draw start line
{
float start = MathEx.Clamp(keyframes[0].time, 0.0f, xRange);
int startPixel = (int)(start / lengthPerPixel);
int xPosStart = 0;
int xPosEnd = GUIGraphTime.PADDING + startPixel;
int yPos = (int)(curve.Native.Evaluate(0.0f, false) * pixelsPerHeight);
yPos = heightOffset - yPos; // Offset and flip height (canvas Y goes down)
Vector2I a = new Vector2I(xPosStart, yPos);
Vector2I b = new Vector2I(xPosEnd, yPos);
canvas.DrawLine(a, b, COLOR_MID_GRAY);
}
List linePoints = new List();
// Draw in between keyframes
for (int i = 0; i < keyframes.Length - 1; i++)
{
float start = MathEx.Clamp(keyframes[i].time, 0.0f, xRange);
float end = MathEx.Clamp(keyframes[i + 1].time, 0.0f, xRange);
int startPixel = (int)(start / lengthPerPixel);
int endPixel = (int)(end / lengthPerPixel);
bool isStep = keyframes[i].outTangent == float.PositiveInfinity ||
keyframes[i + 1].inTangent == float.PositiveInfinity;
// If step tangent, draw the required lines without sampling, as the sampling will miss the step
if (isStep)
{
// Line from left to right frame
int xPos = startPixel;
int yPosStart = (int)(curve.Native.Evaluate(start, false) * pixelsPerHeight);
yPosStart = heightOffset - yPosStart; // Offset and flip height (canvas Y goes down)
linePoints.Add(new Vector2I(GUIGraphTime.PADDING + xPos, yPosStart));
xPos = endPixel;
linePoints.Add(new Vector2I(GUIGraphTime.PADDING + xPos, yPosStart));
// Line representing the step
int yPosEnd = (int)(curve.Native.Evaluate(end, false) * pixelsPerHeight);
yPosEnd = heightOffset - yPosEnd; // Offset and flip height (canvas Y goes down)
linePoints.Add(new Vector2I(GUIGraphTime.PADDING + xPos, yPosEnd));
}
else // Draw normally
{
int numSplits;
float timeIncrement;
if (startPixel != endPixel)
{
float fNumSplits = (endPixel - startPixel)/(float) LINE_SPLIT_WIDTH;
numSplits = MathEx.FloorToInt(fNumSplits);
float remainder = fNumSplits - numSplits;
float lengthRounded = (end - start)*(numSplits/fNumSplits);
timeIncrement = lengthRounded/numSplits;
numSplits += MathEx.CeilToInt(remainder) + 1;
}
else
{
numSplits = 1;
timeIncrement = 0.0f;
}
for (int j = 0; j < numSplits; j++)
{
int xPos = Math.Min(startPixel + j * LINE_SPLIT_WIDTH, endPixel);
float t = Math.Min(start + j * timeIncrement, end);
int yPos = (int)(curve.Native.Evaluate(t, false) * pixelsPerHeight);
yPos = heightOffset - yPos; // Offset and flip height (canvas Y goes down)
linePoints.Add(new Vector2I(GUIGraphTime.PADDING + xPos, yPos));
}
}
}
canvas.DrawPolyLine(linePoints.ToArray(), color);
// Draw end line
{
float end = MathEx.Clamp(keyframes[keyframes.Length - 1].time, 0.0f, xRange);
int endPixel = (int)(end / lengthPerPixel);
int xPosStart = GUIGraphTime.PADDING + endPixel;
int xPosEnd = width;
int yPos = (int)(curve.Native.Evaluate(xRange, false) * pixelsPerHeight);
yPos = heightOffset - yPos; // Offset and flip height (canvas Y goes down)
Vector2I a = new Vector2I(xPosStart, yPos);
Vector2I b = new Vector2I(xPosEnd, yPos);
canvas.DrawLine(a, b, COLOR_MID_GRAY);
}
}
}
/** }@ */
}