Ve druhém článku o projektu GoGPU navážeme na úvodní článek, ve kterém jsme se primárně věnovali popisu základů knihovny gg určené pro vykreslování (rendering) 2D grafiky. Dnes popis této knihovny dokončíme a navíc si ukážeme i k ní „přidružené“ knihovny pojmenované gg-pdf a gg-svg, které slouží k exportu 2D grafiky do vektorových formátů PDF a SVG.
Co se dozvíte v článku
- Metody Context.Stroke a Context.Fill
- Specifikace barvy hexa tripletem
- Koncept štětce (brush)
- Kombinace výplně a obrysu?
- Vykreslení překrývajících se obrazců
- Překrývající se obrazce a alfa složka barvy, režimy prolínání
- Pravidla pro vyplňování obrazců, jejichž hrany se překrývají
- Gradientní barevné přechody využitelné při kreslení vyplněných obrazců
- Lineární gradient
- Specifikace většího množství barev gradientního přechodu
- Radiální gradient
- Gradient závislý na úhlu
- Uložení 2D scény do vektorových formátů
- Vektorový formát SVG
- Provedení exportu vektorové kresby do formátu SVG
- Export složitějších kreseb do formátu SVG
- Propojení 2D grafiky a grafického uživatelského rozhraní
- Repositář s demonstračními příklady
- Odkazy na Internetu
Export do rastrových formátů (PNG, JPG, GIF atd.) totiž podporuje přímo knihovna gg, takže pro něj není zapotřebí instalovat další knihovnu. V závěrečné části si navíc řekneme, jak lze knihovnu gg využít pro vykreslování (interaktivní) 2D grafiky do okna. Projekt GoGPU totiž podporuje i tvorbu plnohodnotných desktopových aplikací s vlastními okny, grafickým uživatelským rozhraním a interaktivní 2D či 3D grafikou.
Metody Context.Stroke a Context.Fill
Začněme krátkým zopakováním základů knihovny gg. V dnešním prvním demonstračním příkladu se nejprve smaže plocha celého rastrového obrázku s rozlišením 400×300 pixelů zvolenou barvou. Následně je do obrázku vykreslen obrys obdélníku:
Obrázek 1: Obdélník, u něhož je vykreslen jen jeho obrys.
Úplný zdrojový kód dnešního prvního demonstračního příkladu vypadá následovně:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "01.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
const border = 40
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.SetRGB(0.0, 0.0, 0.0)
// vykreslení obrysu
dc.Stroke()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Kromě vykreslení obrysu obrazců či cesty metodou Context.Stroke je možné provést i vyplnění uzavřeného 2D obrazce metodou Context.Fill. Výsledný obrázek bude (podle očekávání) v tomto případě zcela odlišný:
Obrázek 2: Vyplněný obdélník.
Opět si pochopitelně uvedeme úplný zdrojový kód tohoto příkladu:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "02.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
const border = 40
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.SetRGB(0.3, 0.7, 0.9)
// vyplnění tvaru
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Specifikace barvy hexa tripletem
V předchozích dvou demonstračních příkladech jsme barvu obrysu obdélníku či jeho vnitřní plochy specifikovali metodou Context.SetRGB, které se předávaly tři barvové složky R, G, B v rozsahu 0,0 až 1,0:
func (c *Context) SetRGB(r, g, b float64)
SetRGB sets the current color using RGB values (0-1).
Například:
dc.SetRGB(0.3, 0.7, 0.9)
V úvodním článku jsme si taktéž ukázali metodu Context.SetRGBA, která umožňuje barvě přiřadit i alfa kanál:
func (c *Context) SetRGBA(r, g, b, a float64)
SetRGBA sets the current color using RGBA values (0-1).
Například:
dc.SetRGBA(0.3, 0.7, 0.9, 0.5)
Ovšem barvu lze zadat i dalšími způsoby. Užitečná je metoda Context.SetHexColor, která umožňuje specifikovat barvu v prostoru RGB klasickým hexa tripletem (popř. i s přidanou informací o hodnotě alfa složky):
func (c *Context) SetHexColor(hex string)
SetHexColor sets the current color using a hex string.
Právě tuto metodu použijeme i v dalších demonstračních příkladech, protože zápis barvy tímto způsobem je poměrně dobře známý:
dc.DrawRectangle(border, border, width-border*2, height-border*2)
// barva zapsaná v hexa triplet formátu
dc.SetHexColor("44aadd")
// vyplnění tvaru
dc.Fill()
Výsledek by měl vypadat následovně:
Obrázek 3: Specifikace barvy výplně hexa tripletem.
Opět si pochopitelně ukážeme celý zdrojový kód příkladu, který vykreslil třetí obrázek:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "03.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
const border = 40
dc.DrawRectangle(border, border, width-border*2, height-border*2)
// barva zapsaná v hexa triplet formátu
dc.SetHexColor("44aadd")
// vyplnění tvaru
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Koncept štětce (brush)
S pojmem štětce (brush) se můžeme setkat v mnoha knihovnách určených pro vykreslování dvourozměrné grafiky. Většinou se rozlišuje mezi kreslicím perem (pen) a štětcem (brush), přičemž pero slouží pro vykreslení cest či obrysů a štětec pro výplň uzavřených tvarů. V knihovně gg je tomu ovšem (alespoň prozatím) jinak, protože štětec je použit jak pro kreslení obrysů, tak i pro výplně. Odlišuje se ovšem způsob jeho nastavení – SetStrokeBrush nebo SetFillBrush:
func (c *Context) SetStrokeBrush(b Brush)
SetStrokeBrush sets the brush used for stroke operations. Note: In the
current implementation, fill and stroke share the same brush. This method is
provided for API symmetry and future extensibility.
Example:
ctx.SetStrokeBrush(gg.Solid(gg.Black))
ctx.SetStrokeBrush(gg.SolidRGB(0.5, 0.5, 0.5))
a:
func (c *Context) SetFillBrush(b Brush)
SetFillBrush sets the brush used for fill operations. This is the preferred
way to set fill styling in new code.
Example:
ctx.SetFillBrush(gg.Solid(gg.Red))
ctx.SetFillBrush(gg.SolidHex("#FF5733"))
ctx.SetFillBrush(gg.HorizontalGradient(gg.Red, gg.Blue, 0, 100))
Ukažme si nejprve způsob použití metody Context.SetStrokeBrush pro nastavení štětce vykreslujícího černou barvou obrysy obrazů:
Obrázek 4: Štětec použitý pro vykreslení obrysu obrazce.
Úplný zdrojový kód demonstračního příkladu:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "04.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
blackColor := gg.Hex("000000")
dc.SetStrokeBrush(gg.Solid(blackColor))
const border = 40
dc.DrawRectangle(border, border, width-border*2, height-border*2)
// vykreslení obrysu
dc.Stroke()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Nyní naopak vykreslíme vyplněný obdélník štětcem nastaveným metodou Context.SetFillBrush s výsledkem:
Obrázek 5: Štětec použitý pro vykreslení vyplněného obrazce.
Opět si ukažme úplný zdrojový kód takto upraveného demonstračního příkladu:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "05.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
customColor := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor))
const border = 40
dc.DrawRectangle(border, border, width-border*2, height-border*2)
// vykreslení obrysu
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Kombinace výplně a obrysu?
Kvůli tomu, že se v současné implementaci knihovny gg štětec používá jak pro výplně, tak i pro obrysy, není možné použít současně dva štětce tak, aby se například vykreslil obrys jedenkrát zapsaného obdélníku černou barvou a jeho výplň barvou odlišnou:
blackColor := gg.Hex("000000")
dc.SetStrokeBrush(gg.Solid(blackColor))
customColor := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor))
dc.DrawRectangle(border, border, width-border*2, height-border*2)
// vykreslení vyplněného obrysu
dc.Fill()
dc.Stroke()
Výsledek nebude v tomto případě odpovídat očekávání, protože se zobrazí jen výplň a nikoli obrys:
Obrázek 6: Kombinace výplně a obrysu prozatím není možná.
Zdrojový kód tohoto demonstračního příkladu vypadá následovně:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "06.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
blackColor := gg.Hex("000000")
dc.SetStrokeBrush(gg.Solid(blackColor))
customColor := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor))
const border = 40
dc.DrawRectangle(border, border, width-border*2, height-border*2)
// vykreslení obrysu
dc.Fill()
dc.Stroke()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Vykreslení překrývajících se obrazců
Dvourozměrná scéna se v přímém (immediate) režimu vykresluje přímo při vykonávání jednotlivých příkazů. To znamená, že pořadí příkazů přímo ovlivňuje i překrývání jednotlivých tvarů. Ukažme si to na jednoduchém příkladu s trojicí obdélníků, které se částečně překrývají:
customColor1 := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor1))
dc.DrawRectangle(border, border, width-border*4, height-border*4)
dc.Fill()
customColor2 := gg.Hex("ddaa44")
dc.SetFillBrush(gg.Solid(customColor2))
dc.DrawRectangle(border*2, border*2, width-border*4, height-border*4)
dc.Fill()
customColor3 := gg.Hex("dd44aa")
dc.SetFillBrush(gg.Solid(customColor3))
dc.DrawRectangle(border*3, border*3, width-border*4, height-border*4)
dc.Fill()
Výsledkem je obrázek, ve kterém poslední nakreslený obdélník překrývá první dva obdélníky:
Obrázek 7: Překrývající se obrazce.
Celý příklad provádějící vykreslení sedmého obrázku má tento zdrojový kód:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "07.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
const border = 40
customColor1 := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor1))
dc.DrawRectangle(border, border, width-border*4, height-border*4)
// vyplnění obrysu
dc.Fill()
customColor2 := gg.Hex("ddaa44")
dc.SetFillBrush(gg.Solid(customColor2))
dc.DrawRectangle(border*2, border*2, width-border*4, height-border*4)
// vyplnění obrysu
dc.Fill()
customColor3 := gg.Hex("dd44aa")
dc.SetFillBrush(gg.Solid(customColor3))
dc.DrawRectangle(border*3, border*3, width-border*4, height-border*4)
// vyplnění obrysu
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Překrývající se obrazce a alfa složka barvy, režimy prolínání
Knihovna gg provádí vykreslování stále stejným způsobem, tj. tvary jsou vykreslovány ihned po zavolání příslušných metod Stroke nebo Fill. To znamená, že lze snadno realizovat i vykreslení poloprůhledných objektů:
customColor1 := gg.Hex("44aadd80")
dc.SetFillBrush(gg.Solid(customColor1))
dc.DrawRectangle(border, border, width-border*4, height-border*4)
dc.Fill()
customColor2 := gg.Hex("ddaa4480")
dc.SetFillBrush(gg.Solid(customColor2))
dc.DrawRectangle(border*2, border*2, width-border*4, height-border*4)
dc.Fill()
customColor3 := gg.Hex("dd44aa80")
dc.SetFillBrush(gg.Solid(customColor3))
dc.DrawRectangle(border*3, border*3, width-border*4, height-border*4)
dc.Fill()
Výsledek bude vypadat takto:
Obrázek 8: Překrývající se obrazce s barvou obsahující alfa složku.
Celý zdrojový kód:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "08.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
const border = 40
customColor1 := gg.Hex("44aadd80")
dc.SetFillBrush(gg.Solid(customColor1))
dc.DrawRectangle(border, border, width-border*4, height-border*4)
// vyplnění obrysu
dc.Fill()
customColor2 := gg.Hex("ddaa4480")
dc.SetFillBrush(gg.Solid(customColor2))
dc.DrawRectangle(border*2, border*2, width-border*4, height-border*4)
// vyplnění obrysu
dc.Fill()
customColor3 := gg.Hex("dd44aa80")
dc.SetFillBrush(gg.Solid(customColor3))
dc.DrawRectangle(border*3, border*3, width-border*4, height-border*4)
// vyplnění obrysu
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Navíc je možné metodou SetBlendMode nastavit režim výpočtů překrývajících se barev.
dc.SetBlendMode(gg.BlendOverlay)
Obrázek 9: Pravidla pro překryv obrazců.
K dispozici je několik možných operací mezi dvojicí barev, ovšem zdá se, že ne všechny jsou (prozatím?) plně realizovány:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "09.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
dc.SetBlendMode(gg.BlendOverlay)
const border = 40
customColor1 := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor1))
dc.DrawRectangle(border, border, width-border*4, height-border*4)
// vyplnění obrysu
dc.Fill()
customColor2 := gg.Hex("ddaa44")
dc.SetFillBrush(gg.Solid(customColor2))
dc.DrawRectangle(border*2, border*2, width-border*4, height-border*4)
// vyplnění obrysu
dc.Fill()
customColor3 := gg.Hex("dd44aa")
dc.SetFillBrush(gg.Solid(customColor3))
dc.DrawRectangle(border*3, border*3, width-border*4, height-border*4)
// vyplnění obrysu
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Pravidla pro vyplňování obrazců, jejichž hrany se překrývají
U uzavřené cesty, tedy například u polygonu, je možné nastavit pravidlo pro režim vyplňování. Buď se vyplní celá plocha polygonu nebo jiné uzavřené cesty, nebo pouze ta část, která je označena jako vnitřní algoritmem sudý-lichý. Tento algoritmus pro libovolný bod X v rovině (ploše) vykreslování vytvoří paprsek z tohoto bodu do nekonečna a zjišťuje počet průsečíků paprsku s hranami objektu. V případě, že je počet průsečíků lichý, leží bod X uvnitř objektu, jinak leží vně. Rozdíl uvidíme při kresbě hvězdy vytvořené polynomem, jehož hrany se protínají:
Obrázek 10: Uzavřený obrazec, jehož hrany se přetínají.
Tento obrázek byl vykreslen následujícím příkladem:
package main
import (
"math"
"github.com/gogpu/gg"
)
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "10.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
customColor := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor))
const radius = height / 3
// začátek vykreslování cesty
dc.MoveTo(width/2, height/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(width/2+x, height/2-y)
}
dc.ClosePath()
// vyplnění obrysu
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Pravidlo pro alternativní způsob vyplňování založené na zjištění počtu protnutí paprsku zvoleným tvarem se nastavuje následovně:
dc.SetFillRule(gg.FillRuleEvenOdd)
Výsledek je (pochopitelně) odlišný:
Obrázek 11: Uzavřený obrazec, jehož hrany se přetínají – pravidlo lichého počtu protnutí paprskem.
Upravený zdrojový kód:
package main
import (
"math"
"github.com/gogpu/gg"
)
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "11.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
customColor := gg.Hex("44aadd")
dc.SetFillBrush(gg.Solid(customColor))
dc.SetFillRule(gg.FillRuleEvenOdd)
const radius = height / 3
// začátek vykreslování cesty
dc.MoveTo(width/2, height/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(width/2+x, height/2-y)
}
dc.ClosePath()
// vyplnění obrysu
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Gradientní barevné přechody využitelné při kreslení vyplněných obrazců
Další možnost vyplňování uzavřených tvarů spočívá v použití gradientních (barevných) přechodů. Existuje několik variant těchto přechodů, přičemž nejpoužívanější je lineární přechod (v libovolném směru) nebo radiální přechod. U lineárních přechodů je nutné specifikovat dva body a taktéž libovolné množství barev. Přechod je vypočten na úsečce ležící mezi těmito dvěma body – tím je určen jak směr přechodu, tak i nepřímo jeho šířka (čím jsou body blíže u sebe, tím je šířka přechodu menší). Barvy je možné specifikovat přímo v souřadném systému používaném pro kresby. Celkový počet zadaných barev není omezen na dvě; můžeme například vytvořit přechod mezi třemi barvami. U specifikace radiálního přechodu se zadává střed a poloměry kružnic, v nichž k výpočtu změny barev dochází. Barvy se zadávají na úsečce ze středu směrem k zadané kružnici. Ani zde není počet barev omezen. A konečně existuje i gradientní přechod, ve kterém je barva vypočtena na základě úhlu od zadaného středu.
Příslušné konstruktory pro vytvoření objektu typu „gradient“ vypadají takto:
func NewLinearGradientBrush(x0, y0, x1, y1 float64) *LinearGradientBrush
NewLinearGradientBrush creates a new linear gradient from (x0, y0) to (x1,
y1).
func NewRadialGradientBrush(cx, cy, startRadius, endRadius float64) *RadialGradientBrush
NewRadialGradientBrush creates a new radial gradient. The gradient
transitions from startRadius to endRadius around (cx, cy). Focus defaults to
center.
func NewSweepGradientBrush(cx, cy, startAngle float64) *SweepGradientBrush
NewSweepGradientBrush creates a new sweep (conic) gradient centered at (cx,
cy). startAngle is the angle where the gradient begins (in radians).
The gradient sweeps a full 360 degrees by default.
Lineární gradient
Ukažme si použití jednoduchého lineárního gradientu použitého pro vyplnění obrazce (v našem případě obdélníku). Gradient se vytvoří předáním dvojice bodů a libovolného počtu barev. U každé barvy je specifikována její souřadnice 0 až 1 v parametrickém prostoru:
// lineární přechod mezi dvěma barvami
gradientFill := gg.NewLinearGradientBrush(
border, height/2, width-border*2, height/2).
AddColorStop(0, customColor1).
AddColorStop(1, customColor2)
// použije se výše definoaný lineární přechod mezi dvěma barvami
dc.SetFillBrush(gradientFill)
Výsledek bude vypadat následovně:
Obrázek 12: Lineární gradientní přechod.
Příklad s vykreslením obrazce vyplněného gradientním přechodem:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "12.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("44aadd")
customColor2 := gg.Hex("dd44aa")
const border = 40
// lineární přechod mezi dvěma barvami
gradientFill := gg.NewLinearGradientBrush(
border, height/2, width-border*2, height/2).
AddColorStop(0, customColor1).
AddColorStop(1, customColor2)
// použije se výše definoaný lineární přechod mezi dvěma barvami
dc.SetFillBrush(gradientFill)
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Samozřejmě je možné souřadnice začátku a konce lineárního přechodu zadat libovolné a provést tak „natočení“ výplně, změny měřítka apod. Natočení po úhlopříčce se provede takto:
// lineární přechod mezi dvěma barvami
gradientFill := gg.NewLinearGradientBrush(
border, border, width-border*2, height-border*2).
AddColorStop(0, customColor1).
AddColorStop(1, customColor2)
S výsledkem:
Obrázek 13: Lineární gradientní přechod pod jiným úhlem.
Úplný zdrojový kód takto upraveného příkladu vypadá následovně:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "13.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("44aadd")
customColor2 := gg.Hex("dd44aa")
const border = 40
// lineární přechod mezi dvěma barvami
gradientFill := gg.NewLinearGradientBrush(
border, border, width-border*2, height-border*2).
AddColorStop(0, customColor1).
AddColorStop(1, customColor2)
// použije se výše definoaný lineární přechod mezi dvěma barvami
dc.SetFillBrush(gradientFill)
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Specifikace většího množství barev gradientního přechodu
Jak jsme si již řekli v předchozím textu, je možné specifikovat větší množství barev tvořících gradientní přechod. Tři barvy rozmístěné v parametrickém prostoru pravidelně (na souřadnicích 0, 1/2 a 1) lze zadat takto:
// lineární přechod mezi třemi barvami
gradientFill := gg.NewLinearGradientBrush(
border, height/2, width-border*2, height/2).
AddColorStop(0, customColor1).
AddColorStop(0.5, customColor2).
AddColorStop(1, customColor3)
Výsledek:
Obrázek 14: Lineární gradientní přechod tří barev.
A příslušný zdrojový kód příkladu:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "14.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("44aadd")
customColor2 := gg.Hex("dd44aa")
customColor3 := gg.Hex("aadd44aa")
const border = 40
// lineární přechod mezi třemi barvami
gradientFill := gg.NewLinearGradientBrush(
border, height/2, width-border*2, height/2).
AddColorStop(0, customColor1).
AddColorStop(0.5, customColor2).
AddColorStop(1, customColor3)
// použije se výše definoaný lineární přechod mezi dvěma barvami
dc.SetFillBrush(gradientFill)
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Barvy ovšem můžeme v parametrickém prostoru „posouvat“ a měnit tak tvar gradientního přechodu. V následujícím úryvku kódu se druhá barva posunula směrem k barvě třetí. První barevný přechod tedy bude delší než přechod druhý:
// lineární přechod mezi dvěma barvami
gradientFill := gg.NewLinearGradientBrush(
border, height/2, width-border*2, height/2).
AddColorStop(0, customColor1).
AddColorStop(0.7, customColor2).
AddColorStop(1, customColor3)
Výsledek:
Obrázek 15: Lineární gradientní přechod tří barev.
A příslušný zdrojový kód demonstračního příkladu:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "15.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("44aadd")
customColor2 := gg.Hex("dd44aa")
customColor3 := gg.Hex("aadd44aa")
const border = 40
// lineární přechod mezi dvěma barvami
gradientFill := gg.NewLinearGradientBrush(
border, height/2, width-border*2, height/2).
AddColorStop(0, customColor1).
AddColorStop(0.7, customColor2).
AddColorStop(1, customColor3)
// použije se výše definoaný lineární přechod mezi dvěma barvami
dc.SetFillBrush(gradientFill)
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Radiální gradient
Objekt představující radiální gradient se konstruuje zavoláním funkce NewRadialGradientBrush. Specifikovat je nutné (společný) střed dvou kružnic i jejich poloměry v rovině kreslení. V takto vymezeném mezikruží dochází k postupné změně barev. Přechod mezi dvěma barvami od středu (poloměr=0) až po kružnici s poloměrem height/2 lze specifikovat takto:
gradientFill := gg.NewRadialGradientBrush(width/2, height/2, 0, height/2).
AddColorStop(0.0, customColor1).
AddColorStop(1.0, customColor2)
Výsledek:
Obrázek 16: Radiální gradientní přechod.
Příslušný zdrojový kód příkladu:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "16.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("dd4444")
customColor2 := gg.Hex("ffffff")
gradientFill := gg.NewRadialGradientBrush(width/2, height/2, 0, height/2).
AddColorStop(0.0, customColor1).
AddColorStop(1.0, customColor2)
dc.SetFillBrush(gradientFill)
const border = 0
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Pochopitelně můžeme specifikovat i více barev, opět se zadáním jejich souřadnic v parametrickém prostoru. Přechod mezi třemi barvami se konstruuje takto:
gradientFill := gg.NewRadialGradientBrush(width/2, height/2, 0, height/2).
AddColorStop(0.0, customColor1).
AddColorStop(0.4, customColor2).
AddColorStop(1.0, customColor3)
Výsledek:
Obrázek 17: Radiální gradientní přechod tří barev.
A opět, jak se zde stalo zvykem, následuje zdrojový kód příkladu, který vykreslil předchozí obrázek:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "17.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("dd4444")
customColor2 := gg.Hex("000000")
customColor3 := gg.Hex("ffffff")
gradientFill := gg.NewRadialGradientBrush(width/2, height/2, 0, height/2).
AddColorStop(0.0, customColor1).
AddColorStop(0.4, customColor2).
AddColorStop(1.0, customColor3)
dc.SetFillBrush(gradientFill)
const border = 0
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Gradient závislý na úhlu
Posledním podporovaným typem gradientního přechodu je přechod, ve kterém je barva vypočtena na základě úhlu (a nikoli vzdálenosti). Tento přechod se od předchozí dvojice přechodů odlišuje tím, že se ve vyplňované ploše vlastně střetává počáteční a koncová barva. Následuje ukázka tohoto přechodu tvořeného pouze počáteční a koncovou barvou. Prvním a druhým parametrem je souřadnice středu gradientního přechodu, třetím parametrem konstruktoru pak je počáteční úhel zadaný ve stupních:
gradientFill := gg.NewSweepGradientBrush(width/2, height/2, 0).
AddColorStop(0.0, customColor1).
AddColorStop(1.0, customColor2)
Výsledek:
Obrázek 18: Gradient závislý na úhlu.
Opět si ukažme celý zdrojový kód demonstračního příkladu, který tento obrázek vykreslil:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "18.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("dd4444")
customColor2 := gg.Hex("ffffff")
gradientFill := gg.NewSweepGradientBrush(width/2, height/2, 0).
AddColorStop(0.0, customColor1).
AddColorStop(1.0, customColor2)
dc.SetFillBrush(gradientFill)
const border = 0
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
U tohoto typu gradientního přechodu většinou vyžadujeme, aby počáteční barva byla totožná s koncovou barvou, protože tyto dvě barvy se střetávají. Řešení je snadné – viz volání konstruktoru gradientního přechodu:
gradientFill := gg.NewSweepGradientBrush(width/2, height/2, 0).
AddColorStop(0.0, customColor1).
AddColorStop(0.25, customColor2).
AddColorStop(0.50, customColor3).
AddColorStop(1.0, customColor1)
Výsledek již vypadá mnohem lépe:
Obrázek 19: Gradient závislý na úhlu, tři barvy.
Zdrojový kód příkladu, který vykreslil devatenáctý obrázek:
package main
import "github.com/gogpu/gg"
func main() {
// rozměry rastrového obrázku
const width = 400
const height = 300
// jméno souboru s výsledným obrázkem
const fileName = "18.png"
// vytvoření kontextu
dc := gg.NewContext(width, height)
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
dc.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("dd4444")
customColor2 := gg.Hex("000000")
customColor3 := gg.Hex("ffffff")
gradientFill := gg.NewSweepGradientBrush(width/2, height/2, 0).
AddColorStop(0.0, customColor1).
AddColorStop(0.25, customColor2).
AddColorStop(0.50, customColor3).
AddColorStop(1.0, customColor1)
dc.SetFillBrush(gradientFill)
const border = 0
dc.DrawRectangle(border, border, width-border*2, height-border*2)
dc.Fill()
// uložení výsledného rastrového obrázku
dc.SavePNG(fileName)
}
Uložení 2D scény do vektorových formátů
S postupným rozšiřováním BBS (kdo si je ještě pamatuje?), komunitních sítí a následně i Internetu, který nabízel službu Gopher a především pak WWW, se zvyšovaly i požadavky uživatelů na přenos, zobrazování a sdílení grafických informací. Tato potřeba dále vzrostla s rostoucí komercionalizací Internetu, protože bylo zapotřebí rozumným způsobem přenášet a zobrazovat reklamu. V oblasti rastrové grafiky byl výběr grafického formátu poměrně jednoduchý, protože již před rozšířením služby WWW se na síti Compuserve i na BBS používal grafický formát GIF, který byl v pozdější době částečně nahrazen formáty JPEG a PNG (o „kauze GIF“ již před mnoha lety vyšel článek; GIF má stále svoji roli pro tvorbu „malých“ animací).
Mnohem složitější je však situace okolo grafiky vektorové, která se paradoxně na WWW hodí mnohem více než grafika rastrová (například proto, že v samotných HTML stránkách je možné měnit velikost písma a stránka se automaticky znovu zalomí, ovšem rastrové obrázky mají stále stejnou velikost; nehledě na obecně menší nároky na kapacitu linek u grafiky vektorové). Adeptů na standard v oblasti webové vektorové grafiky nebylo a není málo. Mezi v tomto ohledu neúspěšné formáty patří WML, málo rozšířený je také DWF (formát navržený firmou AutoDesk pro sdílení výkresů na webu), všeobecně se neujalo ani využití DXF či PostScriptu. Částečným řešením bylo využití Flashe, jedná se však o uzavřený formát, což omezovalo tvorbu grafických editorů, prohlížečů i pluginů do webových prohlížečů, které by tento formát podporovaly (a Flash samotný byl později z mnoha dobrých i špatných důvodů opuštěn).
V projektu GoGPU je podporován export do formátu SVG a do určité míry i do PDF. Oběma způsobům exportu (zejména však exportu do SVG) budou věnovány navazující kapitoly.
Vektorový formát SVG
Vektorový grafický formát SVG (Scalable Vector Graphics) byl navržen právě pro oblast webové grafiky, není to však zdaleka jediná oblast, ve které je možné se s tímto formátem setkat. Právě naopak: tento formát se postupně stává průmyslovým standardem pro přenos vektorové grafiky mezi různými platformami i aplikacemi. Dochází také k integraci knihoven pro práci se SVG do grafických uživatelských rozhraní operačních systémů. Vzhledem k tomu, že se implementace celého SVG ukázala jako poměrně problematická, vznikly dvě podmnožiny SVG nazvané SVG Basic a SVG Tiny, přičemž se předpokládá, že aplikace určené například pro mobilní zařízení s omezeným výpočetním výkonem a menší kapacitou paměti budou používat právě tyto méně náročné, ale pro mnoho služeb více než dostačující, podmnožiny celého standardu.
Jaké vlastnosti nám grafický formát SVG přináší z uživatelského hlediska? Především se jedná o plnohodnotný vektorový formát podporující základní geometrické tvary, cesty (obecné křivky), pokročilou práci s textem, průhlednost apod. Jedná se o vlastnosti, které můžeme v prakticky stejné podobě najít například i v PostScriptu či Portable Document Formatu (PDF). SVG však umožňuje i tvorbu animací a především interaktivitu – SVG tedy nemusí sloužit pouze k zobrazování statických či animovaných obrázků, ale může se nad ním vybudovat například interaktivní mapový portál, geografický informační systém, jednodušší hry, grafické editory integrované do HTML stránek apod. To vše bez nutnosti vázat se na jednoho dodavatele technologie, jeden prohlížeč či platformu. SVG soubory je možné komprimovat pomocí GZIPu a tím dále snížit velikost celého souboru, což se příznivě projeví zejména při pomalejším síťovém připojení.
SVG je zajímavý i z programátorského hlediska. Jedná se totiž o XML aplikaci, tj. grafické objekty i přidružené informace (například animace) jsou uloženy v XML formátu odpovídajícímu oficiálně vydané specifikaci. Každý SVG dokument je možné před vlastním zpracováním zkontrolovat na validitu, což zjednodušuje další práci. Díky XML je možné (s využitím DOM – Document Object Modelu) měnit, přidávat či ubírat jednotlivé uzly stromu, ze kterého se dokument skládá a ovlivňovat tak výslednou podobu kresby.
Provedení exportu vektorové kresby do formátu SVG
Pro export vektorové kresby do formátu SVG (nebo i PDF) se musí pracovat s objekty, které zaznamenávají prováděné vykreslovací operace. Tyto operace jsou následně serializovány do zvoleného vektorového formátu. Samotný objekt, který zaznamenává vykreslovací operace, se používá podobně, jako grafický kontext, který jsme doposud používali. Kostra programů vypadá následovně:
// objekt realizující zápis zaznamenaných kreslicích příkazů do SVG
recorder := recording.NewRecorder(width, height)
// vykreslování s využitím kontextu recorder
...
...
...
// dokončení záznamu
drawing_recording := recorder.FinishRecording()
// backed pro zápis do SVG
backend, err := recording.NewBackend("svg")
if err != nil {
log.Fatal(err)
}
// provedení zápisu do SVG
drawing_recording.Playback(backend)
// uložení výsledného vektorového obrázku
if fb, ok := backend.(recording.FileBackend); ok {
fb.SaveToFile(fileName)
}
Ukažme si, jak se tímto způsobem vykreslí obdélník do plochy vyplněné zadanou barvou (takže se vlastně vracíme k dnešnímu prvnímu demonstračnímu příkladu):
package main
import (
"log"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg-svg"
"github.com/gogpu/gg/recording"
)
func main() {
// rozměry obrázku
const width = 400
const height = 300
// objekt realizující zápis zaznamenaných kreslicích příkazů do SVG
recorder := recording.NewRecorder(width, height)
// jméno souboru s výslednou kresbou
const fileName = "20.svg"
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
recorder.ClearWithColor(color)
const border = 40
recorder.DrawRectangle(border, border, width-border*2, height-border*2)
recorder.SetRGB(0.0, 0.0, 0.0)
// vykreslení obrysu
recorder.Stroke()
// dokončení záznamu
drawing_recording := recorder.FinishRecording()
// backed pro zápis do SVG
backend, err := recording.NewBackend("svg")
if err != nil {
log.Fatal(err)
}
// provedení zápisu do SVG
drawing_recording.Playback(backend)
// uložení výsledného vektorového obrázku
if fb, ok := backend.(recording.FileBackend); ok {
fb.SaveToFile(fileName)
}
}
Výsledkem bude velmi stručný soubor ve formátu SVG, který skutečně obsahuje všechny potřebné grafické objekty 2D scény:
<?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="300" viewBox="0 0 400 300"> <rect x="0" y="0" width="400" height="300" fill="rgb(229,229,178)" stroke="none"/> <path d="M40 40L360 40L360 260L40 260Z" fill="none" stroke="rgb(0,0,0)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> </svg>
Export složitějších kreseb do formátu SVG
Do formátu SVG lze pochopitelně exportovat i složitější 2D kresby, nejenom pouhý obdélník na barevném podkladu. Ukažme si úpravu příkladu z předchozího článku. V tomto příkladu se vykreslí několik Bézierových křivek i se svými řídicími body atd. Jedná se tedy o relativně složitou 2D scénu:
package main
import (
"log"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg-svg"
"github.com/gogpu/gg/recording"
)
func drawQuadraticBezier(dc *recording.Recorder, x0 float64, y0 float64, x1 float64, y1 float64, x2 float64, y2 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.SetRGBA(0.0, 0.0, 0.0, 1.0)
dc.MoveTo(x0, y0)
dc.QuadraticTo(x1, y1, x2, y2)
dc.Stroke()
dc.SetRGBA(0.2, 0.2, 1.0, 1.0)
dc.DrawCircle(x0, y0, 3)
// vykreslení
dc.Stroke()
dc.DrawCircle(x1, y1, 3)
// vykreslení
dc.Stroke()
dc.DrawCircle(x2, y2, 3)
// vykreslení
dc.Stroke()
}
func main() {
// rozměry obrázku
const width = 400
const height = 300
// objekt realizující zápis zaznamenaných kreslicích příkazů do SVG
recorder := recording.NewRecorder(width, height)
// jméno souboru s výslednou kresbou
const fileName = "21.svg"
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
recorder.ClearWithColor(color)
// vykreslení cest
drawQuadraticBezier(recorder, 10, 150, 50, 10, 90, 150)
drawQuadraticBezier(recorder, 110, 100, 190, 100, 150, 190)
drawQuadraticBezier(recorder, 250, 150, 210, 60, 290, 150)
// vykreslení obrysu
recorder.Stroke()
// dokončení záznamu
drawing_recording := recorder.FinishRecording()
// backed pro zápis do SVG
backend, err := recording.NewBackend("svg")
if err != nil {
log.Fatal(err)
}
// provedení zápisu do SVG
drawing_recording.Playback(backend)
// uložení výsledného vektorového obrázku
if fb, ok := backend.(recording.FileBackend); ok {
fb.SaveToFile(fileName)
}
}
Výsledkem bude sice poněkud delší soubor ve formátu SVG, ovšem jeho čitelnost je stále poměrně dobrá. Stačí jen vědět, jakým způsobem se zapisují cesty, například že znak C znamená kubickou Bézierovu křivku:
<?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="300" viewBox="0 0 400 300"> <rect x="0" y="0" width="400" height="300" fill="rgb(229,229,178)" stroke="none"/> <path d="M10 150L50 10" fill="none" stroke="rgb(255,127,127)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M50 10L90 150" fill="none" stroke="rgb(255,127,127)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M10 150Q50 10 90 150" fill="none" stroke="rgb(0,0,0)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M13 150C13 151.65685424949237 11.65685424949238 153 10 153C8.34314575050762 153 7 151.65685424949237 7 150C7 148.34314575050763 8.34314575050762 147 10 147C11.65685424949238 147 13 148.34314575050763 13 150Z" fill="none" stroke="rgb(51,51,255)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M53 10C53 11.65685424949238 51.65685424949238 13 50 13C48.34314575050762 13 47 11.65685424949238 47 10C47 8.34314575050762 48.34314575050762 7 50 7C51.65685424949238 7 53 8.34314575050762 53 10Z" fill="none" stroke="rgb(51,51,255)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M93 150C93 151.65685424949237 91.65685424949238 153 90 153C88.34314575050762 153 87 151.65685424949237 87 150C87 148.34314575050763 88.34314575050762 147 90 147C91.65685424949238 147 93 148.34314575050763 93 150Z" fill="none" stroke="rgb(51,51,255)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M110 100L190 100" fill="none" stroke="rgb(255,127,127)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M190 100L150 190" fill="none" stroke="rgb(255,127,127)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M110 100Q190 100 150 190" fill="none" stroke="rgb(0,0,0)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M113 100C113 101.65685424949238 111.65685424949238 103 110 103C108.34314575050762 103 107 101.65685424949238 107 100C107 98.34314575050762 108.34314575050762 97 110 97C111.65685424949238 97 113 98.34314575050762 113 100Z" fill="none" stroke="rgb(51,51,255)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M193 100C193 101.65685424949238 191.65685424949237 103 190 103C188.34314575050763 103 187 101.65685424949238 187 100C187 98.34314575050762 188.34314575050763 97 190 97C191.65685424949237 97 193 98.34314575050762 193 100Z" fill="none" stroke="rgb(51,51,255)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M153 190C153 191.65685424949237 151.65685424949237 193 150 193C148.34314575050763 193 147 191.65685424949237 147 190C147 188.34314575050763 148.34314575050763 187 150 187C151.65685424949237 187 153 188.34314575050763 153 190Z" fill="none" stroke="rgb(51,51,255)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M250 150L210 60" fill="none" stroke="rgb(255,127,127)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M210 60L290 150" fill="none" stroke="rgb(255,127,127)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M250 150Q210 60 290 150" fill="none" stroke="rgb(0,0,0)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M253 150C253 151.65685424949237 251.65685424949237 153 250 153C248.34314575050763 153 247 151.65685424949237 247 150C247 148.34314575050763 248.34314575050763 147 250 147C251.65685424949237 147 253 148.34314575050763 253 150Z" fill="none" stroke="rgb(51,51,255)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M213 60C213 61.65685424949238 211.65685424949237 63 210 63C208.34314575050763 63 207 61.65685424949238 207 60C207 58.34314575050762 208.34314575050763 57 210 57C211.65685424949237 57 213 58.34314575050762 213 60Z" fill="none" stroke="rgb(51,51,255)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> <path d="M293 150C293 151.65685424949237 291.65685424949237 153 290 153C288.34314575050763 153 287 151.65685424949237 287 150C287 148.34314575050763 288.34314575050763 147 290 147C291.65685424949237 147 293 148.34314575050763 293 150Z" fill="none" stroke="rgb(51,51,255)" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4"/> </svg>
Formát SVG dokonce podporuje gradientní barvové přechody, což si můžeme relativně snadno ověřit. Necháme do SVG vyexportovat obdélník vyplněný lineárním přechodem:
package main
import (
"log"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg-svg"
"github.com/gogpu/gg/recording"
)
func main() {
// rozměry obrázku
const width = 400
const height = 300
// objekt realizující zápis zaznamenaných kreslicích příkazů do SVG
recorder := recording.NewRecorder(width, height)
// jméno souboru s výslednou kresbou
const fileName = "22.svg"
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
recorder.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("44aadd")
customColor2 := gg.Hex("dd44aa")
customColor3 := gg.Hex("aadd44aa")
const border = 40
// lineární přechod mezi dvěma barvami
gradientFill := gg.NewLinearGradientBrush(
border, height/2, width-border*2, height/2).
AddColorStop(0, customColor1).
AddColorStop(0.7, customColor2).
AddColorStop(1, customColor3)
// použije se výše definoaný lineární přechod mezi dvěma barvami
recorder.SetFillBrush(gradientFill)
recorder.DrawRectangle(border, border, width-border*2, height-border*2)
recorder.Fill()
// dokončení záznamu
drawing_recording := recorder.FinishRecording()
// backed pro zápis do SVG
backend, err := recording.NewBackend("svg")
if err != nil {
log.Fatal(err)
}
// provedení zápisu do SVG
drawing_recording.Playback(backend)
// uložení výsledného vektorového obrázku
if fb, ok := backend.(recording.FileBackend); ok {
fb.SaveToFile(fileName)
}
}
Z výsledku je patrné, že se v tomto případě skutečně využily schopnosti poskytované formátem SVG:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="300" viewBox="0 0 400 300">
<defs>
<linearGradient id="lg1" gradientUnits="userSpaceOnUse" x1="40" y1="150" x2="320" y2="150">
<stop offset="0" stop-color="rgb(68,170,221)"/>
<stop offset="0.7" stop-color="rgb(221,68,170)"/>
<stop offset="1" stop-color="rgb(170,221,68)" stop-opacity="0.6666666666666666"/>
</linearGradient>
</defs>
<rect x="0" y="0" width="400" height="300" fill="rgb(229,229,178)" stroke="none"/>
<path d="M40 40L360 40L360 260L40 260Z" fill="url(#lg1)" stroke="none"/>
</svg>
Totéž platí u radiálních barvových přechodů, které jsou taktéž formátem SVG podporovány:
package main
import (
"log"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg-svg"
"github.com/gogpu/gg/recording"
)
func main() {
// rozměry obrázku
const width = 400
const height = 300
// objekt realizující zápis zaznamenaných kreslicích příkazů do SVG
recorder := recording.NewRecorder(width, height)
// jméno souboru s výslednou kresbou
const fileName = "23.svg"
// vykreslování s využitím kontextu
color := gg.RGBA{R: 0.9, G: 0.9, B: 0.7, A: 1.0}
recorder.ClearWithColor(color)
// barvy použité pro výplně gradientním přechodem
customColor1 := gg.Hex("dd4444")
customColor2 := gg.Hex("000000")
customColor3 := gg.Hex("ffffff")
gradientFill := gg.NewRadialGradientBrush(width/2, height/2, 0, height/2).
AddColorStop(0.0, customColor1).
AddColorStop(0.4, customColor2).
AddColorStop(1.0, customColor3)
recorder.SetFillBrush(gradientFill)
// použije se výše definoaný lineární přechod mezi dvěma barvami
recorder.SetFillBrush(gradientFill)
const border = 0
recorder.DrawRectangle(border, border, width-border*2, height-border*2)
recorder.Fill()
// dokončení záznamu
drawing_recording := recorder.FinishRecording()
// backed pro zápis do SVG
backend, err := recording.NewBackend("svg")
if err != nil {
log.Fatal(err)
}
// provedení zápisu do SVG
drawing_recording.Playback(backend)
// uložení výsledného vektorového obrázku
if fb, ok := backend.(recording.FileBackend); ok {
fb.SaveToFile(fileName)
}
}
Výsledný SVG soubor je skutečně velmi stručný:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="300" viewBox="0 0 400 300">
<defs>
<radialGradient id="rg1" gradientUnits="userSpaceOnUse" cx="200" cy="150" r="150" fx="200" fy="150">
<stop offset="0" stop-color="rgb(221,68,68)"/>
<stop offset="0.4" stop-color="rgb(0,0,0)"/>
<stop offset="1" stop-color="rgb(255,255,255)"/>
</radialGradient>
</defs>
<rect x="0" y="0" width="400" height="300" fill="rgb(229,229,178)" stroke="none"/>
<path d="M0 0L400 0L400 300L0 300Z" fill="url(#rg1)" stroke="none"/>
</svg>
Propojení 2D grafiky a grafického uživatelského rozhraní
Největší předností balíčků, které jsou součástí projektu GoGPU, je možnost jejich vzájemného propojení. V navazujícím článku si ukážeme základy tvorby GUI (ovšem skutečně prozatím jen základy). Grafické uživatelské rozhraní lze zkombinovat jak s 2D grafikou, tak i s 3D grafikou. Prozatím známe jen knihovnu gg pro 2D grafiku, takže si ve stručnosti ukažme, jak ze otevřít okno a provést do něj vykreslení našeho obdélníku:
package main
import (
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
)
func main() {
const width, height = 800, 600
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height).
WithContinuousRender(true))
var canvas *ggcanvas.Canvas
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())
})
app.OnClose(func() {
gg.CloseAccelerator()
})
app.Run()
}
func renderFrame(dc *gg.Context, width, height int) {
dc.SetRGBA(0.0, 0.0, 0.0, 1)
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
dc.SetRGBA(0.3, 0.7, 0.3, 0.9)
const Border = 50
dc.DrawRectangle(Border, Border, float64(width-Border*2), float64(height-Border*2))
dc.Fill()
}
Ve skutečnosti je v tomto případě více než vhodné kontrolovat všechny návratové kódy, což příklad poněkud „natáhne“ kvůli způsobu reakce na chyby, která se v jazyce Go používá:
package main
import (
"log"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
)
func main() {
const width, height = 800, 600
app := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("GoGPU + gg").
WithSize(width, height).
WithContinuousRender(true))
var canvas *ggcanvas.Canvas
app.OnDraw(func(dc *gogpu.Context) {
w, h := dc.Width(), dc.Height()
if w <= 0 || h <= 0 {
return
}
if canvas == nil {
provider := app.GPUContextProvider()
if provider == nil {
return
}
var err error
canvas, err = ggcanvas.New(provider, w, h)
if err != nil {
log.Fatalf("Failed to create canvas: %v", err)
}
log.Printf("Canvas created: %dx%d", w, h)
}
cw, ch := canvas.Size()
if cw != w || ch != h {
if err := canvas.Resize(w, h); err != nil {
log.Printf("Resize error: %v", err)
}
cw, ch = w, h
}
if err := canvas.Draw(func(cc *gg.Context) {
renderFrame(cc, cw, ch)
}); err != nil {
log.Printf("Draw error: %v", err)
}
if err := canvas.Render(dc.RenderTarget()); err != nil {
log.Printf("Render error: %v", err)
}
})
app.OnClose(func() {
gg.CloseAccelerator()
})
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
func renderFrame(dc *gg.Context, width, height int) {
dc.SetRGBA(0.0, 0.0, 0.0, 1)
dc.DrawRectangle(0, 0, float64(width), float64(height))
dc.Fill()
dc.SetRGBA(0.3, 0.7, 0.3, 0.9)
const Border = 50
dc.DrawRectangle(Border, Border, float64(width-Border*2), float64(height-Border*2))
dc.Fill()
}
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/
