moved image loading and drawing into backend

This commit is contained in:
Thomas Friedel 2019-02-20 19:59:17 +01:00
parent 7cef867541
commit 476dbac6e2
10 changed files with 406 additions and 320 deletions

View file

@ -1,12 +1,36 @@
package backendbase
import "image/color"
import (
"image"
"image/color"
)
type Style struct {
// Backend is used by the canvas to actually do the final
// drawing. This enables the backend to be implemented by
// various methods (OpenGL, but also other APIs or software)
type Backend interface {
ClearRect(x, y, w, h int)
Clear(pts [4][2]float64)
Fill(style *FillStyle, pts [][2]float64)
LoadImage(img image.Image) (Image, error)
DrawImage(dimg Image, sx, sy, sw, sh, dx, dy, dw, dh float64, alpha float64)
}
// FillStyle is the color and other details on how to fill
type FillStyle struct {
Color color.RGBA
GlobalAlpha float64
Blur float64
// radialGradient *RadialGradient
// linearGradient *LinearGradient
// image *Image
}
type Image interface {
Width() int
Height() int
Size() (w, h int)
Delete()
IsDeleted() bool
Replace(src image.Image) error
IsOpaque() bool
}

View file

@ -45,7 +45,7 @@ func (b *GoGLBackend) Clear(pts [4][2]float64) {
gl.Enable(gl.BLEND)
}
func (b *GoGLBackend) Fill(style *backendbase.Style, pts [][2]float64) {
func (b *GoGLBackend) Fill(style *backendbase.FillStyle, pts [][2]float64) {
if style.Blur > 0 {
b.offscr1.alpha = true
b.enableTextureRenderTarget(&b.offscr1)
@ -71,7 +71,7 @@ func (b *GoGLBackend) Fill(style *backendbase.Style, pts [][2]float64) {
gl.BindBuffer(gl.ARRAY_BUFFER, b.buf)
gl.BufferData(gl.ARRAY_BUFFER, len(b.ptsBuf)*4, unsafe.Pointer(&b.ptsBuf[0]), gl.STREAM_DRAW)
if style.GlobalAlpha >= 1 && style.Color.A >= 255 {
if style.Color.A >= 255 {
vertex := b.useShader(style)
gl.EnableVertexAttribArray(vertex)

View file

@ -5,7 +5,6 @@ import (
"image/color"
"github.com/go-gl/gl/v3.2-core/gl"
"github.com/tfriedel6/canvas"
"github.com/tfriedel6/canvas/backend/backendbase"
)
@ -46,7 +45,7 @@ type offscreenBuffer struct {
alpha bool
}
func New(x, y, w, h int) (canvas.Backend, error) {
func New(x, y, w, h int) (backendbase.Backend, error) {
err := gl.Init()
if err != nil {
return nil, err
@ -219,6 +218,23 @@ func glError() error {
return nil
}
// Activate makes this GL backend active and sets the viewport. Only
// needs to be called if any other GL code changes the viewport
func (b *GoGLBackend) Activate() {
// if b.offscreen {
// gli.Viewport(0, 0, int32(cv.w), int32(cv.h))
// cv.enableTextureRenderTarget(&cv.offscrBuf)
// cv.offscrImg.w = cv.offscrBuf.w
// cv.offscrImg.h = cv.offscrBuf.h
// cv.offscrImg.tex = cv.offscrBuf.tex
// } else {
gl.Viewport(int32(b.x), int32(b.y), int32(b.w), int32(b.h))
b.disableTextureRenderTarget()
// }
// b.applyScissor()
gl.Clear(gl.STENCIL_BUFFER_BIT)
}
type glColor struct {
r, g, b, a float64
}
@ -232,7 +248,7 @@ func colorGoToGL(c color.RGBA) glColor {
return glc
}
func (b *GoGLBackend) useShader(style *backendbase.Style) (vertexLoc uint32) {
func (b *GoGLBackend) useShader(style *backendbase.FillStyle) (vertexLoc uint32) {
// if lg := style.LinearGradient; lg != nil {
// lg.load()
// gl.ActiveTexture(gl.TEXTURE0)
@ -293,11 +309,11 @@ func (b *GoGLBackend) useShader(style *backendbase.Style) (vertexLoc uint32) {
gl.Uniform2f(b.sr.CanvasSize, float32(b.fw), float32(b.fh))
c := colorGoToGL(style.Color)
gl.Uniform4f(b.sr.Color, float32(c.r), float32(c.g), float32(c.b), float32(c.a))
gl.Uniform1f(b.sr.GlobalAlpha, float32(style.GlobalAlpha))
gl.Uniform1f(b.sr.GlobalAlpha, 1)
return b.sr.Vertex
}
func (b *GoGLBackend) useAlphaShader(style *backendbase.Style, alphaTexSlot int32) (vertexLoc, alphaTexCoordLoc uint32) {
func (b *GoGLBackend) useAlphaShader(style *backendbase.FillStyle, alphaTexSlot int32) (vertexLoc, alphaTexCoordLoc uint32) {
// if lg := style.LinearGradient; lg != nil {
// lg.load()
// gl.ActiveTexture(gl.TEXTURE0)
@ -362,7 +378,7 @@ func (b *GoGLBackend) useAlphaShader(style *backendbase.Style, alphaTexSlot int3
c := colorGoToGL(style.Color)
gl.Uniform4f(b.sar.Color, float32(c.r), float32(c.g), float32(c.b), float32(c.a))
gl.Uniform1i(b.sar.AlphaTex, alphaTexSlot)
gl.Uniform1f(b.sar.GlobalAlpha, float32(style.GlobalAlpha))
gl.Uniform1f(b.sar.GlobalAlpha, 1)
return b.sar.Vertex, b.sar.AlphaTexCoord
}

250
backend/gogl/images.go Normal file
View file

@ -0,0 +1,250 @@
package goglbackend
import (
"errors"
"image"
"runtime"
"unsafe"
"github.com/go-gl/gl/v3.2-core/gl"
"github.com/tfriedel6/canvas/backend/backendbase"
)
// Image represents a loaded image that can be used in various drawing functions
type Image struct {
w, h int
tex uint32
deleted bool
opaque bool
}
func (b *GoGLBackend) LoadImage(src image.Image) (backendbase.Image, error) {
var tex uint32
gl.GenTextures(1, &tex)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, tex)
if src == nil {
return &Image{tex: tex}, nil
}
img, err := loadImage(src, tex)
if err != nil {
return nil, err
}
runtime.SetFinalizer(img, func(img *Image) {
if !img.deleted {
b.glChan <- func() {
gl.DeleteTextures(1, &img.tex)
}
}
})
return img, nil
}
func loadImage(src image.Image, tex uint32) (*Image, error) {
var img *Image
var err error
switch v := src.(type) {
case *image.RGBA:
img, err = loadImageRGBA(v, tex)
if err != nil {
return nil, err
}
case *image.Gray:
img, err = loadImageGray(v, tex)
if err != nil {
return nil, err
}
case image.Image:
img, err = loadImageConverted(v, tex)
if err != nil {
return nil, err
}
default:
return nil, errors.New("Unsupported source type")
}
return img, nil
}
func loadImageRGBA(src *image.RGBA, tex uint32) (*Image, error) {
img := &Image{tex: tex, w: src.Bounds().Dx(), h: src.Bounds().Dy(), opaque: true}
checkOpaque:
for y := 0; y < img.h; y++ {
off := src.PixOffset(0, y) + 3
for x := 0; x < img.w; x++ {
if src.Pix[off] < 255 {
img.opaque = false
break checkOpaque
}
off += 4
}
}
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
if err := glError(); err != nil {
return nil, err
}
if src.Stride == img.w*4 {
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(img.w), int32(img.h), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&src.Pix[0]))
} else {
data := make([]uint8, 0, img.w*img.h*4)
for y := 0; y < img.h; y++ {
start := y * src.Stride
end := start + img.w*4
data = append(data, src.Pix[start:end]...)
}
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(img.w), int32(img.h), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&data[0]))
}
if err := glError(); err != nil {
return nil, err
}
gl.GenerateMipmap(gl.TEXTURE_2D)
if err := glError(); err != nil {
return nil, err
}
return img, nil
}
func loadImageGray(src *image.Gray, tex uint32) (*Image, error) {
img := &Image{tex: tex, w: src.Bounds().Dx(), h: src.Bounds().Dy()}
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
if err := glError(); err != nil {
return nil, err
}
if src.Stride == img.w {
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(img.w), int32(img.h), 0, gl.RED, gl.UNSIGNED_BYTE, gl.Ptr(&src.Pix[0]))
} else {
data := make([]uint8, 0, img.w*img.h)
for y := 0; y < img.h; y++ {
start := y * src.Stride
end := start + img.w
data = append(data, src.Pix[start:end]...)
}
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, int32(img.w), int32(img.h), 0, gl.RED, gl.UNSIGNED_BYTE, gl.Ptr(&data[0]))
}
if err := glError(); err != nil {
return nil, err
}
gl.GenerateMipmap(gl.TEXTURE_2D)
if err := glError(); err != nil {
return nil, err
}
return img, nil
}
func loadImageConverted(src image.Image, tex uint32) (*Image, error) {
img := &Image{tex: tex, w: src.Bounds().Dx(), h: src.Bounds().Dy(), opaque: true}
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
if err := glError(); err != nil {
return nil, err
}
data := make([]uint8, 0, img.w*img.h*4)
for y := 0; y < img.h; y++ {
for x := 0; x < img.w; x++ {
ir, ig, ib, ia := src.At(x, y).RGBA()
r, g, b, a := uint8(ir>>8), uint8(ig>>8), uint8(ib>>8), uint8(ia>>8)
data = append(data, r, g, b, a)
if a < 255 {
img.opaque = false
}
}
}
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(img.w), int32(img.h), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&data[0]))
if err := glError(); err != nil {
return nil, err
}
gl.GenerateMipmap(gl.TEXTURE_2D)
if err := glError(); err != nil {
return nil, err
}
return img, nil
}
// Width returns the width of the image
func (img *Image) Width() int { return img.w }
// Height returns the height of the image
func (img *Image) Height() int { return img.h }
// Size returns the width and height of the image
func (img *Image) Size() (int, int) { return img.w, img.h }
// Delete deletes the image from memory. Any draw calls
// with a deleted image will not do anything
func (img *Image) Delete() {
gl.DeleteTextures(1, &img.tex)
img.deleted = true
}
// IsDeleted returns true if the Delete function has been
// called on this image
func (img *Image) IsDeleted() bool { return img.deleted }
// Replace replaces the image with the new one
func (img *Image) Replace(src image.Image) error {
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, img.tex)
newImg, err := loadImage(src, img.tex)
if err != nil {
return err
}
*img = *newImg
return nil
}
// IsOpaque returns true if all pixels in the image
// have a full alpha value
func (img *Image) IsOpaque() bool { return img.opaque }
func (b *GoGLBackend) DrawImage(dimg backendbase.Image, sx, sy, sw, sh, dx, dy, dw, dh float64, alpha float64) {
img := dimg.(*Image)
sx /= float64(img.w)
sy /= float64(img.h)
sw /= float64(img.w)
sh /= float64(img.h)
gl.StencilFunc(gl.EQUAL, 0, 0xFF)
gl.BindBuffer(gl.ARRAY_BUFFER, b.buf)
data := [16]float32{
float32(dx), float32(dy),
float32(dx), float32(dy + dh),
float32(dx + dw), float32(dy + dh),
float32(dx + dw), float32(dy),
float32(sx), float32(sy),
float32(sx), float32(sy + sh),
float32(sx + sw), float32(sy + sh),
float32(sx + sw), float32(sy),
}
gl.BufferData(gl.ARRAY_BUFFER, len(data)*4, unsafe.Pointer(&data[0]), gl.STREAM_DRAW)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, img.tex)
gl.UseProgram(b.ir.ID)
gl.Uniform1i(b.ir.Image, 0)
gl.Uniform2f(b.ir.CanvasSize, float32(b.fw), float32(b.fh))
gl.Uniform1f(b.ir.GlobalAlpha, float32(alpha))
gl.VertexAttribPointer(b.ir.Vertex, 2, gl.FLOAT, false, 0, nil)
gl.VertexAttribPointer(b.ir.TexCoord, 2, gl.FLOAT, false, 0, gl.PtrOffset(8*4))
gl.EnableVertexAttribArray(b.ir.Vertex)
gl.EnableVertexAttribArray(b.ir.TexCoord)
gl.DrawArrays(gl.TRIANGLE_FAN, 0, 4)
gl.DisableVertexAttribArray(b.ir.Vertex)
gl.DisableVertexAttribArray(b.ir.TexCoord)
gl.StencilFunc(gl.ALWAYS, 0, 0xFF)
}

View file

@ -18,7 +18,7 @@ import (
// Canvas represents an area on the viewport on which to draw
// using a set of functions very similar to the HTML5 canvas
type Canvas struct {
b Backend
b backendbase.Backend
x, y, w, h int
fx, fy, fw, fh float64
@ -34,16 +34,9 @@ type Canvas struct {
offscrBuf offscreenBuffer
offscrImg Image
shadowBuf [][2]float64
}
images map[interface{}]*Image
// Backend is used by the canvas to actually do the final
// drawing. This enables the backend to be implemented by
// various methods (OpenGL, but also other APIs or software)
type Backend interface {
ClearRect(x, y, w, h int)
Clear(pts [4][2]float64)
Fill(style *backendbase.Style, pts [][2]float64)
shadowBuf [][2]float64
}
type drawState struct {
@ -149,11 +142,15 @@ var Performance = struct {
// While all functions on the canvas use the top left point as
// the origin, since GL uses the bottom left coordinate, the
// coordinates given here also use the bottom left as origin
func New(backend Backend, x, y, w, h int) *Canvas {
func New(backend backendbase.Backend, x, y, w, h int) *Canvas {
if gli == nil {
panic("LoadGL must be called before a canvas can be created")
}
cv := &Canvas{b: backend, stateStack: make([]drawState, 0, 20)}
cv := &Canvas{
b: backend,
stateStack: make([]drawState, 0, 20),
images: make(map[interface{}]*Image),
}
cv.SetBounds(x, y, w, h)
cv.state.lineWidth = 1
cv.state.lineAlpha = 1
@ -169,7 +166,7 @@ func New(backend Backend, x, y, w, h int) *Canvas {
// does not render directly to the screen but renders to a
// texture instead. If alpha is set to true, the offscreen
// canvas will have an alpha channel
func NewOffscreen(backend Backend, w, h int, alpha bool) *Canvas {
func NewOffscreen(backend backendbase.Backend, w, h int, alpha bool) *Canvas {
cv := New(backend, 0, 0, w, h)
cv.offscreen = true
cv.offscrBuf.alpha = alpha
@ -217,9 +214,9 @@ func (cv *Canvas) Activate() {
if cv.offscreen {
gli.Viewport(0, 0, int32(cv.w), int32(cv.h))
cv.enableTextureRenderTarget(&cv.offscrBuf)
cv.offscrImg.w = cv.offscrBuf.w
cv.offscrImg.h = cv.offscrBuf.h
cv.offscrImg.tex = cv.offscrBuf.tex
// cv.offscrImg.w = cv.offscrBuf.w
// cv.offscrImg.h = cv.offscrBuf.h
// cv.offscrImg.tex = cv.offscrBuf.tex
} else {
gli.Viewport(int32(cv.x), int32(cv.y), int32(cv.w), int32(cv.h))
cv.disableTextureRenderTarget()
@ -456,7 +453,7 @@ func glError() error {
// the range 0-255, 3 or 4 float values for RGB(A) in the range 0-1, hex strings
// in the format "#AABBCC", "#AABBCCDD", "#ABC", or "#ABCD"
func (cv *Canvas) SetFillStyle(value ...interface{}) {
cv.state.fill = parseStyle(value...)
cv.state.fill = cv.parseStyle(value...)
}
// SetStrokeStyle sets the color, gradient, or image for any line drawing calls.
@ -464,10 +461,10 @@ func (cv *Canvas) SetFillStyle(value ...interface{}) {
// RGB(A) in the range 0-255, 3 or 4 float values for RGB(A) in the range 0-1,
// hex strings in the format "#AABBCC", "#AABBCCDD", "#ABC", or "#ABCD"
func (cv *Canvas) SetStrokeStyle(value ...interface{}) {
cv.state.stroke = parseStyle(value...)
cv.state.stroke = cv.parseStyle(value...)
}
func parseStyle(value ...interface{}) drawStyle {
func (cv *Canvas) parseStyle(value ...interface{}) drawStyle {
var style drawStyle
if len(value) == 1 {
switch v := value[0].(type) {
@ -485,7 +482,7 @@ func parseStyle(value ...interface{}) drawStyle {
} else if len(value) == 1 {
switch v := value[0].(type) {
case *Image, string:
style.image = getImage(v)
style.image = cv.getImage(v)
}
}
return style
@ -499,16 +496,16 @@ func (s *drawStyle) isOpaque() bool {
return rg.opaque
}
if img := s.image; img != nil {
return img.opaque
return img.img.IsOpaque()
}
return s.color.A >= 255
}
func (cv *Canvas) backendStyle(s *drawStyle, alpha float64) backendbase.Style {
return backendbase.Style{
Color: s.color,
GlobalAlpha: cv.state.globalAlpha * alpha,
}
func (cv *Canvas) backendFillStyle(s *drawStyle, alpha float64) backendbase.FillStyle {
col := s.color
finalAlpha := (float64(s.color.A) / 255) * alpha * cv.state.globalAlpha
col.A = uint8(finalAlpha * 255)
return backendbase.FillStyle{Color: col}
}
func (cv *Canvas) useShader(style *drawStyle) (vertexLoc uint32) {
@ -551,16 +548,18 @@ func (cv *Canvas) useShader(style *drawStyle) (vertexLoc uint32) {
gli.Uniform1f(rgr.globalAlpha, float32(cv.state.globalAlpha))
return rgr.vertex
}
if img := style.image; img != nil {
gli.UseProgram(ipr.id)
gli.ActiveTexture(gl_TEXTURE0)
gli.BindTexture(gl_TEXTURE_2D, img.tex)
gli.Uniform2f(ipr.canvasSize, float32(cv.fw), float32(cv.fh))
gli.Uniform2f(ipr.imageSize, float32(img.w), float32(img.h))
gli.Uniform1i(ipr.image, 0)
gli.Uniform1f(ipr.globalAlpha, float32(cv.state.globalAlpha))
return ipr.vertex
}
// if img := style.image; img != nil {
// gli.UseProgram(ipr.id)
// gli.ActiveTexture(gl_TEXTURE0)
// gli.BindTexture(gl_TEXTURE_2D, img.tex)
// gli.Uniform2f(ipr.canvasSize, float32(cv.fw), float32(cv.fh))
// inv := cv.state.transform.invert().f32()
// gli.UniformMatrix3fv(ipr.invmat, 1, false, &inv[0])
// gli.Uniform2f(ipr.imageSize, float32(img.w), float32(img.h))
// gli.Uniform1i(ipr.image, 0)
// gli.Uniform1f(ipr.globalAlpha, float32(cv.state.globalAlpha))
// return ipr.vertex
// }
gli.UseProgram(sr.id)
gli.Uniform2f(sr.canvasSize, float32(cv.fw), float32(cv.fh))
@ -612,17 +611,19 @@ func (cv *Canvas) useAlphaShader(style *drawStyle, alphaTexSlot int32) (vertexLo
gli.Uniform1f(rgar.globalAlpha, float32(cv.state.globalAlpha))
return rgar.vertex, rgar.alphaTexCoord
}
if img := style.image; img != nil {
gli.UseProgram(ipar.id)
gli.ActiveTexture(gl_TEXTURE0)
gli.BindTexture(gl_TEXTURE_2D, img.tex)
gli.Uniform2f(ipar.canvasSize, float32(cv.fw), float32(cv.fh))
gli.Uniform2f(ipar.imageSize, float32(img.w), float32(img.h))
gli.Uniform1i(ipar.image, 0)
gli.Uniform1i(ipar.alphaTex, alphaTexSlot)
gli.Uniform1f(ipar.globalAlpha, float32(cv.state.globalAlpha))
return ipar.vertex, ipar.alphaTexCoord
}
// if img := style.image; img != nil {
// gli.UseProgram(ipar.id)
// gli.ActiveTexture(gl_TEXTURE0)
// gli.BindTexture(gl_TEXTURE_2D, img.tex)
// gli.Uniform2f(ipar.canvasSize, float32(cv.fw), float32(cv.fh))
// inv := cv.state.transform.invert().f32()
// gli.UniformMatrix3fv(ipar.invmat, 1, false, &inv[0])
// gli.Uniform2f(ipar.imageSize, float32(img.w), float32(img.h))
// gli.Uniform1i(ipar.image, 0)
// gli.Uniform1i(ipar.alphaTex, alphaTexSlot)
// gli.Uniform1f(ipar.globalAlpha, float32(cv.state.globalAlpha))
// return ipar.vertex, ipar.alphaTexCoord
// }
gli.UseProgram(sar.id)
gli.Uniform2f(sar.canvasSize, float32(cv.fw), float32(cv.fh))

View file

@ -17,15 +17,13 @@ import (
)
func run(t *testing.T, fn func(cv *canvas.Canvas)) {
wnd, cv2, err := sdlcanvas.CreateWindow(100, 100, "test")
wnd, cv, err := sdlcanvas.CreateWindow(100, 100, "test")
if err != nil {
t.Fatalf("Failed to crete window: %v", err)
return
}
defer wnd.Destroy()
cv := canvas.NewOffscreen(wnd.Backend, 100, 100, false)
gl.Disable(gl.MULTISAMPLE)
wnd.StartFrame()
@ -34,9 +32,6 @@ func run(t *testing.T, fn func(cv *canvas.Canvas)) {
fn(cv)
img := cv.GetImageData(0, 0, 100, 100)
cv2.DrawImage(cv)
img2 := cv2.GetImageData(0, 0, 100, 100)
caller, _, _, ok := runtime.Caller(1)
if !ok {
t.Fatal("Failed to get caller")
@ -87,16 +82,9 @@ func run(t *testing.T, fn func(cv *canvas.Canvas)) {
for y := 0; y < 100; y++ {
for x := 0; x < 100; x++ {
r1, g1, b1, a1 := img.At(x, y).RGBA()
r2, g2, b2, a2 := img2.At(x, y).RGBA()
r3, g3, b3, a3 := refImg.At(x, y).RGBA()
if r1 != r3 || g1 != g3 || b1 != b3 || a1 != a3 {
r2, g2, b2, a2 := refImg.At(x, y).RGBA()
if r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2 {
writeImage(img, fmt.Sprintf("testdata/%s_fail.png", callerFuncName))
t.Error("onscreen canvas failed")
t.FailNow()
}
if r2 != r3 || g2 != g3 || b2 != b3 || a2 != a3 {
writeImage(img2, fmt.Sprintf("testdata/%s_fail.png", callerFuncName))
t.Error("offscreen canvas failed")
t.FailNow()
}
}

286
images.go
View file

@ -7,260 +7,108 @@ import (
"image"
"io/ioutil"
"os"
"runtime"
"strings"
"unsafe"
"github.com/tfriedel6/canvas/backend/backendbase"
)
// Image represents a loaded image that can be used in various drawing functions
type Image struct {
w, h int
tex uint32
deleted bool
opaque bool
cv *Canvas
img backendbase.Image
}
var images = make(map[interface{}]*Image)
// LoadImage loads an image. The src parameter can be either an image from the
// standard image package, a byte slice that will be loaded, or a file name
// string. If you want the canvas package to load the image, make sure you
// import the required format packages
func LoadImage(src interface{}) (*Image, error) {
if gli == nil {
panic("LoadGL must be called before images can be loaded")
}
var tex uint32
gli.GenTextures(1, &tex)
gli.ActiveTexture(gl_TEXTURE0)
gli.BindTexture(gl_TEXTURE_2D, tex)
if src == nil {
return &Image{tex: tex}, nil
}
img, err := loadImage(src, tex)
if err != nil {
return nil, err
}
runtime.SetFinalizer(img, func(img *Image) {
if !img.deleted {
glChan <- func() {
gli.DeleteTextures(1, &img.tex)
}
}
})
return img, nil
}
func loadImage(src interface{}, tex uint32) (*Image, error) {
var img *Image
var err error
func (cv *Canvas) LoadImage(src interface{}) (*Image, error) {
var srcImg image.Image
switch v := src.(type) {
case *image.RGBA:
img, err = loadImageRGBA(v, tex)
if err != nil {
return nil, err
}
case *image.Gray:
img, err = loadImageGray(v, tex)
if err != nil {
return nil, err
}
case image.Image:
img, err = loadImageConverted(v, tex)
if err != nil {
return nil, err
}
srcImg = v
case string:
data, err := ioutil.ReadFile(v)
if err != nil {
return nil, err
}
srcImg, _, err := image.Decode(bytes.NewReader(data))
srcImg, _, err = image.Decode(bytes.NewReader(data))
if err != nil {
return nil, err
}
return loadImage(srcImg, tex)
case []byte:
srcImg, _, err := image.Decode(bytes.NewReader(v))
var err error
srcImg, _, err = image.Decode(bytes.NewReader(v))
if err != nil {
return nil, err
}
return loadImage(srcImg, tex)
case *Canvas:
src = cv.GetImageData(0, 0, cv.Width(), cv.Height())
default:
return nil, errors.New("Unsupported source type")
}
return img, nil
backendImg, err := cv.b.LoadImage(srcImg)
if err != nil {
return nil, err
}
return &Image{cv: cv, img: backendImg}, nil
}
func getImage(src interface{}) *Image {
if img, ok := images[src]; ok {
func (cv *Canvas) getImage(src interface{}) *Image {
if img, ok := cv.images[src]; ok {
return img
}
switch v := src.(type) {
case *Image:
return v
case image.Image:
img, err := LoadImage(v)
img, err := cv.LoadImage(v)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading image: %v\n", err)
images[src] = nil
cv.images[src] = nil
return nil
}
images[v] = img
cv.images[v] = img
return img
case string:
img, err := LoadImage(v)
img, err := cv.LoadImage(v)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "format") {
fmt.Fprintf(os.Stderr, "Error loading image %s: %v\nIt may be necessary to import the appropriate decoder, e.g.\nimport _ \"image/jpeg\"\n", v, err)
} else {
fmt.Fprintf(os.Stderr, "Error loading image %s: %v\n", v, err)
}
images[src] = nil
cv.images[src] = nil
return nil
}
images[v] = img
cv.images[v] = img
return img
}
fmt.Fprintf(os.Stderr, "Unknown image type: %T\n", src)
images[src] = nil
cv.images[src] = nil
return nil
}
func loadImageRGBA(src *image.RGBA, tex uint32) (*Image, error) {
img := &Image{tex: tex, w: src.Bounds().Dx(), h: src.Bounds().Dy(), opaque: true}
checkOpaque:
for y := 0; y < img.h; y++ {
off := src.PixOffset(0, y) + 3
for x := 0; x < img.w; x++ {
if src.Pix[off] < 255 {
img.opaque = false
break checkOpaque
}
off += 4
}
}
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR_MIPMAP_LINEAR)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_LINEAR)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE)
if err := glError(); err != nil {
return nil, err
}
if src.Stride == img.w*4 {
gli.TexImage2D(gl_TEXTURE_2D, 0, gl_RGBA, int32(img.w), int32(img.h), 0, gl_RGBA, gl_UNSIGNED_BYTE, gli.Ptr(&src.Pix[0]))
} else {
data := make([]uint8, 0, img.w*img.h*4)
for y := 0; y < img.h; y++ {
start := y * src.Stride
end := start + img.w*4
data = append(data, src.Pix[start:end]...)
}
gli.TexImage2D(gl_TEXTURE_2D, 0, gl_RGBA, int32(img.w), int32(img.h), 0, gl_RGBA, gl_UNSIGNED_BYTE, gli.Ptr(&data[0]))
}
if err := glError(); err != nil {
return nil, err
}
gli.GenerateMipmap(gl_TEXTURE_2D)
if err := glError(); err != nil {
return nil, err
}
return img, nil
}
func loadImageGray(src *image.Gray, tex uint32) (*Image, error) {
img := &Image{tex: tex, w: src.Bounds().Dx(), h: src.Bounds().Dy()}
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR_MIPMAP_LINEAR)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_LINEAR)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE)
if err := glError(); err != nil {
return nil, err
}
if src.Stride == img.w {
gli.TexImage2D(gl_TEXTURE_2D, 0, gl_RED, int32(img.w), int32(img.h), 0, gl_RED, gl_UNSIGNED_BYTE, gli.Ptr(&src.Pix[0]))
} else {
data := make([]uint8, 0, img.w*img.h)
for y := 0; y < img.h; y++ {
start := y * src.Stride
end := start + img.w
data = append(data, src.Pix[start:end]...)
}
gli.TexImage2D(gl_TEXTURE_2D, 0, gl_RED, int32(img.w), int32(img.h), 0, gl_RED, gl_UNSIGNED_BYTE, gli.Ptr(&data[0]))
}
if err := glError(); err != nil {
return nil, err
}
gli.GenerateMipmap(gl_TEXTURE_2D)
if err := glError(); err != nil {
return nil, err
}
return img, nil
}
func loadImageConverted(src image.Image, tex uint32) (*Image, error) {
img := &Image{tex: tex, w: src.Bounds().Dx(), h: src.Bounds().Dy(), opaque: true}
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR_MIPMAP_LINEAR)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_LINEAR)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE)
gli.TexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE)
if err := glError(); err != nil {
return nil, err
}
data := make([]uint8, 0, img.w*img.h*4)
for y := 0; y < img.h; y++ {
for x := 0; x < img.w; x++ {
ir, ig, ib, ia := src.At(x, y).RGBA()
r, g, b, a := uint8(ir>>8), uint8(ig>>8), uint8(ib>>8), uint8(ia>>8)
data = append(data, r, g, b, a)
if a < 255 {
img.opaque = false
}
}
}
gli.TexImage2D(gl_TEXTURE_2D, 0, gl_RGBA, int32(img.w), int32(img.h), 0, gl_RGBA, gl_UNSIGNED_BYTE, gli.Ptr(&data[0]))
if err := glError(); err != nil {
return nil, err
}
gli.GenerateMipmap(gl_TEXTURE_2D)
if err := glError(); err != nil {
return nil, err
}
return img, nil
}
// Width returns the width of the image
func (img *Image) Width() int { return img.w }
func (img *Image) Width() int { return img.img.Width() }
// Height returns the height of the image
func (img *Image) Height() int { return img.h }
func (img *Image) Height() int { return img.img.Height() }
// Size returns the width and height of the image
func (img *Image) Size() (int, int) { return img.w, img.h }
func (img *Image) Size() (int, int) { return img.img.Size() }
// Delete deletes the image from memory. Any draw calls with a deleted image
// will not do anything
func (img *Image) Delete() {
gli.DeleteTextures(1, &img.tex)
img.deleted = true
}
func (img *Image) Delete() { img.img.Delete() }
// Replace replaces the image with the new one
func (img *Image) Replace(src interface{}) {
gli.ActiveTexture(gl_TEXTURE0)
gli.BindTexture(gl_TEXTURE_2D, img.tex)
newImg, err := loadImage(src, img.tex)
func (img *Image) Replace(src interface{}) error {
newImg, err := img.cv.LoadImage(src)
if err != nil {
fmt.Fprintf(os.Stderr, "Error replacing image: %v\n", err)
return
return err
}
*img = *newImg
img.img = newImg.img
return nil
}
// DrawImage draws the given image to the given coordinates. The image
@ -281,22 +129,22 @@ func (cv *Canvas) DrawImage(image interface{}, coords ...float64) {
img = &cv2.offscrImg
flip = true
} else {
img = getImage(image)
img = cv.getImage(image)
}
if img == nil {
return
}
if img.deleted {
if img.img.IsDeleted() {
return
}
cv.activate()
var sx, sy, sw, sh, dx, dy, dw, dh float64
sw, sh = float64(img.w), float64(img.h)
dw, dh = float64(img.w), float64(img.h)
sw, sh = float64(img.Width()), float64(img.Height())
dw, dh = float64(img.Width()), float64(img.Height())
if len(coords) == 2 {
dx, dy = coords[0], coords[1]
} else if len(coords) == 4 {
@ -314,55 +162,13 @@ func (cv *Canvas) DrawImage(image interface{}, coords ...float64) {
dh = -dh
}
sx /= float64(img.w)
sy /= float64(img.h)
sw /= float64(img.w)
sh /= float64(img.h)
var data [4][2]float64
data[0] = cv.tf(vec{dx, dy})
data[1] = cv.tf(vec{dx, dy + dh})
data[2] = cv.tf(vec{dx + dw, dy + dh})
data[3] = cv.tf(vec{dx + dw, dy})
p0 := cv.tf(vec{dx, dy})
p1 := cv.tf(vec{dx, dy + dh})
p2 := cv.tf(vec{dx + dw, dy + dh})
p3 := cv.tf(vec{dx + dw, dy})
cv.drawShadow2(data[:])
if cv.state.shadowColor.A != 0 {
tris := [24]float32{
0, 0,
float32(cv.fw), 0,
float32(cv.fw), float32(cv.fh),
0, 0,
float32(cv.fw), float32(cv.fh),
0, float32(cv.fh),
float32(p0[0]), float32(p0[1]),
float32(p3[0]), float32(p3[1]),
float32(p2[0]), float32(p2[1]),
float32(p0[0]), float32(p0[1]),
float32(p2[0]), float32(p2[1]),
float32(p1[0]), float32(p1[1]),
}
cv.drawShadow(tris[:])
}
gli.StencilFunc(gl_EQUAL, 0, 0xFF)
gli.BindBuffer(gl_ARRAY_BUFFER, buf)
data := [16]float32{float32(p0[0]), float32(p0[1]), float32(p1[0]), float32(p1[1]), float32(p2[0]), float32(p2[1]), float32(p3[0]), float32(p3[1]),
float32(sx), float32(sy), float32(sx), float32(sy + sh), float32(sx + sw), float32(sy + sh), float32(sx + sw), float32(sy)}
gli.BufferData(gl_ARRAY_BUFFER, len(data)*4, unsafe.Pointer(&data[0]), gl_STREAM_DRAW)
gli.ActiveTexture(gl_TEXTURE0)
gli.BindTexture(gl_TEXTURE_2D, img.tex)
gli.UseProgram(ir.id)
gli.Uniform1i(ir.image, 0)
gli.Uniform2f(ir.canvasSize, float32(cv.fw), float32(cv.fh))
gli.Uniform1f(ir.globalAlpha, float32(cv.state.globalAlpha))
gli.VertexAttribPointer(ir.vertex, 2, gl_FLOAT, false, 0, 0)
gli.VertexAttribPointer(ir.texCoord, 2, gl_FLOAT, false, 0, 8*4)
gli.EnableVertexAttribArray(ir.vertex)
gli.EnableVertexAttribArray(ir.texCoord)
gli.DrawArrays(gl_TRIANGLE_FAN, 0, 4)
gli.DisableVertexAttribArray(ir.vertex)
gli.DisableVertexAttribArray(ir.texCoord)
gli.StencilFunc(gl_ALWAYS, 0, 0xFF)
cv.b.DrawImage(img.img, sx, sy, sw, sh, dx, dy, dw, dh, cv.state.globalAlpha)
}

View file

@ -158,7 +158,7 @@ func (cv *Canvas) strokePath(path *Path2D) {
cv.drawShadow2(tris)
stl := cv.backendStyle(&cv.state.stroke, 1)
stl := cv.backendFillStyle(&cv.state.stroke, 1)
cv.b.Fill(&stl, tris)
}
@ -352,7 +352,7 @@ func (cv *Canvas) FillPath(path *Path2D) {
cv.drawShadow2(tris)
stl := cv.backendStyle(&cv.state.fill, 1)
stl := cv.backendFillStyle(&cv.state.fill, 1)
cv.b.Fill(&stl, tris)
}
@ -528,7 +528,7 @@ func (cv *Canvas) FillRect(x, y, w, h float64) {
cv.drawShadow2(data[:])
stl := cv.backendStyle(&cv.state.fill, 1)
stl := cv.backendFillStyle(&cv.state.fill, 1)
cv.b.Fill(&stl, data[:])
}

View file

@ -12,6 +12,7 @@ import (
"github.com/go-gl/gl/v3.2-core/gl"
"github.com/tfriedel6/canvas"
"github.com/tfriedel6/canvas/backend/backendbase"
"github.com/tfriedel6/canvas/backend/gogl"
"github.com/tfriedel6/canvas/glimpl/gogl"
"github.com/veandco/go-sdl2/sdl"
@ -23,7 +24,7 @@ type Window struct {
Window *sdl.Window
WindowID uint32
GLContext sdl.GLContext
Backend canvas.Backend
Backend backendbase.Backend
canvas *canvas.Canvas
frameTimes [10]time.Time
frameIndex int

View file

@ -28,7 +28,7 @@ func (cv *Canvas) drawShadow2(pts [][2]float64) {
})
}
style := backendbase.Style{Color: cv.state.shadowColor, GlobalAlpha: 1, Blur: cv.state.shadowBlur}
style := backendbase.FillStyle{Color: cv.state.shadowColor, Blur: cv.state.shadowBlur}
cv.b.Fill(&style, cv.shadowBuf)
}