Import code and add info to readme
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
This commit is contained in:
parent
b7ed8b6f36
commit
85b669385f
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_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 |
|
||||
| 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 | big in-/decrement | +/- one 10th |
|
||||
| Mouse click on value entry up/down arrow | fine in-/decrement | +/- 0.1 |
|
||||
| Mouse wheel scroll over value entry | fine in-/decrement | +/- 0.1 / click |
|
||||
| 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