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)

More Camera examples