From 476dbac6e233008bd779bf2ba10fa15e0eac51fa Mon Sep 17 00:00:00 2001 From: Thomas Friedel Date: Wed, 20 Feb 2019 19:59:17 +0100 Subject: [PATCH] moved image loading and drawing into backend --- backend/backendbase/base.go | 34 ++++- backend/gogl/fill.go | 4 +- backend/gogl/gogl.go | 28 +++- backend/gogl/images.go | 250 +++++++++++++++++++++++++++++++ canvas.go | 95 ++++++------ canvas_test.go | 18 +-- images.go | 286 ++++++------------------------------ paths.go | 6 +- sdlcanvas/sdlcanvas.go | 3 +- shadows.go | 2 +- 10 files changed, 406 insertions(+), 320 deletions(-) create mode 100644 backend/gogl/images.go diff --git a/backend/backendbase/base.go b/backend/backendbase/base.go index cf47793..2546508 100644 --- a/backend/backendbase/base.go +++ b/backend/backendbase/base.go @@ -1,12 +1,36 @@ package backendbase -import "image/color" +import ( + "image" + "image/color" +) -type Style struct { - Color color.RGBA - GlobalAlpha float64 - Blur float64 +// 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 + 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 +} diff --git a/backend/gogl/fill.go b/backend/gogl/fill.go index 5ab3e8a..a04b448 100644 --- a/backend/gogl/fill.go +++ b/backend/gogl/fill.go @@ -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) diff --git a/backend/gogl/gogl.go b/backend/gogl/gogl.go index 4dd6453..196440f 100644 --- a/backend/gogl/gogl.go +++ b/backend/gogl/gogl.go @@ -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 } diff --git a/backend/gogl/images.go b/backend/gogl/images.go new file mode 100644 index 0000000..d9f943c --- /dev/null +++ b/backend/gogl/images.go @@ -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) +} diff --git a/canvas.go b/canvas.go index a5fcb4b..0e5b073 100644 --- a/canvas.go +++ b/canvas.go @@ -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)) diff --git a/canvas_test.go b/canvas_test.go index 58d8b92..2b57de5 100644 --- a/canvas_test.go +++ b/canvas_test.go @@ -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() } } diff --git a/images.go b/images.go index 004b605..0a835d4 100644 --- a/images.go +++ b/images.go @@ -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) } diff --git a/paths.go b/paths.go index 76a3bd4..80be987 100644 --- a/paths.go +++ b/paths.go @@ -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[:]) } diff --git a/sdlcanvas/sdlcanvas.go b/sdlcanvas/sdlcanvas.go index 096b87c..779af0a 100644 --- a/sdlcanvas/sdlcanvas.go +++ b/sdlcanvas/sdlcanvas.go @@ -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 diff --git a/shadows.go b/shadows.go index 1fe5b8c..7f6c6b2 100644 --- a/shadows.go +++ b/shadows.go @@ -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) }