Na předchozí dvojici článků o knihovně gg [1] [2] dnes navážeme. Ukážeme si totiž, jakým způsobem lze otevřít okno a přes gg provést vykreslení celé 2D scény do plochy tohoto okna. Samotná knihovna gg takové operace sice nepodporuje, ovšem to ve skutečnosti nevadí, protože dokáže spolupracovat s knihovnou gogpu patřící do stejně pojmenovaného projektu.
Co se dozvíte v článku
- Události a callback funkce, které na ně reagují
- Události Draw a Close
- První demonstrační příklad: aplikace s prázdným oknem
- Vykreslení pozadí zvolené barvy v ploše okna
- Úplný zdrojový kód demonstračního příkladu, který inicializuje pozadí okna
- Povolení kontinuálního překreslování okna
- Využití canvasu při kreslení do okna: vykreslení obrysu obrazce zvolenou barvou
- Vykreslení hvězdy do plochy okna: základ pro navazující příklady
- Další základ pro další příklady: vykreslení Bézierových křivek i s jejich řídicími body
- Reakce aplikace na stisk klávesy či více kláves
- Zjištění stisku kláves použitých pro ovládání aplikace
- Demonstrační příklad: rotace hvězdy s využitím kurzorových kláves
- Demonstrační příklad: změna barvy hvězdy dalšími klávesami
- Přečtení aktuální pozice kurzoru myši popř. detekce stisku tlačítek myši
- Demonstrační příklad: posun řídicích bodů Bézierových kubických křivek
- Demonstrační příklad: jednoduchý kreslicí program
- Vylepšení: kreslení „gumové čáry“ sledující pohyb kurzoru
- Repositář s demonstračními příklady
- Odkazy na Internetu
To znamená, že ve všech demonstračních příkladech, které si dnes ukážeme, bude importována mj. i tato knihovna:
"github.com/gogpu/gogpu"
Aplikaci s grafickým uživatelským rozhraním je možné v tom nejjednodušším případě inicializovat konstruktorem NewApp, kterému se předává struktura s konfigurací aplikace:
func NewApp(config Config) *App
NewApp creates a new application with the given configuration.
Při inicializaci struktury Config můžeme využít konstruktor DefaultConfig vracející, jak název napovídá, výchozí konfiguraci:
func DefaultConfig() Config
DefaultConfig returns a sensible default configuration. By default,
uses event-driven rendering (0% CPU when idle). For game loops, use
WithContinuousRender(true).
V tom nejjednodušším případě tedy může inicializace aplikace vypadat takto:
// inicializace aplikace s GUI app := gogpu.NewApp(gogpu.DefaultConfig())
Samotné spuštění aplikace (což vede k zobrazení okna atd.) pak vypadá následovně:
// spuštění aplikace app.Run()
Pro úpravu výchozí konfigurace slouží řada metod objektu Config, které vrací upravenou podobu tohoto objektu. To umožňuje řetězit volání těchto metod, což si ihned ukážeme:
func (c Config) WithAppName(name string) Config func (c Config) WithBackend(backend types.BackendType) Config func (c Config) WithContinuousRender(continuous bool) Config func (c Config) WithFrameless(frameless bool) Config func (c Config) WithFullscreen() Config func (c Config) WithGraphicsAPI(api types.GraphicsAPI) Config func (c Config) WithHeaderAlignment(alignment HeaderAlignment) Config func (c Config) WithIcon(img image.Image) Config func (c Config) WithMaxSize(width, height int) Config func (c Config) WithMinSize(width, height int) Config func (c Config) WithPowerPreference(pref gputypes.PowerPreference) Config func (c Config) WithRenderMode(mode RenderMode) Config func (c Config) WithResizable(resizable bool) Config func (c Config) WithSize(width, height int) Config func (c Config) WithTabbingIdentifier(id string) Config func (c Config) WithTabbingMode(mode TabbingMode) Config func (c Config) WithTitle(title string) Config func (c Config) WithVSync(vsync bool) Config
Příkladem může být inicializace aplikace s oknem o rozměrech 400×300 pixelů a s nastaveným titulkem:
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
Události a callback funkce, které na ně reagují
Vývoj aplikací s interaktivním grafickým uživatelským rozhraním z pochopitelných důvodů vyžaduje použití odlišných technik, než je tomu u vývoje dávkových souborů či jednoduchých skriptů. V mnoha frameworcích určených pro tvorbu grafického uživatelského rozhraní se z tohoto důvodu setkáme s konceptem událostí (event) a reakcí na vznik těchto událostí. V případě knihovny gogpu se na událost (určitého typu) reaguje tak, že se zavolá nějaká callback funkce. Příslušné funkce musí být nejdříve zaregistrovány, a to konkrétně v metodách struktury získané výše zmíněným konstruktorem gogpu.NewApp.
Události Draw a Close
V první sadě demonstračních příkladů si vystačíme se dvěma typy událostí:
- Draw – požadavek na překreslení obsahu okna aplikace
- Close – požadavek na ukončení aplikace
Na tyto události můžeme (vhodným způsobem) reagovat ve funkcích, které jsou zaregistrovány metodami nazvanými OnDraw a OnClose:
func (a *App) OnDraw(fn func(*Context)) *App
OnDraw sets the callback for rendering each frame. The Context is only
valid during the callback. If the primary window has been created (after Run
starts), its draw callback is also updated to stay in sync.
func (a *App) OnClose(fn func()) *App
OnClose sets the callback invoked when the application is shutting down,
before the GPU renderer is destroyed. Use this to release GPU resources
(e.g., ggcanvas.Canvas) that depend on the renderer being alive.
The callback runs on the render thread.
Jednodušší z těchto metod je OnClose, protože se této metodě předává callback funkce bez parametrů i bez návratové hodnoty. V tom nejjednodušším případě příslušná callback funkce pouze zavolá metodu CloseAccelerator (což je asi v tomto případě výhodnější, než se spoléhat na defer). Registrace příslušné callback funkce může vypadat takto:
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
gg.CloseAccelerator()
})
Callback funkci volané v případě, že je nutné překreslit okno, se předává parametr typu gogpu.Context, který – jak dále uvidíme – můžeme využít podobně jako grafický kontext v knihovně gg:
type Context struct {
// Has unexported fields.
}
Context provides drawing operations for a single frame. It is only valid
during the OnDraw callback and should not be stored.
In multi-window mode, each Context targets a specific window surface.
The surface field points to the target RenderTarget; if nil, the renderer's
primary surface is used (single-window backward compat).
I když to není zcela korektní, je možné ponechat tělo této callback funkce prázdné popř. do něj pouze přidat výpis nějaké zprávy na standardní výstup:
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
fmt.Println("Redraw....")
})
První demonstrační příklad: aplikace s prázdným oknem
Konečně se dostáváme ke zdrojovému kódu dnešního prvního demonstračního příkladu. Po jeho překladu a spuštění by se mělo vykreslit okno, jehož plocha může mít v podstatě libovolný obsah (není totiž překreslena). Typicky je možné v ploše okna vidět původní obsah obrazovky:
Obrázek 1: Plocha okna se nepřekresluje a proto vidíme původní obsah desktopu pod oknem.
Úplný zdrojový kód tohoto demonstračního příkladu vypadá následovně:
package main
import (
"fmt"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gogpu"
)
func main() {
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
fmt.Println("Redraw....")
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
Vykreslení pozadí zvolené barvy v ploše okna
Aby se aplikace chovala zcela korektně, musí při požadavku na překreslení okna skutečně toto překreslení provést. K tomuto účelu si připravíme pomocnou funkci nazvanou renderFrame (protože vlastně vždy vykreslíme jeden snímek okna). Této funkci se bude předávat grafický kontext a taktéž šířka a výška plochy, která se má překreslit:
func renderFrame(dc *gg.Context, width, height int) {
...
...
...
}
Grafický kontext již dobře známe z popisu knihovny gg. Můžeme tedy v kontextu nastavit barvu vykreslování (RGB=000) a vykreslit vyplněný obdélník o stejných rozměrech, jaké má plocha okna:
func renderFrame(dc *gg.Context, width, height int) {
// barva pozadí
dc.SetRGBA(0.0, 0.0, 0.0, 1)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
}
Zbývá nám funkci renderFrame zavolat ve chvíli, kdy vznikne událost typu Draw. Teoreticky může celý kód vypadat následovně – získáme rozměry kreslicího plátna, zavoláme metodu Draw, které se předá reference na naši funkci renderFrame a nakonec se zavolá druhá metoda Render, která vykreslení dokončí:
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch)
})
canvas.Render(dc.RenderTarget())
})
Ovšem ve skutečnosti to zase tak jednoduché není, protože musíme nějakým způsobem získat objekt typu ggcanvas.Canvas. Teoreticky je možné neustále, tedy při každém překreslení okna, canvas získávat tímto kódem:
provider := app.GPUContextProvider() w, h := dc.Width(), dc.Height() canvas, _ = ggcanvas.New(provider, w, h)
To ovšem znamená neustálé inicializace objektu, zatížení GC atd. Proto je výhodnější mít ve funkci main připravenou proměnnou typu ggcanvas.Canvas, která se naplní před prvním překreslením okna a poté již není nutné opakovat její inicializaci:
var canvas *ggcanvas.Canvas
Callback funkce zaregistrovaná přes app.OnDraw je tedy v praxi poněkud komplikovaná (ovšem již nebude nutné ji později měnit):
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch)
})
canvas.Render(dc.RenderTarget())
})
Obrázek 2: Plocha okna se korektně překresluje černou barvou.
Úplný zdrojový kód demonstračního příkladu, který inicializuje pozadí okna
Úplný zdrojový kód dnešního druhého demonstračního příkladu, který korektně inicializuje pozadí okna (a samozřejmě provede i jeho překreslení, pokud je to nutné), vypadá následovně:
package main
import (
"fmt"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
)
func main() {
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
var canvas *ggcanvas.Canvas
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
fmt.Println("Redraw....")
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func renderFrame(dc *gg.Context, width, height int) {
// barva pozadí
dc.SetRGBA(0.0, 0.0, 0.0, 1)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
}
Povolení kontinuálního překreslování okna
Při inicializaci aplikace s grafickým uživatelským rozhraním jsme prozatím postupovali následovně:
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
Takto inicializovaná aplikace vyvolávala událost typu Draw jen tehdy, pokud bylo skutečně nutné provést překreslení okna (nebo jeho části). Ovšem existují situace, ve kterých je například vyžadováno zobrazení animace, obnovení obsahu nějakého dashboardu atd. V takových případech může být výhodnější vynutit si kontinuální překreslování okna. To je zajištěno zavoláním metody WithContinuousRender:
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height).
WithContinuousRender(true))
Obrázek 3: Plocha okna se periodicky překresluje černou barvou.
Pokud si přeložíte a spustíte následující demonstrační příklad s výše uvedenou úpravou, bude sice obsah okna zdánlivě stále stejný, ale při pohledu na terminál lze snadno zjistit, že se neustále vypisuje zpráva „Redraw…“ a tudíž se neustále volá obslužná callback funkce pro událost typu Draw:
package main
import (
"fmt"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
)
func main() {
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height).
WithContinuousRender(true))
var canvas *ggcanvas.Canvas
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
fmt.Println("Redraw....")
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func renderFrame(dc *gg.Context, width, height int) {
// barva pozadí
dc.SetRGBA(0.0, 0.0, 0.0, 1)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
}
Využití canvasu při kreslení do okna: vykreslení obrysu obrazce zvolenou barvou
Vzhledem k tomu, že je kreslení do plochy okna prováděno přes canvas, můžeme upravit všechny demonstrační příklady z úvodního článku o knihovně gg do podoby, ve které se obrázek namísto uložení na disk vykreslí do okna. Pochopitelně zde všechny tyto příklady nebudou přepsány, ale ukážeme si alespoň tři z nich (protože na ně navážeme v dalším textu).
Nejdříve je (v naprosté většině případů) nutné překreslit celou plochu okna konstantní barvou. Posléze do plochy okna vykreslíme obrys obdélníku, jehož hrany budou od okrajů okna vzdáleny o čtyřicet pixelů:
func renderFrame(dc *gg.Context, width, height int) {
// barva pozadí
dc.SetRGB(0.9, 0.9, 0.7)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
const border = 40
dc.DrawRectangle(border, border, float64(width)-border*2, float64(height)-border*2)
dc.SetRGB(0.0, 0.0, 0.0)
// vykreslení obrysu
dc.Stroke()
}
Obrázek 4: Vykreslení grafického obrazce do plochy okna.
Úplný zdrojový kód takto upraveného demonstračního příkladu vypadá následovně:
package main
import (
"fmt"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
)
func main() {
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
var canvas *ggcanvas.Canvas
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
fmt.Println("Redraw....")
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func renderFrame(dc *gg.Context, width, height int) {
// barva pozadí
dc.SetRGB(0.9, 0.9, 0.7)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
const border = 40
dc.DrawRectangle(border, border, float64(width)-border*2, float64(height)-border*2)
dc.SetRGB(0.0, 0.0, 0.0)
// vykreslení obrysu
dc.Stroke()
}
Vykreslení hvězdy do plochy okna: základ pro navazující příklady
V dalším příkladu, který bude později rozšířen, se ve funkci renderFrame nejprve vyplní plocha okna a následně se do středu vykreslí pěticípá hvězda. Prakticky stejný příklad byl ukázán již v předchozím článku, takže si jen v krátkosti ukažme, jak je vykreslení hvězdy realizováno:
customColor := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor))
dc.SetFillRule(gg.FillRuleEvenOdd)
radius := float64(height) / 3
w := float64(width)
h := float64(height)
// začátek vykreslování cesty
dc.MoveTo(w/2, h/2-radius)
for i := range 4 {
angle := float64(i+1) * 3.0 * 360 / 5.0
x := radius * math.Sin(angle*math.Pi/180.0)
y := radius * math.Cos(angle*math.Pi/180.0)
dc.LineTo(w/2+x, h/2-y)
}
dc.ClosePath()
// vyplnění obrysu
dc.Fill()
Obrázek 5: Vykreslení grafického obrazce (s výplní) do plochy okna.
Následuje výpis úplného zdrojového kódu tohoto příkladu:
package main
import (
"fmt"
"math"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
)
func main() {
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
var canvas *ggcanvas.Canvas
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
fmt.Println("Redraw....")
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func renderFrame(dc *gg.Context, width, height int) {
// barva pozadí
dc.SetRGB(0.9, 0.9, 0.7)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
customColor := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor))
dc.SetFillRule(gg.FillRuleEvenOdd)
radius := float64(height) / 3
w := float64(width)
h := float64(height)
// začátek vykreslování cesty
dc.MoveTo(w/2, h/2-radius)
for i := range 4 {
angle := float64(i+1) * 3.0 * 360 / 5.0
x := radius * math.Sin(angle*math.Pi/180.0)
y := radius * math.Cos(angle*math.Pi/180.0)
dc.LineTo(w/2+x, h/2-y)
}
dc.ClosePath()
// vyplnění obrysu
dc.Fill()
}
Další základ pro další příklady: vykreslení Bézierových křivek i s jejich řídicími body
Posledním „zahřívacím“ příkladem, který v rámci dalších kapitol rozšíříme, je příklad, který do plochy okna vykreslí dvojici Bézierových kubických křivek i s jejich řídicími body. Připomeňme si, že Bézierovy kubické křivky se do cesty (path) vloží metodou CubicTo. První bod křivky je již součástí cesty a tudíž se nezapisuje:
dc.CubicTo(x1, y1, x2, y2, x3, y3)
Obrázek 6: Vykreslení složitějšího grafického obrazce do plochy okna.
Strukturu celého příkladu již známe z úvodní dvojice článků, takže si jen v krátkosti uvedeme celý jeho zdrojový kód:
package main
import (
"fmt"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
)
func drawCubicBezier(dc *gg.Context,
x0 float64, y0 float64, x1 float64, y1 float64,
x2 float64, y2 float64, x3 float64, y3 float64) {
dc.SetRGBA(1.0, 0.5, 0.5, 1.0)
dc.DrawLine(x0, y0, x1, y1)
dc.Stroke()
dc.DrawLine(x1, y1, x2, y2)
dc.Stroke()
dc.DrawLine(x2, y2, x3, y3)
dc.Stroke()
dc.SetRGBA(0.0, 0.0, 0.0, 1.0)
dc.MoveTo(x0, y0)
dc.CubicTo(x1, y1, x2, y2, x3, y3)
dc.Stroke()
dc.SetRGBA(0.2, 0.2, 1.0, 1.0)
dc.DrawCircle(x0, y0, 3)
dc.Stroke()
dc.DrawCircle(x1, y1, 3)
dc.Stroke()
dc.DrawCircle(x2, y2, 3)
// vykreslení
dc.Stroke()
dc.DrawCircle(x2, y2, 3)
// vykreslení
dc.Stroke()
dc.DrawCircle(x3, y3, 3)
// vykreslení
dc.Stroke()
}
func main() {
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
var canvas *ggcanvas.Canvas
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
fmt.Println("Redraw....")
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func renderFrame(dc *gg.Context, width, height int) {
// barva pozadí
dc.SetRGB(0.9, 0.9, 0.7)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
// vykreslení cest
drawCubicBezier(dc, 50, 220, 50, 50, 160, 220, 160, 50)
drawCubicBezier(dc, 270, 220, 320, 100, 270, 100, 320, 220)
}
Reakce aplikace na stisk klávesy či více kláves
Aplikace s grafickým uživatelským rozhraním jsou většinou interaktivní, tj. musí nějakým způsobem reagovat na stisk kláves, posun myši, stisk tlačítka myši apod. Nejprve se zaměříme na detekci stisku kláves. Při stisku klávesy (kupodivu) nevznikne událost, protože se předpokládá, že čtení stavu klávesnice bude probíhat v rámci zpracování události Update, tedy v této funkci:
app.OnUpdate(func(dt float64) {
...
...
...
}
Objekt (struktura) typu App poskytuje metodu Input vracející ukazatel na strukturu State, z níž lze vyčíst informaci a stisknutých klávesách (ale i o dalších typech uživatelského vstupu):
func (a *App) Input() *input.State
Input returns the input state for Ebiten-style polling. This enables
game-loop style input handling with KeyPressed, KeyJustPressed, etc.
Funkce a metody struktury Input:
package input // import "github.com/gogpu/gogpu/input"
type State struct {
// Has unexported fields.
}
State holds the current input state. All methods are thread-safe.
func New() *State
func (s *State) Keyboard() *KeyboardState
func (s *State) Mouse() *MouseState
func (s *State) Update()
Nás bude v této chvíli nejvíce zajímat metoda Keyboard vracející další strukturu KeyboardState:
package input // import "github.com/gogpu/gogpu/input"
type KeyboardState struct {
// Has unexported fields.
}
KeyboardState holds keyboard input state. All methods are thread-safe.
func (k *KeyboardState) AnyPressed() bool
func (k *KeyboardState) JustPressed(key Key) bool
func (k *KeyboardState) JustReleased(key Key) bool
func (k *KeyboardState) Modifier(mod Modifier) bool
func (k *KeyboardState) Pressed(key Key) bool
func (k *KeyboardState) SetKey(key Key, pressed bool)
func (k *KeyboardState) UpdateFrame()
Z názvů poskytovaných metod je zřejmé, že stisk nějaké klávesy lze zjistit přes metodu Pressed popř. JustPressed. Záleží na tom, zda potřebujeme reagovat jen na událost vznikající při stisku klávesy (tedy jedenkrát), nebo zda již stisknutá klávesa povede k opakování nějaké operace.
Zjištění stisku kláves použitých pro ovládání aplikace
Poměrně jednoduchým postupem lze zajistit, aby aplikace reagovala na stisk kurzorových kláves šipka doleva a šipka doprava. Pokud bude jedna z těchto kláves stlačena, bude se hvězda otáčet příslušným směrem. Povšimněte si, že v tomto případě musíme reagovat nikoli přímo na stisk klávesy (směrem dolů), ale na její stav – pokud totiž kurzorovou šipku podržíme, měla by se hvězda neustále otáčet. Z tohoto důvodu bude stav kurzorových šipek zjišťován metodami Pressed a nikoli JustPressed:
// callback metoda volaná v případě, že se má scéna překreslit
app.OnUpdate(func(dt float64) {
inp := app.Input()
const rotationDelta = 1.5
// reakce na stisk šipky doleva
if inp.Keyboard().Pressed(input.KeyLeft) {
rotation -= rotationDelta
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk šipky doprava
if inp.Keyboard().Pressed(input.KeyRight) {
rotation += rotationDelta
// vynucení překreslení okna
app.RequestRedraw()
}
})
Samotná programová realizace otáčení může být postavena na manipulaci s transformační maticí (RotateAbout) nebo nepatrnou úpravou výpočtů pro zjištění souřadnic vrcholů hvězdy. Pro zajímavost použijeme druhou možnost:
// začátek vykreslování cesty
dc.MoveTo(
w/2+radius*math.Sin(rad(rotation)),
h/2-radius*math.Cos(rad(rotation)))
// další segmenty cesty
for i := range 4 {
angle := float64(i+1)*3.0*360/5.0 + rotation
x := radius * math.Sin(rad(angle))
y := radius * math.Cos(rad(angle))
dc.LineTo(w/2+x, h/2-y)
}
// uzavření cesty
dc.ClosePath()
Obrázek 7: Rotace hvězdy stiskem kurzorových kláves.
Obrázek 8: Rotace hvězdy stiskem kurzorových kláves.
Demonstrační příklad: rotace hvězdy s využitím kurzorových kláves
Úplný zdrojový kód demonstračního příkladu, který umožní rotaci hvězdy umístěné do středu okna s využitím kurzorových kláves, vypadá následovně. Příklad vlastně ani není příliš dlouhý – má jen cca 115 řádků i s komentáři:
package main
import (
"fmt"
"math"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
"github.com/gogpu/gogpu/input"
)
func main() {
// rozměry okna
const width = 400
const height = 300
rotation := 0.0
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
var canvas *ggcanvas.Canvas
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
fmt.Println("Redraw....")
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch, rotation)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda volaná v případě, že se má scéna překreslit
app.OnUpdate(func(dt float64) {
inp := app.Input()
const rotationDelta = 1.5
// reakce na stisk šipky doleva
if inp.Keyboard().Pressed(input.KeyLeft) {
rotation -= rotationDelta
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk šipky doprava
if inp.Keyboard().Pressed(input.KeyRight) {
rotation += rotationDelta
// vynucení překreslení okna
app.RequestRedraw()
}
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func rad(angle float64) float64 {
return angle * math.Pi / 180.0
}
func renderFrame(dc *gg.Context, width, height int, rotation float64) {
// barva pozadí
dc.SetRGB(0.9, 0.9, 0.7)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
customColor := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor))
dc.SetFillRule(gg.FillRuleEvenOdd)
radius := float64(height) / 3
w := float64(width)
h := float64(height)
// začátek vykreslování cesty
dc.MoveTo(
w/2+radius*math.Sin(rad(rotation)),
h/2-radius*math.Cos(rad(rotation)))
// další segmenty cesty
for i := range 4 {
angle := float64(i+1)*3.0*360/5.0 + rotation
x := radius * math.Sin(rad(angle))
y := radius * math.Cos(rad(angle))
dc.LineTo(w/2+x, h/2-y)
}
// uzavření cesty
dc.ClosePath()
// vyplnění obrysu
dc.Fill()
}
Demonstrační příklad: změna barvy hvězdy dalšími klávesami
Příklad lze podobným postupem libovolně rozšiřovat. Například je možné přidat změnu barvy hvězdy s využitím kláves Q, W, E a A, S, D – pro každou barvovou složku se používá dvojice pod sebou umístěných kláves:
Obrázek 9: Interaktivní modifikace barvy hvězdy.
Tento příklad uvádím z toho důvodu, aby bylo zřejmé, jak objem zdrojového kódu narůstá (například v porovnání s odlišným přístupem použitým v knihovně SDL). V Go (a GoGPU) musíme na stisk každé klávesy reagovat ve zvláštním bloku:
// reakce na stisk klávesy Q
if keyboard.Pressed(input.KeyQ) {
// změna červené barvové složky
red = math.Min(1.0, red+colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy W
if keyboard.Pressed(input.KeyW) {
// změna zelené barvové složky
green = math.Min(1.0, green+colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy E
if keyboard.Pressed(input.KeyE) {
// změna modré barvové složky
blue = math.Min(1.0, blue+colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy A
if keyboard.Pressed(input.KeyA) {
// změna červené barvové složky
red = math.Max(0.0, red-colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy S
if keyboard.Pressed(input.KeyS) {
// změna zelené barvové složky
green = math.Max(0.0, green-colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy D
if keyboard.Pressed(input.KeyD) {
// změna modré barvové složky
blue = math.Max(0.0, blue-colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
Přepis na konstrukci switch-case je kratší jen o několik řádků (ale pravděpodobně čitelnější):
switch {
// reakce na stisk šipky doleva
case keyboard.Pressed(input.KeyLeft):
rotation -= rotationDelta
// vynucení překreslení okna
app.RequestRedraw()
// reakce na stisk šipky doprava
case keyboard.Pressed(input.KeyRight):
rotation += rotationDelta
// vynucení překreslení okna
app.RequestRedraw()
// reakce na stisk klávesy Q
case keyboard.Pressed(input.KeyQ):
// změna červené barvové složky
red = math.Min(1.0, red+colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
// reakce na stisk klávesy W
case keyboard.Pressed(input.KeyW):
// změna zelené barvové složky
green = math.Min(1.0, green+colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
// reakce na stisk klávesy E
case keyboard.Pressed(input.KeyE):
// změna modré barvové složky
blue = math.Min(1.0, blue+colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
// reakce na stisk klávesy A
case keyboard.Pressed(input.KeyA):
// změna červené barvové složky
red = math.Max(0.0, red-colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
// reakce na stisk klávesy S
case keyboard.Pressed(input.KeyS):
// změna zelené barvové složky
green = math.Max(0.0, green-colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
// reakce na stisk klávesy D
case keyboard.Pressed(input.KeyD):
// změna modré barvové složky
blue = math.Max(0.0, blue-colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
// reakce na stisk klávesy Escape
case inp.Keyboard().Pressed(input.KeyEscape):
app.Quit()
Úplný zdrojový kód tohoto demonstračního příkladu vypadá následovně:
package main
import (
"fmt"
"math"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
"github.com/gogpu/gogpu/input"
)
func main() {
// rozměry okna
const width = 400
const height = 300
rotation := 0.0
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
var canvas *ggcanvas.Canvas
var red = 0.3
var green = 0.7
var blue = 0.9
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch, rotation, red, green, blue)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda volaná v případě, že se má scéna překreslit
app.OnUpdate(func(dt float64) {
inp := app.Input()
const rotationDelta = 1.5
const colorDelta = 0.02
keyboard := inp.Keyboard()
// reakce na stisk šipky doleva
if keyboard.Pressed(input.KeyLeft) {
rotation -= rotationDelta
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk šipky doprava
if keyboard.Pressed(input.KeyRight) {
rotation += rotationDelta
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy Q
if keyboard.Pressed(input.KeyQ) {
// změna červené barvové složky
red = math.Min(1.0, red+colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy W
if keyboard.Pressed(input.KeyW) {
// změna zelené barvové složky
green = math.Min(1.0, green+colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy E
if keyboard.Pressed(input.KeyE) {
// změna modré barvové složky
blue = math.Min(1.0, blue+colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy A
if keyboard.Pressed(input.KeyA) {
// změna červené barvové složky
red = math.Max(0.0, red-colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy S
if keyboard.Pressed(input.KeyS) {
// změna zelené barvové složky
green = math.Max(0.0, green-colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy D
if keyboard.Pressed(input.KeyD) {
// změna modré barvové složky
blue = math.Max(0.0, blue-colorDelta)
// vynucení překreslení okna
app.RequestRedraw()
}
// reakce na stisk klávesy Escape
if inp.Keyboard().Pressed(input.KeyEscape) {
app.Quit()
}
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func rad(angle float64) float64 {
return angle * math.Pi / 180.0
}
func renderFrame(dc *gg.Context, width, height int, rotation, red, green, blue float64) {
// barva pozadí
dc.SetRGB(0.9, 0.9, 0.7)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
// výpočet barvy hvězdy
customColor := gg.RGB(red, green, blue)
dc.SetFillBrush(gg.Solid(customColor))
dc.SetFillRule(gg.FillRuleEvenOdd)
radius := float64(height) / 3
w := float64(width)
h := float64(height)
// začátek vykreslování cesty
dc.MoveTo(
w/2+radius*math.Sin(rad(rotation)),
h/2-radius*math.Cos(rad(rotation)))
// další segmenty cesty
for i := range 4 {
angle := float64(i+1)*3.0*360/5.0 + rotation
x := radius * math.Sin(rad(angle))
y := radius * math.Cos(rad(angle))
dc.LineTo(w/2+x, h/2-y)
}
// uzavření cesty
dc.ClosePath()
// vyplnění obrysu
dc.Fill()
}
Přečtení aktuální pozice kurzoru myši popř. detekce stisku tlačítek myši
Poměrně jednoduchá a přímočará je práce s myší. Při obsluze události Update získáme objekt (strukturu) typu MouseState následujícím způsobem:
inp := app.Input() mouseState := inp.Mouse()
Tato struktura nabízí sadu metod, které dokážou zjistit pozici kurzoru myši, vypočítat relativní pohyb myši, zjistit stav tlačítek myši atd. Popis struktury MouseState lze pochopitelně získat v nápovědě:
type MouseState struct {
// Has unexported fields.
}
MouseState holds mouse input state. All methods are thread-safe.
func (m *MouseState) Delta() (dx, dy float32)
func (m *MouseState) JustPressed(button MouseButton) bool
func (m *MouseState) JustReleased(button MouseButton) bool
func (m *MouseState) Position() (x, y float32)
func (m *MouseState) Pressed(button MouseButton) bool
func (m *MouseState) Scroll() (x, y float32)
func (m *MouseState) SetButton(button MouseButton, pressed bool)
func (m *MouseState) SetPosition(x, y float32)
func (m *MouseState) SetScroll(x, y float32)
func (m *MouseState) UpdateFrame()
func (m *MouseState) X() float32
func (m *MouseState) Y() float32
V navazujících příkladech nám budou postačovat metody Pressed, X a Y.
Demonstrační příklad: posun řídicích bodů Bézierových kubických křivek
V předchozích kapitolách byl ukázán příklad, který vykreslil dvojici Bézierových kubických křivek. Relativně snadno lze do tohoto kódu doimplementovat editaci těchto křivek, která se provádí posunem řídicích bodů. Po stisku levého tlačítka myši se získají souřadnice kurzoru myši a následně se nalezne nejbližší řídicí bod. Tento bod je posunut na pozici kurzoru (to je sice značně primitivní způsob, ovšem je ho možné realizovat poměrně přímočarým způsobem:
func findNearestControlPoint(controlPoints []Point, x, y float64) int {
nearestIndex := 0
nearestDistance := math.MaxFloat64
for i, point := range controlPoints {
distance := math.Hypot(point.X-x, point.Y-y)
if distance < nearestDistance {
nearestDistance = distance
nearestIndex = i
}
}
return nearestIndex
}
Obrázek 10: Interaktivní změna tvaru Bézierových křivek.
Obrázek 11: Interaktivní změna tvaru Bézierových křivek.
Opět následuje úplný výpis zdrojového kódu tohoto demonstračního příkladu:
package main
import (
"fmt"
"math"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
"github.com/gogpu/gogpu/input"
)
type Point struct {
X float64
Y float64
}
func drawCubicBezier(dc *gg.Context,
p0, p1, p2, p3 Point) {
dc.SetRGBA(1.0, 0.5, 0.5, 1.0)
dc.DrawLine(p0.X, p0.Y, p1.X, p1.Y)
dc.Stroke()
dc.DrawLine(p1.X, p1.Y, p2.X, p2.Y)
dc.Stroke()
dc.DrawLine(p2.X, p2.Y, p3.X, p3.Y)
dc.Stroke()
dc.SetRGBA(0.0, 0.0, 0.0, 1.0)
dc.MoveTo(p0.X, p0.Y)
dc.CubicTo(p1.X, p1.Y, p2.X, p2.Y, p3.X, p3.Y)
dc.Stroke()
dc.SetRGBA(0.2, 0.2, 1.0, 1.0)
dc.DrawCircle(p0.X, p0.Y, 3)
dc.Stroke()
dc.DrawCircle(p1.X, p1.Y, 3)
dc.Stroke()
dc.DrawCircle(p2.X, p2.Y, 3)
// vykreslení
dc.Stroke()
dc.DrawCircle(p2.X, p2.Y, 3)
// vykreslení
dc.Stroke()
dc.DrawCircle(p3.X, p3.Y, 3)
// vykreslení
dc.Stroke()
}
func main() {
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
var canvas *ggcanvas.Canvas
// všechny řídicí body obou Bézierových kubik
controlPoints := []Point{
Point{50, 220},
Point{50, 50},
Point{160, 220},
Point{160, 50},
Point{270, 220},
Point{320, 100},
Point{270, 100},
Point{320, 220}}
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch, controlPoints)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda volaná v případě, že se má scéna překreslit
app.OnUpdate(func(dt float64) {
inp := app.Input()
// reakce na stisk levého tlačítka myši
if inp.Mouse().Pressed(input.MouseButtonLeft) {
x, y := inp.Mouse().Position()
i := findNearestControlPoint(controlPoints, float64(x), float64(y))
// nastavení nových souřadnic
controlPoints[i].X = float64(x)
controlPoints[i].Y = float64(y)
// vynucení překreslení okna
app.RequestRedraw()
}
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func renderFrame(dc *gg.Context, width, height int, cp []Point) {
// barva pozadí
dc.SetRGB(0.9, 0.9, 0.7)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
// vykreslení cest
drawCubicBezier(dc, cp[0], cp[1], cp[2], cp[3])
drawCubicBezier(dc, cp[4], cp[5], cp[6], cp[7])
}
func findNearestControlPoint(controlPoints []Point, x, y float64) int {
nearestIndex := 0
nearestDistance := math.MaxFloat64
for i, point := range controlPoints {
distance := math.Hypot(point.X-x, point.Y-y)
if distance < nearestDistance {
nearestDistance = distance
nearestIndex = i
}
}
return nearestIndex
}
Demonstrační příklad: jednoduchý kreslicí program
Snad ještě jednodušší je implementace jednoduchého kreslicího programu. Levým tlačítkem myši se postupně vytváří kresba složená z na sebe navazujících úseček. Koncové body úseček jsou ukládány do řezu:
// body spojené úsečkami
points := []Point{}
Stiskem levého tlačítka myši se do řezu vloží další bod a provede se překreslení celé scény:
// reakce na stisk levého tlačítka myši
if inp.Mouse().Pressed(input.MouseButtonLeft) {
x, y := inp.Mouse().Position()
point := Point{X: float64(x), Y: float64(y)}
points = append(points, point)
// vynucení překreslení okna
app.RequestRedraw()
}
Obrázek 12: Postupné kreslení domečku v grafickém editoru.
Obrázek 13: Postupné kreslení domečku v grafickém editoru.
Úplný zdrojový kód kreslicího programu vypadá takto:
package main
import (
"fmt"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
"github.com/gogpu/gogpu/input"
)
type Point struct {
X float64
Y float64
}
func drawPath(dc *gg.Context,
points []Point) {
dc.SetRGBA(0.0, 0.0, 0.0, 1.0)
for i := 0; i < len(points)-1; i++ {
dc.DrawLine(
points[i].X, points[i].Y,
points[i+1].X, points[i+1].Y,
)
}
dc.Stroke()
}
func main() {
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
var canvas *ggcanvas.Canvas
// body spojené úsečkami
points := []Point{}
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch, points)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda volaná v případě, že se má scéna překreslit
app.OnUpdate(func(dt float64) {
inp := app.Input()
// reakce na stisk levého tlačítka myši
if inp.Mouse().Pressed(input.MouseButtonLeft) {
x, y := inp.Mouse().Position()
point := Point{X: float64(x), Y: float64(y)}
points = append(points, point)
// vynucení překreslení okna
app.RequestRedraw()
}
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func renderFrame(dc *gg.Context, width, height int, points []Point) {
// barva pozadí
dc.SetRGB(0.9, 0.9, 0.7)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
// vykreslení cest
drawPath(dc, points)
}
Vylepšení: kreslení „gumové čáry“ sledující pohyb kurzoru
Většina grafických editorů při kreslení úseček zobrazuje tzv. gumovou čáru mezi naposledy nakresleným segmentem a pozicí kurzoru myši. Čára pochopitelně sleduje pohyb myši. Tuto funkcionalitu je velmi snadné přidat i do našeho editoru. Aktuální pozici kurzoru myši si zapamatujeme ve struktuře currentPoint:
// vybrané koncové body
points := []Point{}
// aktuální bod
currentPoint := Point{X: 0, Y: 0}
Následně se ve funkci provádějící vykreslování scény tato „gumová čára“ bude neustále překreslovat (odlišnou barvou), ovšem pouze za předpokladu, že již existuje alespoň jeden nakreslený segment. Navíc ještě na pozici kurzoru myši vykreslíme malé kolečko:
// barva vykreslení
dc.SetRGBA(0.8, 0.0, 0.0, 1.0)
dc.DrawCircle(currentPoint.X, currentPoint.Y, 4)
// kreslení gumové čáry
if len(points) > 0 {
lastPointIndex := len(points) - 1
dc.DrawLine(
points[lastPointIndex].X, points[lastPointIndex].Y,
currentPoint.X, currentPoint.Y)
dc.Stroke()
}
Výsledek vlastně ani není špatný – na program, který má jen 120 řádků včetně komentářů:
Obrázek 14: Postupné kreslení domečku v grafickém editoru. Červeně je zobrazena úsečka ovládaná myší.
Obrázek 15: Postupné kreslení domečku v grafickém editoru. Červeně je zobrazena úsečka ovládaná myší.
Zdrojový kód dnešního posledního demonstračního příkladu vypadá následovně:
package main
import (
"fmt"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
"github.com/gogpu/gogpu/input"
)
type Point struct {
X float64
Y float64
}
func drawPath(dc *gg.Context,
points []Point, currentPoint Point) {
dc.SetRGBA(0.0, 0.0, 0.0, 1.0)
for i := 0; i < len(points)-1; i++ {
dc.DrawLine(
points[i].X, points[i].Y,
points[i+1].X, points[i+1].Y,
)
}
dc.Stroke()
// barva vykreslení
dc.SetRGBA(0.8, 0.0, 0.0, 1.0)
dc.DrawCircle(currentPoint.X, currentPoint.Y, 4)
// kreslení gumové čáry
if len(points) > 0 {
lastPointIndex := len(points) - 1
dc.DrawLine(
points[lastPointIndex].X, points[lastPointIndex].Y,
currentPoint.X, currentPoint.Y)
dc.Stroke()
}
}
func main() {
// rozměry okna
const width = 400
const height = 300
// inicializace aplikace s GUI
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height))
var canvas *ggcanvas.Canvas
// vybrané koncové body
points := []Point{}
// aktuální bod
currentPoint := Point{X: 0, Y: 0}
// callback metoda zavolaná ve chvíli, kdy se má překreslit okno
app.OnDraw(func(dc *gogpu.Context) {
if canvas == nil {
provider := app.GPUContextProvider()
w, h := dc.Width(), dc.Height()
canvas, _ = ggcanvas.New(provider, w, h)
}
cw, ch := canvas.Size()
canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch, points, currentPoint)
})
canvas.Render(dc.RenderTarget())
})
// callback metoda volaná v případě, že se má scéna překreslit
app.OnUpdate(func(dt float64) {
inp := app.Input()
// reakce na stisk levého tlačítka myši
if inp.Mouse().Pressed(input.MouseButtonLeft) {
x, y := inp.Mouse().Position()
point := Point{X: float64(x), Y: float64(y)}
points = append(points, point)
// vynucení překreslení okna
app.RequestRedraw()
} else {
x, y := inp.Mouse().Position()
currentPoint.X = float64(x)
currentPoint.Y = float64(y)
// vynucení překreslení okna
app.RequestRedraw()
}
})
// callback metoda zavolaná ve chvíli, kdy se okno uzavírá
app.OnClose(func() {
fmt.Println("Closing")
gg.CloseAccelerator()
})
// spuštění aplikace
app.Run()
}
func renderFrame(dc *gg.Context, width, height int, points []Point, currentPoint Point) {
// barva pozadí
dc.SetRGB(0.9, 0.9, 0.7)
// obdélník s plochou odpovídající oknu
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
// vykreslení cest
drawPath(dc, points, currentPoint)
}
Repositář s demonstračními příklady
Zdrojové kódy všech minule i dnes použitých demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – relativně malý, dnes má přibližně šest až sedm megabajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
Odkazy na Internetu
- Projekt GoGPU na GitHubu
https://github.com/orgs/gogpu/repositories - Balíček gogpu/gg
https://github.com/gogpu/gg - Balíček gogpu/gg-svg
https://github.com/gogpu/gg-svg - Balíček gogpu/gg-pdf
https://github.com/gogpu/gg-pdf - package gg (což je jiná varianta gg)
https://godoc.org/github.com/fogleman/gg - Generate an animated GIF with Golang
http://tech.nitoyon.com/en/blog/2016/01/07/go-animated-gif-gen/ - Generate an image programmatically with Golang
http://tech.nitoyon.com/en/blog/2015/12/31/go-image-gen/ - The Go image package
https://blog.golang.org/go-image-package - Balíček draw2D: 2D rendering for different output (raster, pdf, svg)
https://github.com/llgcode/draw2d - Draw a rectangle in Golang?
https://stackoverflow.com/questions/28992396/draw-a-rectangle-in-golang - Immediate mode (computer graphics)
https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics) - Retained mode
https://en.wikipedia.org/wiki/Retained_mode - Lecture 11: Linear Interpolation Again – Bézier Curves
http://www.math.kent.edu/~reichel/courses/intr.num.comp.1/fall09/lecture12/bez.pdf - Geometrie/Úvod do křivek
https://cs.wikibooks.org/wiki/Geometrie/%C3%9Avod_do_k%C5%99ivek - Seriál Grafická knihovna OpenGL
https://www.root.cz/serialy/graficka-knihovna-opengl/ - Seriál Tvorba přenositelných grafických aplikací využívajících knihovnu GLUT
https://www.root.cz/serialy/tvorba-prenositelnych-grafickych-aplikaci-vyuzivajicich-knihovnu-glut/ - Seriál OpenGL a nadstavbová knihovna GLU
https://www.root.cz/serialy/opengl-a-nadstavbova-knihovna-glu/ - Seriál OpenGL evaluátory
https://www.root.cz/serialy/opengl-evaluatory/ - Khronos Group
https://www.khronos.org/ - Khronos Group (Wikipedia)
https://en.wikipedia.org/wiki/Khronos_Group - OpenVG (Wikipedia)
https://en.wikipedia.org/wiki/OpenVG - OpenVG Quick Reference Card
https://www.khronos.org/files/openvg-quick-reference-card.pdf - OpenVG on the Raspberry Pi
http://mindchunk.blogspot.cz/2012/09/openvg-on-raspberry-pi.html - ShivaVG: open-source ANSI C OpenVG
http://ivanleben.blogspot.cz/2007/07/shivavg-open-source-ansi-c-openvg.html - Testbed for exploring OpenVG on the Raspberry Pi
https://github.com/ajstarks/openvg - Seriál Grafické karty a grafické akcelerátory
http://www.root.cz/serialy/graficke-karty-a-graficke-akceleratory/ - (Linux) Framebuffer
http://wiki.linuxquestions.org/wiki/Framebuffer - (Linux) Framebuffer HOWTO
http://tldp.org/HOWTO/Framebuffer-HOWTO/ - Linux framebuffer (Wikipedia)
https://en.wikipedia.org/wiki/Linux_framebuffer - Seriál Grafické formáty
http://www.root.cz/serialy/graficke-formaty/ - Seriál Grafický formát GIF
https://www.root.cz/serialy/graficky-format-gif/ - PNG is Not GIF
https://www.root.cz/clanky/png-is-not-gif/
