Canvas Editor
A small image editor on a canvas.Canvas: move the image by dragging it and scale it with the corner handles, all driven by pointer events on the server.

package main
import (
_ "embed"
"go.wdy.de/nago/application"
"go.wdy.de/nago/presentation/core"
"go.wdy.de/nago/presentation/ui"
"go.wdy.de/nago/presentation/ui/canvas"
"go.wdy.de/nago/web/vuejs"
)
//go:embed morty_vanilla.png
var morty application.StaticBytes
const (
handleSize = 10.0 // Größe der Anfasser in Pixeln
minSize = 30.0 // Mindestgröße des Bildes in Pixeln
)
// dragMode beschreibt den aktuellen Drag-Zustand.
type dragMode int
const (
dragNone dragMode = iota
dragMove // Bild verschieben
dragTL // Skalierung: oben-links
dragTR // Skalierung: oben-rechts
dragBL // Skalierung: unten-links
dragBR // Skalierung: unten-rechts
)
// editorState hält die gesamte veränderliche Zustandsinformation.
type editorState struct {
x, y float64 // Position des Bildes (oben-links)
w, h float64 // Größe des Bildes
mode dragMode
startX, startY float64 // Zeiger-Position beim PointerDown
origX, origY float64 // Bild-Position beim PointerDown
origW, origH float64 // Bild-Größe beim PointerDown
}
// inHandle prüft, ob der Zeiger (ex, ey) innerhalb des Anfassers bei (hx, hy) liegt.
func inHandle(ex, ey, hx, hy float64) bool {
half := handleSize / 2
return ex >= hx-half && ex <= hx+half && ey >= hy-half && ey <= hy+half
}
// inRect prüft, ob der Zeiger innerhalb des Bild-Rechtecks liegt.
func inRect(s *editorState, ex, ey float64) bool {
return ex >= s.x && ex <= s.x+s.w && ey >= s.y && ey <= s.y+s.h
}
func main() {
application.Configure(func(cfg *application.Configurator) {
cfg.SetApplicationID("de.worldiety.tutorial_94")
cfg.Serve(vuejs.Dist())
mortyUrl := cfg.Resource(morty)
cfg.RootView(".", func(wnd core.Window) core.View {
const myCanvas = "morty-editor"
canvasCtx := canvas.Context2D(wnd, myCanvas)
state := &editorState{
x: 120, y: 80,
w: 160, h: 160,
}
wnd.AddInputListener(myCanvas, func(e core.InputEvent) {
// InputEventInvalidate und andere → Initialbild zeichnen
canvasCtx.LoadImage(1, mortyUrl)
cx, cy, w, h := state.x, state.y, state.w, state.h
switch e.Type {
case core.InputEventPointerDown:
// Anfasser zuerst prüfen (höhere Priorität als das Bild-Innere)
switch {
case inHandle(e.X, e.Y, cx, cy):
state.mode = dragTL
case inHandle(e.X, e.Y, cx+w, cy):
state.mode = dragTR
case inHandle(e.X, e.Y, cx, cy+h):
state.mode = dragBL
case inHandle(e.X, e.Y, cx+w, cy+h):
state.mode = dragBR
case inRect(state, e.X, e.Y):
state.mode = dragMove
default:
state.mode = dragNone
}
state.startX, state.startY = e.X, e.Y
state.origX, state.origY = state.x, state.y
state.origW, state.origH = state.w, state.h
case core.InputEventPointerMove:
if state.mode == dragNone {
return
}
dx := e.X - state.startX
dy := e.Y - state.startY
switch state.mode {
case dragMove:
state.x = state.origX + dx
state.y = state.origY + dy
case dragTL:
nw, nh := state.origW-dx, state.origH-dy
if nw >= minSize && nh >= minSize {
state.x, state.y = state.origX+dx, state.origY+dy
state.w, state.h = nw, nh
}
case dragTR:
nw, nh := state.origW+dx, state.origH-dy
if nw >= minSize && nh >= minSize {
state.y = state.origY + dy
state.w, state.h = nw, nh
}
case dragBL:
nw, nh := state.origW-dx, state.origH+dy
if nw >= minSize && nh >= minSize {
state.x = state.origX + dx
state.w, state.h = nw, nh
}
case dragBR:
nw, nh := state.origW+dx, state.origH+dy
if nw >= minSize && nh >= minSize {
state.w, state.h = nw, nh
}
}
redraw(canvasCtx, state)
case core.InputEventPointerUp, core.InputEventPointerCancel:
state.mode = dragNone
default:
redraw(canvasCtx, state)
}
},
core.InputEventInvalidate,
core.InputEventPointerDown,
core.InputEventPointerMove,
core.InputEventPointerUp,
core.InputEventPointerCancel,
)
return ui.VStack(
ui.Text("Morty-Canvas-Editor").Font(ui.Font{Size: "1.2rem", Weight: ui.HeadlineAndTitleFontWeight}),
ui.Text("Bild verschieben: ins Bild klicken und ziehen • Skalieren: Eck-Anfasser (■) ziehen").
Font(ui.Font{Size: "0.85rem"}),
canvas.Canvas(myCanvas).Frame(ui.Frame{Width: "600px", Height: "480px"}),
).Gap(ui.L8).Frame(ui.Frame{}.MatchScreen())
})
}).Run()
}
// redraw löscht den Canvas und zeichnet Bild, Auswahlrahmen und Eck-Anfasser.
func redraw(ctx canvas.TContext2D, s *editorState) {
ctx.NewList(1)
// Canvas leeren
ctx.ClearRect(0, 0, 4000, 4000)
// Morty-Bild skaliert zeichnen (5-Argument-Form via DrawImage2 mit sx=sy=sw=sh=0)
ctx.DrawImage2(1, s.x, s.y, s.w, s.h, 0, 0, 0, 0)
// Auswahlrahmen
ctx.StrokeColor("#1565C0")
ctx.LineWidth(1.5)
ctx.StrokeRect(s.x, s.y, s.w, s.h)
// 4 Eck-Anfasser
drawHandle(ctx, s.x, s.y) // oben-links
drawHandle(ctx, s.x+s.w, s.y) // oben-rechts
drawHandle(ctx, s.x, s.y+s.h) // unten-links
drawHandle(ctx, s.x+s.w, s.y+s.h) // unten-rechts
ctx.EndList()
ctx.CallList(1)
}
// drawHandle zeichnet einen ausgefüllten quadratischen Anfasser zentriert bei (hx, hy).
func drawHandle(ctx canvas.TContext2D, hx, hy float64) {
half := handleSize / 2
ctx.FillColor("#1565C0")
ctx.FillRect(hx-half, hy-half, handleSize, handleSize)
ctx.StrokeColor("#ffffff")
ctx.LineWidth(1)
ctx.StrokeRect(hx-half, hy-half, handleSize, handleSize)
}
View on GitHub ↗
· run it with go run go.wdy.de/nago/example/cmd/tutorial-94-canvas-editor@latest