/// <summary> /// Attempts to find a curve under the provided coordinates. /// </summary> /// <param name="pixelCoords">Coordinates relative to this GUI element in pixels.</param> /// <returns>Index of the curve, or -1 if none found.</returns> public int FindCurve(Vector2I pixelCoords) { PixelToCurveSpace(pixelCoords, out var curveCoords, true); float time = curveCoords.x; float nearestDistance = float.MaxValue; int curveIdx = -1; for (int i = 0; i < curveInfos.Length; i++) { EdAnimationCurve curve = curveInfos[i].curve; float value = curve.Evaluate(time, false); Vector2I curPixelPos = CurveToPixelSpace(new Vector2(time, value)); float distanceToKey = Vector2I.Distance(pixelCoords, curPixelPos); if (distanceToKey < nearestDistance) { nearestDistance = distanceToKey; curveIdx = i; } } // We're not near any curve if (nearestDistance > 5.0f) { return(-1); } return(curveIdx); }
/// <summary> /// Returns the position of the tangent, in element's pixel space. /// </summary> /// <param name="keyFrame">Keyframe that the tangent belongs to.</param> /// <param name="type">Which tangent to retrieve the position for.</param> /// <returns>Position of the tangent, relative to the this GUI element's origin, in pixels.</returns> private Vector2I GetTangentPosition(KeyFrame keyFrame, TangentType type) { Vector2I position = CurveToPixelSpace(new Vector2(keyFrame.time, keyFrame.value)); Vector2 normal; if (type == TangentType.In) { normal = -EdAnimationCurve.TangentToNormal(keyFrame.inTangent); } else { normal = EdAnimationCurve.TangentToNormal(keyFrame.outTangent); } // X/Y ranges aren't scaled 1:1, adjust normal accordingly normal.x /= GetRange(); normal.y /= yRange; normal = Vector2.Normalize(normal); // Convert normal (in percentage) to pixel values Vector2I offset = new Vector2I((int)(normal.x * TANGENT_LINE_DISTANCE), (int)(-normal.y * TANGENT_LINE_DISTANCE)); return(position + offset); }
/// <summary> /// Attempts to find a keyframe under the provided coordinates. /// </summary> /// <param name="pixelCoords">Coordinates relative to this GUI element in pixels.</param> /// <param name="keyframe">Output object containing keyframe index and index of the curve it belongs to. Only valid /// if method returns true.</param> /// <returns>True if there is a keyframe under the coordinates, false otherwise.</returns> public bool FindKeyFrame(Vector2I pixelCoords, out KeyframeRef keyframe) { keyframe = new KeyframeRef(); float nearestDistance = float.MaxValue; for (int i = 0; i < curveInfos.Length; i++) { EdAnimationCurve curve = curveInfos[i].curve; KeyFrame[] keyframes = curve.KeyFrames; for (int j = 0; j < keyframes.Length; j++) { Vector2 keyframeCurveCoords = new Vector2(keyframes[j].time, keyframes[j].value); Vector2I keyframeCoords = CurveToPixelSpace(keyframeCurveCoords); float distanceToKey = Vector2I.Distance(pixelCoords, keyframeCoords); if (distanceToKey < nearestDistance) { nearestDistance = distanceToKey; keyframe.keyIdx = j; keyframe.curveIdx = i; } } } // We're not near any keyframe if (nearestDistance > 5.0f) { return(false); } return(true); }
/// <summary> /// Removes all currently selected keyframes from the curves. /// </summary> private void DeleteSelectedKeyframes() { if (!disableCurveEdit) { foreach (var selectedEntry in selectedKeyframes) { EdAnimationCurve curve = curveInfos[selectedEntry.curveIdx].curve; // Sort keys from highest to lowest so the indices don't change selectedEntry.keyIndices.Sort((x, y) => { return(y.CompareTo(x)); }); foreach (var keyframeIdx in selectedEntry.keyIndices) { curve.RemoveKeyframe(keyframeIdx); } curve.Apply(); } } else { ShowReadOnlyMessage(); } // TODO - UNDOREDO ClearSelection(); OnCurveModified?.Invoke(); guiCurveDrawing.Rebuild(); UpdateEventsGUI(); }
/// <summary> /// Attempts to find a a tangent handle under the provided coordinates. /// </summary> /// <param name="pixelCoords">Coordinates relative to this GUI element in pixels.</param> /// <param name="tangent">Output object containing keyframe information and tangent type. Only valid if method /// returns true.</param> /// <returns>True if there is a tangent handle under the coordinates, false otherwise.</returns> public bool FindTangent(Vector2I pixelCoords, out TangentRef tangent) { tangent = new TangentRef(); float nearestDistance = float.MaxValue; for (int i = 0; i < curveInfos.Length; i++) { EdAnimationCurve curve = curveInfos[i].curve; KeyFrame[] keyframes = curve.KeyFrames; for (int j = 0; j < keyframes.Length; j++) { if (!IsSelected(i, j)) { continue; } TangentMode tangentMode = curve.TangentModes[j]; if (IsTangentDisplayed(tangentMode, TangentType.In)) { Vector2I tangentCoords = GetTangentPosition(keyframes[j], TangentType.In); float distanceToHandle = Vector2I.Distance(pixelCoords, tangentCoords); if (distanceToHandle < nearestDistance) { nearestDistance = distanceToHandle; tangent.keyframeRef.keyIdx = j; tangent.keyframeRef.curveIdx = i; tangent.type = TangentType.In; } ; } if (IsTangentDisplayed(tangentMode, TangentType.Out)) { Vector2I tangentCoords = GetTangentPosition(keyframes[j], TangentType.Out); float distanceToHandle = Vector2I.Distance(pixelCoords, tangentCoords); if (distanceToHandle < nearestDistance) { nearestDistance = distanceToHandle; tangent.keyframeRef.keyIdx = j; tangent.keyframeRef.curveIdx = i; tangent.type = TangentType.Out; } } } } // We're not near any keyframe if (nearestDistance > 5.0f) { return(false); } return(true); }
private CurveEditorWindow(AnimationCurve curve, Action <bool, AnimationCurve> closedCallback = null) : base(false) { Title = new LocString("Curve editor"); Width = 600; Height = 460; curveA = new EdAnimationCurve(curve ?? new AnimationCurve(new KeyFrame[] {}), null); this.closedCallback = closedCallback; }
/// <summary> /// Changes the tangent mode for all currently selected keyframes. /// </summary> /// <param name="mode">Tangent mode to set. If only in or out tangent mode is provided, the mode for the opposite /// tangent will be kept as is.</param> private void ChangeSelectionTangentMode(TangentMode mode) { if (disableCurveEdit) { ShowReadOnlyMessage(); return; } foreach (var selectedEntry in selectedKeyframes) { EdAnimationCurve curve = curveInfos[selectedEntry.curveIdx].curve; foreach (var keyframeIdx in selectedEntry.keyIndices) { if (mode == TangentMode.Auto || mode == TangentMode.Free) { curve.SetTangentMode(keyframeIdx, mode); } else { TangentMode newMode = curve.TangentModes[keyframeIdx]; if (mode.HasFlag((TangentMode)TangentType.In)) { // Replace only the in tangent mode, keeping the out tangent as is TangentMode inFlags = (TangentMode.InAuto | TangentMode.InFree | TangentMode.InLinear | TangentMode.InStep); newMode &= ~inFlags; newMode |= (mode & inFlags); } else { // Replace only the out tangent mode, keeping the in tangent as is TangentMode outFlags = (TangentMode.OutAuto | TangentMode.OutFree | TangentMode.OutLinear | TangentMode.OutStep); newMode &= ~outFlags; newMode |= (mode & outFlags); } curve.SetTangentMode(keyframeIdx, newMode); } } curve.Apply(); } // TODO - UNDOREDO OnCurveModified?.Invoke(); guiCurveDrawing.Rebuild(); }
/// <summary> /// Opens the edit window for the currently selected keyframe. /// </summary> private void EditSelectedKeyframe() { if (disableCurveEdit) { ShowReadOnlyMessage(); return; } if (selectedKeyframes.Count == 0) { return; } EdAnimationCurve curve = curveInfos[selectedKeyframes[0].curveIdx].curve; KeyFrame[] keyFrames = curve.KeyFrames; int keyIndex = selectedKeyframes[0].keyIndices[0]; KeyFrame keyFrame = keyFrames[keyIndex]; Vector2I position = guiCurveDrawing.CurveToPixelSpace(new Vector2(keyFrame.time, keyFrame.value)); Rect2I drawingBounds = GUIUtility.CalculateBounds(drawingPanel, window.GUI); position.x = MathEx.Clamp(position.x, 0, drawingBounds.width); position.y = MathEx.Clamp(position.y, 0, drawingBounds.height); Vector2I windowPos = position + new Vector2I(drawingBounds.x, drawingBounds.y); KeyframeEditWindow editWindow = DropDownWindow.Open <KeyframeEditWindow>(window, windowPos); editWindow.Initialize(keyFrame, x => { curve.UpdateKeyframe(keyIndex, x.time, x.value); curve.Apply(); // TODO UNDOREDO guiCurveDrawing.Rebuild(); OnCurveModified?.Invoke(); }); }
public CurveDrawInfo(EdAnimationCurve curve, Color color) { this.curve = curve; this.color = color; }
/// <summary> /// Draws the curve using the provided color. /// </summary> /// <param name="curve">Curve to draw within the currently set range. </param> /// <param name="color">Color to draw the curve with.</param> private void DrawCurve(EdAnimationCurve curve, Color color) { float range = GetRange(true); float lengthPerPixel = range / drawableWidth; KeyFrame[] keyframes = curve.KeyFrames; if (keyframes.Length <= 0) { return; } // Draw start line { float curveStart = keyframes[0].time; float curveValue = curve.Evaluate(curveStart, false); Vector2I end = CurveToPixelSpace(new Vector2(curveStart, curveValue)); Vector2I start = new Vector2I(-GUIGraphTime.PADDING, end.y); if (start.x < end.x) { canvas.DrawLine(start, end, COLOR_MID_GRAY); } } List <Vector2I> linePoints = new List <Vector2I>(); // Draw in between keyframes float startVisibleTime = rangeOffset; float endVisibleTime = startVisibleTime + range; for (int i = 0; i < keyframes.Length - 1; i++) { float start = keyframes[i].time; float end = keyframes[i + 1].time; if (end < startVisibleTime || start > endVisibleTime) { continue; } 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) { float startValue = curve.Evaluate(start, false); float endValue = curve.Evaluate(end, false); linePoints.Add(CurveToPixelSpace(new Vector2(start, startValue))); linePoints.Add(CurveToPixelSpace(new Vector2(end, startValue))); linePoints.Add(CurveToPixelSpace(new Vector2(end, endValue))); } else // Draw normally { float splitIncrement = LINE_SPLIT_WIDTH * lengthPerPixel; float startValue = keyframes[i].value; float endValue = keyframes[i + 1].value; Vector2I startPixel = new Vector2I(); startPixel.x = (int)(start / lengthPerPixel); startPixel.y = (int)(startValue / lengthPerPixel); Vector2I endPixel = new Vector2I(); endPixel.x = (int)(end / lengthPerPixel); endPixel.y = (int)(endValue / lengthPerPixel); int distance = Vector2I.Distance(startPixel, endPixel); int numSplits; if (distance > 0) { float fNumSplits = distance / splitIncrement; numSplits = MathEx.CeilToInt(fNumSplits); splitIncrement = distance / (float)numSplits; } else { numSplits = 1; splitIncrement = 0.0f; } for (int j = 0; j < numSplits; j++) { float t = Math.Min(start + j * splitIncrement, end); float value = curve.Evaluate(t, false); linePoints.Add(CurveToPixelSpace(new Vector2(t, value))); } } } canvas.DrawPolyLine(linePoints.ToArray(), color); // Draw end line { float curveEnd = keyframes[keyframes.Length - 1].time; float curveValue = curve.Evaluate(curveEnd, false); Vector2I start = CurveToPixelSpace(new Vector2(curveEnd, curveValue)); Vector2I end = new Vector2I(width, start.y); if (start.x < end.x) { canvas.DrawLine(start, end, COLOR_MID_GRAY); } } }
/// <summary> /// Draws the curve using the provided color. /// </summary> /// <param name="curve">Curve to draw within the currently set range. </param> /// <param name="color">Color to draw the curve with.</param> private void DrawCurve(EdAnimationCurve curve, Color color) { float range = GetRange(); float lengthPerPixel = range / drawableWidth; KeyFrame[] keyframes = curve.KeyFrames; if (keyframes.Length <= 0) { return; } // Draw start line { float curveStart = MathEx.Clamp(keyframes[0].time, 0.0f, range); float curveValue = curve.Evaluate(0.0f, false); Vector2I start = CurveToPixelSpace(new Vector2(0.0f, curveValue)); start.x -= GUIGraphTime.PADDING; Vector2I end = CurveToPixelSpace(new Vector2(curveStart, curveValue)); canvas.DrawLine(start, end, COLOR_MID_GRAY); } List <Vector2I> linePoints = new List <Vector2I>(); // Draw in between keyframes for (int i = 0; i < keyframes.Length - 1; i++) { float start = MathEx.Clamp(keyframes[i].time, 0.0f, range); float end = MathEx.Clamp(keyframes[i + 1].time, 0.0f, range); 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) { float startValue = curve.Evaluate(start, false); float endValue = curve.Evaluate(end, false); linePoints.Add(CurveToPixelSpace(new Vector2(start, startValue))); linePoints.Add(CurveToPixelSpace(new Vector2(end, startValue))); linePoints.Add(CurveToPixelSpace(new Vector2(end, endValue))); } else // Draw normally { float timeIncrement = LINE_SPLIT_WIDTH * lengthPerPixel; int startPixel = (int)(start / lengthPerPixel); int endPixel = (int)(end / lengthPerPixel); int numSplits; if (startPixel != endPixel) { float fNumSplits = (end - start) / timeIncrement; 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++) { float t = Math.Min(start + j * timeIncrement, end); float value = curve.Evaluate(t, false); linePoints.Add(CurveToPixelSpace(new Vector2(t, value))); } } } canvas.DrawPolyLine(linePoints.ToArray(), color); // Draw end line { float curveEnd = MathEx.Clamp(keyframes[keyframes.Length - 1].time, 0.0f, range); float curveValue = curve.Evaluate(range, false); Vector2I start = CurveToPixelSpace(new Vector2(curveEnd, curveValue)); Vector2I end = new Vector2I(width, start.y); canvas.DrawLine(start, end, COLOR_MID_GRAY); } }
/// <summary> /// Handles input. Should be called by the owning window whenever a pointer is moved. /// </summary> /// <param name="ev">Object containing pointer move event information.</param> internal void OnPointerMoved(PointerEvent ev) { if (ev.Button != PointerButton.Left) { return; } if (isPointerHeld) { Vector2I windowPos = window.ScreenToWindowPos(ev.ScreenPos); Rect2I elementBounds = GUIUtility.CalculateBounds(gui, window.GUI); Vector2I pointerPos = windowPos - new Vector2I(elementBounds.x, elementBounds.y); if (isMousePressedOverKey || isMousePressedOverTangent) { Rect2I drawingBounds = drawingPanel.Bounds; Vector2I drawingPos = pointerPos - new Vector2I(drawingBounds.x, drawingBounds.y); if (!isDragInProgress) { int distance = Vector2I.Distance(drawingPos, dragStart); if (distance >= DRAG_START_DISTANCE) { if (isMousePressedOverKey && !disableCurveEdit) { draggedKeyframes.Clear(); foreach (var selectedEntry in selectedKeyframes) { EdAnimationCurve curve = curveInfos[selectedEntry.curveIdx].curve; KeyFrame[] keyFrames = curve.KeyFrames; DraggedKeyframes newEntry = new DraggedKeyframes(); newEntry.curveIdx = selectedEntry.curveIdx; draggedKeyframes.Add(newEntry); foreach (var keyframeIdx in selectedEntry.keyIndices) { newEntry.keys.Add(new DraggedKeyframe(keyframeIdx, keyFrames[keyframeIdx])); } } } // TODO - UNDOREDO record keyframe or tangent isDragInProgress = true; } } if (isDragInProgress) { if (isMousePressedOverKey && !disableCurveEdit) { Vector2 diff = Vector2.Zero; Vector2 dragStartCurve; if (guiCurveDrawing.PixelToCurveSpace(dragStart, out dragStartCurve)) { Vector2 currentPosCurve; if (guiCurveDrawing.PixelToCurveSpace(drawingPos, out currentPosCurve)) { diff = currentPosCurve - dragStartCurve; } } foreach (var draggedEntry in draggedKeyframes) { EdAnimationCurve curve = curveInfos[draggedEntry.curveIdx].curve; for (int i = 0; i < draggedEntry.keys.Count; i++) { DraggedKeyframe draggedKey = draggedEntry.keys[i]; float newTime = draggedKey.original.time + diff.x; float newValue = draggedKey.original.value + diff.y; int newIndex = curve.UpdateKeyframe(draggedKey.index, newTime, newValue); // It's possible key changed position due to time change, but since we're moving all // keys at once they cannot change position relative to one another, otherwise we would // need to update indices for other keys as well. draggedKey.index = newIndex; draggedEntry.keys[i] = draggedKey; } curve.Apply(); } // Rebuild selected keys from dragged keys (after potential sorting) ClearSelection(); foreach (var draggedEntry in draggedKeyframes) { foreach (var keyframe in draggedEntry.keys) { SelectKeyframe(new KeyframeRef(draggedEntry.curveIdx, keyframe.index)); } } OnCurveModified?.Invoke(); guiCurveDrawing.Rebuild(); UpdateEventsGUI(); } else if (isMousePressedOverTangent && !disableCurveEdit) { EdAnimationCurve curve = curveInfos[draggedTangent.keyframeRef.curveIdx].curve; KeyFrame keyframe = curve.KeyFrames[draggedTangent.keyframeRef.keyIdx]; Vector2 keyframeCurveCoords = new Vector2(keyframe.time, keyframe.value); Vector2 currentPosCurve; if (guiCurveDrawing.PixelToCurveSpace(drawingPos, out currentPosCurve)) { Vector2 normal = currentPosCurve - keyframeCurveCoords; normal = normal.Normalized; float tangent = EdAnimationCurve.NormalToTangent(normal); if (draggedTangent.type == TangentType.In) { if (normal.x > 0.0f) { tangent = float.PositiveInfinity; } keyframe.inTangent = -tangent; if (curve.TangentModes[draggedTangent.keyframeRef.keyIdx] == TangentMode.Free) { keyframe.outTangent = -tangent; } } else { if (normal.x < 0.0f) { tangent = float.PositiveInfinity; } keyframe.outTangent = tangent; if (curve.TangentModes[draggedTangent.keyframeRef.keyIdx] == TangentMode.Free) { keyframe.inTangent = tangent; } } curve.KeyFrames[draggedTangent.keyframeRef.keyIdx] = keyframe; curve.Apply(); OnCurveModified?.Invoke(); guiCurveDrawing.Rebuild(); } } } } else // Move frame marker { int frameIdx = guiTimeline.GetFrame(pointerPos); if (frameIdx != -1) { SetMarkedFrame(frameIdx); } OnFrameSelected?.Invoke(frameIdx); } } }