Import code and add info to readme
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
This commit is contained in:
parent
b7ed8b6f36
commit
9153be2272
43
README.md
43
README.md
|
@ -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_knob.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.
|
||||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -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()
|
|
@ -0,0 +1 @@
|
||||||
|
nanogui==0.2.0
|
Loading…
Reference in New Issue