no-wrap option; pivoted exponential zoom

This commit is contained in:
Ilia Denisov
2026-03-07 11:35:18 +02:00
committed by GitHub
parent 1de621c743
commit 477e656008
22 changed files with 605 additions and 81 deletions
+82 -2
View File
@@ -2,6 +2,7 @@ package client
import (
"image"
"math"
"sync"
"fyne.io/fyne/v2"
@@ -78,11 +79,89 @@ func (e *editor) updateSizes() {
e.wp.CameraZoom = e.world.CorrectCameraZoom(e.wp.CameraZoom, e.wp.ViewportWidthPx, e.wp.ViewportHeightPx)
e.world.IndexOnViewportChange(e.wp.ViewportWidthPx, e.wp.ViewportHeightPx, e.wp.CameraZoom)
e.world.ClampRenderParamsNoWrap(e.wp)
e.co.Request(*e.wp)
}
}
func (e *editor) onScrolled(s *fyne.ScrollEvent) {
vw := e.wp.ViewportWidthPx
vh := e.wp.ViewportHeightPx
if vw <= 0 || vh <= 0 {
return
}
// Cursor position in viewport pixels (Fyne units -> px by multiplying).
cxPx := int(float32(s.Position.X) * e.canvasScale)
cyPx := int(float32(s.Position.Y) * e.canvasScale)
if cxPx < 0 {
cxPx = 0
} else if cxPx > vw {
cxPx = vw
}
if cyPx < 0 {
cyPx = 0
} else if cyPx > vh {
cyPx = vh
}
oldZoom := e.wp.CameraZoom
// Exponential zoom:
// - Each "notch" multiplies zoom by a fixed factor.
// - Using DY directly as exponent gives smooth trackpad behavior too.
//
// Tune base:
// base=1.10 => ~10% per wheel step (if DY≈1 per step)
// base=1.05 => ~5% per step
//
// In user settings, better store on percents userZoomLevelPercent = (0,100]: base = 1 + (userZoomLevelPercent) / 10
const base = 1.005
// Negative DY => zoom out, positive DY => zoom in (depending on platform settings).
// If you want inverted direction, negate float64(s.Scrolled.DY).
delta := float64(s.Scrolled.DY)
newZoom := oldZoom * math.Pow(base, delta)
// Clamp/correct (min/max + prevent wrap if needed).
newZoom = e.world.CorrectCameraZoom(newZoom, vw, vh)
if newZoom == oldZoom {
return
}
oldZoomFp, err := world.CameraZoomToWorldFixed(oldZoom)
if err != nil {
return
}
newZoomFp, err := world.CameraZoomToWorldFixed(newZoom)
if err != nil {
return
}
newCamX, newCamY := world.PivotZoomCameraNoWrap(
e.wp.CameraXWorldFp, e.wp.CameraYWorldFp,
vw, vh,
cxPx, cyPx,
oldZoomFp, newZoomFp,
)
e.wp.CameraZoom = newZoom
e.wp.CameraXWorldFp = newCamX
e.wp.CameraYWorldFp = newCamY
e.world.IndexOnViewportChange(vw, vh, e.wp.CameraZoom)
// No-wrap clamp to avoid "detaching" from borders.
e.world.ClampRenderParamsNoWrap(e.wp)
// Zoom changes are best done as full redraw.
e.world.ForceFullRedrawNext()
e.co.Request(*e.wp)
}
func (e *editor) onDragged(ev *fyne.DragEvent) {
e.pan.Dragged(ev)
}
@@ -120,6 +199,7 @@ func NewEditor() *editor {
CameraXWorldFp: 300 * world.SCALE,
CameraYWorldFp: 300 * world.SCALE,
// Viewport sizes and margins will be filled from draw(w,h).
Options: &world.RenderOptions{DisableWrapScroll: false},
},
canvasScale: 1.0,
}
@@ -132,7 +212,7 @@ func NewEditor() *editor {
return e.draw(wPx, hPx)
})
e.canvas = newInteractiveRaster(e, e.raster, e.onMapLayout, e.onDragged, e.onDradEnd)
e.canvas = newInteractiveRaster(e, e.raster, e.onMapLayout, e.onScrolled, e.onDragged, e.onDradEnd)
e.pan = NewPanController(e)
// Wire coalescer: it schedules raster.Refresh() on UI thread and renders once per draw call.