Last active
June 7, 2020 04:36
-
-
Save kudaba/a553ca56eb528a867868fdf974c92c16 to your computer and use it in GitHub Desktop.
Virtual canvas utility class.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "ImguiCanvas.h" | |
#define IMGUI_DEFINE_MATH_OPERATORS | |
#include <imgui_internal.h> | |
// Not true epsilon, this one is much more useful | |
constexpr float ImEpsilonf = 0.00001f; | |
//------------------------------------------------------------------------------------------------- | |
// Return -1 or +1 (0 returns +1) | |
//------------------------------------------------------------------------------------------------- | |
template<typename Type> | |
inline Type ImSign(Type const& aValue) | |
{ | |
return Type((aValue >= 0) - (aValue < 0)); | |
} | |
//------------------------------------------------------------------------------------------------- | |
// Smooth step converts a linear curve to an s curve, great for animations! | |
// https://en.wikipedia.org/wiki/Smoothstep | |
//------------------------------------------------------------------------------------------------- | |
template <class Type> | |
inline Type ImSmoothStep(Type const& aValue) | |
{ | |
Type x = ImSaturate(aValue); | |
return x * x*(3 - 2 * x); | |
} | |
//------------------------------------------------------------------------------------------------- | |
//------------------------------------------------------------------------------------------------- | |
bool ImPointOnLine(ImVec2 const& aPoint, ImVec2 const& aStart, ImVec2 const& anEnd, float aMinDistance) | |
{ | |
IM_ASSERT(aMinDistance >= 0); | |
ImVec2 startToPoint = aPoint - aStart; | |
ImVec2 startToEnd = anEnd - aStart; | |
float lineLengthSqr = ImLengthSqr(startToEnd); | |
if (lineLengthSqr < ImEpsilonf) | |
{ | |
aMinDistance += ImEpsilonf; | |
// distance to point | |
float distanceSqr = ImLengthSqr(startToPoint); | |
return distanceSqr <= aMinDistance * aMinDistance; | |
} | |
startToEnd /= ImSqrt(lineLengthSqr); | |
ImVec2 startToPointOnLine = startToEnd * ImDot(startToPoint, startToEnd); | |
float distanceToPointOnLineSqr = ImLengthSqr(startToPointOnLine); | |
if (distanceToPointOnLineSqr > lineLengthSqr) | |
return false; | |
ImVec2 distanceToPoint = startToPoint - startToPointOnLine; | |
float distanceToPointSqr = ImLengthSqr(distanceToPoint); | |
aMinDistance += ImEpsilonf; | |
return distanceToPointSqr <= aMinDistance * aMinDistance; | |
} | |
//------------------------------------------------------------------------------------------------- | |
// Unlerp will give you at what point the input is relative to the start and end points | |
// I.e. give the start 0 and end 4, 2 would be at the halfway point (0.5) between them. | |
//------------------------------------------------------------------------------------------------- | |
float ImUnlerp(float aValue, float aStart, float anEnd) | |
{ | |
float range = anEnd - aStart; | |
return ImFabs(range) > ImEpsilonf ? (aValue - aStart) / range : 0.f; | |
} | |
//------------------------------------------------------------------------------------------------- | |
// Remap will convert a value in the input range to a value in the output range at the same point | |
// GC_Remap(50, 0, 100, 20, 30) will return 25 | |
//------------------------------------------------------------------------------------------------- | |
template <typename Type1, typename Type2> | |
Type2 ImRemap(Type1 const& aValue, Type1 const& anInStart, Type1 const& anInEnd, Type2 const& anOutStart, Type2 const& anOutEnd) | |
{ | |
return ImLerp(anOutStart, anOutEnd, ImUnlerp(aValue, anInStart, anInEnd)); | |
} | |
//------------------------------------------------------------------------------------------------- | |
//------------------------------------------------------------------------------------------------- | |
ImCanvas::ImCanvas(float gridSize) | |
: MouseCapture(0, 0) | |
, IsMouseCaptured(false) | |
, PixelOffset(0, 0) | |
, ScrollInterpolation(-1) | |
, Zoom(1.0f) | |
, GridSize(gridSize) | |
, LineWidth(1) | |
, LineWidthThick(3) | |
{ | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::Update(bool checkPrimaryMouse, float deltaTime) | |
{ | |
WindowMin = ImGui::GetWindowPos() + ImGui::GetWindowContentRegionMin(); | |
WindowMax = ImGui::GetWindowPos() + ImGui::GetWindowContentRegionMax(); | |
UpdateZoom(); | |
UpdateScroll(checkPrimaryMouse, deltaTime); | |
VirtualMin = PixelOffset / -Zoom; | |
VirtualMax = ((WindowMax - WindowMin) - PixelOffset) / Zoom; | |
float dpiScale = ImGui::GetWindowViewport()->DpiScale; | |
if (!dpiScale) dpiScale = 1; | |
LineWidth = dpiScale; | |
LineWidthThick = dpiScale * 3; | |
} | |
//------------------------------------------------------------------------------------------------- | |
bool ImCanvas::IsHovered() const | |
{ | |
ImVec2 mousePos = ImGui::GetMousePos(); | |
return ImGui::IsWindowHovered() && ImRect(WindowMin, WindowMax).Contains(mousePos); | |
} | |
//------------------------------------------------------------------------------------------------- | |
bool ImCanvas::IsVisible(ImVec2 start, ImVec2 end) const | |
{ | |
// AABB Test | |
if (end.x < VirtualMin.x || end.y < VirtualMin.y || | |
start.x > VirtualMax.x || start.y > VirtualMax.y) | |
return false; | |
return true; | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::DrawGrid() | |
{ | |
// Ideal pixels between lines | |
float gridSizeScaled = GridSize * LineWidth; | |
float gridSizeZoomed = gridSizeScaled * Zoom; | |
while (gridSizeZoomed >= gridSizeScaled) gridSizeZoomed /= 2; | |
while (gridSizeZoomed < gridSizeScaled / 2) gridSizeZoomed *= 2; | |
IM_ASSERT(gridSizeZoomed > 5); // Grid is too condensed, use a bigger size | |
// draw two grids that scale in color and size | |
if (gridSizeZoomed > gridSizeScaled) | |
DrawGrid(gridSizeZoomed / 2, gridSizeScaled); | |
DrawGrid(gridSizeZoomed, gridSizeScaled); | |
if (gridSizeZoomed < gridSizeScaled) | |
DrawGrid(gridSizeZoomed * 2, gridSizeScaled); | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::DrawLine(ImU32 color, ImVec2 start, ImVec2 end, LineType type) const | |
{ | |
DrawLine(color, start, end, type, false, nullptr); | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::DrawLineHovered(ImU32 color, ImVec2 start, ImVec2 end, LineType type) const | |
{ | |
DrawLine(color, start, end, type, true, nullptr); | |
} | |
//------------------------------------------------------------------------------------------------- | |
bool ImCanvas::DrawLineCheckHover(ImU32 color, ImVec2 start, ImVec2 end, LineType type) const | |
{ | |
bool isHovered = false; | |
DrawLine(color, start, end, type, false, &isHovered); | |
return isHovered; | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::DrawLine(ImU32 color, ImVec2 start, ImVec2 end, LineType type, bool isHovered, bool* checkHover) const | |
{ | |
if (!ImRect(VirtualMin, VirtualMax).Overlaps(ImRect(start, end))) | |
return; | |
ImVec2 s = ToScreenSpace(start); | |
ImVec2 e = ToScreenSpace(end); | |
uint const MaxPoints = 40; | |
ImVec2 points[MaxPoints]; | |
points[0] = s; | |
uint pointCount = 0; | |
switch (type) | |
{ | |
// simple straight line | |
case Linear: | |
{ | |
points[1] = e; | |
pointCount = 2; | |
break; | |
} | |
// --- | |
// | | |
// --- | |
case Stepped: | |
{ | |
float mid = ImLerp(s.x, e.x, 0.5f); | |
points[1] = { mid, s.y }; | |
points[2] = { mid, e.y }; | |
points[3] = e; | |
pointCount = 4; | |
break; | |
} | |
// Normal s-curve | |
case Bezier: | |
{ | |
float const step = 1.0f / MaxPoints; | |
float f = 0; | |
for (uint i = 0; i < MaxPoints - 1; ++i, f += step) | |
points[i] = { ImLerp(s.x, e.x, f), ImLerp(s.y, e.y, ImSmoothStep(f)) }; | |
points[MaxPoints - 1] = e; | |
pointCount = MaxPoints; | |
break; | |
} | |
// --\ | |
// | | |
// \-- | |
case SteppedBezier: | |
{ | |
ImVec2 distance = e - s; | |
ImVec2 mid = s + distance * 0.5f; | |
ImVec2 absDistance = { ImFabs(distance.x), ImFabs(distance.y) }; | |
uint const curveSize = (MaxPoints - 6) / 2; | |
uint curve1; | |
uint curve2; | |
float const curveLength = (absDistance.x <= absDistance.y ? absDistance.x : absDistance.y) / 2; | |
float const curveLengthX = curveLength * ImSign(distance.x); | |
float const curveLengthY = curveLength * ImSign(distance.y); | |
if (absDistance.x <= absDistance.y) | |
{ | |
// tall curves have 4 control points that are the curve end points | |
// 1\ | |
// 2 | |
// | | |
// 3 | |
// \4 | |
curve1 = 0; | |
curve2 = curveSize + 1; | |
points[curveSize] = { mid.x, s.y + curveLengthY }; | |
points[curve2] = { mid.x, e.y - curveLengthY }; | |
points[curve2 + curveSize] = e; | |
pointCount = curve2 + curveSize + 1; | |
} | |
else | |
{ | |
// wide curves have 5 control points | |
// 1--2\ | |
// 3 | |
// \4--5 | |
curve1 = 1; | |
curve2 = 1 + curveSize; | |
points[1] = { mid.x - curveLengthX, s.y }; | |
points[curve1 + curveSize] = mid; | |
points[curve1 + curveSize * 2] = { mid.x + curveLengthX, e.y }; | |
points[curve1 + curveSize * 2 + 1] = e; | |
pointCount = curve1 + curveSize * 2 + 2; | |
} | |
// lookup table for the y of the curve given the x | |
static float yLookup[curveSize]; | |
static bool yLookupGen; | |
if (!yLookupGen) | |
{ | |
for (uint i = 0; i < curveSize; ++i) | |
{ | |
float x = (float)i / curveSize; | |
yLookup[i] = 1.0f - ImSqrt(1.f - x * x); | |
} | |
} | |
IM_ASSERT(pointCount <= MaxPoints); | |
IM_ASSERT(curve1 <= curve2 - curveSize); | |
IM_ASSERT(curve2 <= MaxPoints - curveSize); | |
float const step = 1.0f / curveSize; | |
float stepX = step; | |
for (uint i = 1; i < curveSize; ++i, stepX += step) | |
{ | |
ImVec2 curveOffset(stepX * curveLengthX, yLookup[i] * curveLengthY); | |
points[curve1 + i] = points[curve1] + curveOffset; | |
points[curve2 + (curveSize - i)] = points[curve2 + curveSize] - curveOffset; | |
} | |
break; | |
} | |
default: | |
IM_ASSERT(false); // unreachable | |
} | |
IM_ASSERT(pointCount); | |
if (checkHover) | |
{ | |
ImVec2 mpos = ImGui::GetMousePos(); | |
isHovered = false; | |
for (uint i = 1; i < pointCount && !isHovered; ++i) | |
isHovered = ImPointOnLine(mpos, points[i - 1], points[i], LineWidthThick); | |
*checkHover = isHovered; | |
} | |
ImGui::GetWindowDrawList()->AddPolyline(points, pointCount, color, false, isHovered ? LineWidthThick : LineWidth); | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::DrawRect(ImU32 color, ImVec2 start, ImVec2 end, float rounding) const | |
{ | |
ImVec2 s = ToScreenSpace(start); | |
ImVec2 e = ToScreenSpace(end); | |
ImGui::GetWindowDrawList()->AddRectFilled(s, e, color, rounding); | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::DrawRectOutline(ImU32 color, ImVec2 start, ImVec2 end, float rounding, float thickness) const | |
{ | |
ImVec2 s = ToScreenSpace(start); | |
ImVec2 e = ToScreenSpace(end); | |
ImGui::GetWindowDrawList()->AddRect(s, e, color, rounding, ImDrawCornerFlags_All, thickness); | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::DrawText(char const* text, ImU32 color, ImVec2 pos) const | |
{ | |
ImVec2 s = ToScreenSpace(pos); | |
ImGui::GetWindowDrawList()->AddText(ImGui::GetFont(), ImGui::GetFontSize() * Zoom, s, color, text); | |
} | |
//------------------------------------------------------------------------------------------------- | |
ImVec2 ImCanvas::ToVirtualSpace(ImVec2 screenPos) const | |
{ | |
return (screenPos - (PixelOffset + WindowMin)) / Zoom; | |
} | |
//------------------------------------------------------------------------------------------------- | |
ImVec2 ImCanvas::ToScreenSpace(ImVec2 virtualPos) const | |
{ | |
return virtualPos * Zoom + PixelOffset + WindowMin; | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::ScrollTo(ImVec2 virtualPos, float overTime) | |
{ | |
ImVec2 halfSize = (WindowMax - WindowMin) * 0.5f; | |
ImVec2 targetOffset = (virtualPos * Zoom - halfSize) * -1; | |
if (overTime > 0) | |
{ | |
// Start an animated scroll | |
ScrollSourcePosition = PixelOffset; | |
ScrollTargetPosition = targetOffset; | |
ScrollTime = overTime; | |
ScrollInterpolation = 0; | |
} | |
else | |
{ | |
SetOffset(targetOffset); | |
} | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::SetOffset(ImVec2 offset) | |
{ | |
PixelOffset = offset; | |
VirtualMin = PixelOffset / -Zoom; | |
VirtualMax = ((WindowMax - WindowMin) - PixelOffset) / Zoom; | |
} | |
//------------------------------------------------------------------------------------------------- | |
// Draw grid lines to help navigate the virtual area | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::DrawGrid(float gridSizeZoomed, float scale) const | |
{ | |
// Scale color give better zoom motion, closer to ideal size it more opaque | |
float alpha = 1 - ImFabs(ImRemap(gridSizeZoomed, scale / 2, scale * 2, -1.f, 1.f)); | |
if (alpha < 0) | |
return; | |
ImColor color = ImVec4(1, 1, 1, alpha); | |
// Need to offset one when hitting negative coordinates | |
ImVec2 windowOffset(fmod(PixelOffset.x, gridSizeZoomed), fmod(PixelOffset.y, gridSizeZoomed)); | |
if (windowOffset.x < 0) windowOffset.x += gridSizeZoomed; | |
if (windowOffset.y < 0) windowOffset.y += gridSizeZoomed; | |
for (float i = WindowMin.x + windowOffset.x; i < WindowMax.x; i += gridSizeZoomed) | |
ImGui::GetWindowDrawList()->AddLine({ i, WindowMin.y }, { i, WindowMax.y }, color); | |
for (float i = WindowMin.y + windowOffset.y; i < WindowMax.y; i += gridSizeZoomed) | |
ImGui::GetWindowDrawList()->AddLine({ WindowMin.x, i }, { WindowMax.x, i }, color); | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::UpdateZoom() | |
{ | |
float const zoomMin = 0.1f; | |
float const zoomMax = 2.f; | |
float const zoomMinLog = logf(zoomMin); | |
float const zoomMaxLog = logf(zoomMax); | |
float const zoomSteps = 30; | |
if (ImGui::IsWindowHovered()) | |
{ | |
float logZoom = logf(Zoom); | |
float newLogZoom = logZoom + (zoomMaxLog - zoomMinLog) / zoomSteps * ImGui::GetIO().InputCurrentFrame->MouseWheel; | |
float newZoom = ImClamp(expf(newLogZoom), zoomMin, zoomMax); | |
if (newZoom != Zoom) | |
{ | |
// Adjust offset to make sure we're zooming on mouse point | |
ImVec2 mpos = ImGui::GetMousePos() - WindowMin; | |
PixelOffset = ((PixelOffset - mpos) / Zoom) * newZoom + mpos; | |
Zoom = newZoom; | |
} | |
} | |
} | |
//------------------------------------------------------------------------------------------------- | |
void ImCanvas::UpdateScroll(bool checkPrimaryMouse, float deltaTime) | |
{ | |
if (!IsMouseCaptured) | |
{ | |
if (IsHovered() && ((checkPrimaryMouse && ImGui::GetIO().MouseClicked[0]) || ImGui::GetIO().MouseClicked[2])) | |
{ | |
MouseCapture = ImGui::GetMousePos(); | |
PixelOffsetCapture = PixelOffset; | |
IsMouseCaptured = true; | |
} | |
} | |
else if (!ImGui::GetIO().MouseDown[0] && !ImGui::GetIO().MouseDown[2]) | |
IsMouseCaptured = false; | |
if (IsMouseCaptured) | |
{ | |
PixelOffset = PixelOffsetCapture + ImGui::GetMousePos() - MouseCapture; | |
ScrollInterpolation = -1; | |
} | |
else if (deltaTime > 0 && ScrollInterpolation >= 0 && ScrollInterpolation <= 1) | |
{ | |
ScrollInterpolation += deltaTime / ScrollTime; | |
if (ScrollInterpolation >= 1) | |
{ | |
PixelOffset = ScrollTargetPosition; | |
ScrollInterpolation = -1; | |
} | |
else | |
{ | |
// Scroll using the upper half of a smooth step function to give fast motion to start that slows down as we approach the target | |
float const smoothStepPos = 0.5f + (ScrollInterpolation * 0.5f); | |
float const smoothStepValue = smoothStepPos * smoothStepPos * (3 - 2 * smoothStepPos); | |
float const finalInterpolation = (smoothStepValue - 0.5f) * 2; | |
PixelOffset = ImLerp(ScrollSourcePosition, ScrollTargetPosition, finalInterpolation); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
#include <imgui.h> | |
//------------------------------------------------------------------------------------------------- | |
// Helper for managing an unbounded virtual area. Includes mouse interaction, zoom and draw helpers. | |
//------------------------------------------------------------------------------------------------- | |
struct ImCanvas | |
{ | |
enum LineType { Linear, Stepped, Bezier, SteppedBezier }; | |
ImCanvas(float gridSize = 50); | |
// Call once per frame. Updates mouse state and drawing constants | |
void Update(bool checkPrimaryMouse, float deltaTime); | |
// Check that it's window is hovered and that the mouse is in the canvas area | |
bool IsHovered() const; | |
// No drawing, just does a cull check against the area and returns true if valid to draw (no End required) | |
bool IsVisible(ImVec2 start, ImVec2 end) const; | |
// Draw an even spaced grid for users to reference position and scale | |
void DrawGrid(); | |
// Simply draw a line | |
void DrawLine(ImU32 color, ImVec2 start, ImVec2 end, LineType type = Linear) const; | |
// Draw a line hovered (i.e. thick) | |
void DrawLineHovered(ImU32 color, ImVec2 start, ImVec2 end, LineType type = Linear) const; | |
// Draw a line while checking for hover status, prevents having to generate line multiple times | |
bool DrawLineCheckHover(ImU32 color, ImVec2 start, ImVec2 end, LineType type = Linear) const; | |
// The following assume they are being used on a widget so no additional culling is performed | |
void DrawRect(ImU32 color, ImVec2 start, ImVec2 end, float rounding = 0) const; | |
void DrawRectOutline(ImU32 color, ImVec2 start, ImVec2 end, float rounding = 0, float thickness = 1) const; | |
void DrawText(char const* text, ImU32 color, ImVec2 pos) const; | |
ImVec2 ToVirtualSpace(ImVec2 screenPos) const; | |
ImVec2 ToScreenSpace(ImVec2 virtualPos) const; | |
void ScrollTo(ImVec2 virtualPos, float overTime = 0.4f); // Make the input position centered | |
void SetOffset(ImVec2 offset); | |
float GetZoom() const { return Zoom; } | |
ImVec2 GetOffset() const { return PixelOffset; } | |
ImVec2 GetVirtualMin() const { return VirtualMin; } | |
ImVec2 GetVirtualMax() const { return VirtualMax; } | |
private: | |
void DrawLine(ImU32 color, ImVec2 start, ImVec2 end, LineType type, bool isHovered, bool* checkHover) const; | |
void DrawGrid(float gridSizeZoomed, float scale) const; | |
void UpdateZoom(); | |
void UpdateScroll(bool checkPrimaryMouse, float deltaTime); | |
ImVec2 MouseCapture; | |
ImVec2 PixelOffsetCapture; | |
bool IsMouseCaptured; | |
ImVec2 PixelOffset; | |
ImVec2 WindowMin; | |
ImVec2 WindowMax; | |
ImVec2 VirtualMin; | |
ImVec2 VirtualMax; | |
// Animated scrolling | |
ImVec2 ScrollSourcePosition; | |
ImVec2 ScrollTargetPosition; | |
float ScrollInterpolation; | |
float ScrollTime; | |
float Zoom; | |
float GridSize; | |
float LineWidth; | |
float LineWidthThick; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGuiColors::Dark_grey); | |
ImGui::Begin("Canvas", nullptr, { 300, 300 }, 1.0f); | |
static ImCanvas canvas; | |
canvas.Update(true, dt); | |
if (canvas.IsVisible({ 100, 100 }, { 200, 200 })) | |
canvas.DrawRect(ImColor(0.f, 0.f, 1.f), { 100, 100 }, { 200, 200 }); | |
ImGui::BeginChild("debug", { 200, 80 }, true); | |
ImGui::Text("%dx%d", (int)canvas.GetOffset().x, (int)canvas.GetOffset().y); | |
ImGui::Text("%.1f%%", canvas.GetZoom()*100); | |
ImGui::End(); | |
ImGui::End(); | |
ImGui::PopStyleColor(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment