-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
558 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
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: float = 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: bool = true | ||
|
||
var show_tilt_vector: bool = 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 | ||
# Adjust round precision depending on line width to improve performance | ||
# and ensure it doesn't go above the default. | ||
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 |
Oops, something went wrong.