Skip to content

Commit fdb2f50

Browse files
authored
Add a graphics tablet input demo (#1162)
1 parent 0d46333 commit fdb2f50

File tree

8 files changed

+558
-0
lines changed

8 files changed

+558
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Graphics Tablet Input
2+
3+
A demo showing how to use graphics tablet input in Godot. Godot has full support
4+
for pressure sensitivity, tilt and pen inversion (i.e. checking whether the
5+
"eraser" is being used on the pen). Note that some platforms and tablets
6+
may not support reporting tilt.
7+
8+
Input accumulation and V-Sync are disabled by default in this demo to minimize
9+
input lag and get crisp lines (even at low FPS). This makes for the most
10+
responsive drawing experience possible. You can toggle them in the sidebar to
11+
see the difference it makes. Note that on Android, iOS and Web platforms, V-Sync
12+
is forced at a system level and cannot be disabled.
13+
14+
Lines are drawn using the Line2D node. Every time you lift off the open and start a new
15+
line, a new Line2D node is created. Line antialiasing is provided by enabling 2D MSAA
16+
in the Project Settings.
17+
18+
Mouse input can also be used to draw in this demo, but using a tablet is recommended.
19+
20+
> [!NOTE]
21+
>
22+
> If you experience issues on Windows, try changing the tablet driver in the Project
23+
> Settings to **wintab** instead of the default **winink**. Also, try changing your
24+
> tablet's input mode from relative to absolute mode.
25+
26+
Language: GDScript
27+
28+
Renderer: Mobile
29+
30+
## Screenshots
31+
32+
![Screenshot](screenshots/graphics_tablet_input.webp)
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
extends Control
2+
3+
# Automatically split lines at regular intervals to avoid performance issues
4+
# while drawing. This is especially due to the width curve which has to be recreated
5+
# on every new point.
6+
const SPLIT_POINT_COUNT = 1024
7+
8+
var stroke: Line2D
9+
var width_curve: Curve
10+
var pressures := PackedFloat32Array()
11+
var event_position: Vector2
12+
var event_tilt: Vector2
13+
14+
var line_color := Color.BLACK
15+
var line_width: float = 3.0
16+
17+
# If `true`, modulate line width accordding to pen pressure.
18+
# This is done using a width curve that is continuously recreated to match the line's actual profile
19+
# as the line is being drawn by the user.
20+
var pressure_sensitive: bool = true
21+
22+
var show_tilt_vector: bool = true
23+
24+
@onready var tablet_info: Label = %TabletInfo
25+
26+
27+
func _ready() -> void:
28+
# This makes tablet and mouse input reported as often as possible regardless of framerate.
29+
# When accumulated input is disabled, we can query the pen/mouse position at every input event
30+
# seen by the operating system, without being limited to the framerate the application runs at.
31+
# The downside is that this uses more CPU resources, so input accumulation should only be
32+
# disabled when you need to have access to precise input coordinates.
33+
Input.use_accumulated_input = false
34+
start_stroke()
35+
%TabletDriver.text = "Tablet driver: %s" % DisplayServer.tablet_get_current_driver()
36+
37+
38+
func _input(event: InputEvent) -> void:
39+
if event is InputEventKey:
40+
if Input.is_action_pressed(&"increase_line_width"):
41+
$CanvasLayer/PanelContainer/Options/LineWidth/HSlider.value += 0.5
42+
#_on_line_width_value_changed(line_width)
43+
if Input.is_action_pressed(&"decrease_line_width"):
44+
$CanvasLayer/PanelContainer/Options/LineWidth/HSlider.value -= 0.5
45+
#_on_line_width_value_changed(line_width)
46+
47+
if not stroke:
48+
return
49+
50+
if event is InputEventMouseMotion:
51+
var event_mouse_motion := event as InputEventMouseMotion
52+
tablet_info.text = "Pressure: %.3f\nTilt: %.3v\nInverted pen: %s" % [
53+
event_mouse_motion.pressure,
54+
event_mouse_motion.tilt,
55+
"Yes" if event_mouse_motion.pen_inverted else "No",
56+
]
57+
58+
if event_mouse_motion.pressure <= 0 and stroke.points.size() > 1:
59+
# Initial part of a stroke; create a new line.
60+
start_stroke()
61+
# Enable the buttons if they were previously disabled.
62+
%ClearAllLines.disabled = false
63+
%UndoLastLine.disabled = false
64+
if event_mouse_motion.pressure > 0:
65+
# Continue existing line.
66+
stroke.add_point(event_mouse_motion.position)
67+
pressures.push_back(event_mouse_motion.pressure)
68+
# Only compute the width curve if it's present, as it's not even created
69+
# if pressure sensitivity is disabled.
70+
if width_curve:
71+
width_curve.clear_points()
72+
for pressure_idx in range(pressures.size()):
73+
width_curve.add_point(Vector2(
74+
float(pressure_idx) / pressures.size(),
75+
pressures[pressure_idx]
76+
))
77+
78+
# Split into a new line if it gets too long to avoid performance issues.
79+
# This is mostly reached when input accumulation is disabled, as enabling
80+
# input accumulation will naturally reduce point count by a lot.
81+
if stroke.get_point_count() >= SPLIT_POINT_COUNT:
82+
start_stroke()
83+
84+
event_position = event_mouse_motion.position
85+
event_tilt = event_mouse_motion.tilt
86+
queue_redraw()
87+
88+
89+
func _draw() -> void:
90+
if show_tilt_vector:
91+
# Draw tilt vector.
92+
draw_line(event_position, event_position + event_tilt * 50, Color(1, 0, 0, 0.5), 2, true)
93+
94+
95+
func start_stroke() -> void:
96+
var new_stroke := Line2D.new()
97+
new_stroke.begin_cap_mode = Line2D.LINE_CAP_ROUND
98+
new_stroke.end_cap_mode = Line2D.LINE_CAP_ROUND
99+
new_stroke.joint_mode = Line2D.LINE_JOINT_ROUND
100+
# Adjust round precision depending on line width to improve performance
101+
# and ensure it doesn't go above the default.
102+
new_stroke.round_precision = mini(line_width, 8)
103+
new_stroke.default_color = line_color
104+
new_stroke.width = line_width
105+
if pressure_sensitive:
106+
new_stroke.width_curve = Curve.new()
107+
add_child(new_stroke)
108+
109+
new_stroke.owner = self
110+
stroke = new_stroke
111+
if pressure_sensitive:
112+
width_curve = new_stroke.width_curve
113+
else:
114+
width_curve = null
115+
pressures.clear()
116+
117+
118+
func _on_undo_last_line_pressed() -> void:
119+
# Remove last node of type Line2D in the scene.
120+
var last_line_2d: Line2D = find_children("", "Line2D")[-1]
121+
if last_line_2d:
122+
# Remove stray empty line present at the end due to mouse motion.
123+
# Note that doing it once doesn't always suffice, as multiple empty lines
124+
# may exist at the end of the list (e.g. after changing line width/color settings).
125+
# In this case, the user will have to use undo multiple times.
126+
if last_line_2d.get_point_count() == 0:
127+
last_line_2d.queue_free()
128+
129+
var other_last_line_2d: Line2D = find_children("", "Line2D")[-2]
130+
if other_last_line_2d:
131+
other_last_line_2d.queue_free()
132+
else:
133+
last_line_2d.queue_free()
134+
135+
# Since a new line is created as soon as mouse motion occurs (even if nothing is visible yet),
136+
# we consider the list of lines to be empty with up to 2 items in it here.
137+
%UndoLastLine.disabled = find_children("", "Line2D").size() <= 2
138+
start_stroke()
139+
140+
141+
func _on_clear_all_lines_pressed() -> void:
142+
# Remove all nodes of type Line2D in the scene.
143+
for node in find_children("", "Line2D"):
144+
node.queue_free()
145+
146+
%ClearAllLines.disabled = true
147+
start_stroke()
148+
149+
150+
func _on_line_color_changed(color: Color) -> void:
151+
line_color = color
152+
# Required to make the setting change apply immediately.
153+
start_stroke()
154+
155+
func _on_line_width_value_changed(value: float) -> void:
156+
line_width = value
157+
$CanvasLayer/PanelContainer/Options/LineWidth/Value.text = "%.1f" % value
158+
# Required to make the setting change apply immediately.
159+
start_stroke()
160+
161+
162+
func _on_pressure_sensitive_toggled(toggled_on: bool) -> void:
163+
pressure_sensitive = toggled_on
164+
# Required to make the setting change apply immediately.
165+
start_stroke()
166+
167+
168+
func _on_show_tilt_vector_toggled(toggled_on: bool) -> void:
169+
show_tilt_vector = toggled_on
170+
171+
172+
func _on_msaa_item_selected(index: int) -> void:
173+
get_viewport().msaa_2d = index as Viewport.MSAA
174+
175+
176+
func _on_max_fps_value_changed(value: float) -> void:
177+
# Since the project has low-processor usage mode enabled, we change its sleep interval instead.
178+
# Since this is a value in microseconds between frames, we have to convert it from a FPS value.
179+
@warning_ignore("narrowing_conversion")
180+
OS.low_processor_usage_mode_sleep_usec = 1_000_000.0 / value
181+
$CanvasLayer/PanelContainer/Options/MaxFPS/Value.text = str(roundi(value))
182+
183+
184+
func _on_v_sync_toggled(toggled_on: bool) -> void:
185+
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED if toggled_on else DisplayServer.VSYNC_DISABLED)
186+
187+
188+
func _on_input_accumulation_toggled(toggled_on: bool) -> void:
189+
Input.use_accumulated_input = toggled_on

0 commit comments

Comments
 (0)