Import code and add info to readme

Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
This commit is contained in:
Christopher Arndt 2024-04-09 13:35:43 +02:00
parent b7ed8b6f36
commit 72b676e5b8
7 changed files with 504 additions and 1 deletions

View File

@ -1,2 +1,43 @@
# nanogui-experiments
# NanoGUI Experiments
![NanoGUI Knobs example app](nanogui_knobs.png)
## Quickstart
```con
python3 -m venv venv
source venv/bin/activate
(venv) pip install -r requirenments.txt
(venv) python3 nanogui_helloworld.py
```
## Knobs Example
```con
(venv) python3 nanogui_knobs.py
```
### Key and Mouse Bindings
| | | |
| ----------------------------------------- | -------------------- |------------------------ |
| Mouse click-and-drag knob | coarse in-/decrement | +/- one 100th / pixel |
| Shift + mouse click-and-drag knob | fine in-/decrement | +/- 0.1 / pixel |
| Mouse wheel scroll over knob | coarse in-/decrement | one 100th / click * 2.0 |
| Shift + mouse wheel scroll over knob | fine in-/decrement | +/- 0.1 / click |
| Mouse wheel scroll over value entry | fine in-/decrement | +/- 0.1 |
| Ctrl + left click on knob | set to default value | |
| Up/down key while mouse over knob | coarse in-/decrement | +/- one 100th |
| Shift + up/down key while mouse over knob | fine in-/decrement | +/- 0.1 |
| Page up/down key while mouse over knob | fine in-/decrement | +/- one 10th |
| Mouse click on value entry up/down arrow | fine in-/decrement | +/- 0.1 |
| Return/Enter while editing value entry | accept & set value | |
| Escape key | quit | |
### Notes
* No support for scaling or re-layouting on window resize yet.
* Only the knob value gauge color can currently be specified on instantation,
the knob gradient and outline colors are currently hard-coded.

247
knob.py Normal file
View File

@ -0,0 +1,247 @@
#!/usr/bin/env python3
import math
from nanogui import Color, Cursor, FloatBox, Vector2f, Vector2i, Widget, glfw
from nanogui.nanovg import BUTT, RGB, RGBA, RGBAf, LerpRGBA, NVGwinding, RGBAf
class Knob(Widget):
def __init__(
self,
parent,
label,
default=0.0,
min_val=0.0,
max_val=127.0,
unit="%",
increment=0.1,
size=80,
color=RGBA(192, 192, 192, 255),
):
super().__init__(parent)
self.label = label
# sizes
self._size = size
# value gauge width as ratio of knob radius
self.gauge_width = 0.125
# value indicator line length as ratio of knob radius
self.indicator_size = 0.35
# value and range
self.value = default
self.min_val = min_val
self.max_val = max_val
self.default = default
self.unit = unit
self.increment = increment
# behaviour
self.fine_mode = False
self.scroll_speed = 2.0
self.scroll_increment = (self.max_val - self.min_val) / 100.0 * self.scroll_speed
self.drag_increment = (self.max_val - self.min_val) / 100.0
self.callback = None
self.set_cursor(Cursor.Hand)
# colors
self.color = color
self.knob_color_1 = RGB(86, 92, 95)
self.knob_color_2 = RGB(39, 42, 43)
self.outline_color_1 = RGBA(190, 190, 190, 0.0)
self.outline_color_2 = RGB(23, 23, 23)
@property
def color(self):
return self._color
@color.setter
def color(self, col):
if isinstance(col, Color):
col = RGBAf(col.r, col.g, col.b, col.w)
self._color = col
self.gauge_color = LerpRGBA(RGB(40, 40, 40), col, 0.3)
def preferred_size(self, ctx):
return Vector2i(self._size, self._size)
def _adjust_value(self, val, inc):
if self.fine_mode:
inc = self.increment
new_val = self.value + val * inc
new_val = max(self.min_val, min(self.max_val, new_val))
if new_val != self.value:
self.value = new_val
if self.callback:
self.callback(new_val)
return True
return False
def mouse_enter_event(self, pos: Vector2i, enter: bool):
if enter:
self.request_focus()
self.set_focused(enter)
return True
def mouse_button_event(self, pos: Vector2i, button: int, down: bool, modifiers: int):
if button == glfw.MOUSE_BUTTON_1 and modifiers == glfw.MOD_CONTROL and down:
if self.value != self.default:
self.value = self.default
if self.callback:
self.callback(self.value)
return True
def mouse_drag_event(self, p: Vector2i, rel: Vector2i, button: int, modifiers: int):
self._adjust_value(-rel[1], self.drag_increment)
return True
def scroll_event(self, p: Vector2i, rel: Vector2f):
self._adjust_value(rel[1], self.scroll_increment)
return True
def keyboard_event(self, key, scancode, action, modifiers):
if key in (glfw.KEY_LEFT_SHIFT, glfw.KEY_RIGHT_SHIFT):
if action == glfw.PRESS:
self.fine_mode = True
elif action == glfw.RELEASE:
self.fine_mode = False
return True
elif action in (glfw.PRESS, glfw.REPEAT):
if key == glfw.KEY_UP:
self._adjust_value(1, self.drag_increment)
return True
elif key == glfw.KEY_DOWN:
self._adjust_value(-1, self.drag_increment)
return True
elif key == glfw.KEY_PAGE_UP:
self._adjust_value(10, self.drag_increment)
return True
elif key == glfw.KEY_PAGE_DOWN:
self._adjust_value(-10, self.drag_increment)
return True
return False
def set_callback(self, callback):
if callable(callback):
self.callback = callback
else:
raise TypeError("Not a callable.")
def set_value(self, value, trigger_callback=False):
new_val = max(self.min_val, min(self.max_val, value))
if new_val != self.value:
self.value = new_val
if trigger_callback and self.callback:
self.callback(value)
def create_value_entry(self, parent, editable=True, font_size=20, widget=FloatBox, **kwargs):
entry = widget(parent, **kwargs)
entry.set_size((int(self._size * 1.1), int(font_size * 1.2)))
entry.set_font_size(font_size)
entry.set_editable(editable)
entry.set_spinnable(True)
entry.set_value(self.value)
# entry.number_format("%02.2f") # method missing in Python bindings
entry.set_min_value(self.min_val)
entry.set_max_value(self.max_val)
entry.set_value_increment(self.increment)
if self.unit:
entry.set_units(self.unit)
self.set_callback(entry.set_value)
entry.set_callback(self.set_value)
return entry
def draw(self, ctx):
pos = self.position()
size = self.size()
height = float(size[1])
radius = height / 2.0
gauge_width = radius * self.gauge_width
margin = gauge_width / 2.0
percent_filled = (self.value - self.min_val) / (self.max_val - self.min_val)
knob_diameter = (radius - gauge_width) * 2.0 - margin
indicator_length = radius * self.indicator_size
ctx.Save()
ctx.Translate(pos[0], pos[1])
# Gauge (background)
ctx.BeginPath()
ctx.StrokeWidth(gauge_width)
ctx.StrokeColor(self.gauge_color)
ctx.LineCap(BUTT.ROUND)
ctx.Arc(radius, radius, radius - margin, 0.75 * math.pi, 0.25 * math.pi, NVGwinding.CW)
ctx.Stroke()
# Gauge (fill)
ctx.BeginPath()
ctx.StrokeWidth(gauge_width)
ctx.StrokeColor(self.color)
ctx.LineCap(BUTT.ROUND)
ctx.Arc(
radius,
radius,
radius - margin,
0.75 * math.pi,
(0.75 + 1.5 * percent_filled) * math.pi,
NVGwinding.CW,
)
ctx.Stroke()
# Knob
ctx.BeginPath()
ctx.StrokeWidth(2.0)
outline_paint = ctx.LinearGradient(
0,
0,
0,
height - 10,
self.outline_color_1,
self.outline_color_2,
)
ctx.StrokePaint(outline_paint)
knob_paint = ctx.LinearGradient(
radius, gauge_width, radius, knob_diameter, self.knob_color_1, self.knob_color_2
)
ctx.FillPaint(knob_paint)
ctx.Circle(radius, radius, knob_diameter / 2.0)
ctx.Fill()
ctx.Stroke()
# Indicator
ctx.BeginPath()
ctx.Translate(radius, radius)
ctx.Rotate((2.0 + ((percent_filled - 0.5) * 1.5)) * math.pi)
ctx.StrokeColor(self.color)
ctx.StrokeWidth(gauge_width)
ctx.LineCap(BUTT.ROUND)
indicator_start = radius - margin - indicator_length
ctx.MoveTo(0, -indicator_start)
ctx.LineTo(0, -(indicator_start + indicator_length))
ctx.Stroke()
ctx.Restore()
ctx.ClosePath()

78
nanogui_customwidget.py Normal file
View File

@ -0,0 +1,78 @@
#!/usr/bin/env python3
import nanogui
from nanogui import glfw
from nanogui import BoxLayout, Color, Label, Orientation, Screen, Widget, Window, Vector2i
from nanogui.nanovg import RGBAf, RGB
class CustomWidget(Widget):
def __init__(self, parent, size=(100, 100), color=Color(192, 0, 0, 255)):
super().__init__(parent)
self._size = size
self._color = RGBAf(color.r, color.g, color.b, color.w)
def preferred_size(self, ctx):
return Vector2i(self._size)
def draw(self, ctx):
pos = self.position()
size = self.size()
ctx.BeginPath()
ctx.Rect(pos[0], pos[1], size[0], size[1])
ctx.FillColor(RGB(128, 128, 0))
ctx.Fill()
ctx.BeginPath()
ctx.StrokeWidth(5.0)
ctx.StrokeColor(self._color)
ctx.MoveTo(pos[0], pos[1])
ctx.LineTo(pos[0] + size[0], pos[1] + size[1] / 3.0)
ctx.Stroke()
ctx.ClosePath()
class CustomWidgetApp(Screen):
def __init__(self, size=(400, 300)):
super().__init__(size, "NanoGUI CustomWidget")
self.win = Window(self, "Demo Window")
self.win.set_layout(BoxLayout(Orientation.Vertical, margin=20, spacing=20))
self.resize_event(size)
Label(self.win, "NanoGUI CustomWidget Demo", "sans-bold")
cw = CustomWidget(self.win, (200, 100))
self.draw_all()
self.set_visible(True)
self.perform_layout()
def resize_event(self, size):
self.win.set_fixed_size(size)
self.win.set_size(size)
self.win.center()
self.win.perform_layout(self.nvg_context())
super().resize_event(size)
return True
def keyboard_event(self, key, scancode, action, modifiers):
if super().keyboard_event(key, scancode, action, modifiers):
return True
if key == glfw.KEY_ESCAPE and action == glfw.PRESS:
self.set_visible(False)
return True
return False
if __name__ == "__main__":
import gc
nanogui.init()
app = CustomWidgetApp()
nanogui.mainloop(refresh=1 / 60.0 * 1000)
del app
gc.collect()
nanogui.shutdown()

54
nanogui_helloworld.py Normal file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
import nanogui
from nanogui import glfw
from nanogui import BoxLayout, Button, Label, Orientation, Screen, Window
class HelloWorldApp(Screen):
def __init__(self, size=(400, 300)):
super().__init__(size, "NanoGUI Hello World")
self.win = Window(self, "Demo Window")
self.win.set_layout(BoxLayout(Orientation.Vertical, margin=20, spacing=20))
self.resize_event(size)
Label(self.win, "NanoGUI Demo", "sans-bold")
b = Button(self.win, "Say hello!")
def cb():
print("Well, 'ello there!")
b.set_callback(cb)
self.set_visible(True)
self.perform_layout()
def resize_event(self, size):
self.win.set_fixed_size(size)
self.win.set_size(size)
self.win.center()
self.win.perform_layout(self.nvg_context())
super().resize_event(size)
return True
def keyboard_event(self, key, scancode, action, modifiers):
if super().keyboard_event(key, scancode, action, modifiers):
return True
if key == glfw.KEY_ESCAPE and action == glfw.PRESS:
self.set_visible(False)
return True
return False
if __name__ == "__main__":
import gc
nanogui.init()
app = HelloWorldApp()
nanogui.mainloop(refresh=1 / 60.0 * 1000)
del app
gc.collect()
nanogui.shutdown()

BIN
nanogui_knobs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

82
nanogui_knobs.py Normal file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3
from random import randint, shuffle, uniform
import nanogui
from nanogui import BoxLayout, Color, Label, Orientation, Screen, Widget, Window, glfw
from nanogui.nanovg import RGB
from knob import Knob
class KnobsApp(Screen):
def __init__(self, size=(450, 250)):
super().__init__(size, "NanoGUI Knobs")
self.set_background(Color(96, 96, 96, 255))
self.win = Window(self, "Envelope")
self.win.set_layout(BoxLayout(Orientation.Horizontal, margin=20, spacing=20))
self.resize_event(size)
knobs = (
("Attack", 0.0, 0.0, 5.0, "s", 0.01),
("Decay", 0.0, 0.0, 5.0, "s", 0.01),
("Sustain", 0.0, 0.0, 100.0, "%", 0.01),
("Release", 0.0, 0.0, 5.0, "s", 0.01),
)
self.knobs = []
self.entries = []
self.labels = []
for i, knobspec in enumerate(knobs):
box = Widget(self.win)
box.set_layout(BoxLayout(Orientation.Vertical, spacing=10))
rgb = [128 + randint(0, 127), randint(0, 127), 64]
shuffle(rgb)
knob = Knob(box, *knobspec)
knob.color = RGB(*rgb)
knob.value = round(uniform(knob.min_val, knob.max_val), 2)
self.knobs.append(knob)
self.entries.append(knob.create_value_entry(box))
self.labels.append(Label(box, knob.label, font="sans", font_size=20))
self.draw_all()
self.set_visible(True)
self.perform_layout()
def resize_event(self, size):
self.win.set_fixed_size(size)
self.win.set_size(size)
self.win.center()
self.win.perform_layout(self.nvg_context())
super().resize_event(size)
return True
def keyboard_event(self, key, scancode, action, modifiers):
if super().keyboard_event(key, scancode, action, modifiers):
return True
if key == glfw.KEY_ESCAPE and action == glfw.PRESS:
self.set_visible(False)
return True
return True
def __del__(self):
# callbacks referencing other widgets cause memory leakage at cleanup
for entry in self.entries:
entry.set_callback(lambda x: True)
for entry in self.knobs:
entry.set_callback(lambda x: True)
if __name__ == "__main__":
import gc
nanogui.init()
app = KnobsApp()
nanogui.mainloop(refresh=1 / 60.0 * 1000)
del app
gc.collect()
nanogui.shutdown()

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
nanogui==0.2.0