Skip to content

Commit

Permalink
Add a graphics tablet input demo
Browse files Browse the repository at this point in the history
  • Loading branch information
Calinou committed Feb 8, 2025
1 parent 0d46333 commit 12d43ba
Show file tree
Hide file tree
Showing 8 changed files with 556 additions and 0 deletions.
32 changes: 32 additions & 0 deletions misc/graphics_tablet_input/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Graphics Tablet Input

A demo showing how to use graphics tablet input in Godot. Godot has full support
for pressure sensitivity, tilt and pen inversion (i.e. checking whether the
"eraser" is being used on the pen). Note that some platforms and tablets
may not support reporting tilt.

Input accumulation and V-Sync are disabled by default in this demo to minimize
input lag and get crisp lines (even at low FPS). This makes for the most
responsive drawing experience possible. You can toggle them in the sidebar to
see the difference it makes. Note that on Android, iOS and Web platforms, V-Sync
is forced at a system level and cannot be disabled.

Lines are drawn using the Line2D node. Every time you lift off the open and start a new
line, a new Line2D node is created. Line antialiasing is provided by enabling 2D MSAA
in the Project Settings.

Mouse input can also be used to draw in this demo, but using a tablet is recommended.

> [!NOTE]
>
> If you experience issues on Windows, try changing the tablet driver in the Project
> Settings to **wintab** instead of the default **winink**. Also, try changing your
> tablet's input mode from relative to absolute mode.
Language: GDScript

Renderer: Mobile

## Screenshots

![Screenshot](screenshots/graphics_tablet_input.webp)
187 changes: 187 additions & 0 deletions misc/graphics_tablet_input/graphics_tablet_input.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
extends Control

# Automatically split lines at regular intervals to avoid performance issues
# while drawing. This is especially due to the width curve which has to be recreated
# on every new point.
const SPLIT_POINT_COUNT = 1024

var stroke: Line2D
var width_curve: Curve
var pressures := PackedFloat32Array()
var event_position: Vector2
var event_tilt: Vector2

var line_color := Color.BLACK
var line_width := 3.0

# If `true`, modulate line width accordding to pen pressure.
# This is done using a width curve that is continuously recreated to match the line's actual profile
# as the line is being drawn by the user.
var pressure_sensitive := true

var show_tilt_vector := true

@onready var tablet_info: Label = %TabletInfo


func _ready() -> void:
# This makes tablet and mouse input reported as often as possible regardless of framerate.
# When accumulated input is disabled, we can query the pen/mouse position at every input event
# seen by the operating system, without being limited to the framerate the application runs at.
# The downside is that this uses more CPU resources, so input accumulation should only be
# disabled when you need to have access to precise input coordinates.
Input.use_accumulated_input = false
start_stroke()
%TabletDriver.text = "Tablet driver: %s" % DisplayServer.tablet_get_current_driver()


func _input(event: InputEvent) -> void:
if event is InputEventKey:
if Input.is_action_pressed(&"increase_line_width"):
$CanvasLayer/PanelContainer/Options/LineWidth/HSlider.value += 0.5
#_on_line_width_value_changed(line_width)
if Input.is_action_pressed(&"decrease_line_width"):
$CanvasLayer/PanelContainer/Options/LineWidth/HSlider.value -= 0.5
#_on_line_width_value_changed(line_width)

if not stroke:
return

if event is InputEventMouseMotion:
var event_mouse_motion := event as InputEventMouseMotion
tablet_info.text = "Pressure: %.3f\nTilt: %.3v\nInverted pen: %s" % [
event_mouse_motion.pressure,
event_mouse_motion.tilt,
"Yes" if event_mouse_motion.pen_inverted else "No",
]

if event_mouse_motion.pressure <= 0 and stroke.points.size() > 1:
# Initial part of a stroke; create a new line.
start_stroke()
# Enable the buttons if they were previously disabled.
%ClearAllLines.disabled = false
%UndoLastLine.disabled = false
if event_mouse_motion.pressure > 0:
# Continue existing line.
stroke.add_point(event_mouse_motion.position)
pressures.push_back(event_mouse_motion.pressure)
# Only compute the width curve if it's present, as it's not even created
# if pressure sensitivity is disabled.
if width_curve:
width_curve.clear_points()
for pressure_idx in range(pressures.size()):
width_curve.add_point(Vector2(
float(pressure_idx) / pressures.size(),
pressures[pressure_idx]
))

# Split into a new line if it gets too long to avoid performance issues.
# This is mostly reached when input accumulation is disabled, as enabling
# input accumulation will naturally reduce point count by a lot.
if stroke.get_point_count() >= SPLIT_POINT_COUNT:
start_stroke()

event_position = event_mouse_motion.position
event_tilt = event_mouse_motion.tilt
queue_redraw()


func _draw() -> void:
if show_tilt_vector:
# Draw tilt vector.
draw_line(event_position, event_position + event_tilt * 50, Color(1, 0, 0, 0.5), 2, true)


func start_stroke() -> void:
var new_stroke := Line2D.new()
new_stroke.begin_cap_mode = Line2D.LINE_CAP_ROUND
new_stroke.end_cap_mode = Line2D.LINE_CAP_ROUND
new_stroke.joint_mode = Line2D.LINE_JOINT_ROUND
new_stroke.round_precision = mini(line_width, 8)
new_stroke.default_color = line_color
new_stroke.width = line_width
if pressure_sensitive:
new_stroke.width_curve = Curve.new()
add_child(new_stroke)

new_stroke.owner = self
stroke = new_stroke
if pressure_sensitive:
width_curve = new_stroke.width_curve
else:
width_curve = null
pressures.clear()


func _on_undo_last_line_pressed() -> void:
# Remove last node of type Line2D in the scene.
var last_line_2d: Line2D = find_children("", "Line2D")[-1]
if last_line_2d:
# Remove stray empty line present at the end due to mouse motion.
# Note that doing it once doesn't always suffice, as multiple empty lines
# may exist at the end of the list (e.g. after changing line width/color settings).
# In this case, the user will have to use undo multiple times.
if last_line_2d.get_point_count() == 0:
last_line_2d.queue_free()

var other_last_line_2d: Line2D = find_children("", "Line2D")[-2]
if other_last_line_2d:
other_last_line_2d.queue_free()
else:
last_line_2d.queue_free()

# Since a new line is created as soon as mouse motion occurs (even if nothing is visible yet),
# we consider the list of lines to be empty with up to 2 items in it here.
%UndoLastLine.disabled = find_children("", "Line2D").size() <= 2
start_stroke()


func _on_clear_all_lines_pressed() -> void:
# Remove all nodes of type Line2D in the scene.
for node in find_children("", "Line2D"):
node.queue_free()

%ClearAllLines.disabled = true
start_stroke()


func _on_line_color_changed(color: Color) -> void:
line_color = color
# Required to make the setting change apply immediately.
start_stroke()

func _on_line_width_value_changed(value: float) -> void:
line_width = value
$CanvasLayer/PanelContainer/Options/LineWidth/Value.text = "%.1f" % value
# Required to make the setting change apply immediately.
start_stroke()


func _on_pressure_sensitive_toggled(toggled_on: bool) -> void:
pressure_sensitive = toggled_on
# Required to make the setting change apply immediately.
start_stroke()


func _on_show_tilt_vector_toggled(toggled_on: bool) -> void:
show_tilt_vector = toggled_on


func _on_msaa_item_selected(index: int) -> void:
get_viewport().msaa_2d = index as Viewport.MSAA


func _on_max_fps_value_changed(value: float) -> void:
# Since the project has low-processor usage mode enabled, we change its sleep interval instead.
# Since this is a value in microseconds between frames, we have to convert it from a FPS value.
@warning_ignore("narrowing_conversion")
OS.low_processor_usage_mode_sleep_usec = 1_000_000.0 / value
$CanvasLayer/PanelContainer/Options/MaxFPS/Value.text = str(roundi(value))


func _on_v_sync_toggled(toggled_on: bool) -> void:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED if toggled_on else DisplayServer.VSYNC_DISABLED)


func _on_input_accumulation_toggled(toggled_on: bool) -> void:
Input.use_accumulated_input = toggled_on
Loading

0 comments on commit 12d43ba

Please sign in to comment.