Zoom and pan
Drag to pan and roll the wheel to zoom at the pointer instead of the screen center, which is two lines of math on top of Camera2D.zoom.
The editor above is running this project. Change a line and it reloads.
Godot classes used
Camera2DInputEventMouseButtonLine2D
The code
scripts/main.gd
extends Node
const CELL: float = 120.0
const HALF: Vector2 = Vector2(1200, 960)
const MIN_ZOOM: Vector2 = Vector2(0.35, 0.35)
const MAX_ZOOM: Vector2 = Vector2(3.0, 3.0)
var camera: Camera2D
var readout: Label
var elapsed: float = 0.0
var manual: bool = false
func _ready() -> void:
_add_background()
_add_grid()
_add_landmarks()
camera = Camera2D.new()
add_child(camera)
camera.make_current()
_add_hud()
print('drag with the left button to pan, use the wheel to zoom at the pointer')
func _process(delta: float) -> void:
elapsed += delta
if not manual:
camera.position = Vector2(sin(elapsed * 0.5) * 520.0, sin(elapsed * 0.31) * 320.0)
camera.zoom = Vector2.ONE * (0.95 + 0.45 * sin(elapsed * 0.7))
readout.text = 'zoom %.2f camera %d, %d' % [camera.zoom.x, camera.position.x, camera.position.y]
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
var button: InputEventMouseButton = event as InputEventMouseButton
if button.button_index == MOUSE_BUTTON_WHEEL_UP:
_zoom_at(button.position, 1.12)
elif button.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_zoom_at(button.position, 1.0 / 1.12)
elif event is InputEventMouseMotion:
var motion: InputEventMouseMotion = event as InputEventMouseMotion
if motion.button_mask & MOUSE_BUTTON_MASK_LEFT:
manual = true
camera.position -= motion.relative / camera.zoom
func _zoom_at(screen_point: Vector2, factor: float) -> void:
manual = true
var view: Vector2 = get_viewport().get_visible_rect().size
var before: Vector2 = camera.position + (screen_point - view * 0.5) / camera.zoom
camera.zoom = (camera.zoom * factor).clamp(MIN_ZOOM, MAX_ZOOM)
camera.position = before - (screen_point - view * 0.5) / camera.zoom
func _add_grid() -> void:
var x: float = -HALF.x
while x <= HALF.x:
var accent: bool = is_zero_approx(x)
_add_line(Vector2(x, -HALF.y), Vector2(x, HALF.y), 6.0 if accent else 2.0, Color('#8da5f3') if accent else Color('#2a2f3d'))
x += CELL
var y: float = -HALF.y
while y <= HALF.y:
var accent: bool = is_zero_approx(y)
_add_line(Vector2(-HALF.x, y), Vector2(HALF.x, y), 6.0 if accent else 2.0, Color('#8da5f3') if accent else Color('#2a2f3d'))
y += CELL
func _add_landmarks() -> void:
var colors: Array[Color] = [Color('#8eef97'), Color('#fc7f7f'), Color('#f3d48d'), Color('#8da5f3')]
var rng: RandomNumberGenerator = RandomNumberGenerator.new()
rng.seed = 11
for row in 8:
for column in 10:
var size: float = rng.randf_range(34, 78)
var block: Polygon2D = Polygon2D.new()
block.polygon = PackedVector2Array([Vector2(-size, -size) * 0.5, Vector2(size, -size) * 0.5, Vector2(size, size) * 0.5, Vector2(-size, size) * 0.5])
block.color = colors[(row + column) % colors.size()]
block.position = Vector2(-1080 + column * 240, -840 + row * 240)
block.rotation = rng.randf_range(-0.4, 0.4)
add_child(block)
var origin: Label = Label.new()
origin.text = '0, 0'
origin.add_theme_font_size_override('font_size', 28)
origin.add_theme_color_override('font_color', Color('#8da5f3'))
origin.position = Vector2(14, 6)
add_child(origin)
func _add_line(from: Vector2, to: Vector2, width: float, color: Color) -> void:
var line: Line2D = Line2D.new()
line.points = PackedVector2Array([from, to])
line.width = width
line.default_color = color
add_child(line)
func _add_hud() -> void:
var hud: CanvasLayer = CanvasLayer.new()
add_child(hud)
var title: Label = Label.new()
title.text = 'drag to pan, wheel to zoom at the pointer'
title.add_theme_font_size_override('font_size', 22)
title.add_theme_color_override('font_color', Color('#8eef97'))
title.position = Vector2(40, 34)
hud.add_child(title)
readout = Label.new()
readout.add_theme_font_size_override('font_size', 20)
readout.add_theme_color_override('font_color', Color('#f3d48d'))
readout.position = Vector2(40, 66)
hud.add_child(readout)
func _add_background() -> void:
var layer: CanvasLayer = CanvasLayer.new()
layer.layer = -1
add_child(layer)
var background: ColorRect = ColorRect.new()
background.color = Color('#12141a')
layer.add_child(background)
# ColorRect defaults to MOUSE_FILTER_STOP and would eat the click before _unhandled_input
background.mouse_filter = Control.MOUSE_FILTER_IGNORE
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)