├── example ├── image │ ├── gopher.png │ ├── README.md │ └── main.go ├── breakout │ ├── brick.go │ ├── paddle.go │ ├── vec2.go │ ├── ball.go │ ├── main.go │ └── game.go ├── hilbert │ └── main.go ├── clock │ └── main.go ├── particles │ └── main.go ├── retro-synthwave │ └── main.go ├── trail │ └── main.go └── cloth │ └── main.go ├── go.mod ├── go.sum ├── doc.go ├── .github ├── workflows │ └── build.yml └── dependabot.yml ├── serve_test.go ├── pattern.go ├── web ├── index.html.tmpl └── canvas-websocket.js ├── LICENSE ├── image_test.go ├── image.go ├── gradient.go ├── options_test.go ├── options.go ├── benchmark_test.go ├── buffer.go ├── serve.go ├── buffer_test.go ├── README.md ├── enums.go ├── event_test.go ├── event.go ├── context_test.go └── context.go /example/image/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fzipp/canvas/HEAD/example/image/gopher.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fzipp/canvas 2 | 3 | go 1.25.0 4 | 5 | require github.com/gorilla/websocket v1.5.3 6 | 7 | // Test dependencies 8 | require github.com/google/go-cmp v0.7.0 9 | -------------------------------------------------------------------------------- /example/image/README.md: -------------------------------------------------------------------------------- 1 | [The Go gopher] was designed by [Renee French]. 2 | The design is licensed under the Creative Commons 3.0 Attributions license. 3 | 4 | [Renee French]: https://reneefrench.blogspot.com/ 5 | [The Go gopher]: https://go.dev/blog/gopher 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 4 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package canvas communicates with an HTML canvas in a web browser via 6 | // WebSocket. The server program sends draw commands to the canvas and the 7 | // canvas sends mouse and keyboard events to the server. 8 | package canvas 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | permissions: 8 | contents: read 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v6 12 | - name: Setup Go 13 | uses: actions/setup-go@v6 14 | with: 15 | go-version: 'stable' 16 | - name: Run tests 17 | run: go test -cover ./... 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /example/breakout/brick.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "image" 9 | "image/color" 10 | 11 | "github.com/fzipp/canvas" 12 | ) 13 | 14 | type brick struct { 15 | rect image.Rectangle 16 | color color.Color 17 | points int 18 | } 19 | 20 | func (b *brick) draw(ctx *canvas.Context) { 21 | ctx.SetFillStyle(b.color) 22 | ctx.FillRect( 23 | float64(b.rect.Min.X), float64(b.rect.Min.Y), 24 | float64(b.rect.Dx()), float64(b.rect.Dy())) 25 | } 26 | 27 | func (b *brick) bounds() image.Rectangle { 28 | return b.rect 29 | } 30 | -------------------------------------------------------------------------------- /example/breakout/paddle.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "image" 9 | "image/color" 10 | 11 | "github.com/fzipp/canvas" 12 | ) 13 | 14 | type paddle struct { 15 | pos vec2 16 | size vec2 17 | color color.Color 18 | } 19 | 20 | func (p *paddle) draw(ctx *canvas.Context) { 21 | ctx.SetFillStyle(p.color) 22 | x := p.pos.x - (p.size.x / 2) 23 | y := p.pos.y - (p.size.y / 2) 24 | ctx.FillRect(x, y, p.size.x, p.size.y) 25 | } 26 | 27 | func (p *paddle) bounds() image.Rectangle { 28 | return image.Rect( 29 | int(p.pos.x-p.size.x/2), int(p.pos.y-p.size.y/2), 30 | int(p.pos.x+p.size.x/2), int(p.pos.y+p.size.y/2)) 31 | } 32 | -------------------------------------------------------------------------------- /example/breakout/vec2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "math" 8 | 9 | type vec2 struct { 10 | x, y float64 11 | } 12 | 13 | func (v vec2) add(w vec2) vec2 { 14 | return vec2{x: v.x + w.x, y: v.y + w.y} 15 | } 16 | 17 | func (v vec2) sub(w vec2) vec2 { 18 | return vec2{x: v.x - w.x, y: v.y - w.y} 19 | } 20 | 21 | func (v vec2) dot(w vec2) float64 { 22 | return v.x*w.x + v.y*w.y 23 | } 24 | 25 | func (v vec2) mul(s float64) vec2 { 26 | return vec2{v.x * s, v.y * s} 27 | } 28 | 29 | func (v vec2) div(s float64) vec2 { 30 | return vec2{v.x / s, v.y / s} 31 | } 32 | 33 | func (v vec2) norm() vec2 { 34 | return v.div(v.len()) 35 | } 36 | 37 | func (v vec2) len() float64 { 38 | return math.Sqrt(v.sqLen()) 39 | } 40 | 41 | func (v vec2) sqLen() float64 { 42 | return v.dot(v) 43 | } 44 | 45 | func (v vec2) reflect(n vec2) vec2 { 46 | return v.sub(n.mul(2 * v.dot(n))) 47 | } 48 | -------------------------------------------------------------------------------- /serve_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | "image/color" 9 | "testing" 10 | ) 11 | 12 | func TestRGBAString(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | color color.Color 16 | want string 17 | }{ 18 | {"zero", color.RGBA{}, "rgba(0, 0, 0, 0)"}, 19 | {"black", color.Black, "rgba(0, 0, 0, 1)"}, 20 | {"white", color.White, "rgba(255, 255, 255, 1)"}, 21 | {"red", color.RGBA{R: 0xFF, A: 0xFF}, "rgba(255, 0, 0, 1)"}, 22 | {"green", color.RGBA{G: 0xFF, A: 0xFF}, "rgba(0, 255, 0, 1)"}, 23 | {"blue", color.RGBA{B: 0xFF, A: 0xFF}, "rgba(0, 0, 255, 1)"}, 24 | {"grey", color.RGBA{R: 0x7F, G: 0x7F, B: 0x7F, A: 0xFF}, "rgba(127, 127, 127, 1)"}, 25 | {"grey semi-transparent", color.RGBA{R: 0x7F, G: 0x7F, B: 0x7F, A: 0x7F}, "rgba(127, 127, 127, 0.5)"}, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | got := rgbaString(tt.color) 30 | if got != tt.want { 31 | t.Errorf("rgbaString(%v) = %v, want: %v", tt.color, got, tt.want) 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pattern.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // The API doc comments are based on the MDN Web Docs for the [Canvas API] 6 | // by Mozilla Contributors and are licensed under [CC-BY-SA 2.5]. 7 | // 8 | // [Canvas API]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D 9 | // [CC-BY-SA 2.5]: https://creativecommons.org/licenses/by-sa/2.5/ 10 | 11 | package canvas 12 | 13 | // Pattern represents a pattern, based on an image, created by the 14 | // Context.CreatePattern method. It can be used with the 15 | // Context.SetFillStylePattern and Context.SetStrokeStylePattern methods. 16 | // 17 | // The pattern should be released with the Release method when it is no longer 18 | // needed. 19 | type Pattern struct { 20 | id uint32 21 | ctx *Context 22 | released bool 23 | } 24 | 25 | // Release releases the pattern on the client side. 26 | func (p *Pattern) Release() { 27 | if p.released { 28 | return 29 | } 30 | p.ctx.buf.addByte(bReleasePattern) 31 | p.ctx.buf.addUint32(p.id) 32 | p.released = true 33 | } 34 | 35 | func (p *Pattern) checkUseAfterRelease() { 36 | if p.released { 37 | panic("Pattern: use after release") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web/index.html.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | {{.Title}} 11 | 12 | 30 | 31 | 32 | 33 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Frederik Zipp. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of the copyright owner nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /example/breakout/ball.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "image" 9 | "image/color" 10 | "math" 11 | 12 | "github.com/fzipp/canvas" 13 | ) 14 | 15 | type ball struct { 16 | pos vec2 17 | radius float64 18 | v vec2 19 | color color.Color 20 | } 21 | 22 | func (b *ball) update() { 23 | b.pos = b.pos.add(b.v) 24 | } 25 | 26 | func (b *ball) draw(ctx *canvas.Context) { 27 | ctx.SetFillStyle(b.color) 28 | ctx.BeginPath() 29 | ctx.Arc( 30 | b.pos.x, b.pos.y, 31 | b.radius, 0, 2*math.Pi, false) 32 | ctx.ClosePath() 33 | ctx.Fill() 34 | } 35 | 36 | func (b *ball) bounds() image.Rectangle { 37 | return image.Rect( 38 | int(b.pos.x-b.radius), int(b.pos.y-b.radius), 39 | int(b.pos.x+b.radius), int(b.pos.y+b.radius)) 40 | } 41 | 42 | func (b *ball) bounceOnCollision(rect image.Rectangle) bool { 43 | n := b.checkCollision(rect) 44 | if n == (vec2{}) { 45 | return false 46 | } 47 | b.pos = b.pos.sub(b.v) 48 | b.v = b.v.reflect(n) 49 | b.pos = b.pos.add(b.v) 50 | return true 51 | } 52 | 53 | func (b *ball) checkCollision(rect image.Rectangle) (normal vec2) { 54 | is := b.bounds().Intersect(rect) 55 | if is == (image.Rectangle{}) { 56 | return normal 57 | } 58 | if is.Min.Y == rect.Min.Y { 59 | normal = normal.add(vec2{0, -1}) 60 | } 61 | if is.Max.Y == rect.Max.Y { 62 | normal = normal.add(vec2{0, 1}) 63 | } 64 | if is.Min.X == rect.Min.X { 65 | normal = normal.add(vec2{-1, 0}) 66 | } 67 | if is.Max.X == rect.Max.X { 68 | normal = normal.add(vec2{1, 0}) 69 | } 70 | return normal.norm() 71 | } 72 | -------------------------------------------------------------------------------- /image_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | "image" 9 | "image/color" 10 | "testing" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestEnsureRGBA(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | img image.Image 19 | want *image.RGBA 20 | }{ 21 | { 22 | "Gray", 23 | &image.Gray{ 24 | Pix: []uint8{ 25 | 0x00, 0x4a, 26 | 0xb0, 0x12, 27 | 0xff, 0xa0, 28 | }, 29 | Stride: 2, 30 | Rect: image.Rect(0, 0, 2, 3), 31 | }, 32 | &image.RGBA{ 33 | Pix: []uint8{ 34 | 0x00, 0x00, 0x00, 0xff, 0x4a, 0x4a, 0x4a, 0xff, 35 | 0xb0, 0xb0, 0xb0, 0xff, 0x12, 0x12, 0x12, 0xff, 36 | 0xff, 0xff, 0xff, 0xff, 0xa0, 0xa0, 0xa0, 0xff, 37 | }, 38 | Stride: 8, 39 | Rect: image.Rect(0, 0, 2, 3), 40 | }, 41 | }, 42 | { 43 | "Paletted", 44 | &image.Paletted{ 45 | Pix: []uint8{ 46 | 0x01, 0x02, 47 | 0x03, 0x01, 48 | 0x00, 0x02, 49 | }, 50 | Stride: 2, 51 | Rect: image.Rect(0, 0, 2, 3), 52 | Palette: color.Palette{ 53 | color.RGBA{R: 0xa1, G: 0xa2, B: 0xa3, A: 0xa4}, 54 | color.RGBA{R: 0xb1, G: 0xb2, B: 0xb3, A: 0xb4}, 55 | color.RGBA{R: 0xc1, G: 0xc2, B: 0xc3, A: 0xc4}, 56 | color.RGBA{R: 0xd1, G: 0xd2, B: 0xd3, A: 0xd4}, 57 | }, 58 | }, 59 | &image.RGBA{ 60 | Pix: []uint8{ 61 | 0xb1, 0xb2, 0xb3, 0xb4, 0xc1, 0xc2, 0xc3, 0xc4, 62 | 0xd1, 0xd2, 0xd3, 0xd4, 0xb1, 0xb2, 0xb3, 0xb4, 63 | 0xa1, 0xa2, 0xa3, 0xa4, 0xc1, 0xc2, 0xc3, 0xc4, 64 | }, 65 | Stride: 8, 66 | Rect: image.Rect(0, 0, 2, 3), 67 | }, 68 | }, 69 | } 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | got := ensureRGBA(tt.img) 73 | if diff := cmp.Diff(tt.want, got); diff != "" { 74 | t.Errorf("mismatch (-want +got):\n%s", diff) 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // The API doc comments are based on the MDN Web Docs for the [Canvas API] 6 | // by Mozilla Contributors and are licensed under [CC-BY-SA 2.5]. 7 | // 8 | // [Canvas API]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D 9 | // [CC-BY-SA 2.5]: https://creativecommons.org/licenses/by-sa/2.5/ 10 | 11 | package canvas 12 | 13 | import ( 14 | "image" 15 | "image/draw" 16 | ) 17 | 18 | // ImageData represents the underlying pixel data of an image. It is 19 | // created using the Context.CreateImageData and Context.GetImageData methods. 20 | // It can also be used to set a part of the canvas by using 21 | // Context.PutImageData, Context.PutImageDataDirty, Context.DrawImage and 22 | // Context.DrawImageScaled. 23 | // 24 | // The image data should be released with the Release method when it is no 25 | // longer needed. 26 | type ImageData struct { 27 | id uint32 28 | ctx *Context 29 | width int 30 | height int 31 | released bool 32 | } 33 | 34 | // Width returns the actual width, in pixels, of the image. 35 | func (m *ImageData) Width() int { 36 | return m.width 37 | } 38 | 39 | // Height returns the actual height, in pixels, of the image. 40 | func (m *ImageData) Height() int { 41 | return m.height 42 | } 43 | 44 | // Release releases the image data on the client side. 45 | func (m *ImageData) Release() { 46 | if m.released { 47 | return 48 | } 49 | m.ctx.buf.addByte(bReleaseImageData) 50 | m.ctx.buf.addUint32(m.id) 51 | m.released = true 52 | } 53 | 54 | func (m *ImageData) checkUseAfterRelease() { 55 | if m.released { 56 | panic("ImageData: use after release") 57 | } 58 | } 59 | 60 | func ensureRGBA(img image.Image) *image.RGBA { 61 | switch im := img.(type) { 62 | case *image.RGBA: 63 | return im 64 | default: 65 | rgba := image.NewRGBA(im.Bounds()) 66 | draw.Draw(rgba, im.Bounds(), im, image.Pt(0, 0), draw.Src) 67 | return rgba 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/breakout/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Breakout is a classic arcade game where the player controls a paddle at the 6 | // bottom of the screen to bounce a ball and break through a wall of bricks at 7 | // the top of the screen. The objective is to clear all the bricks while 8 | // keeping the ball in play and preventing it from falling off the bottom of 9 | // the screen. 10 | // 11 | // Usage: 12 | // 13 | // breakout [-http address] 14 | // 15 | // Flags: 16 | // 17 | // -http HTTP service address (e.g., '127.0.0.1:8080' or just ':8080'). 18 | // The default is ':8080'. 19 | package main 20 | 21 | import ( 22 | "flag" 23 | "fmt" 24 | "log" 25 | "time" 26 | 27 | "github.com/fzipp/canvas" 28 | ) 29 | 30 | func main() { 31 | http := flag.String("http", ":8080", "HTTP service address (e.g., '127.0.0.1:8080' or just ':8080')") 32 | flag.Parse() 33 | 34 | fmt.Println("Listening on " + httpLink(*http)) 35 | err := canvas.ListenAndServe(*http, run, &canvas.Options{ 36 | Title: "Breakout", 37 | Width: 1334, 38 | Height: 750, 39 | ScaleToPageWidth: true, 40 | ScaleToPageHeight: true, 41 | MouseCursorHidden: true, 42 | EnabledEvents: []canvas.Event{ 43 | canvas.MouseMoveEvent{}, 44 | canvas.KeyDownEvent{}, 45 | canvas.TouchStartEvent{}, 46 | canvas.TouchMoveEvent{}, 47 | }, 48 | ReconnectInterval: time.Second, 49 | }) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | } 54 | 55 | func run(ctx *canvas.Context) { 56 | size := vec2{x: float64(ctx.CanvasWidth()), y: float64(ctx.CanvasHeight())} 57 | game := newGame(size) 58 | ctx.SetFont("30px sans-serif") 59 | for !game.quit { 60 | select { 61 | case event := <-ctx.Events(): 62 | game.handle(event) 63 | default: 64 | game.update() 65 | game.draw(ctx) 66 | ctx.Flush() 67 | time.Sleep(5 * time.Millisecond) 68 | } 69 | } 70 | } 71 | 72 | func httpLink(addr string) string { 73 | if addr[0] == ':' { 74 | addr = "localhost" + addr 75 | } 76 | return "http://" + addr 77 | } 78 | -------------------------------------------------------------------------------- /gradient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // The API doc comments are based on the MDN Web Docs for the [Canvas API] 6 | // by Mozilla Contributors and are licensed under [CC-BY-SA 2.5]. 7 | // 8 | // [Canvas API]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D 9 | // [CC-BY-SA 2.5]: https://creativecommons.org/licenses/by-sa/2.5/ 10 | 11 | package canvas 12 | 13 | import "image/color" 14 | 15 | // Gradient represents a gradient. It is returned by the methods 16 | // Context.CreateLinearGradient and Context.CreateRadialGradient. 17 | // It can be used with the Context.SetFillStyleGradient and 18 | // Context.SetStrokeStyleGradient methods. 19 | // 20 | // The gradient should be released with the Release method when it is no longer 21 | // needed. 22 | type Gradient struct { 23 | id uint32 24 | ctx *Context 25 | released bool 26 | } 27 | 28 | // AddColorStop adds a new stop, defined by an offset and a color, to the 29 | // gradient. 30 | func (g *Gradient) AddColorStop(offset float64, c color.Color) { 31 | g.checkUseAfterRelease() 32 | g.ctx.buf.addByte(bGradientAddColorStop) 33 | g.ctx.buf.addUint32(g.id) 34 | g.ctx.buf.addFloat64(offset) 35 | g.ctx.buf.addColor(c) 36 | } 37 | 38 | // AddColorStopString adds a new stop, defined by an offset and a color, to 39 | // the gradient. 40 | // 41 | // The color is parsed as a CSS color value like "#a100cb", "#ccc", 42 | // "darkgreen", "rgba(0.5, 0.2, 0.7, 1.0)", etc. 43 | func (g *Gradient) AddColorStopString(offset float64, color string) { 44 | g.checkUseAfterRelease() 45 | g.ctx.buf.addByte(bGradientAddColorStopString) 46 | g.ctx.buf.addUint32(g.id) 47 | g.ctx.buf.addFloat64(offset) 48 | g.ctx.buf.addString(color) 49 | } 50 | 51 | // Release releases the gradient on the client side. 52 | func (g *Gradient) Release() { 53 | if g.released { 54 | return 55 | } 56 | g.ctx.buf.addByte(bReleaseGradient) 57 | g.ctx.buf.addUint32(g.id) 58 | g.released = true 59 | } 60 | 61 | func (g *Gradient) checkUseAfterRelease() { 62 | if g.released { 63 | panic("Gradient: use after release") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/image/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Image demonstrates how to draw image data on the canvas. 6 | // It displays a scaling animation of a Go gopher image loaded from PNG data. 7 | // 8 | // Usage: 9 | // 10 | // image [-http address] 11 | // 12 | // Flags: 13 | // 14 | // -http HTTP service address (e.g., '127.0.0.1:8080' or just ':8080'). 15 | // The default is ':8080'. 16 | package main 17 | 18 | import ( 19 | "bytes" 20 | _ "embed" 21 | "flag" 22 | "fmt" 23 | "image" 24 | _ "image/png" 25 | "log" 26 | "time" 27 | 28 | "github.com/fzipp/canvas" 29 | ) 30 | 31 | //go:embed gopher.png 32 | var gopherPNG []byte 33 | 34 | func main() { 35 | http := flag.String("http", ":8080", "HTTP service address (e.g., '127.0.0.1:8080' or just ':8080')") 36 | flag.Parse() 37 | 38 | fmt.Println("Listening on " + httpLink(*http)) 39 | err := canvas.ListenAndServe(*http, run, &canvas.Options{ 40 | Title: "ImageData", 41 | Width: 1280, 42 | Height: 720, 43 | }) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | } 48 | 49 | func run(ctx *canvas.Context) { 50 | img, _, err := image.Decode(bytes.NewBuffer(gopherPNG)) 51 | if err != nil { 52 | log.Println(err) 53 | return 54 | } 55 | gopher := ctx.CreateImageData(img) 56 | d := &demo{ 57 | gopher: gopher, 58 | } 59 | for { 60 | select { 61 | case event := <-ctx.Events(): 62 | if _, ok := event.(canvas.CloseEvent); ok { 63 | return 64 | } 65 | default: 66 | d.update() 67 | d.draw(ctx) 68 | ctx.Flush() 69 | time.Sleep(20 * time.Millisecond) 70 | } 71 | } 72 | } 73 | 74 | type demo struct { 75 | x, y int 76 | w, h int 77 | gopher *canvas.ImageData 78 | } 79 | 80 | func (d *demo) update() { 81 | d.x = (d.x + 10) % 1280 82 | d.w = d.x 83 | d.h = d.x 84 | } 85 | 86 | func (d *demo) draw(ctx *canvas.Context) { 87 | ctx.ClearRect(0, 0, float64(ctx.CanvasWidth()), float64(ctx.CanvasHeight())) 88 | ctx.DrawImageScaled(d.gopher, float64(d.x), float64(d.y), float64(d.w), float64(d.h)) 89 | } 90 | 91 | func httpLink(addr string) string { 92 | if addr[0] == ':' { 93 | addr = "localhost" + addr 94 | } 95 | return "http://" + addr 96 | } 97 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | "image/color" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestOptionsApplyDefaults(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | opts *Options 18 | want *Options 19 | }{ 20 | { 21 | "empty options", 22 | &Options{}, 23 | &Options{ 24 | Width: 300, 25 | Height: 150, 26 | PageBackground: color.White, 27 | }, 28 | }, 29 | { 30 | "width and height given", 31 | &Options{ 32 | Width: 800, 33 | Height: 600, 34 | }, 35 | &Options{ 36 | Width: 800, 37 | Height: 600, 38 | PageBackground: color.White, 39 | }, 40 | }, 41 | { 42 | "background color given", 43 | &Options{ 44 | PageBackground: color.Black, 45 | }, 46 | &Options{ 47 | Width: 300, 48 | Height: 150, 49 | PageBackground: color.Black, 50 | }, 51 | }, 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | got := *tt.opts 56 | got.applyDefaults() 57 | if diff := cmp.Diff(tt.want, &got, cmp.AllowUnexported(Options{})); diff != "" { 58 | t.Errorf("mismatch (-want +got):\n%s", diff) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestOptionsEventMask(t *testing.T) { 65 | tests := []struct { 66 | name string 67 | opts *Options 68 | want eventMask 69 | }{ 70 | { 71 | "empty options", 72 | &Options{}, 73 | 0, 74 | }, 75 | { 76 | "multiple events", 77 | &Options{ 78 | EnabledEvents: []Event{ 79 | KeyUpEvent{}, 80 | MouseMoveEvent{}, 81 | TouchStartEvent{}, 82 | }, 83 | }, 84 | 0b1000010001, 85 | }, 86 | { 87 | "keyboard events", 88 | &Options{ 89 | EnabledEvents: []Event{ 90 | KeyboardEvent{}, 91 | }, 92 | }, 93 | 0b00011000, 94 | }, 95 | { 96 | "mouse events", 97 | &Options{ 98 | EnabledEvents: []Event{ 99 | MouseEvent{}, 100 | }, 101 | }, 102 | 0b11100111, 103 | }, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | got := tt.opts.eventMask() 108 | if got != tt.want { 109 | t.Errorf("opts.EnabledEvents = %#v\nopts.eventMask() = %#b, want: %#b", 110 | tt.opts.EnabledEvents, got, tt.want) 111 | } 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | "image/color" 9 | "time" 10 | ) 11 | 12 | // Options configure various aspects of the canvas. 13 | // The zero value of this struct is useful out of the box, 14 | // but most users probably want to set at least Width and Height. 15 | type Options struct { 16 | // Title sets the title of the browser tab/window. 17 | Title string 18 | // Width sets the width of the canvas. 19 | // If Width is not set (i.e. 0) a default value of 300 will be used. 20 | Width int 21 | // Height sets the height of the canvas. 22 | // If Height is not set (i.e. 0) a default value of 150 will be used. 23 | Height int 24 | // PageBackground configures the background color 25 | // of the served HTML page. 26 | // If PageBackground is not set (i.e. nil) a default value of 27 | // color.White will be used. 28 | PageBackground color.Color 29 | // EnabledEvents enables transmission of the given event types 30 | // from the client to the server. 31 | // It is sufficient to list the zero values of the events here. 32 | // CloseEvent is always implicitly enabled, 33 | // it doesn't have to be part of this slice. 34 | EnabledEvents []Event 35 | // MouseCursorHidden hides the mouse cursor on the canvas. 36 | MouseCursorHidden bool 37 | // ContextMenuDisabled disables the context menu on the canvas. 38 | ContextMenuDisabled bool 39 | // ScaleToPageWidth scales the canvas to the full horizontal extent 40 | // of the page in the browser window. 41 | // This scaling does not change the width within the coordinate system 42 | // as set by Width. 43 | ScaleToPageWidth bool 44 | // ScaleToPageHeight scales the canvas to the full vertical extent 45 | // of the page in the browser window. 46 | // This scaling does not change the height within the coordinate system 47 | // as set by Height. 48 | ScaleToPageHeight bool 49 | // ReconnectInterval configures the client to reconnect after 50 | // the given duration if the WebSocket connection was lost. 51 | // The client tries to reconnect repeatedly until it is successful. 52 | // If ReconnectInterval is not set (i.e. 0) the canvas will not try 53 | // to reconnect if the connection was lost. 54 | ReconnectInterval time.Duration 55 | } 56 | 57 | func (o *Options) applyDefaults() { 58 | if o.Width == 0 { 59 | o.Width = 300 60 | } 61 | if o.Height == 0 { 62 | o.Height = 150 63 | } 64 | if o.PageBackground == nil { 65 | o.PageBackground = color.White 66 | } 67 | } 68 | 69 | func (o *Options) eventMask() (mask eventMask) { 70 | for _, e := range o.EnabledEvents { 71 | mask |= e.mask() 72 | } 73 | return mask 74 | } 75 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | "image" 9 | "image/color" 10 | "testing" 11 | ) 12 | 13 | func BenchmarkContext(b *testing.B) { 14 | draws := make(chan []byte) 15 | go func() { 16 | for range draws { 17 | } 18 | }() 19 | ctx := newContext(draws, nil, nil) 20 | for b.Loop() { 21 | ctx.SetFillStyle(color.White) 22 | ctx.SetFillStyleString("green") 23 | ctx.SetFillStyleGradient(&Gradient{}) 24 | ctx.SetFillStylePattern(&Pattern{}) 25 | ctx.SetFont("bold 48px serif") 26 | ctx.SetGlobalAlpha(1) 27 | ctx.SetGlobalCompositeOperation(OpDestinationOut) 28 | ctx.SetImageSmoothingEnabled(true) 29 | ctx.SetLineCap(CapRound) 30 | ctx.SetLineDashOffset(1) 31 | ctx.SetLineJoin(JoinBevel) 32 | ctx.SetLineWidth(1) 33 | ctx.SetMiterLimit(1) 34 | ctx.SetShadowBlur(1) 35 | ctx.SetShadowColor(color.White) 36 | ctx.SetShadowColorString("#ffa0b3") 37 | ctx.SetShadowOffsetX(1) 38 | ctx.SetShadowOffsetY(1) 39 | ctx.SetStrokeStyle(color.White) 40 | ctx.SetStrokeStyleString("yellow") 41 | ctx.SetStrokeStyleGradient(&Gradient{}) 42 | ctx.SetStrokeStylePattern(&Pattern{}) 43 | ctx.SetTextAlign(AlignLeft) 44 | ctx.SetTextBaseline(BaselineBottom) 45 | ctx.Arc(1, 1, 1, 1, 1, true) 46 | ctx.ArcTo(1, 1, 1, 1, 1) 47 | ctx.BeginPath() 48 | ctx.BezierCurveTo(1, 1, 1, 1, 1, 1) 49 | ctx.ClearRect(1, 1, 1, 1) 50 | ctx.Clip() 51 | ctx.ClosePath() 52 | ctx.Ellipse(1, 1, 1, 1, 1, 1, 1, true) 53 | ctx.Fill() 54 | ctx.FillRect(1, 1, 1, 1) 55 | ctx.FillText("hello, world", 1, 1) 56 | ctx.FillTextMaxWidth("hello, world", 1, 1, 1) 57 | ctx.LineTo(1, 1) 58 | ctx.MoveTo(1, 1) 59 | ctx.QuadraticCurveTo(1, 1, 1, 1) 60 | ctx.Rect(1, 1, 1, 1) 61 | ctx.Restore() 62 | ctx.Rotate(1) 63 | ctx.Save() 64 | ctx.Scale(1, 1) 65 | ctx.Stroke() 66 | ctx.StrokeText("hello, world", 1, 1) 67 | ctx.StrokeTextMaxWidth("hello, world", 1, 1, 1) 68 | ctx.StrokeRect(1, 1, 1, 1) 69 | ctx.Translate(1, 1) 70 | ctx.Transform(1, 1, 1, 1, 1, 1) 71 | ctx.SetTransform(1, 1, 1, 1, 1, 1) 72 | ctx.SetLineDash([]float64{1, 1, 1}) 73 | ctx.CreateImageData(image.NewRGBA(image.Rect(0, 0, 0, 0))) 74 | ctx.PutImageData(&ImageData{}, 1, 1) 75 | ctx.PutImageDataDirty(&ImageData{}, 1, 1, 1, 1, 1, 1) 76 | ctx.DrawImage(&ImageData{}, 1, 1) 77 | ctx.DrawImageScaled(&ImageData{}, 1, 1, 1, 1) 78 | ctx.DrawImageSubRectangle(&ImageData{}, 1, 1, 1, 1, 1, 1, 1, 1) 79 | ctx.CreateLinearGradient(1, 1, 1, 1) 80 | ctx.CreateRadialGradient(1, 1, 1, 1, 1, 1) 81 | ctx.CreatePattern(&ImageData{}, PatternRepeat) 82 | ctx.GetImageData(1, 1, 1, 1) 83 | ctx.Flush() 84 | } 85 | close(draws) 86 | } 87 | -------------------------------------------------------------------------------- /buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | "encoding/binary" 9 | "image/color" 10 | "math" 11 | ) 12 | 13 | type buffer struct { 14 | bytes []byte 15 | error error 16 | } 17 | 18 | var byteOrder = binary.BigEndian 19 | 20 | func (buf *buffer) addByte(b byte) { 21 | buf.bytes = append(buf.bytes, b) 22 | } 23 | 24 | func (buf *buffer) addFloat64(f float64) { 25 | buf.bytes = append(buf.bytes, 0, 0, 0, 0, 0, 0, 0, 0) 26 | byteOrder.PutUint64(buf.bytes[len(buf.bytes)-8:], math.Float64bits(f)) 27 | } 28 | 29 | func (buf *buffer) addUint32(i uint32) { 30 | buf.bytes = append(buf.bytes, 0, 0, 0, 0) 31 | byteOrder.PutUint32(buf.bytes[len(buf.bytes)-4:], i) 32 | } 33 | 34 | func (buf *buffer) addBool(b bool) { 35 | if b { 36 | buf.addByte(1) 37 | } else { 38 | buf.addByte(0) 39 | } 40 | } 41 | 42 | func (buf *buffer) addBytes(p []byte) { 43 | buf.bytes = append(buf.bytes, p...) 44 | } 45 | 46 | func (buf *buffer) addString(s string) { 47 | buf.addUint32(uint32(len(s))) 48 | buf.bytes = append(buf.bytes, []byte(s)...) 49 | } 50 | 51 | func (buf *buffer) addColor(c color.Color) { 52 | clr := color.RGBAModel.Convert(c).(color.RGBA) 53 | buf.addByte(clr.R) 54 | buf.addByte(clr.G) 55 | buf.addByte(clr.B) 56 | buf.addByte(clr.A) 57 | } 58 | 59 | func (buf *buffer) readByte() byte { 60 | if len(buf.bytes) < 1 { 61 | buf.dataTooShort() 62 | return 0 63 | } 64 | b := buf.bytes[0] 65 | buf.bytes = buf.bytes[1:] 66 | return b 67 | } 68 | 69 | func (buf *buffer) readUint32() uint32 { 70 | if len(buf.bytes) < 4 { 71 | buf.dataTooShort() 72 | return 0 73 | } 74 | i := byteOrder.Uint32(buf.bytes) 75 | buf.bytes = buf.bytes[4:] 76 | return i 77 | } 78 | 79 | func (buf *buffer) readUint64() uint64 { 80 | if len(buf.bytes) < 8 { 81 | buf.dataTooShort() 82 | return 0 83 | } 84 | i := byteOrder.Uint64(buf.bytes) 85 | buf.bytes = buf.bytes[8:] 86 | return i 87 | } 88 | 89 | func (buf *buffer) readFloat64() float64 { 90 | return math.Float64frombits(buf.readUint64()) 91 | } 92 | 93 | func (buf *buffer) readString() string { 94 | length := int(buf.readUint32()) 95 | if len(buf.bytes) < length { 96 | buf.dataTooShort() 97 | return "" 98 | } 99 | s := string(buf.bytes[:length]) 100 | buf.bytes = buf.bytes[length:] 101 | return s 102 | } 103 | 104 | func (buf *buffer) skip(nBytes int) { 105 | if len(buf.bytes) < nBytes { 106 | buf.dataTooShort() 107 | return 108 | } 109 | buf.bytes = buf.bytes[nBytes:] 110 | } 111 | 112 | func (buf *buffer) reset() { 113 | buf.bytes = make([]byte, 0, cap(buf.bytes)) 114 | } 115 | 116 | func (buf *buffer) dataTooShort() { 117 | buf.reset() 118 | buf.error = errDataTooShort{} 119 | } 120 | 121 | type errDataTooShort struct{} 122 | 123 | func (err errDataTooShort) Error() string { 124 | return "data too short" 125 | } 126 | -------------------------------------------------------------------------------- /example/hilbert/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Hilbert draws a graphic pattern consisting of multiple superimposed Hilbert 6 | // curves using the recursive algorithm described in the book "Algorithms and 7 | // Data Structures" by N. Wirth. 8 | // 9 | // Usage: 10 | // 11 | // hilbert [-http address] 12 | // 13 | // Flags: 14 | // 15 | // -http HTTP service address (e.g., '127.0.0.1:8080' or just ':8080'). 16 | // The default is ':8080'. 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "log" 23 | 24 | "github.com/fzipp/canvas" 25 | ) 26 | 27 | func main() { 28 | http := flag.String("http", ":8080", "HTTP service address (e.g., '127.0.0.1:8080' or just ':8080')") 29 | flag.Parse() 30 | 31 | fmt.Println("Listening on " + httpLink(*http)) 32 | err := canvas.ListenAndServe(*http, run, &canvas.Options{ 33 | Title: "Hilbert", 34 | Width: 500, 35 | Height: 500, 36 | ScaleToPageHeight: true, 37 | }) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | } 42 | 43 | func run(ctx *canvas.Context) { 44 | ctx.SetLineWidth(2) 45 | ctx.SetStrokeStyleString("#777") 46 | h := &hilbert{ctx: ctx} 47 | h.draw() 48 | ctx.Flush() 49 | } 50 | 51 | type hilbert struct { 52 | ctx *canvas.Context 53 | x, y, d int 54 | } 55 | 56 | func (h *hilbert) draw() { 57 | fw := h.ctx.CanvasWidth() 58 | fh := h.ctx.CanvasHeight() 59 | w := min(fw, fh) 60 | h.d = 8 61 | k := 0 62 | for h.d*2 < w { 63 | h.d *= 2 64 | k++ 65 | } 66 | x0 := fw / 2 67 | y0 := fh / 2 68 | for n := 1; n <= k; n++ { 69 | h.d /= 2 70 | x0 += h.d / 2 71 | y0 -= h.d / 2 72 | h.x = x0 73 | h.y = y0 74 | h.ctx.MoveTo(float64(h.x), float64(h.y)) 75 | h.A(n) 76 | } 77 | h.ctx.Stroke() 78 | } 79 | 80 | func (h *hilbert) A(i int) { 81 | if i > 0 { 82 | h.D(i - 1) 83 | h.W() 84 | h.A(i - 1) 85 | h.S() 86 | h.A(i - 1) 87 | h.E() 88 | h.B(i - 1) 89 | } 90 | } 91 | 92 | func (h *hilbert) B(i int) { 93 | if i > 0 { 94 | h.C(i - 1) 95 | h.N() 96 | h.B(i - 1) 97 | h.E() 98 | h.B(i - 1) 99 | h.S() 100 | h.A(i - 1) 101 | } 102 | } 103 | 104 | func (h *hilbert) C(i int) { 105 | if i > 0 { 106 | h.B(i - 1) 107 | h.E() 108 | h.C(i - 1) 109 | h.N() 110 | h.C(i - 1) 111 | h.W() 112 | h.D(i - 1) 113 | } 114 | } 115 | 116 | func (h *hilbert) D(i int) { 117 | if i > 0 { 118 | h.A(i - 1) 119 | h.S() 120 | h.D(i - 1) 121 | h.W() 122 | h.D(i - 1) 123 | h.N() 124 | h.C(i - 1) 125 | } 126 | } 127 | 128 | func (h *hilbert) E() { 129 | h.x += h.d 130 | h.ctx.LineTo(float64(h.x), float64(h.y)) 131 | } 132 | 133 | func (h *hilbert) N() { 134 | h.y -= h.d 135 | h.ctx.LineTo(float64(h.x), float64(h.y)) 136 | } 137 | 138 | func (h *hilbert) W() { 139 | h.x -= h.d 140 | h.ctx.LineTo(float64(h.x), float64(h.y)) 141 | } 142 | 143 | func (h *hilbert) S() { 144 | h.y += h.d 145 | h.ctx.LineTo(float64(h.x), float64(h.y)) 146 | } 147 | 148 | func httpLink(addr string) string { 149 | if addr[0] == ':' { 150 | addr = "localhost" + addr 151 | } 152 | return "http://" + addr 153 | } 154 | -------------------------------------------------------------------------------- /example/clock/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // This example was ported from the MDN canvas tutorial [An animated clock]. 6 | // Original copyright: Any copyright is dedicated to the [Public Domain]. 7 | // 8 | // [An animated clock]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_clock 9 | // [Public Domain]: https://creativecommons.org/publicdomain/zero/1.0/ 10 | 11 | // Clock draws an animated clock, showing the current time. 12 | // 13 | // Usage: 14 | // 15 | // clock [-http address] 16 | // 17 | // Flags: 18 | // 19 | // -http HTTP service address (e.g., '127.0.0.1:8080' or just ':8080'). 20 | // The default is ':8080'. 21 | package main 22 | 23 | import ( 24 | "flag" 25 | "fmt" 26 | "image/color" 27 | "log" 28 | "math" 29 | "time" 30 | 31 | "github.com/fzipp/canvas" 32 | ) 33 | 34 | func main() { 35 | http := flag.String("http", ":8080", "HTTP service address (e.g., '127.0.0.1:8080' or just ':8080')") 36 | flag.Parse() 37 | 38 | fmt.Println("Listening on " + httpLink(*http)) 39 | err := canvas.ListenAndServe(*http, run, &canvas.Options{ 40 | Title: "Clock", 41 | Width: 150, 42 | Height: 150, 43 | }) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | } 48 | 49 | func run(ctx *canvas.Context) { 50 | for { 51 | select { 52 | case event := <-ctx.Events(): 53 | if _, ok := event.(canvas.CloseEvent); ok { 54 | return 55 | } 56 | default: 57 | drawClock(ctx) 58 | ctx.Flush() 59 | time.Sleep(time.Second / 2) 60 | } 61 | } 62 | } 63 | 64 | func drawClock(ctx *canvas.Context) { 65 | now := time.Now() 66 | 67 | ctx.Save() 68 | ctx.ClearRect(0, 0, 150, 150) 69 | ctx.Translate(75, 75) 70 | ctx.Scale(0.4, 0.4) 71 | ctx.Rotate(-math.Pi / 2) 72 | ctx.SetStrokeStyle(color.Black) 73 | ctx.SetFillStyle(color.White) 74 | ctx.SetLineWidth(8) 75 | ctx.SetLineCap(canvas.CapRound) 76 | 77 | // Hour marks 78 | ctx.Save() 79 | for range 12 { 80 | ctx.BeginPath() 81 | ctx.Rotate(math.Pi / 6) 82 | ctx.MoveTo(100, 0) 83 | ctx.LineTo(120, 0) 84 | ctx.Stroke() 85 | } 86 | ctx.Restore() 87 | 88 | // Minute marks 89 | ctx.Save() 90 | ctx.SetLineWidth(5) 91 | for i := range 60 { 92 | if i%5 != 0 { 93 | ctx.BeginPath() 94 | ctx.MoveTo(117, 0) 95 | ctx.LineTo(120, 0) 96 | ctx.Stroke() 97 | } 98 | ctx.Rotate(math.Pi / 30) 99 | } 100 | ctx.Restore() 101 | 102 | second := float64(now.Second()) 103 | minute := float64(now.Minute()) 104 | hour := float64(now.Hour()) 105 | if hour >= 12 { 106 | hour = hour - 12 107 | } 108 | 109 | ctx.SetFillStyle(color.Black) 110 | 111 | // write Hours 112 | ctx.Save() 113 | ctx.Rotate(hour*(math.Pi/6) + (math.Pi/360)*minute + (math.Pi/21600)*second) 114 | ctx.SetLineWidth(14) 115 | ctx.BeginPath() 116 | ctx.MoveTo(-20, 0) 117 | ctx.LineTo(80, 0) 118 | ctx.Stroke() 119 | ctx.Restore() 120 | 121 | // write Minutes 122 | ctx.Save() 123 | ctx.Rotate((math.Pi/30)*minute + (math.Pi/1800)*second) 124 | ctx.SetLineWidth(10) 125 | ctx.BeginPath() 126 | ctx.MoveTo(-28, 0) 127 | ctx.LineTo(112, 0) 128 | ctx.Stroke() 129 | ctx.Restore() 130 | 131 | // Write seconds 132 | ctx.Save() 133 | ctx.Rotate(second * math.Pi / 30) 134 | ctx.SetStrokeStyleString("#D40000") 135 | ctx.SetFillStyleString("#D40000") 136 | ctx.SetLineWidth(6) 137 | ctx.BeginPath() 138 | ctx.MoveTo(-30, 0) 139 | ctx.LineTo(83, 0) 140 | ctx.Stroke() 141 | ctx.BeginPath() 142 | ctx.Arc(0, 0, 10, 0, math.Pi*2, true) 143 | ctx.Fill() 144 | ctx.BeginPath() 145 | ctx.Arc(95, 0, 10, 0, math.Pi*2, true) 146 | ctx.Stroke() 147 | ctx.SetFillStyle(color.Transparent) 148 | ctx.Arc(0, 0, 3, 0, math.Pi*2, true) 149 | ctx.Fill() 150 | ctx.Restore() 151 | 152 | ctx.BeginPath() 153 | ctx.SetLineWidth(14) 154 | ctx.SetStrokeStyleString("#325FA2") 155 | ctx.Arc(0, 0, 142, 0, math.Pi*2, true) 156 | ctx.Stroke() 157 | 158 | ctx.Restore() 159 | } 160 | 161 | func httpLink(addr string) string { 162 | if addr[0] == ':' { 163 | addr = "localhost" + addr 164 | } 165 | return "http://" + addr 166 | } 167 | -------------------------------------------------------------------------------- /example/breakout/game.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "image" 10 | "image/color" 11 | 12 | "github.com/fzipp/canvas" 13 | ) 14 | 15 | type game struct { 16 | started bool 17 | quit bool 18 | score int 19 | round int 20 | size vec2 21 | bricks []brick 22 | paddle paddle 23 | ball ball 24 | } 25 | 26 | func newGame(size vec2) *game { 27 | g := &game{size: size} 28 | g.resetGame() 29 | return g 30 | } 31 | 32 | func (g *game) resetGame() { 33 | g.started = true 34 | g.score = 0 35 | g.round = 1 36 | paddleSize := vec2{x: 100, y: 20} 37 | g.paddle = paddle{ 38 | pos: vec2{x: g.size.x / 2, y: g.size.y - (paddleSize.y / 2)}, 39 | size: paddleSize, 40 | color: color.RGBA{R: 0x0a, G: 0x85, B: 0xc2, A: 0xff}, 41 | } 42 | ballRadius := 5.0 43 | g.ball = ball{ 44 | radius: ballRadius, 45 | color: color.White, 46 | } 47 | g.bricks = g.initialBricks(14, 8) 48 | g.resetBall() 49 | } 50 | 51 | var rowGroupColors = []color.Color{ 52 | color.RGBA{R: 0xa3, G: 0x1e, B: 0x0a, A: 0xff}, 53 | color.RGBA{R: 0xc2, G: 0x85, B: 0x0a, A: 0xff}, 54 | color.RGBA{R: 0x0a, G: 0x85, B: 0x33, A: 0xff}, 55 | color.RGBA{R: 0xc2, G: 0xc2, B: 0x29, A: 0xff}, 56 | } 57 | 58 | var rowGroupPoints = []int{7, 5, 3, 1} 59 | 60 | func (g *game) initialBricks(columns, rows int) []brick { 61 | width := int(g.size.x) / columns 62 | height := 30 63 | bricks := make([]brick, 0, columns*rows) 64 | for row := range rows { 65 | for col := range columns { 66 | x := col * width 67 | y := row * height 68 | rowGroup := (row / 2) % len(rowGroupColors) 69 | bricks = append(bricks, brick{ 70 | rect: image.Rect(x+1, y+1, x+width-1, y+height-1), 71 | color: rowGroupColors[rowGroup], 72 | points: rowGroupPoints[rowGroup], 73 | }) 74 | } 75 | } 76 | return bricks 77 | } 78 | 79 | func (g *game) handle(event canvas.Event) { 80 | switch e := event.(type) { 81 | case canvas.CloseEvent: 82 | g.quit = true 83 | case canvas.MouseMoveEvent: 84 | if g.started { 85 | g.paddle.pos.x = float64(e.X) 86 | } 87 | case canvas.TouchStartEvent: 88 | if len(e.Touches) == 1 { 89 | g.paddle.pos.x = float64(e.Touches[0].X) 90 | } 91 | case canvas.TouchMoveEvent: 92 | if len(e.Touches) == 1 { 93 | g.paddle.pos.x = float64(e.Touches[0].X) 94 | } 95 | case canvas.KeyDownEvent: 96 | const paddleSpeedX = 15 97 | switch e.Key { 98 | case "ArrowRight": 99 | g.paddle.pos.x += paddleSpeedX 100 | if g.paddle.pos.x >= g.size.x { 101 | g.paddle.pos.x = g.size.x - 1 102 | } 103 | case "ArrowLeft": 104 | g.paddle.pos.x -= paddleSpeedX 105 | if g.paddle.pos.x < 0 { 106 | g.paddle.pos.x = 0 107 | } 108 | case " ": 109 | g.started = !g.started 110 | } 111 | } 112 | } 113 | 114 | func (g *game) update() { 115 | if !g.started { 116 | return 117 | } 118 | g.ball.update() 119 | g.checkWallCollisions() 120 | g.checkBrickCollisions() 121 | g.checkPaddleCollision() 122 | } 123 | 124 | func (g *game) resetBall() { 125 | g.ball.pos = g.paddle.pos.sub(vec2{x: 0, y: g.ball.radius + (g.paddle.size.y / 2)}) 126 | g.ball.v = vec2{x: 1, y: -1} 127 | } 128 | 129 | func (g *game) checkWallCollisions() { 130 | ballBounds := g.ball.bounds() 131 | gameBounds := g.bounds() 132 | bottom := ballBounds.Max.Y >= gameBounds.Max.Y 133 | left := ballBounds.Min.X <= gameBounds.Min.X 134 | right := ballBounds.Max.X >= gameBounds.Max.X 135 | top := ballBounds.Min.Y <= gameBounds.Min.Y 136 | if left || right { 137 | g.ball.v.x = -g.ball.v.x 138 | } else if top { 139 | g.ball.v.y = -g.ball.v.y 140 | } else if bottom { 141 | g.round++ 142 | if g.round > 3 { 143 | g.resetGame() 144 | } else { 145 | g.resetBall() 146 | } 147 | } 148 | } 149 | 150 | func (g *game) checkBrickCollisions() { 151 | survivingBricks := make([]brick, 0, len(g.bricks)) 152 | for _, brick := range g.bricks { 153 | collision := g.ball.bounceOnCollision(brick.bounds()) 154 | if collision { 155 | g.score += brick.points 156 | } else { 157 | survivingBricks = append(survivingBricks, brick) 158 | } 159 | } 160 | g.bricks = survivingBricks 161 | } 162 | 163 | func (g *game) checkPaddleCollision() { 164 | g.ball.bounceOnCollision(g.paddle.bounds()) 165 | } 166 | 167 | func (g *game) draw(ctx *canvas.Context) { 168 | g.drawBackground(ctx) 169 | for _, brick := range g.bricks { 170 | brick.draw(ctx) 171 | } 172 | g.paddle.draw(ctx) 173 | g.ball.draw(ctx) 174 | g.drawScore(ctx) 175 | } 176 | 177 | func (g *game) drawBackground(ctx *canvas.Context) { 178 | ctx.SetFillStyle(color.Black) 179 | ctx.FillRect(0, 0, g.size.x, g.size.y) 180 | } 181 | 182 | func (g *game) drawScore(ctx *canvas.Context) { 183 | ctx.SetFillStyle(color.White) 184 | ctx.FillText(fmt.Sprintf("%03d %d", g.score, g.round), 10, 35) 185 | } 186 | 187 | func (g *game) bounds() image.Rectangle { 188 | return image.Rect(0, 0, int(g.size.x), int(g.size.y)) 189 | } 190 | -------------------------------------------------------------------------------- /example/particles/main.go: -------------------------------------------------------------------------------- 1 | // Original JavaScript code by Elton Kamami: 2 | // https://codepen.io/eltonkamami/pen/ECrKd 3 | // Ported to Go by Frederik Zipp. Original copyright: 4 | // 5 | // Copyright (c) 2020 by Elton Kamami (https://codepen.io/eltonkamami/pen/ECrKd) 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to 9 | // deal in the Software without restriction, including without limitation the 10 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | // sell copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 23 | // IN THE SOFTWARE. 24 | 25 | // Particles is a non-interactive canvas demo showing colorful animated 26 | // particles. 27 | // 28 | // Usage: 29 | // 30 | // particles [-http address] 31 | // 32 | // Flags: 33 | // 34 | // -http HTTP service address (e.g., '127.0.0.1:8080' or just ':8080'). 35 | // The default is ':8080'. 36 | package main 37 | 38 | import ( 39 | "flag" 40 | "fmt" 41 | "image/color" 42 | "log" 43 | "math" 44 | "math/rand/v2" 45 | "time" 46 | 47 | "github.com/fzipp/canvas" 48 | ) 49 | 50 | const particlesNum = 500 51 | 52 | var colors = []color.RGBA{ 53 | {R: 0xf3, G: 0x5d, B: 0x4f, A: 0xff}, 54 | {R: 0xf3, G: 0x68, B: 0x49, A: 0xff}, 55 | {R: 0xc0, G: 0xd9, B: 0x88, A: 0xff}, 56 | {R: 0x6d, G: 0xda, B: 0xf1, A: 0xff}, 57 | {R: 0xf1, G: 0xe8, B: 0x5b, A: 0xff}, 58 | } 59 | 60 | func main() { 61 | http := flag.String("http", ":8080", "HTTP service address (e.g., '127.0.0.1:8080' or just ':8080')") 62 | flag.Parse() 63 | 64 | fmt.Println("Listening on " + httpLink(*http)) 65 | err := canvas.ListenAndServe(*http, run, &canvas.Options{ 66 | Title: "Particles", 67 | Width: 500, 68 | Height: 500, 69 | PageBackground: color.Black, 70 | }) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | } 75 | 76 | func run(ctx *canvas.Context) { 77 | demo := newParticlesDemo( 78 | float64(ctx.CanvasWidth()), 79 | float64(ctx.CanvasHeight()), 80 | particlesNum) 81 | 82 | ctx.SetGlobalCompositeOperation(canvas.OpLighter) 83 | ctx.SetLineWidth(0.5) 84 | 85 | for { 86 | select { 87 | case event := <-ctx.Events(): 88 | if _, ok := event.(canvas.CloseEvent); ok { 89 | return 90 | } 91 | default: 92 | demo.draw(ctx) 93 | ctx.Flush() 94 | time.Sleep(5 * time.Millisecond) 95 | } 96 | } 97 | } 98 | 99 | type particlesDemo struct { 100 | w, h float64 101 | particles []*particle 102 | } 103 | 104 | func newParticlesDemo(width, height float64, n int) *particlesDemo { 105 | particles := make([]*particle, n) 106 | for i := range particles { 107 | particles[i] = randomParticle(width, height) 108 | } 109 | return &particlesDemo{ 110 | w: width, h: height, 111 | particles: particles, 112 | } 113 | } 114 | 115 | func (p *particlesDemo) draw(ctx *canvas.Context) { 116 | ctx.ClearRect(0, 0, p.w, p.h) 117 | for _, p1 := range p.particles { 118 | factor := 1.0 119 | 120 | for _, p2 := range p.particles { 121 | if p1.rgba == p2.rgba && p1.dist(p2) < 50 { 122 | ctx.SetStrokeStyle(p1.rgba) 123 | ctx.BeginPath() 124 | ctx.MoveTo(p1.x, p1.y) 125 | ctx.LineTo(p2.x, p2.y) 126 | ctx.Stroke() 127 | factor++ 128 | } 129 | } 130 | 131 | ctx.SetFillStyle(p1.rgba) 132 | ctx.SetStrokeStyle(p1.rgba) 133 | 134 | ctx.BeginPath() 135 | ctx.Arc(p1.x, p1.y, p1.rad*factor, 0, math.Pi*2, true) 136 | ctx.Fill() 137 | ctx.ClosePath() 138 | 139 | ctx.BeginPath() 140 | ctx.Arc(p1.x, p1.y, (p1.rad+5)*factor, 0, math.Pi*2, true) 141 | ctx.Stroke() 142 | ctx.ClosePath() 143 | 144 | p1.x += p1.vx 145 | p1.y += p1.vy 146 | 147 | if p1.x > p.w { 148 | p1.x = 0 149 | } 150 | if p1.x < 0 { 151 | p1.x = p.w 152 | } 153 | if p1.y > p.h { 154 | p1.y = 0 155 | } 156 | if p1.y < 0 { 157 | p1.y = p.h 158 | } 159 | } 160 | } 161 | 162 | type particle struct { 163 | rgba color.Color 164 | x, y float64 165 | vx, vy float64 166 | rad float64 167 | } 168 | 169 | func randomParticle(w, h float64) *particle { 170 | p := &particle{} 171 | p.x = math.Round(rand.Float64() * w) 172 | p.y = math.Round(rand.Float64() * h) 173 | p.rad = math.Round(rand.Float64()*1) + 1 174 | p.rgba = colors[int(math.Round(rand.Float64()*3))] 175 | p.vx = math.Round(rand.Float64()*3) - 1.5 176 | p.vy = math.Round(rand.Float64()*3) - 1.5 177 | return p 178 | } 179 | 180 | func (p *particle) dist(other *particle) float64 { 181 | return math.Sqrt(math.Pow(other.x-p.x, 2) + math.Pow(other.y-p.y, 2)) 182 | } 183 | 184 | func httpLink(addr string) string { 185 | if addr[0] == ':' { 186 | addr = "localhost" + addr 187 | } 188 | return "http://" + addr 189 | } 190 | -------------------------------------------------------------------------------- /serve.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | // Package embed is used to embed the HTML template and JavaScript files. 9 | _ "embed" 10 | "fmt" 11 | "html/template" 12 | "image/color" 13 | "log" 14 | "math" 15 | "net/http" 16 | "sync" 17 | "time" 18 | 19 | "github.com/gorilla/websocket" 20 | ) 21 | 22 | var ( 23 | //go:embed web/canvas-websocket.js 24 | javaScriptCode []byte 25 | 26 | //go:embed web/index.html.tmpl 27 | indexHTMLCode string 28 | indexHTMLTemplate = template.Must(template.New("index.html.tmpl").Parse(indexHTMLCode)) 29 | ) 30 | 31 | // ListenAndServe listens on the TCP network address addr and serves 32 | // an HTML page on "/" with a canvas that connects to the server via 33 | // WebSockets on a "/draw" endpoint. It also serves a JavaScript file on 34 | // "/canvas-websocket.js" that is used by the HTML page to provide this 35 | // functionality. 36 | // 37 | // The run function is called when a client canvas connects to the server. 38 | // The Context parameter of the run function allows the server to send draw 39 | // commands to the canvas and receive events from the canvas. Each instance 40 | // of the run function runs on its own goroutine, so the run function should 41 | // not access shared state without proper synchronization. 42 | // 43 | // The options configure various aspects the canvas such as its size, which 44 | // events to handle etc. 45 | func ListenAndServe(addr string, run func(*Context), opts *Options) error { 46 | return http.ListenAndServe(addr, NewServeMux(run, opts)) 47 | } 48 | 49 | // ListenAndServeTLS acts identically to ListenAndServe, except that it 50 | // expects HTTPS / WSS connections. Additionally, files containing a 51 | // certificate and matching private key for the server must be provided. If the 52 | // certificate is signed by a certificate authority, the certFile should be the 53 | // concatenation of the server's certificate, any intermediates, and the CA's 54 | // certificate. 55 | func ListenAndServeTLS(addr, certFile, keyFile string, run func(*Context), opts *Options) error { 56 | return http.ListenAndServeTLS(addr, certFile, keyFile, NewServeMux(run, opts)) 57 | } 58 | 59 | // NewServeMux creates a http.ServeMux as used by ListenAndServe. 60 | func NewServeMux(run func(*Context), opts *Options) *http.ServeMux { 61 | if opts == nil { 62 | opts = &Options{} 63 | } 64 | opts.applyDefaults() 65 | mux := http.NewServeMux() 66 | mux.Handle("GET /", &htmlHandler{ 67 | opts: opts, 68 | }) 69 | mux.HandleFunc("GET /canvas-websocket.js", javaScriptHandler) 70 | mux.Handle("GET /draw", &drawHandler{ 71 | opts: opts, 72 | draw: run, 73 | }) 74 | return mux 75 | } 76 | 77 | type htmlHandler struct { 78 | opts *Options 79 | } 80 | 81 | func (h *htmlHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) { 82 | model := map[string]any{ 83 | "DrawURL": template.URL("draw"), 84 | "Width": h.opts.Width, 85 | "Height": h.opts.Height, 86 | "Title": h.opts.Title, 87 | "PageBackground": template.CSS(rgbaString(h.opts.PageBackground)), 88 | "EventMask": h.opts.eventMask(), 89 | "MouseCursorHidden": h.opts.MouseCursorHidden, 90 | "ContextMenuDisabled": h.opts.ContextMenuDisabled, 91 | "ScaleToPageWidth": h.opts.ScaleToPageWidth, 92 | "ScaleToPageHeight": h.opts.ScaleToPageHeight, 93 | "ReconnectInterval": int64(h.opts.ReconnectInterval / time.Millisecond), 94 | } 95 | err := indexHTMLTemplate.Execute(w, model) 96 | if err != nil { 97 | log.Println(err) 98 | return 99 | } 100 | } 101 | 102 | func rgbaString(c color.Color) string { 103 | clr := color.RGBAModel.Convert(c).(color.RGBA) 104 | return fmt.Sprintf("rgba(%d, %d, %d, %g)", clr.R, clr.G, clr.B, math.Round((float64(clr.A)/255)*100)/100) 105 | } 106 | 107 | func javaScriptHandler(w http.ResponseWriter, _ *http.Request) { 108 | w.Header().Add("Content-Type", "text/javascript") 109 | _, err := w.Write(javaScriptCode) 110 | if err != nil { 111 | log.Println(err) 112 | return 113 | } 114 | } 115 | 116 | var upgrader = websocket.Upgrader{ 117 | ReadBufferSize: 1024, 118 | WriteBufferSize: 1024, 119 | } 120 | 121 | type drawHandler struct { 122 | opts *Options 123 | draw func(*Context) 124 | } 125 | 126 | func (h *drawHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 127 | conn, err := upgrader.Upgrade(w, r, nil) 128 | if err != nil { 129 | log.Println(err) 130 | return 131 | } 132 | 133 | events := make(chan Event) 134 | defer close(events) 135 | draws := make(chan []byte) 136 | defer close(draws) 137 | 138 | wg := sync.WaitGroup{} 139 | wg.Add(2) 140 | go readMessages(conn, events, &wg) 141 | go writeMessages(conn, draws, &wg) 142 | 143 | ctx := newContext(draws, events, h.opts) 144 | go func() { 145 | defer wg.Done() 146 | h.draw(ctx) 147 | }() 148 | 149 | wg.Wait() 150 | wg.Add(1) 151 | events <- CloseEvent{} 152 | wg.Wait() 153 | } 154 | 155 | func writeMessages(conn *websocket.Conn, messages <-chan []byte, wg *sync.WaitGroup) { 156 | defer wg.Done() 157 | for { 158 | message := <-messages 159 | err := conn.WriteMessage(websocket.BinaryMessage, message) 160 | if err != nil { 161 | break 162 | } 163 | } 164 | } 165 | 166 | func readMessages(conn *websocket.Conn, events chan<- Event, wg *sync.WaitGroup) { 167 | defer wg.Done() 168 | for { 169 | messageType, p, err := conn.ReadMessage() 170 | if err != nil { 171 | break 172 | } 173 | if messageType != websocket.BinaryMessage { 174 | continue 175 | } 176 | event, err := decodeEvent(p) 177 | if err != nil { 178 | continue 179 | } 180 | events <- event 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /example/retro-synthwave/main.go: -------------------------------------------------------------------------------- 1 | // Original JavaScript code by Victor Ribeiro: 2 | // https://github.com/victorqribeiro/retroSynthwave 3 | // Ported to Go by Frederik Zipp. Original copyright: 4 | // 5 | // MIT License 6 | // 7 | // Copyright (c) 2021 Victor Ribeiro 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in all 17 | // copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | // SOFTWARE. 26 | 27 | // Retro-synthwave is an animated demo by Victor Ribeiro. 28 | // It shows a flight over a grid-like mountain landscape with a sunset backdrop. 29 | // 30 | // Usage: 31 | // 32 | // retro-synthwave [-http address] 33 | // 34 | // Flags: 35 | // 36 | // -http HTTP service address (e.g., '127.0.0.1:8080' or just ':8080'). 37 | // The default is ':8080'. 38 | package main 39 | 40 | import ( 41 | "flag" 42 | "fmt" 43 | "image/color" 44 | "log" 45 | "math" 46 | "math/rand/v2" 47 | "time" 48 | 49 | "github.com/fzipp/canvas" 50 | ) 51 | 52 | func main() { 53 | http := flag.String("http", ":8080", "HTTP service address (e.g., '127.0.0.1:8080' or just ':8080')") 54 | flag.Parse() 55 | 56 | fmt.Println("Listening on " + httpLink(*http)) 57 | err := canvas.ListenAndServe(*http, run, &canvas.Options{ 58 | Title: "Retro Synthwave", 59 | Width: 1440, 60 | Height: 694, 61 | ScaleToPageHeight: true, 62 | PageBackground: color.Black, 63 | }) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | } 68 | 69 | type demo struct { 70 | w, h float64 71 | points [][]point 72 | offset float64 73 | spacing float64 74 | gradient *canvas.Gradient 75 | background *canvas.Gradient 76 | } 77 | 78 | type point struct { 79 | x, y, z float64 80 | } 81 | 82 | func run(ctx *canvas.Context) { 83 | d := &demo{ 84 | w: float64(ctx.CanvasWidth()), 85 | h: float64(ctx.CanvasHeight()), 86 | } 87 | d.spacing = 40.0 88 | d.points = make([][]point, 30) 89 | for i := range d.points { 90 | d.points[i] = make([]point, 60) 91 | for j := range d.points[i] { 92 | dist := math.Abs(float64(j) - float64(len(d.points[0]))/2) 93 | d.points[i][j] = point{ 94 | x: float64(j) * d.spacing, 95 | y: rand.Float64()*-(dist*dist) + 30, 96 | z: -float64(i) * 10, 97 | } 98 | } 99 | } 100 | d.offset = float64(len(d.points[0])) * d.spacing / 2 101 | 102 | d.gradient = ctx.CreateLinearGradient(0, -150, 0, 100) 103 | d.gradient.AddColorStopString(0, "gold") 104 | d.gradient.AddColorStopString(1, "rgb(200, 0, 100)") 105 | defer d.gradient.Release() 106 | 107 | d.background = ctx.CreateLinearGradient(0, -d.h/2, 0, d.h/2) 108 | d.background.AddColorStopString(0, "black") 109 | d.background.AddColorStopString(0.5, "rgb(100, 0, 50)") 110 | d.background.AddColorStopString(1, "black") 111 | defer d.background.Release() 112 | 113 | ctx.Translate(d.w/2, d.h/2) 114 | for { 115 | select { 116 | case event := <-ctx.Events(): 117 | if _, ok := event.(canvas.CloseEvent); ok { 118 | return 119 | } 120 | default: 121 | d.update() 122 | d.draw(ctx) 123 | ctx.Flush() 124 | time.Sleep(10 * time.Millisecond) 125 | } 126 | } 127 | } 128 | 129 | func (d *demo) update() { 130 | for i, p := range d.points { 131 | gone := false 132 | for j := range p { 133 | d.points[i][j].z -= 0.5 134 | if d.points[i][j].z < -300 { 135 | gone = true 136 | } 137 | } 138 | if gone { 139 | arr := d.points[len(d.points)-1] 140 | for k := range arr { 141 | dist := math.Abs(float64(k) - float64(len(arr))/2) 142 | arr[k].z = 0 143 | arr[k].y = rand.Float64()*-(dist*dist) + 30 144 | } 145 | copy(d.points[1:], d.points) 146 | d.points[0] = arr 147 | } 148 | } 149 | } 150 | 151 | func (d *demo) draw(ctx *canvas.Context) { 152 | ctx.SetFillStyleGradient(d.background) 153 | ctx.FillRect(-d.w/2, -d.h/2, d.w, d.h) 154 | ctx.BeginPath() 155 | ctx.Arc(0, 0, 200, 0, math.Pi*2, false) 156 | ctx.ClosePath() 157 | ctx.SetShadowColorString("orange") 158 | ctx.SetShadowBlur(100) 159 | ctx.SetFillStyleGradient(d.gradient) 160 | ctx.Fill() 161 | ctx.SetShadowBlur(0) 162 | for i := range len(d.points) - 1 { 163 | for j := range len(d.points[i]) - 1 { 164 | size := 300 / (300 + d.points[i][j].z) 165 | nextSize := 300 / (300 + d.points[i+1][j].z) 166 | ctx.BeginPath() 167 | ctx.MoveTo((d.points[i][j].x-d.offset)*size, d.points[i][j].y*size) 168 | ctx.LineTo((d.points[i][j+1].x-d.offset)*size, d.points[i][j+1].y*size) 169 | ctx.LineTo((d.points[i+1][j+1].x-d.offset)*nextSize, d.points[i+1][j+1].y*nextSize) 170 | ctx.LineTo((d.points[i+1][j].x-d.offset)*nextSize, d.points[i+1][j].y*nextSize) 171 | ctx.ClosePath() 172 | ctx.SetFillStyle(color.RGBA{A: uint8(limit(-d.points[i][j].z/100, 1.0) * 255)}) 173 | c := 300 + d.points[i][j].z 174 | ctx.SetStrokeStyle(color.RGBA{ 175 | R: uint8(250 - c), 176 | G: 0, 177 | B: uint8(50 + c), 178 | A: uint8((1 - c/300) * 255), 179 | }) 180 | ctx.Fill() 181 | ctx.Stroke() 182 | } 183 | } 184 | } 185 | 186 | func limit(f, max float64) float64 { 187 | if f > max { 188 | return max 189 | } 190 | return f 191 | } 192 | 193 | func httpLink(addr string) string { 194 | if addr[0] == ':' { 195 | addr = "localhost" + addr 196 | } 197 | return "http://" + addr 198 | } 199 | -------------------------------------------------------------------------------- /buffer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | "image/color" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestBufferWrite(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | use func(buf *buffer) 19 | want []byte 20 | }{ 21 | { 22 | "addByte", 23 | func(buf *buffer) { 24 | buf.addByte(0xfa) 25 | buf.addByte(0x12) 26 | buf.addByte(0x01) 27 | }, 28 | []byte{0xfa, 0x12, 0x01}, 29 | }, 30 | { 31 | "addFloat64", 32 | func(buf *buffer) { 33 | buf.addFloat64(3.1415) 34 | buf.addFloat64(362.5) 35 | }, 36 | []byte{ 37 | 0x40, 0x09, 0x21, 0xca, 0xc0, 0x83, 0x12, 0x6f, 38 | 0x40, 0x76, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 39 | }, 40 | }, 41 | { 42 | "addUint32", 43 | func(buf *buffer) { 44 | buf.addUint32(12) 45 | buf.addUint32(4096) 46 | buf.addUint32(60000) 47 | buf.addUint32(1200000) 48 | }, 49 | []byte{ 50 | 0x00, 0x00, 0x00, 0x0c, 51 | 0x00, 0x00, 0x10, 0x00, 52 | 0x00, 0x00, 0xea, 0x60, 53 | 0x00, 0x12, 0x4f, 0x80, 54 | }, 55 | }, 56 | { 57 | "addBool", 58 | func(buf *buffer) { 59 | buf.addBool(true) 60 | buf.addBool(false) 61 | buf.addBool(true) 62 | }, 63 | []byte{0x01, 0x00, 0x01}, 64 | }, 65 | { 66 | "addBytes", 67 | func(buf *buffer) { 68 | buf.addBytes([]byte{0x01, 0x02, 0xbc, 0xbd}) 69 | buf.addBytes([]byte{0xfe, 0xff}) 70 | }, 71 | []byte{0x01, 0x02, 0xbc, 0xbd, 0xfe, 0xff}, 72 | }, 73 | { 74 | "addString", 75 | func(buf *buffer) { 76 | buf.addString("hello") 77 | buf.addString("äöü") 78 | }, 79 | []byte{ 80 | 0x00, 0x00, 0x00, 0x05, // len(s) 81 | 0x68, 0x65, 0x6c, 0x6c, 0x6f, 82 | 0x00, 0x00, 0x00, 0x06, // len(s) 83 | 0xc3, 0xa4, 0xc3, 0xb6, 0xc3, 0xbc, 84 | }, 85 | }, 86 | { 87 | "addColor", 88 | func(buf *buffer) { 89 | buf.addColor(color.Black) 90 | buf.addColor(color.White) 91 | buf.addColor(color.RGBA{R: 127, G: 32, B: 64, A: 255}) 92 | }, 93 | []byte{ 94 | 0x00, 0x00, 0x00, 0xff, 95 | 0xff, 0xff, 0xff, 0xff, 96 | 0x7f, 0x20, 0x40, 0xff, 97 | }, 98 | }, 99 | { 100 | "reset", 101 | func(buf *buffer) { 102 | buf.addBytes([]byte{0x01, 0x02, 0x03}) 103 | buf.reset() 104 | buf.addBytes([]byte{0x04, 0x05, 0x06, 0x07}) 105 | }, 106 | []byte{0x04, 0x05, 0x06, 0x07}, 107 | }, 108 | } 109 | for _, tt := range tests { 110 | t.Run(tt.name, func(t *testing.T) { 111 | buf := &buffer{} 112 | tt.use(buf) 113 | got := buf.bytes 114 | if diff := cmp.Diff(tt.want, got); diff != "" { 115 | t.Errorf("mismatch (-want, +got):\n%s", diff) 116 | } 117 | }) 118 | } 119 | } 120 | 121 | func TestBufferRead(t *testing.T) { 122 | tests := []struct { 123 | name string 124 | bytes []byte 125 | read func(buf *buffer) any 126 | wantValue any 127 | wantBytes []byte 128 | }{ 129 | { 130 | "readByte", 131 | []byte{0x01, 0x02}, 132 | func(buf *buffer) any { 133 | return buf.readByte() 134 | }, 135 | byte(0x01), 136 | []byte{0x02}, 137 | }, 138 | { 139 | "readUint32", 140 | []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 141 | func(buf *buffer) any { 142 | return buf.readUint32() 143 | }, 144 | uint32(0x01020304), 145 | []byte{0x05, 0x06, 0x07, 0x08}, 146 | }, 147 | { 148 | "readUint64", 149 | []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}, 150 | func(buf *buffer) any { 151 | return buf.readUint64() 152 | }, 153 | uint64(0x0102030405060708), 154 | []byte{0x09}, 155 | }, 156 | { 157 | "readFloat64", 158 | []byte{0x40, 0x09, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a, 0xab}, 159 | func(buf *buffer) any { 160 | return buf.readFloat64() 161 | }, 162 | 3.2, 163 | []byte{0xab}, 164 | }, 165 | { 166 | "readString", 167 | []byte{0x00, 0x00, 0x00, 0x04, 0x54, 0x65, 0x73, 0x74, 0x42}, 168 | func(buf *buffer) any { 169 | return buf.readString() 170 | }, 171 | "Test", 172 | []byte{0x42}, 173 | }, 174 | } 175 | for _, tt := range tests { 176 | t.Run(tt.name, func(t *testing.T) { 177 | buf := &buffer{bytes: tt.bytes} 178 | got := tt.read(buf) 179 | if buf.error != nil { 180 | t.Errorf("did not expect error, but got error: %s", buf.error) 181 | } 182 | if !reflect.DeepEqual(got, tt.wantValue) { 183 | t.Errorf("got: %#v, want: %#v", got, tt.wantValue) 184 | } 185 | if !reflect.DeepEqual(buf.bytes, tt.wantBytes) { 186 | t.Errorf("\n(buf.bytes) got : %#v\n(buf.bytes) want: %#v", buf.bytes, tt.wantBytes) 187 | } 188 | }) 189 | } 190 | } 191 | 192 | func TestBufferReadErrors(t *testing.T) { 193 | tests := []struct { 194 | name string 195 | bytes []byte 196 | read func(buf *buffer) any 197 | wantValue any 198 | }{ 199 | { 200 | "readByte", 201 | []byte{}, 202 | func(buf *buffer) any { 203 | return buf.readByte() 204 | }, 205 | byte(0), 206 | }, 207 | { 208 | "readUint32", 209 | []byte{0x01, 0x02, 0x03}, 210 | func(buf *buffer) any { 211 | return buf.readUint32() 212 | }, 213 | uint32(0), 214 | }, 215 | { 216 | "readUint64", 217 | []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, 218 | func(buf *buffer) any { 219 | return buf.readUint64() 220 | }, 221 | uint64(0), 222 | }, 223 | { 224 | "readFloat64", 225 | []byte{0x40, 0x09, 0x99, 0x99, 0x99, 0x99, 0x99}, 226 | func(buf *buffer) any { 227 | return buf.readFloat64() 228 | }, 229 | float64(0), 230 | }, 231 | { 232 | "readString: length data too short", 233 | []byte{0x00, 0x00, 0x00}, 234 | func(buf *buffer) any { 235 | return buf.readString() 236 | }, 237 | "", 238 | }, 239 | { 240 | "readString: string data too short", 241 | []byte{0x00, 0x00, 0x00, 0x04, 0x54, 0x65, 0x73}, 242 | func(buf *buffer) any { 243 | return buf.readString() 244 | }, 245 | "", 246 | }, 247 | } 248 | wantErrorMessage := "data too short" 249 | for _, tt := range tests { 250 | t.Run(tt.name, func(t *testing.T) { 251 | buf := &buffer{bytes: tt.bytes} 252 | got := tt.read(buf) 253 | if buf.error == nil { 254 | t.Errorf("expected error, but got none") 255 | return 256 | } 257 | if wantType, ok := buf.error.(errDataTooShort); !ok { 258 | t.Errorf("expected %T error, but got: %#v", wantType, buf.error) 259 | } 260 | if buf.error.Error() != wantErrorMessage { 261 | t.Errorf("expected %q error message, but got: %q", wantErrorMessage, buf.error) 262 | } 263 | if !reflect.DeepEqual(got, tt.wantValue) { 264 | t.Errorf("got: %#v, want: %#v", got, tt.wantValue) 265 | } 266 | if len(buf.bytes) != 0 { 267 | t.Errorf("excpected buf.bytes to be empty after short read, but got: %#v", buf.bytes) 268 | } 269 | }) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /example/trail/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // This example was ported from: 6 | // https://codepen.io/hakimel/pen/KanIi 7 | // Original copyright: 8 | // 9 | // Copyright (c) 2021 by Hakim El Hattab (https://codepen.io/hakimel/pen/KanIi) 10 | // 11 | // Permission is hereby granted, free of charge, to any person obtaining a copy 12 | // of this software and associated documentation files (the "Software"), to 13 | // deal in the Software without restriction, including without limitation the 14 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 15 | // sell copies of the Software, and to permit persons to whom the Software is 16 | // furnished to do so, subject to the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be included in 19 | // all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 27 | // IN THE SOFTWARE. 28 | 29 | // Trail is an interactive animation of rotating particles that follow the 30 | // mouse or touch pointer. 31 | // 32 | // Usage: 33 | // 34 | // trail [-http address] 35 | // 36 | // Flags: 37 | // 38 | // -http HTTP service address (e.g., '127.0.0.1:8080' or just ':8080'). 39 | // The default is ':8080'. 40 | package main 41 | 42 | import ( 43 | "flag" 44 | "fmt" 45 | "log" 46 | "math" 47 | "math/rand/v2" 48 | "time" 49 | 50 | "github.com/fzipp/canvas" 51 | ) 52 | 53 | const ( 54 | radius = 110 55 | 56 | radiusScaleMin = 1 57 | radiusScaleMax = 1.5 58 | 59 | // The number of particles that are used to generate the trail 60 | quantity = 25 61 | ) 62 | 63 | func main() { 64 | http := flag.String("http", ":8080", "HTTP service address (e.g., '127.0.0.1:8080' or just ':8080')") 65 | flag.Parse() 66 | 67 | fmt.Println("Listening on " + httpLink(*http)) 68 | err := canvas.ListenAndServe(*http, run, &canvas.Options{ 69 | Width: 750, 70 | Height: 1334, 71 | ScaleToPageWidth: true, 72 | ScaleToPageHeight: true, 73 | EnabledEvents: []canvas.Event{ 74 | canvas.MouseMoveEvent{}, 75 | canvas.MouseDownEvent{}, 76 | canvas.MouseUpEvent{}, 77 | canvas.TouchStartEvent{}, 78 | canvas.TouchMoveEvent{}, 79 | }, 80 | ReconnectInterval: time.Second, 81 | }) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | } 86 | 87 | func run(ctx *canvas.Context) { 88 | d := &demo{ 89 | mouseX: float64(ctx.CanvasWidth() / 2), 90 | mouseY: float64(ctx.CanvasHeight() / 2), 91 | radiusScale: 1.0, 92 | } 93 | d.createParticles() 94 | for { 95 | select { 96 | case event := <-ctx.Events(): 97 | if _, ok := event.(canvas.CloseEvent); ok { 98 | return 99 | } 100 | d.handle(event) 101 | default: 102 | d.draw(ctx) 103 | ctx.Flush() 104 | time.Sleep((1000 / 60) * time.Millisecond) 105 | } 106 | } 107 | } 108 | 109 | type point struct { 110 | x, y float64 111 | } 112 | 113 | type particle struct { 114 | position point 115 | shift point 116 | size float64 117 | angle float64 118 | speed float64 119 | targetSize float64 120 | fillColor string 121 | orbit float64 122 | } 123 | 124 | type demo struct { 125 | particles []particle 126 | radiusScale float64 127 | mouseX float64 128 | mouseY float64 129 | mouseIsDown bool 130 | } 131 | 132 | func (d *demo) createParticles() { 133 | d.particles = make([]particle, 0, quantity) 134 | for range quantity { 135 | p := particle{ 136 | position: point{x: d.mouseX, y: d.mouseY}, 137 | shift: point{x: d.mouseX, y: d.mouseY}, 138 | size: 1, 139 | angle: 0, 140 | speed: 0.01 + rand.Float64()*0.04, 141 | targetSize: 1, 142 | fillColor: "#" + fmt.Sprintf("%6x", int(rand.Float64()*0x404040+0xaaaaaa)), 143 | orbit: radius*.5 + (radius * .5 * rand.Float64()), 144 | } 145 | d.particles = append(d.particles, p) 146 | } 147 | } 148 | 149 | func (d *demo) handle(ev canvas.Event) { 150 | switch e := ev.(type) { 151 | case canvas.MouseMoveEvent: 152 | d.mouseX = float64(e.X) 153 | d.mouseY = float64(e.Y) 154 | case canvas.MouseDownEvent: 155 | d.mouseIsDown = true 156 | case canvas.MouseUpEvent: 157 | d.mouseIsDown = false 158 | case canvas.TouchStartEvent: 159 | if len(e.Touches) == 1 { 160 | d.mouseX = float64(e.Touches[0].X) 161 | d.mouseY = float64(e.Touches[0].Y) 162 | } 163 | case canvas.TouchMoveEvent: 164 | if len(e.Touches) == 1 { 165 | d.mouseX = float64(e.Touches[0].X) 166 | d.mouseY = float64(e.Touches[0].Y) 167 | } 168 | } 169 | } 170 | 171 | func (d *demo) draw(ctx *canvas.Context) { 172 | if d.mouseIsDown { 173 | // Scale upward to the max scale 174 | d.radiusScale += (radiusScaleMax - d.radiusScale) * (0.02) 175 | } else { 176 | // Scale downward to the min scale 177 | d.radiusScale -= (d.radiusScale - radiusScaleMin) * (0.02) 178 | } 179 | 180 | d.radiusScale = min(d.radiusScale, radiusScaleMax) 181 | 182 | // Fade out the lines slowly by drawing a rectangle over the entire canvas 183 | ctx.SetFillStyleString("rgba(0,0,0,0.05)") 184 | ctx.FillRect(0, 0, float64(ctx.CanvasWidth()), float64(ctx.CanvasHeight())) 185 | 186 | for i := range d.particles { 187 | p := &d.particles[i] 188 | 189 | lp := point{x: p.position.x, y: p.position.y} 190 | 191 | // Offset the angle to keep the spin going 192 | p.angle += p.speed 193 | 194 | // Follow mouse with some lag 195 | p.shift.x += (d.mouseX - p.shift.x) * (p.speed) 196 | p.shift.y += (d.mouseY - p.shift.y) * (p.speed) 197 | 198 | // Apply position 199 | p.position.x = p.shift.x + math.Cos(float64(i)+p.angle)*(p.orbit*d.radiusScale) 200 | p.position.y = p.shift.y + math.Sin(float64(i)+p.angle)*(p.orbit*d.radiusScale) 201 | 202 | // Limit to screen bounds 203 | p.position.x = max(min(p.position.x, float64(ctx.CanvasWidth())), 0) 204 | p.position.y = max(min(p.position.y, float64(ctx.CanvasHeight())), 0) 205 | 206 | p.size += (p.targetSize - p.size) * 0.05 207 | 208 | // If we're at the target size, set a new one. Think of it like a regular day at work. 209 | if math.Round(p.size) == math.Round(p.targetSize) { 210 | p.targetSize = 1 + rand.Float64()*7 211 | } 212 | 213 | ctx.BeginPath() 214 | ctx.SetFillStyleString(p.fillColor) 215 | ctx.SetStrokeStyleString(p.fillColor) 216 | ctx.SetLineWidth(p.size) 217 | ctx.MoveTo(lp.x, lp.y) 218 | ctx.LineTo(p.position.x, p.position.y) 219 | ctx.Stroke() 220 | ctx.Arc(p.position.x, p.position.y, p.size/2, 0, math.Pi*2, true) 221 | ctx.Fill() 222 | } 223 | } 224 | 225 | func httpLink(addr string) string { 226 | if addr[0] == ':' { 227 | addr = "localhost" + addr 228 | } 229 | return "http://" + addr 230 | } 231 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # canvas 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/fzipp/canvas)](https://pkg.go.dev/github.com/fzipp/canvas) 4 | ![Build Status](https://github.com/fzipp/canvas/workflows/build/badge.svg) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/fzipp/canvas)](https://goreportcard.com/report/github.com/fzipp/canvas) 6 | 7 | This Go module utilizes WebSockets to establish communication with a 8 | [2D canvas graphics context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) 9 | in a web browser, 10 | providing a portable way to create interactive 2D graphics 11 | from within a Go program. 12 | 13 | The Go program (server) sends draw commands to the web browser (client) 14 | via WebSocket using a binary format. 15 | In return, the client sends keyboard, mouse, and touch events to the server. 16 | 17 | This module does not rely on operating system-specific backends 18 | or Cgo bindings. 19 | It also does not utilize WebAssembly, 20 | which means the Go code runs on the server side, 21 | rather than in the browser. 22 | The client-server design enables the canvas 23 | to be displayed on a different machine over the network. 24 | 25 | ## Examples 26 | 27 | The [example](example) subdirectory contains a variety of demo programs. 28 | 29 | ![Screenshots of examples](https://github.com/fzipp/canvas/blob/assets/examples.png) 30 | 31 | ## Usage 32 | 33 | ### Drawing 34 | 35 | The `ListenAndServe` function initializes the canvas server 36 | and takes the following arguments: 37 | the network address with the port number to bind to, 38 | a run function, 39 | and an options structure that configures various aspects 40 | such as the canvas size in pixels 41 | or a title for the browser tab. 42 | 43 | The `run` function is called when a client connects to the server. 44 | This serves as the entry point for drawing. 45 | 46 | ```go 47 | package main 48 | 49 | import ( 50 | "image/color" 51 | "log" 52 | 53 | "github.com/fzipp/canvas" 54 | ) 55 | 56 | func main() { 57 | err := canvas.ListenAndServe(":8080", run, &canvas.Options{ 58 | Title: "Example 1: Drawing", 59 | Width: 100, 60 | Height: 80, 61 | }) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | } 66 | 67 | func run(ctx *canvas.Context) { 68 | ctx.SetFillStyle(color.RGBA{R: 200, A: 255}) 69 | ctx.FillRect(10, 10, 50, 50) 70 | // ... 71 | ctx.Flush() 72 | } 73 | ``` 74 | 75 | After starting the program, 76 | you can access the canvas by opening http://localhost:8080 77 | in a web browser. 78 | 79 | The server doesn't immediately send each drawing operation to the client 80 | but instead buffers them until the `Flush` method is called. 81 | The flush should occur once the image or an animation frame is complete; 82 | otherwise, nothing will be displayed. 83 | 84 | Each client connection starts its own run function as a goroutine. 85 | Access to shared state between client connections must be synchronized. 86 | If you don't want to share state between connections, 87 | you should keep it local to the run function 88 | and pass the state to other functions called by the run function. 89 | 90 | ### An animation loop 91 | 92 | To create an animation, 93 | you can use a `for` loop within the `run` function. 94 | Inside this loop, 95 | observe the `ctx.Events()` channel 96 | for a `canvas.CloseEvent` to exit the loop 97 | when the connection is closed. 98 | 99 | A useful pattern is to create a struct 100 | that holds the animation state 101 | and has both an update and a draw method: 102 | 103 | ```go 104 | package main 105 | 106 | import ( 107 | "log" 108 | "time" 109 | 110 | "github.com/fzipp/canvas" 111 | ) 112 | 113 | func main() { 114 | err := canvas.ListenAndServe(":8080", run, &canvas.Options{ 115 | Title: "Example 2: Animation", 116 | Width: 800, 117 | Height: 600, 118 | }) 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | } 123 | 124 | func run(ctx *canvas.Context) { 125 | d := &demo{} 126 | for { 127 | select { 128 | case event := <-ctx.Events(): 129 | if _, ok := event.(canvas.CloseEvent); ok { 130 | return 131 | } 132 | default: 133 | d.update() 134 | d.draw(ctx) 135 | ctx.Flush() 136 | time.Sleep(time.Second / 6) 137 | } 138 | } 139 | } 140 | 141 | type demo struct { 142 | // Animation state, for example: 143 | x, y int 144 | // ... 145 | } 146 | 147 | func (d *demo) update() { 148 | // Update animation state for the next frame 149 | // ... 150 | } 151 | 152 | func (d *demo) draw(ctx *canvas.Context) { 153 | // Draw the frame here, based on the animation state 154 | // ... 155 | } 156 | ``` 157 | 158 | ### Keyboard, mouse and touch events 159 | 160 | To handle keyboard, mouse, and touch events, 161 | you need to specify which events the client should observe 162 | and send to the server. 163 | This is achieved by passing an `EnabledEvents` option 164 | to the `ListenAndServe` function. 165 | Mouse move events typically generate more WebSocket communication 166 | than the others, 167 | so you may want to enable them only if necessary. 168 | 169 | The `ctx.Events()` channel receives the observed events, 170 | and a type switch is used to determine the specific event type. 171 | A useful pattern involves creating a `handle` method for event handling: 172 | 173 | ```go 174 | package main 175 | 176 | import ( 177 | "log" 178 | 179 | "github.com/fzipp/canvas" 180 | ) 181 | 182 | func main() { 183 | err := canvas.ListenAndServe(":8080", run, &canvas.Options{ 184 | Title: "Example 3: Events", 185 | Width: 800, 186 | Height: 600, 187 | EnabledEvents: []canvas.Event{ 188 | canvas.MouseDownEvent{}, 189 | canvas.MouseMoveEvent{}, 190 | canvas.TouchStartEvent{}, 191 | canvas.TouchMoveEvent{}, 192 | canvas.KeyDownEvent{}, 193 | }, 194 | }) 195 | if err != nil { 196 | log.Fatal(err) 197 | } 198 | } 199 | 200 | func run(ctx *canvas.Context) { 201 | d := &demo{} 202 | for !d.quit { 203 | select { 204 | case event := <-ctx.Events(): 205 | d.handle(event) 206 | default: 207 | d.update() 208 | d.draw(ctx) 209 | ctx.Flush() 210 | } 211 | } 212 | } 213 | 214 | type demo struct { 215 | quit bool 216 | // ... 217 | } 218 | 219 | func (d *demo) handle(event canvas.Event) { 220 | switch e := event.(type) { 221 | case canvas.CloseEvent: 222 | d.quit = true 223 | case canvas.MouseDownEvent: 224 | // ... 225 | case canvas.MouseMoveEvent: 226 | // ... 227 | case canvas.TouchStartEvent: 228 | // ... 229 | case canvas.TouchMoveEvent: 230 | // ... 231 | case canvas.KeyDownEvent: 232 | // ... 233 | } 234 | } 235 | 236 | func (d *demo) update() { 237 | // ... 238 | } 239 | 240 | func (d *demo) draw(ctx *canvas.Context) { 241 | // ... 242 | } 243 | ``` 244 | 245 | Note that the `canvas.CloseEvent` does not have to be explicitly enabled. 246 | It is always enabled by default. 247 | 248 | ## Alternatives 249 | 250 | * [github.com/tfriedel6/canvas](https://github.com/tfriedel6/canvas) - 251 | A canvas implementation for Go with OpenGL backends for various 252 | operating systems. 253 | * [github.com/llgcode/draw2d](https://github.com/llgcode/draw2d) - 254 | A 2D vector graphics library for Go with support for multiple outputs 255 | such as images, PDF documents, OpenGL and SVG. 256 | * [github.com/ajstarks/svgo](https://github.com/ajstarks/svgo) - 257 | A Go library for SVG generation. 258 | * [github.com/tdewolff/canvas](https://github.com/tdewolff/canvas) - 259 | A common vector drawing target that can output SVG, PDF, EPS, 260 | raster images (PNG, JPG, GIF, ...), HTML Canvas through WASM, and OpenGL. 261 | * [github.com/fogleman/gg](https://github.com/fogleman/gg) - 262 | A library for rendering 2D graphics in pure Go. 263 | 264 | 2D game engines: 265 | 266 | * [github.com/faiface/pixel](https://github.com/faiface/pixel) - Pixel 267 | * [github.com/hajimehoshi/ebiten](https://github.com/hajimehoshi/ebiten) - Ebiten 268 | * [github.com/oakmound/oak](https://github.com/oakmound/oak) - Oak 269 | 270 | ## License 271 | 272 | This project is free and open source software licensed under the 273 | [BSD 3-Clause License](LICENSE). 274 | -------------------------------------------------------------------------------- /example/cloth/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // This example was ported from: 6 | // https://codepen.io/dissimulate/pen/KrAwx 7 | // Original copyright: 8 | // 9 | // Copyright (c) 2020 by dissimulate (https://codepen.io/dissimulate/pen/KrAwx) 10 | // 11 | // Permission is hereby granted, free of charge, to any person obtaining a copy 12 | // of this software and associated documentation files (the "Software"), to 13 | // deal in the Software without restriction, including without limitation the 14 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 15 | // sell copies of the Software, and to permit persons to whom the Software is 16 | // furnished to do so, subject to the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be included in 19 | // all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 27 | // IN THE SOFTWARE. 28 | 29 | // Cloth is an interactive physics simulation of a cloth that can be torn by 30 | // clicking and dragging the mouse. 31 | // 32 | // Usage: 33 | // 34 | // cloth [-http address] 35 | // 36 | // Flags: 37 | // 38 | // -http HTTP service address (e.g., '127.0.0.1:8080' or just ':8080'). 39 | // The default is ':8080'. 40 | package main 41 | 42 | import ( 43 | "flag" 44 | "fmt" 45 | "image/color" 46 | "log" 47 | "math" 48 | "time" 49 | 50 | "github.com/fzipp/canvas" 51 | ) 52 | 53 | const ( 54 | physicsAccuracy = 3 55 | mouseInfluence = 20 56 | mouseCut = 5 57 | gravity = 1200 58 | clothHeight = 30 59 | clothWidth = 50 60 | startY = 20 61 | spacing = 7 62 | tearDistance = 60 63 | ) 64 | 65 | func main() { 66 | http := flag.String("http", ":8080", "HTTP service address (e.g., '127.0.0.1:8080' or just ':8080')") 67 | flag.Parse() 68 | 69 | fmt.Println("Listening on " + httpLink(*http)) 70 | err := canvas.ListenAndServe(*http, run, &canvas.Options{ 71 | Title: "Tearable Cloth", 72 | Width: 560, 73 | Height: 350, 74 | EnabledEvents: []canvas.Event{ 75 | canvas.MouseMoveEvent{}, 76 | canvas.MouseDownEvent{}, 77 | canvas.MouseUpEvent{}, 78 | }, 79 | }) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | } 84 | 85 | func run(ctx *canvas.Context) { 86 | ctx.SetStrokeStyle(color.RGBA{R: 0x88, G: 0x88, B: 0x88, A: 0xFF}) 87 | 88 | cloth := newCloth( 89 | ctx.CanvasWidth(), 90 | float64(ctx.CanvasWidth()-1), 91 | float64(ctx.CanvasHeight()-1), 92 | ) 93 | 94 | for { 95 | select { 96 | case event := <-ctx.Events(): 97 | if _, ok := event.(canvas.CloseEvent); ok { 98 | return 99 | } 100 | cloth.handle(event) 101 | default: 102 | cloth.update() 103 | cloth.draw(ctx) 104 | ctx.Flush() 105 | time.Sleep(5 * time.Millisecond) 106 | } 107 | } 108 | } 109 | 110 | type cloth struct { 111 | boundsX float64 112 | boundsY float64 113 | mouse mouse 114 | points []*point 115 | } 116 | 117 | func newCloth(canvasWidth int, boundsX, boundsY float64) *cloth { 118 | cloth := &cloth{ 119 | boundsX: boundsX, 120 | boundsY: boundsY, 121 | } 122 | startX := float64(canvasWidth)/2 - clothWidth*spacing/2 123 | for y := range clothHeight + 1 { 124 | for x := range clothWidth + 1 { 125 | p := newPoint( 126 | startX+float64(x*spacing), 127 | startY+float64(y*spacing), 128 | ) 129 | if x != 0 { 130 | p.attach(cloth.points[len(cloth.points)-1]) 131 | } 132 | if y == 0 { 133 | p.pin(p.x, p.y) 134 | } 135 | if y != 0 { 136 | p.attach(cloth.points[x+(y-1)*(clothWidth+1)]) 137 | } 138 | cloth.points = append(cloth.points, p) 139 | } 140 | } 141 | return cloth 142 | } 143 | 144 | func (c *cloth) handle(event canvas.Event) { 145 | switch e := event.(type) { 146 | case canvas.MouseMoveEvent: 147 | c.mouse.px = c.mouse.x 148 | c.mouse.py = c.mouse.y 149 | c.mouse.x = float64(e.X) 150 | c.mouse.y = float64(e.Y) 151 | case canvas.MouseUpEvent: 152 | c.mouse.down = false 153 | case canvas.MouseDownEvent: 154 | c.mouse.button = e.Buttons 155 | c.mouse.px = c.mouse.x 156 | c.mouse.py = c.mouse.y 157 | c.mouse.x = float64(e.X) 158 | c.mouse.y = float64(e.Y) 159 | c.mouse.down = true 160 | } 161 | } 162 | 163 | func (c *cloth) update() { 164 | for range physicsAccuracy { 165 | for _, p := range c.points { 166 | p.resolveConstraints(c.boundsX, c.boundsY) 167 | } 168 | } 169 | for _, p := range c.points { 170 | p.update(.016, &c.mouse) 171 | } 172 | } 173 | 174 | func (c *cloth) draw(ctx *canvas.Context) { 175 | ctx.ClearRect(0, 0, 176 | float64(ctx.CanvasWidth()), 177 | float64(ctx.CanvasHeight())) 178 | 179 | ctx.BeginPath() 180 | for _, p := range c.points { 181 | p.draw(ctx) 182 | } 183 | ctx.Stroke() 184 | } 185 | 186 | type mouse struct { 187 | down bool 188 | button canvas.MouseButtons 189 | x, y float64 190 | px, py float64 191 | } 192 | 193 | type point struct { 194 | x, y float64 195 | px, py float64 196 | vx, vy float64 197 | pinX, pinY float64 198 | constraints []*constraint 199 | } 200 | 201 | func newPoint(x, y float64) *point { 202 | return &point{ 203 | x: x, y: y, 204 | px: x, py: y, 205 | vx: 0, vy: 0, 206 | pinX: math.NaN(), pinY: math.NaN(), 207 | } 208 | } 209 | 210 | func (p *point) update(delta float64, mouse *mouse) { 211 | if mouse.down { 212 | diffX := p.x - mouse.x 213 | diffY := p.y - mouse.y 214 | dist := math.Sqrt(diffX*diffX + diffY*diffY) 215 | 216 | if mouse.button == 1 { 217 | if dist < mouseInfluence { 218 | p.px = p.x - (mouse.x-mouse.px)*1.8 219 | p.py = p.y - (mouse.y-mouse.py)*1.8 220 | } 221 | } else if dist < mouseCut { 222 | p.constraints = p.constraints[:0] 223 | } 224 | } 225 | 226 | p.addForce(0, gravity) 227 | 228 | delta *= delta 229 | nx := p.x + ((p.x - p.px) * .99) + ((p.vx / 2) * delta) 230 | ny := p.y + ((p.y - p.py) * .99) + ((p.vy / 2) * delta) 231 | 232 | p.px = p.x 233 | p.py = p.y 234 | 235 | p.x = nx 236 | p.y = ny 237 | 238 | p.vy = 0 239 | p.vx = 0 240 | } 241 | 242 | func (p *point) draw(ctx *canvas.Context) { 243 | for _, c := range p.constraints { 244 | c.draw(ctx) 245 | } 246 | } 247 | 248 | func (p *point) attach(q *point) { 249 | p.constraints = append(p.constraints, newConstraint(p, q)) 250 | } 251 | 252 | func (p *point) pin(x, y float64) { 253 | p.pinX = x 254 | p.pinY = y 255 | } 256 | 257 | func (p *point) addForce(x, y float64) { 258 | p.vx += x 259 | p.vy += y 260 | 261 | const round = 400 262 | p.vx = math.Floor(p.vx*round) / round 263 | p.vy = math.Floor(p.vy*round) / round 264 | } 265 | 266 | func (p *point) resolveConstraints(boundsX, boundsY float64) { 267 | if !math.IsNaN(p.pinX) && !math.IsNaN(p.pinY) { 268 | p.x = p.pinX 269 | p.y = p.pinY 270 | return 271 | } 272 | 273 | for _, c := range p.constraints { 274 | c.resolve() 275 | } 276 | 277 | if p.x > boundsX { 278 | p.x = 2*boundsX - p.x 279 | } else { 280 | if 1 > p.x { 281 | p.x = 2 - p.x 282 | } 283 | } 284 | if p.y < 1 { 285 | p.y = 2 - p.y 286 | } else { 287 | if p.y > boundsY { 288 | p.y = 2*boundsY - p.y 289 | } 290 | } 291 | } 292 | 293 | func (p *point) removeConstraint(c *constraint) { 294 | for i, elem := range p.constraints { 295 | if elem == c { 296 | p.constraints = append(p.constraints[:i], p.constraints[i+1:]...) 297 | return 298 | } 299 | } 300 | } 301 | 302 | type constraint struct { 303 | p1, p2 *point 304 | length float64 305 | } 306 | 307 | func newConstraint(p1, p2 *point) *constraint { 308 | return &constraint{p1: p1, p2: p2, length: spacing} 309 | } 310 | 311 | func (c *constraint) draw(ctx *canvas.Context) { 312 | ctx.MoveTo(c.p1.x, c.p1.y) 313 | ctx.LineTo(c.p2.x, c.p2.y) 314 | } 315 | 316 | func (c *constraint) resolve() { 317 | diffX := c.p1.x - c.p2.x 318 | diffY := c.p1.y - c.p2.y 319 | dist := math.Sqrt(diffX*diffX + diffY*diffY) 320 | diff := (c.length - dist) / dist 321 | 322 | if dist > tearDistance { 323 | c.p1.removeConstraint(c) 324 | } 325 | 326 | px := diffX * diff * 0.5 327 | py := diffY * diff * 0.5 328 | 329 | c.p1.x += px 330 | c.p1.y += py 331 | c.p2.x -= px 332 | c.p2.y -= py 333 | } 334 | 335 | func httpLink(addr string) string { 336 | if addr[0] == ':' { 337 | addr = "localhost" + addr 338 | } 339 | return "http://" + addr 340 | } 341 | -------------------------------------------------------------------------------- /enums.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // The API doc comments are based on the MDN Web Docs for the [Canvas API] 6 | // by Mozilla Contributors and are licensed under [CC-BY-SA 2.5]. 7 | // 8 | // [Canvas API]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D 9 | // [CC-BY-SA 2.5]: https://creativecommons.org/licenses/by-sa/2.5/ 10 | 11 | package canvas 12 | 13 | // LineCap represents the shape used to draw the end points of lines. 14 | type LineCap byte 15 | 16 | const ( 17 | // CapButt squares off the ends of lines at the endpoints. 18 | CapButt LineCap = iota 19 | // CapRound rounds the ends of lines. 20 | CapRound 21 | // CapSquare squares off the ends of lines by adding a box with an equal 22 | // width and half the height of the line's thickness. 23 | CapSquare 24 | ) 25 | 26 | // LineJoin represents the shape used to join two line segments where they 27 | // meet. 28 | type LineJoin byte 29 | 30 | const ( 31 | // JoinMiter joins connected segments by extending their outside edges to 32 | // connect at a single point, with the effect of filling an additional 33 | // lozenge-shaped area. This setting is affected by Context.SetMiterLimit. 34 | JoinMiter LineJoin = iota 35 | // JoinRound rounds off the corners of a shape by filling an additional 36 | // sector of disc centered at the common endpoint of connected segments. 37 | // The radius for these rounded corners is equal to the line width. 38 | JoinRound 39 | // JoinBevel fills an additional triangular area between the common 40 | // endpoint of connected segments, and the separate outside rectangular 41 | // corners of each segment. 42 | JoinBevel 43 | ) 44 | 45 | // CompositeOperation represents the type of compositing operation to apply 46 | // when drawing new shapes. 47 | // 48 | // For visual explanations of the composite operations see the [MDN docs] 49 | // for CanvasRenderingContext2D.globalCompositeOperation. 50 | // 51 | // [MDN docs]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation#operations 52 | type CompositeOperation byte 53 | 54 | const ( 55 | // OpSourceOver draws new shapes on top of the existing canvas content. 56 | OpSourceOver CompositeOperation = iota 57 | // OpSourceIn draws the new shape only where both the new shape and the 58 | // destination canvas overlap. Everything else is made transparent. 59 | OpSourceIn 60 | // OpSourceOut draws the new shape where it doesn't overlap the existing 61 | // canvas content. 62 | OpSourceOut 63 | // OpSourceAtop draws the new shape only where it overlaps the existing 64 | // canvas content. 65 | OpSourceAtop 66 | // OpDestinationOver draws new shapes behind the existing canvas content. 67 | OpDestinationOver 68 | // OpDestinationIn keeps the existing canvas content where both the new 69 | // shape and existing canvas content overlap. Everything else is made 70 | // transparent. 71 | OpDestinationIn 72 | // OpDestinationOut keeps the existing content where it doesn't overlap 73 | // the new shape. 74 | OpDestinationOut 75 | // OpDestinationAtop keeps the existing canvas content only where it 76 | // overlaps the new shape. The new shape is drawn behind the canvas 77 | // content. 78 | OpDestinationAtop 79 | // OpLighter determines the color by adding color values where both shapes 80 | // overlap. 81 | OpLighter 82 | // OpCopy shows only the new shape. 83 | OpCopy 84 | // OpXOR makes shapes transparent where both overlap and draws them normal 85 | // everywhere else. 86 | OpXOR 87 | // OpMultiply multiplies the pixels of the top layer with the corresponding 88 | // pixels of the bottom layer. A darker picture is the result. 89 | OpMultiply 90 | // OpScreen inverts, multiplies, and inverts the pixels again. 91 | // A lighter picture is the result (opposite of multiply) 92 | OpScreen 93 | // OpOverlay is a combination of OpMultiply and OpScreen. Dark parts on the 94 | // base layer become darker, and light parts become lighter. 95 | OpOverlay 96 | // OpDarken retains the darkest pixels of both layers. 97 | OpDarken 98 | // OpLighten retains the lightest pixels of both layers. 99 | OpLighten 100 | // OpColorDodge divides the bottom layer by the inverted top layer. 101 | OpColorDodge 102 | // OpColorBurn divides the inverted bottom layer by the top layer, and 103 | // then inverts the result. 104 | OpColorBurn 105 | // OpHardLight is a combination of multiply and screen like overlay, but 106 | // with top and bottom layer swapped. 107 | OpHardLight 108 | // OpSoftLight is a softer version of hard-light. Pure black or white does 109 | // not result in pure black or white. 110 | OpSoftLight 111 | // OpDifference subtracts the bottom layer from the top layer or the other 112 | // way round to always get a positive value. 113 | OpDifference 114 | // OpExclusion is like OpDifference, but with lower contrast. 115 | OpExclusion 116 | // OpHue preserves the luma and chroma of the bottom layer, while adopting 117 | // the hue of the top layer. 118 | OpHue 119 | // OpSaturation preserves the luma and hue of the bottom layer, while 120 | // adopting the chroma of the top layer. 121 | OpSaturation 122 | // OpColor preserves the luma of the bottom layer, while adopting the hue 123 | // and chroma of the top layer. 124 | OpColor 125 | // OpLuminosity preserves the hue and chroma of the bottom layer, while 126 | // adopting the luma of the top layer. 127 | OpLuminosity 128 | ) 129 | 130 | // TextAlign represents the text alignment used when drawing text. 131 | type TextAlign byte 132 | 133 | const ( 134 | // AlignStart means the text is aligned at the normal start of the line 135 | // (left-aligned for left-to-right locales, right-aligned for right-to-left 136 | // locales). 137 | AlignStart TextAlign = iota 138 | // AlignEnd means the text is aligned at the normal end of the line 139 | // (right-aligned for left-to-right locales, left-aligned for right-to-left 140 | // locales). 141 | AlignEnd 142 | // AlignLeft means the text is left-aligned. 143 | AlignLeft 144 | // AlignRight means the text is right-aligned. 145 | AlignRight 146 | // AlignCenter means the text is centered. 147 | AlignCenter 148 | ) 149 | 150 | // TextBaseline represents the text baseline used when drawing text. 151 | type TextBaseline byte 152 | 153 | const ( 154 | // BaselineAlphabetic means the text baseline is the normal alphabetic 155 | // baseline. 156 | BaselineAlphabetic TextBaseline = iota 157 | // BaselineIdeographic means the text baseline is the ideographic baseline; 158 | // this is the bottom of the body of the characters, if the main body of 159 | // characters protrudes beneath the alphabetic baseline. 160 | // (Used by Chinese, Japanese, and Korean scripts.) 161 | BaselineIdeographic 162 | // BaselineTop means the text baseline is the top of the em square. 163 | BaselineTop 164 | // BaselineBottom means the text baseline is the bottom of the bounding 165 | // box. This differs from the ideographic baseline in that the ideographic 166 | // baseline doesn't consider descenders. 167 | BaselineBottom 168 | // BaselineHanging means the text baseline is the hanging baseline. 169 | // (Used by Tibetan and other Indic scripts.) 170 | BaselineHanging 171 | // BaselineMiddle means the text baseline is the middle of the em square. 172 | BaselineMiddle 173 | ) 174 | 175 | // PatternRepetition indicates how to repeat a pattern's image. 176 | type PatternRepetition byte 177 | 178 | const ( 179 | // PatternRepeat repeats the image in both directions. 180 | PatternRepeat PatternRepetition = iota 181 | // PatternRepeatX repeats the image only horizontally. 182 | PatternRepeatX 183 | // PatternRepeatY repeats the image only vertically. 184 | PatternRepeatY 185 | // PatternNoRepeat repeats the image in neither direction. 186 | PatternNoRepeat 187 | ) 188 | 189 | const ( 190 | bArc byte = 1 + iota 191 | bArcTo 192 | bBeginPath 193 | bBezierCurveTo 194 | bClearRect 195 | bClip 196 | bClosePath 197 | bCreateImageData 198 | bCreateLinearGradient 199 | bCreatePattern 200 | bCreateRadialGradient 201 | _ 202 | bDrawImage 203 | bEllipse 204 | bFill 205 | bFillRect 206 | bFillStyle 207 | bFillText 208 | bFont 209 | bGradientAddColorStop 210 | bGradientAddColorStopString 211 | bFillStyleGradient 212 | bGlobalAlpha 213 | bGlobalCompositeOperation 214 | bImageSmoothingEnabled 215 | bStrokeStyleGradient 216 | bReleasePattern 217 | bLineCap 218 | bLineDashOffset 219 | bLineJoin 220 | bLineTo 221 | bLineWidth 222 | bReleaseGradient 223 | bMiterLimit 224 | bMoveTo 225 | bPutImageData 226 | bQuadraticCurveTo 227 | bRect 228 | bRestore 229 | bRotate 230 | bSave 231 | bScale 232 | bSetLineDash 233 | bSetTransform 234 | bShadowBlur 235 | bShadowColor 236 | bShadowOffsetX 237 | bShadowOffsetY 238 | bStroke 239 | bStrokeRect 240 | bStrokeStyle 241 | bStrokeText 242 | bTextAlign 243 | bTextBaseline 244 | bTransform 245 | bTranslate 246 | bFillTextMaxWidth 247 | bStrokeTextMaxWidth 248 | bFillStyleString 249 | bStrokeStyleString 250 | bShadowColorString 251 | bPutImageDataDirty 252 | bDrawImageScaled 253 | bDrawImageSubRectangle 254 | bReleaseImageData 255 | bFillStylePattern 256 | bStrokeStylePattern 257 | bGetImageData 258 | ) 259 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestModifierKeys(t *testing.T) { 15 | type modifierKeyStates struct { 16 | altKey bool 17 | shiftKey bool 18 | ctrlKey bool 19 | metaKey bool 20 | } 21 | tests := []struct { 22 | name string 23 | modKeys ModifierKeys 24 | want modifierKeyStates 25 | }{ 26 | { 27 | name: "AltKey", 28 | modKeys: 0b0001, 29 | want: modifierKeyStates{altKey: true}, 30 | }, 31 | { 32 | name: "ShiftKey", 33 | modKeys: 0b0010, 34 | want: modifierKeyStates{shiftKey: true}, 35 | }, 36 | { 37 | name: "CtrlKey", 38 | modKeys: 0b0100, 39 | want: modifierKeyStates{ctrlKey: true}, 40 | }, 41 | { 42 | name: "MetaKey", 43 | modKeys: 0b1000, 44 | want: modifierKeyStates{metaKey: true}, 45 | }, 46 | { 47 | name: "Multiple modifier keys", 48 | modKeys: 0b1111, 49 | want: modifierKeyStates{ 50 | altKey: true, 51 | shiftKey: true, 52 | ctrlKey: true, 53 | metaKey: true, 54 | }, 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | got := modifierKeyStates{ 60 | altKey: tt.modKeys.AltKey(), 61 | shiftKey: tt.modKeys.ShiftKey(), 62 | ctrlKey: tt.modKeys.CtrlKey(), 63 | metaKey: tt.modKeys.MetaKey(), 64 | } 65 | if got != tt.want { 66 | t.Errorf("\ngot : %#v\nwant: %#v", got, tt.want) 67 | } 68 | }) 69 | } 70 | } 71 | 72 | func TestDecodeEvent(t *testing.T) { 73 | tests := []struct { 74 | name string 75 | p []byte 76 | want Event 77 | }{ 78 | { 79 | "MouseMoveEvent", 80 | []byte{ 81 | 0x01, // Event type 82 | 0b00000000, // Buttons 83 | 0x00, 0x00, 0x00, 0xc8, // X 84 | 0x00, 0x00, 0x00, 0x96, // Y 85 | 0b00000101, // Modifier keys 86 | }, 87 | MouseMoveEvent{ 88 | MouseEvent{ 89 | Buttons: ButtonNone, 90 | X: 200, 91 | Y: 150, 92 | Mod: modKeyCtrl | modKeyAlt, 93 | }, 94 | }, 95 | }, 96 | { 97 | "MouseDownEvent", 98 | []byte{ 99 | 0x02, // Event type 100 | 0b00010000, // Buttons 101 | 0x00, 0x00, 0x02, 0xfc, // X 102 | 0x00, 0x00, 0x03, 0xff, // Y 103 | 0b00001000, // Modifier keys 104 | }, 105 | MouseDownEvent{ 106 | MouseEvent{ 107 | Buttons: Button5th, 108 | X: 764, 109 | Y: 1023, 110 | Mod: modKeyMeta, 111 | }, 112 | }, 113 | }, 114 | { 115 | "MouseUpEvent", 116 | []byte{ 117 | 0x03, // Event type 118 | 0b00000001, // Buttons 119 | 0x00, 0x00, 0x00, 0xf0, // X 120 | 0x00, 0x00, 0x0c, 0x8a, // Y 121 | 0b00000100, // Modifier keys 122 | }, 123 | MouseUpEvent{ 124 | MouseEvent{ 125 | Buttons: ButtonPrimary, 126 | X: 240, 127 | Y: 3210, 128 | Mod: modKeyCtrl, 129 | }, 130 | }, 131 | }, 132 | { 133 | "KeyDownEvent", 134 | []byte{ 135 | 0x04, // Event type 136 | 0b00001010, // Modifier keys 137 | 0x00, 0x00, 0x00, 0x09, // len(Key) 138 | 0x41, 0x72, 0x72, 0x6f, 0x77, 0x4c, 0x65, 0x66, 0x74, // Key 139 | }, 140 | KeyDownEvent{ 141 | KeyboardEvent{ 142 | Key: "ArrowLeft", 143 | Mod: modKeyShift | modKeyMeta, 144 | }, 145 | }, 146 | }, 147 | { 148 | "KeyUpEvent", 149 | []byte{ 150 | 0x05, // Event type 151 | 0b00000011, // Modifier keys 152 | 0x00, 0x00, 0x00, 0x0a, // len(Key) 153 | 0x41, 0x72, 0x72, 0x6f, 0x77, 0x52, 0x69, 0x67, 0x68, 0x74, // Key 154 | }, 155 | KeyUpEvent{ 156 | KeyboardEvent{ 157 | Key: "ArrowRight", 158 | Mod: modKeyAlt | modKeyShift, 159 | }, 160 | }, 161 | }, 162 | { 163 | "ClickEvent", 164 | []byte{ 165 | 0x06, // Event type 166 | 0b00000010, // Buttons 167 | 0x00, 0x00, 0x07, 0x81, // X 168 | 0x00, 0x00, 0x02, 0x02, // Y 169 | 0b00000010, // Modifier keys 170 | }, 171 | ClickEvent{ 172 | MouseEvent{ 173 | Buttons: ButtonSecondary, 174 | X: 1921, 175 | Y: 514, 176 | Mod: modKeyShift, 177 | }, 178 | }, 179 | }, 180 | { 181 | "DblClickEvent", 182 | []byte{ 183 | 0x07, // Event type 184 | 0b00000001, // Buttons 185 | 0x00, 0x00, 0x0f, 0xc0, // X 186 | 0x00, 0x00, 0x14, 0xdf, // Y 187 | 0b00000011, // Modifier keys 188 | }, 189 | DblClickEvent{ 190 | MouseEvent{ 191 | Buttons: ButtonPrimary, 192 | X: 4032, 193 | Y: 5343, 194 | Mod: modKeyAlt | modKeyShift, 195 | }, 196 | }, 197 | }, 198 | { 199 | "AuxClickEvent", 200 | []byte{ 201 | 0x08, // Event type 202 | 0b00000001, // Buttons 203 | 0x00, 0x00, 0x01, 0x41, // X 204 | 0x00, 0x00, 0x02, 0x1f, // Y 205 | 0b00001100, // Modifier keys 206 | }, 207 | AuxClickEvent{ 208 | MouseEvent{ 209 | Buttons: ButtonPrimary, 210 | X: 321, 211 | Y: 543, 212 | Mod: modKeyCtrl | modKeyMeta, 213 | }, 214 | }, 215 | }, 216 | { 217 | "WheelEvent", 218 | []byte{ 219 | 0x09, // Event type 220 | 0b00001100, // Buttons 221 | 0x00, 0x00, 0x00, 0x82, // X 222 | 0x00, 0x00, 0x01, 0x9A, // Y 223 | 0b00000010, // Modifier keys 224 | 225 | 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // DeltaX 226 | 0x40, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // DeltaY 227 | 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // DeltaZ 228 | 0x01, // Delta mode 229 | }, 230 | WheelEvent{ 231 | MouseEvent: MouseEvent{ 232 | Buttons: ButtonAuxiliary | Button4th, 233 | X: 130, 234 | Y: 410, 235 | Mod: modKeyShift, 236 | }, 237 | DeltaX: 10, 238 | DeltaY: 24, 239 | DeltaZ: 4, 240 | DeltaMode: DeltaLine, 241 | }, 242 | }, 243 | { 244 | "TouchStartEvent", 245 | []byte{ 246 | 0x0a, // Event type 247 | 0x01, // len(Touches) 248 | 0x00, 0x00, 0x00, 0x00, // Touches[0].Identifier 249 | 0x00, 0x00, 0x01, 0x54, // Touches[0].X 250 | 0x00, 0x00, 0x00, 0xd2, // Touches[0].Y 251 | 0x00, // len(ChangedTouches) 252 | 0x00, // len(TargetTouches) 253 | 0b00001000, // Modifier keys 254 | }, 255 | TouchStartEvent{ 256 | TouchEvent{ 257 | Touches: TouchList{ 258 | {Identifier: 0, X: 340, Y: 210}, 259 | }, 260 | ChangedTouches: TouchList{}, 261 | TargetTouches: TouchList{}, 262 | Mod: modKeyMeta, 263 | }, 264 | }, 265 | }, 266 | { 267 | "TouchMoveEvent", 268 | []byte{ 269 | 0x0b, // Event type 270 | 271 | 0x02, // len(Touches) 272 | 0x00, 0x00, 0x00, 0x00, // Touches[0].Identifier 273 | 0x00, 0x00, 0x0f, 0x00, // Touches[0].X 274 | 0x00, 0x00, 0x00, 0xa5, // Touches[0].Y 275 | 0x00, 0x00, 0x00, 0x01, // Touches[1].Identifier 276 | 0x00, 0x00, 0x03, 0x10, // Touches[1].X 277 | 0x00, 0x00, 0x02, 0x05, // Touches[1].Y 278 | 279 | 0x01, // len(ChangedTouches) 280 | 0x00, 0x00, 0x00, 0x01, // ChangedTouches[0].Identifier 281 | 0x00, 0x00, 0x00, 0xf0, // ChangedTouches[0].X 282 | 0x00, 0x00, 0x00, 0xa2, // ChangedTouches[0].Y 283 | 284 | 0x01, // len(TargetTouches) 285 | 0x00, 0x00, 0x00, 0x02, // TargetTouches[0].Identifier 286 | 0x00, 0x00, 0x01, 0x00, // TargetTouches[0].X 287 | 0x00, 0x00, 0x02, 0x00, // TargetTouches[0].Y 288 | 289 | 0b00000101, // Modifier keys 290 | }, 291 | TouchMoveEvent{ 292 | TouchEvent{ 293 | Touches: TouchList{ 294 | {Identifier: 0, X: 3840, Y: 165}, 295 | {Identifier: 1, X: 784, Y: 517}, 296 | }, 297 | ChangedTouches: TouchList{{Identifier: 1, X: 240, Y: 162}}, 298 | TargetTouches: TouchList{{Identifier: 2, X: 256, Y: 512}}, 299 | Mod: modKeyAlt | modKeyCtrl, 300 | }, 301 | }, 302 | }, 303 | { 304 | "TouchEndEvent", 305 | []byte{ 306 | 0x0c, // Event type 307 | 0x00, // len(Touches) 308 | 0x01, // len(ChangedTouches) 309 | 0x00, 0x00, 0x00, 0x00, // Touches[0].Identifier 310 | 0x00, 0x00, 0x01, 0x54, // Touches[0].X 311 | 0x00, 0x00, 0x00, 0xd2, // Touches[0].Y 312 | 0x00, // len(TargetTouches) 313 | 0b00000001, // Modifier keys 314 | }, 315 | TouchEndEvent{ 316 | TouchEvent{ 317 | Touches: TouchList{}, 318 | ChangedTouches: TouchList{ 319 | {Identifier: 0, X: 340, Y: 210}, 320 | }, 321 | TargetTouches: TouchList{}, 322 | Mod: modKeyAlt, 323 | }, 324 | }, 325 | }, 326 | { 327 | "TouchCancelEvent", 328 | []byte{ 329 | 0x0d, // Event type 330 | 0x00, // len(Touches) 331 | 0x00, // len(ChangedTouches) 332 | 0x01, // len(TargetTouches) 333 | 0x00, 0x00, 0x00, 0x00, // Touches[0].Identifier 334 | 0x00, 0x00, 0x01, 0x54, // Touches[0].X 335 | 0x00, 0x00, 0x00, 0xd2, // Touches[0].Y 336 | 0b00000011, // Modifier keys 337 | }, 338 | TouchCancelEvent{ 339 | TouchEvent{ 340 | Touches: TouchList{}, 341 | ChangedTouches: TouchList{}, 342 | TargetTouches: TouchList{ 343 | {Identifier: 0, X: 340, Y: 210}, 344 | }, 345 | Mod: modKeyAlt | modKeyShift, 346 | }, 347 | }, 348 | }, 349 | } 350 | for _, tt := range tests { 351 | t.Run(tt.name, func(t *testing.T) { 352 | got, err := decodeEvent(tt.p) 353 | if err != nil { 354 | t.Errorf("did not expect error, but got error: %s", err) 355 | return 356 | } 357 | if diff := cmp.Diff(tt.want, got); diff != "" { 358 | t.Errorf("mismatch (-want, +got)\n%s", diff) 359 | } 360 | }) 361 | } 362 | } 363 | 364 | func TestUnsupportedEventType(t *testing.T) { 365 | tests := []byte{ 366 | 0x00, 367 | 0xfa, 368 | 0xfb, 369 | 0xfc, 370 | } 371 | wantErrorMessage := "unknown event type: " 372 | for _, tt := range tests { 373 | got, err := decodeEvent([]byte{tt}) 374 | if err == nil { 375 | t.Errorf("expected error, but got none") 376 | return 377 | } 378 | if wantType, ok := err.(errUnknownEventType); !ok { 379 | t.Errorf("expected %T error, but got: %#v", wantType, err) 380 | } 381 | if !strings.HasPrefix(err.Error(), wantErrorMessage) { 382 | t.Errorf("expected %q error message, but got: %q", wantErrorMessage, err) 383 | } 384 | if got != nil { 385 | t.Errorf("expected nil event, but got: %#v", got) 386 | } 387 | } 388 | } 389 | 390 | func TestEventDataTooShort(t *testing.T) { 391 | tests := []struct { 392 | p []byte 393 | }{ 394 | {[]byte{0x01}}, 395 | {[]byte{0x02, 0x00, 0x00, 0x00}}, 396 | {[]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}, 397 | } 398 | for _, tt := range tests { 399 | got, err := decodeEvent(tt.p) 400 | if err == nil { 401 | t.Errorf("expected error, but got none") 402 | return 403 | } 404 | if wantType, ok := err.(errDataTooShort); !ok { 405 | t.Errorf("expected %T error, but got: %#v", wantType, err) 406 | } 407 | if got != nil { 408 | t.Errorf("expected nil event, but got: %#v", got) 409 | } 410 | } 411 | } 412 | 413 | func TestEventMask(t *testing.T) { 414 | tests := []struct { 415 | event Event 416 | want eventMask 417 | }{ 418 | {CloseEvent{}, 0}, 419 | {MouseEvent{}, 0b11100111}, 420 | {MouseMoveEvent{}, 0b00000001}, 421 | {MouseDownEvent{}, 0b00000010}, 422 | {MouseUpEvent{}, 0b00000100}, 423 | {ClickEvent{}, 0b00100000}, 424 | {DblClickEvent{}, 0b01000000}, 425 | {AuxClickEvent{}, 0b10000000}, 426 | {WheelEvent{}, 0b100000000}, 427 | {KeyboardEvent{}, 0b00011000}, 428 | {KeyDownEvent{}, 0b00001000}, 429 | {KeyUpEvent{}, 0b00010000}, 430 | {TouchEvent{}, 0b1111000000000}, 431 | {TouchStartEvent{}, 0b0001000000000}, 432 | {TouchMoveEvent{}, 0b0010000000000}, 433 | {TouchEndEvent{}, 0b0100000000000}, 434 | {TouchCancelEvent{}, 0b1000000000000}, 435 | } 436 | for _, tt := range tests { 437 | got := tt.event.mask() 438 | if got != tt.want { 439 | t.Errorf("Event mask for %T; got: %#b, want: %#b", tt.event, got, tt.want) 440 | } 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // The API doc comments are based on the MDN Web Docs for the [Canvas API] 6 | // by Mozilla Contributors and are licensed under [CC-BY-SA 2.5]. 7 | // 8 | // [Canvas API]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D 9 | // [CC-BY-SA 2.5]: https://creativecommons.org/licenses/by-sa/2.5/ 10 | 11 | package canvas 12 | 13 | import "fmt" 14 | 15 | // Event is an interface implemented by all event subtypes. Events can be 16 | // received from the channel returned by Context.Events. Use a type switch 17 | // to distinguish between different event types. 18 | type Event interface { 19 | mask() eventMask 20 | } 21 | 22 | // The CloseEvent is fired when the WebSocket connection to the client is 23 | // closed. 24 | // It is not necessary to enable the CloseEvent with Options.EnabledEvents, 25 | // it is always enabled. 26 | // Animation loops should handle the CloseEvent to quit the loop. 27 | type CloseEvent struct{} 28 | 29 | func (e CloseEvent) mask() eventMask { return 0 } 30 | 31 | // MouseEvent represents events that occur due to the user interacting with a 32 | // pointing device (such as a mouse). 33 | type MouseEvent struct { 34 | // Buttons encodes the buttons being depressed (if any) when the mouse 35 | // event was fired. 36 | Buttons MouseButtons 37 | // The X coordinate of the mouse pointer. 38 | X int 39 | // The Y coordinate of the mouse pointer. 40 | Y int 41 | // Mod describes the modifier keys pressed during the event. 42 | Mod ModifierKeys 43 | } 44 | 45 | func (e MouseEvent) mask() eventMask { 46 | return maskMouseMove | maskMouseUp | maskMouseDown | maskClick | maskDblClick | maskAuxClick 47 | } 48 | 49 | // The MouseMoveEvent is fired when a pointing device (usually a mouse) is 50 | // moved. 51 | type MouseMoveEvent struct{ MouseEvent } 52 | 53 | func (e MouseMoveEvent) mask() eventMask { return maskMouseMove } 54 | 55 | // The MouseDownEvent is fired when a pointing device button is pressed. 56 | // 57 | // Note: This differs from the ClickEvent in that click is fired after a full 58 | // click action occurs; that is, the mouse button is pressed and released 59 | // while the pointer remains inside the canvas. MouseDownEvent is fired 60 | // the moment the button is initially pressed. 61 | type MouseDownEvent struct{ MouseEvent } 62 | 63 | func (e MouseDownEvent) mask() eventMask { return maskMouseDown } 64 | 65 | // The MouseUpEvent is fired when a button on a pointing device (such as a 66 | // mouse or trackpad) is released. It is the counterpoint to the 67 | // MouseDownEvent. 68 | type MouseUpEvent struct{ MouseEvent } 69 | 70 | func (e MouseUpEvent) mask() eventMask { return maskMouseUp } 71 | 72 | // The ClickEvent is fired when a pointing device button (such as a mouse's 73 | // primary mouse button) is both pressed and released while the pointer is 74 | // located inside the canvas. 75 | type ClickEvent struct{ MouseEvent } 76 | 77 | func (e ClickEvent) mask() eventMask { return maskClick } 78 | 79 | // The DblClickEvent is fired when a pointing device button (such as a mouse's 80 | // primary button) is double-clicked; that is, when it's rapidly clicked twice 81 | // on the canvas within a very short span of time. 82 | // 83 | // DblClickEvent fires after two ClickEvents (and by extension, after two pairs 84 | // of MouseDownEvents and MouseUpEvents). 85 | type DblClickEvent struct{ MouseEvent } 86 | 87 | func (e DblClickEvent) mask() eventMask { return maskDblClick } 88 | 89 | // The AuxClickEvent is fired when a non-primary pointing device button (any 90 | // mouse button other than the primary—usually leftmost—button) has been 91 | // pressed and released both within the same element. 92 | type AuxClickEvent struct{ MouseEvent } 93 | 94 | func (e AuxClickEvent) mask() eventMask { return maskAuxClick } 95 | 96 | // The WheelEvent is fired due to the user moving a mouse wheel or similar 97 | // input device. 98 | type WheelEvent struct { 99 | MouseEvent 100 | // DeltaX represents the horizontal scroll amount. 101 | DeltaX float64 102 | // DeltaY represents the vertical scroll amount. 103 | DeltaY float64 104 | // DeltaZ represents the scroll amount for the z-axis. 105 | DeltaZ float64 106 | // DeltaMode represents the unit of the delta values' scroll amount. 107 | DeltaMode DeltaMode 108 | } 109 | 110 | func (e WheelEvent) mask() eventMask { 111 | return maskWheel 112 | } 113 | 114 | // DeltaMode represents the unit of the delta values' scroll amount. 115 | type DeltaMode byte 116 | 117 | const ( 118 | // DeltaPixel means the delta values are specified in pixels. 119 | DeltaPixel DeltaMode = iota 120 | // DeltaLine means the delta values are specified in lines. 121 | DeltaLine 122 | // DeltaPage means the delta values are specified in pages. 123 | DeltaPage 124 | ) 125 | 126 | // KeyboardEvent objects describe a user interaction with the keyboard; each 127 | // event describes a single interaction between the user and a key (or 128 | // combination of a key with modifier keys) on the keyboard. 129 | type KeyboardEvent struct { 130 | // Key represents the key value of the key represented by the event. 131 | Key string 132 | // Mod describes the modifier keys pressed during the event. 133 | Mod ModifierKeys 134 | } 135 | 136 | func (e KeyboardEvent) mask() eventMask { 137 | return maskKeyDown | maskKeyUp 138 | } 139 | 140 | // The KeyDownEvent is fired when a key is pressed. 141 | type KeyDownEvent struct{ KeyboardEvent } 142 | 143 | func (e KeyDownEvent) mask() eventMask { return maskKeyDown } 144 | 145 | // The KeyUpEvent is fired when a key is released. 146 | type KeyUpEvent struct{ KeyboardEvent } 147 | 148 | func (e KeyUpEvent) mask() eventMask { return maskKeyUp } 149 | 150 | // The TouchEvent is fired when the state of contacts with a touch-sensitive 151 | // surface changes. This surface can be a touch screen or trackpad, for 152 | // example. The event can describe one or more points of contact with the 153 | // screen and includes support for detecting movement, addition and removal of 154 | // contact points, and so forth. 155 | // 156 | // Touches are represented by the Touch object; each touch is described by a 157 | // position, size and shape, amount of pressure, and target element. Lists of 158 | // touches are represented by TouchList objects. 159 | type TouchEvent struct { 160 | // Touches is a TouchList of all the Touch objects representing all current 161 | // points of contact with the surface, regardless of target or changed 162 | // status. 163 | Touches TouchList 164 | // ChangedTouches is a TouchList of all the Touch objects representing 165 | // individual points of contact whose states changed between the previous 166 | // touch event and this one. 167 | ChangedTouches TouchList 168 | // TargetTouches is a TouchList of all the Touch objects that are both 169 | // currently in contact with the touch surface and were also started on the 170 | // same element that is the target of the event. 171 | TargetTouches TouchList 172 | // Mod describes the modifier keys pressed during the event. 173 | Mod ModifierKeys 174 | } 175 | 176 | func (e TouchEvent) mask() eventMask { 177 | return maskTouchStart | maskTouchMove | maskTouchEnd | maskTouchCancel 178 | } 179 | 180 | // TouchList represents a list of contact points on a touch surface. For 181 | // example, if the user has three fingers on the touch surface (such as a 182 | // screen or trackpad), the corresponding TouchList object would have one 183 | // Touch object for each finger, for a total of three entries. 184 | type TouchList []Touch 185 | 186 | // Touch represents a single contact point on a touch-sensitive device. 187 | // The contact point is commonly a finger or stylus and the device may be a 188 | // touchscreen or trackpad. 189 | type Touch struct { 190 | // Identifier is a unique identifier for this Touch object. A given touch 191 | // point (say, by a finger) will have the same identifier for the duration 192 | // of its movement around the surface. This lets you ensure that you're 193 | // tracking the same touch all the time. 194 | Identifier uint32 195 | // The X coordinate of the touch point. 196 | X int 197 | // The Y coordinate of the touch point. 198 | Y int 199 | } 200 | 201 | // The TouchStartEvent is fired when one or more touch points are placed on 202 | // the touch surface. 203 | type TouchStartEvent struct{ TouchEvent } 204 | 205 | func (e TouchStartEvent) mask() eventMask { return maskTouchStart } 206 | 207 | // The TouchMoveEvent is fired when one or more touch points are moved along 208 | // the touch surface. 209 | type TouchMoveEvent struct{ TouchEvent } 210 | 211 | func (e TouchMoveEvent) mask() eventMask { return maskTouchMove } 212 | 213 | // The TouchEndEvent is fired when one or more touch points are removed from 214 | // the touch surface. 215 | type TouchEndEvent struct{ TouchEvent } 216 | 217 | func (e TouchEndEvent) mask() eventMask { return maskTouchEnd } 218 | 219 | // The TouchCancelEvent is fired when one or more touch points have been 220 | // disrupted in an implementation-specific manner (for example, too many touch 221 | // points are created). 222 | type TouchCancelEvent struct{ TouchEvent } 223 | 224 | func (e TouchCancelEvent) mask() eventMask { return maskTouchCancel } 225 | 226 | // ModifierKeys describes the modifier keys (Alt, Shift, Ctrl, Meta) pressed 227 | // during an event. 228 | type ModifierKeys byte 229 | 230 | const ( 231 | modKeyAlt ModifierKeys = 1 << iota 232 | modKeyShift 233 | modKeyCtrl 234 | modKeyMeta 235 | ) 236 | 237 | // AltKey returns true if the Alt (Option or ⌥ on OS X) key was active when 238 | // the event was generated. 239 | func (m ModifierKeys) AltKey() bool { 240 | return m.isPressed(modKeyAlt) 241 | } 242 | 243 | // ShiftKey returns true if the Shift key was active when the event was 244 | // generated. 245 | func (m ModifierKeys) ShiftKey() bool { 246 | return m.isPressed(modKeyShift) 247 | } 248 | 249 | // CtrlKey returns true if the Ctrl key was active when the event was 250 | // generated. 251 | func (m ModifierKeys) CtrlKey() bool { 252 | return m.isPressed(modKeyCtrl) 253 | } 254 | 255 | // MetaKey returns true if the Meta key (on Mac keyboards, the ⌘ Command key; 256 | // on Windows keyboards, the Windows key (⊞)) was active when the event 257 | // was generated. 258 | func (m ModifierKeys) MetaKey() bool { 259 | return m.isPressed(modKeyMeta) 260 | } 261 | 262 | func (m ModifierKeys) isPressed(k ModifierKeys) bool { 263 | return m&k != 0 264 | } 265 | 266 | type eventMask int 267 | 268 | const ( 269 | maskMouseMove eventMask = 1 << iota 270 | maskMouseDown 271 | maskMouseUp 272 | maskKeyDown 273 | maskKeyUp 274 | maskClick 275 | maskDblClick 276 | maskAuxClick 277 | maskWheel 278 | maskTouchStart 279 | maskTouchMove 280 | maskTouchEnd 281 | maskTouchCancel 282 | ) 283 | 284 | // MouseButtons is a number representing one or more buttons. For more than 285 | // one button pressed simultaneously, the values are combined (e.g., 3 is 286 | // ButtonPrimary + ButtonSecondary). 287 | type MouseButtons int 288 | 289 | const ( 290 | // ButtonPrimary is the primary button (usually the left button). 291 | ButtonPrimary MouseButtons = 1 << iota 292 | // ButtonSecondary is the secondary button (usually the right button). 293 | ButtonSecondary 294 | // ButtonAuxiliary is the auxiliary button (usually the mouse wheel button 295 | // or middle button) 296 | ButtonAuxiliary 297 | // Button4th is the 4th button (typically the "Browser Back" button). 298 | Button4th 299 | // Button5th is the 5th button (typically the "Browser Forward" button). 300 | Button5th 301 | // ButtonNone stands for no button or un-initialized. 302 | ButtonNone MouseButtons = 0 303 | ) 304 | 305 | const ( 306 | evMouseMove byte = 1 + iota 307 | evMouseDown 308 | evMouseUp 309 | evKeyDown 310 | evKeyUp 311 | evClick 312 | evDblClick 313 | evAuxClick 314 | evWheel 315 | evTouchStart 316 | evTouchMove 317 | evTouchEnd 318 | evTouchCancel 319 | ) 320 | 321 | func decodeEvent(p []byte) (Event, error) { 322 | buf := &buffer{bytes: p} 323 | event, err := decodeEventBuf(buf) 324 | if buf.error != nil { 325 | return nil, buf.error 326 | } 327 | return event, err 328 | } 329 | 330 | func decodeEventBuf(buf *buffer) (Event, error) { 331 | eventType := buf.readByte() 332 | switch eventType { 333 | case evMouseMove: 334 | return MouseMoveEvent{decodeMouseEvent(buf)}, nil 335 | case evMouseDown: 336 | return MouseDownEvent{decodeMouseEvent(buf)}, nil 337 | case evMouseUp: 338 | return MouseUpEvent{decodeMouseEvent(buf)}, nil 339 | case evKeyDown: 340 | return KeyDownEvent{decodeKeyboardEvent(buf)}, nil 341 | case evKeyUp: 342 | return KeyUpEvent{decodeKeyboardEvent(buf)}, nil 343 | case evClick: 344 | return ClickEvent{decodeMouseEvent(buf)}, nil 345 | case evDblClick: 346 | return DblClickEvent{decodeMouseEvent(buf)}, nil 347 | case evAuxClick: 348 | return AuxClickEvent{decodeMouseEvent(buf)}, nil 349 | case evWheel: 350 | return decodeWheelEvent(buf), nil 351 | case evTouchStart: 352 | return TouchStartEvent{decodeTouchEvent(buf)}, nil 353 | case evTouchMove: 354 | return TouchMoveEvent{decodeTouchEvent(buf)}, nil 355 | case evTouchEnd: 356 | return TouchEndEvent{decodeTouchEvent(buf)}, nil 357 | case evTouchCancel: 358 | return TouchCancelEvent{decodeTouchEvent(buf)}, nil 359 | } 360 | return nil, errUnknownEventType{unknownType: eventType} 361 | } 362 | 363 | func decodeMouseEvent(buf *buffer) MouseEvent { 364 | return MouseEvent{ 365 | Buttons: MouseButtons(buf.readByte()), 366 | X: int(buf.readUint32()), 367 | Y: int(buf.readUint32()), 368 | Mod: ModifierKeys(buf.readByte()), 369 | } 370 | } 371 | 372 | func decodeKeyboardEvent(buf *buffer) KeyboardEvent { 373 | return KeyboardEvent{ 374 | Mod: ModifierKeys(buf.readByte()), 375 | Key: buf.readString(), 376 | } 377 | } 378 | 379 | func decodeWheelEvent(buf *buffer) WheelEvent { 380 | return WheelEvent{ 381 | MouseEvent: decodeMouseEvent(buf), 382 | DeltaX: buf.readFloat64(), 383 | DeltaY: buf.readFloat64(), 384 | DeltaZ: buf.readFloat64(), 385 | DeltaMode: DeltaMode(buf.readByte()), 386 | } 387 | } 388 | 389 | func decodeTouchEvent(buf *buffer) TouchEvent { 390 | return TouchEvent{ 391 | Touches: decodeTouchList(buf), 392 | ChangedTouches: decodeTouchList(buf), 393 | TargetTouches: decodeTouchList(buf), 394 | Mod: ModifierKeys(buf.readByte()), 395 | } 396 | } 397 | 398 | func decodeTouchList(buf *buffer) TouchList { 399 | const maxTouchListLength = 10 400 | length := buf.readByte() 401 | limitedLength := min(length, maxTouchListLength) 402 | list := make(TouchList, limitedLength) 403 | for i := range list { 404 | list[i] = decodeTouch(buf) 405 | } 406 | if limitedLength < length { 407 | buf.skip(int(length - limitedLength)) 408 | } 409 | return list 410 | } 411 | 412 | func decodeTouch(buf *buffer) Touch { 413 | return Touch{ 414 | Identifier: buf.readUint32(), 415 | X: int(buf.readUint32()), 416 | Y: int(buf.readUint32()), 417 | } 418 | } 419 | 420 | type errUnknownEventType struct { 421 | unknownType byte 422 | } 423 | 424 | func (err errUnknownEventType) Error() string { 425 | return fmt.Sprintf("unknown event type: %#x", err.unknownType) 426 | } 427 | -------------------------------------------------------------------------------- /web/canvas-websocket.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | document.addEventListener("DOMContentLoaded", function () { 6 | "use strict"; 7 | 8 | const allocImageData = {}; 9 | const allocOffscreenCanvas = {}; 10 | const allocGradient = {}; 11 | const allocPattern = {}; 12 | 13 | const enumRepetition = ["repeat", "repeat-x", "repeat-y", "no-repeat"]; 14 | 15 | const enumCompositeOperation = [ 16 | "source-over", "source-in", "source-out", "source-atop", 17 | "destination-over", "destination-in", "destination-out", 18 | "destination-atop", "lighter", "copy", "xor", "multiply", "screen", 19 | "overlay", "darken", "lighten", "color-dodge", "color-burn", 20 | "hard-light", "soft-light", "difference", "exclusion", "hue", 21 | "saturation", "color", "luminosity" 22 | ]; 23 | 24 | const enumLineCap = ["butt", "round", "square"]; 25 | 26 | const enumLineJoin = ["miter", "round", "bevel"]; 27 | 28 | const enumTextAlign = ["start", "end", "left", "right", "center"]; 29 | 30 | const enumTextBaseline = [ 31 | "alphabetic", "ideographic", "top", "bottom", "middle" 32 | ]; 33 | 34 | const canvases = document.getElementsByTagName("canvas"); 35 | for (let i = 0; i < canvases.length; i++) { 36 | const canvas = canvases[i]; 37 | const config = configFrom(canvas.dataset); 38 | if (config.drawUrl) { 39 | webSocketCanvas(canvas, config); 40 | if (config.contextMenuDisabled) { 41 | disableContextMenu(canvas); 42 | } 43 | } 44 | } 45 | 46 | function configFrom(dataset) { 47 | return { 48 | drawUrl: absoluteWebSocketUrl(dataset["websocketDrawUrl"]), 49 | eventMask: parseInt(dataset["websocketEventMask"], 10) || 0, 50 | reconnectInterval: parseInt(dataset["websocketReconnectInterval"], 10) || 0, 51 | contextMenuDisabled: (dataset["disableContextMenu"] === "true") 52 | }; 53 | } 54 | 55 | function absoluteWebSocketUrl(url) { 56 | if (!url) { 57 | return null; 58 | } 59 | if (url.indexOf("ws://") === 0 || url.indexOf("wss://") === 0) { 60 | return url; 61 | } 62 | const wsUrl = new URL(url, window.location.href); 63 | wsUrl.protocol = wsUrl.protocol.replace("http", "ws"); 64 | return wsUrl.href; 65 | } 66 | 67 | function webSocketCanvas(canvas, config) { 68 | const ctx = canvas.getContext("2d"); 69 | const webSocket = new WebSocket(config.drawUrl); 70 | let handlers = {}; 71 | webSocket.binaryType = "arraybuffer"; 72 | webSocket.addEventListener("open", function () { 73 | handlers = addEventListeners(canvas, config.eventMask, webSocket); 74 | }); 75 | webSocket.addEventListener("error", function () { 76 | webSocket.close(); 77 | }); 78 | webSocket.addEventListener("close", function () { 79 | removeEventListeners(canvas, handlers); 80 | if (!config.reconnectInterval) { 81 | return; 82 | } 83 | setTimeout(function () { 84 | webSocketCanvas(canvas, config); 85 | }, config.reconnectInterval); 86 | }); 87 | webSocket.addEventListener("message", function (event) { 88 | const data = event.data; 89 | let offset = 0; 90 | const len = data.byteLength; 91 | while (offset < len) { 92 | offset += draw(ctx, new DataView(data, offset)); 93 | } 94 | }); 95 | } 96 | 97 | function addEventListeners(canvas, eventMask, webSocket) { 98 | const handlers = {}; 99 | 100 | if (eventMask & 1) { 101 | handlers["mousemove"] = sendMouseEvent(1); 102 | } 103 | if (eventMask & 2) { 104 | handlers["mousedown"] = sendMouseEvent(2); 105 | } 106 | if (eventMask & 4) { 107 | handlers["mouseup"] = sendMouseEvent(3); 108 | } 109 | if (eventMask & 8) { 110 | handlers["keydown"] = sendKeyEvent(4); 111 | } 112 | if (eventMask & 16) { 113 | handlers["keyup"] = sendKeyEvent(5); 114 | } 115 | if (eventMask & 32) { 116 | handlers["click"] = sendMouseEvent(6); 117 | } 118 | if (eventMask & 64) { 119 | handlers["dblclick"] = sendMouseEvent(7); 120 | } 121 | if (eventMask & 128) { 122 | handlers["auxclick"] = sendMouseEvent(8); 123 | } 124 | if (eventMask & 256) { 125 | handlers["wheel"] = sendWheelEvent(9); 126 | } 127 | if (eventMask & 512) { 128 | handlers["touchstart"] = sendTouchEvent(10); 129 | } 130 | if (eventMask & 1024) { 131 | handlers["touchmove"] = sendTouchEvent(11); 132 | } 133 | if (eventMask & 2048) { 134 | handlers["touchend"] = sendTouchEvent(12); 135 | } 136 | if (eventMask & 4096) { 137 | handlers["touchcancel"] = sendTouchEvent(13); 138 | } 139 | 140 | Object.keys(handlers).forEach(function (type) { 141 | const target = (type.indexOf("key") !== 0) ? canvas : document; 142 | target.addEventListener(type, handlers[type], {passive: false}); 143 | }); 144 | 145 | const rect = canvas.getBoundingClientRect(); 146 | 147 | function sendMouseEvent(eventType) { 148 | const mouseMoveThreshold = 25; 149 | let lastMouseMoveTime = 0; 150 | 151 | return function (event) { 152 | event.preventDefault(); 153 | if (eventType === 1) { 154 | const now = new Date().getTime(); 155 | if ((now - lastMouseMoveTime) < mouseMoveThreshold) { 156 | return; 157 | } 158 | lastMouseMoveTime = now; 159 | } 160 | const eventMessage = new ArrayBuffer(11); 161 | const dataView = new DataView(eventMessage); 162 | setMouseEvent(dataView, eventType, event); 163 | webSocket.send(eventMessage); 164 | }; 165 | } 166 | 167 | function sendWheelEvent(eventType) { 168 | return function (event) { 169 | event.preventDefault(); 170 | const eventMessage = new ArrayBuffer(36); 171 | const dataView = new DataView(eventMessage); 172 | setMouseEvent(dataView, eventType, event); 173 | dataView.setFloat64(11, event.deltaX); 174 | dataView.setFloat64(19, event.deltaY); 175 | dataView.setFloat64(27, event.deltaZ); 176 | dataView.setUint8(35, event.deltaMode); 177 | webSocket.send(eventMessage); 178 | }; 179 | } 180 | 181 | function setMouseEvent(dataView, eventType, event) { 182 | dataView.setUint8(0, eventType); 183 | dataView.setUint8(1, event.buttons); 184 | dataView.setUint32(2, ((event.clientX - rect.left) / canvas.offsetWidth) * canvas.width); 185 | dataView.setUint32(6, ((event.clientY - rect.top) / canvas.offsetHeight) * canvas.height); 186 | dataView.setUint8(10, encodeModifierKeys(event)); 187 | } 188 | 189 | function sendTouchEvent(eventType) { 190 | return function (event) { 191 | event.preventDefault(); 192 | const touchBytes = 12; 193 | const eventMessage = new ArrayBuffer(1 + 194 | 1 + (event.touches.length * touchBytes) + 195 | 1 + (event.changedTouches.length * touchBytes) + 196 | 1 + (event.targetTouches.length * touchBytes) + 197 | 1); 198 | const dataView = new DataView(eventMessage); 199 | let offset = 0; 200 | dataView.setUint8(offset, eventType); 201 | offset++; 202 | offset = setTouches(dataView, offset, event.touches); 203 | offset = setTouches(dataView, offset, event.changedTouches); 204 | offset = setTouches(dataView, offset, event.targetTouches); 205 | dataView.setUint8(offset, encodeModifierKeys(event)); 206 | webSocket.send(eventMessage); 207 | }; 208 | } 209 | 210 | function setTouches(dataView, offset, touches) { 211 | const len = touches.length; 212 | dataView.setUint8(offset, len); 213 | offset++; 214 | for (let i = 0; i < len; i++) { 215 | const touch = touches[i]; 216 | dataView.setUint32(offset, touch.identifier); 217 | offset += 4; 218 | dataView.setUint32(offset, ((touch.clientX - rect.left) / canvas.offsetWidth) * canvas.width); 219 | offset += 4; 220 | dataView.setUint32(offset, ((touch.clientY - rect.top) / canvas.offsetHeight) * canvas.height); 221 | offset += 4; 222 | } 223 | return offset; 224 | } 225 | 226 | function sendKeyEvent(eventType) { 227 | return function (event) { 228 | event.preventDefault(); 229 | const keyBytes = new TextEncoder().encode(event.key); 230 | const eventMessage = new ArrayBuffer(6 + keyBytes.byteLength); 231 | const data = new DataView(eventMessage); 232 | data.setUint8(0, eventType); 233 | data.setUint8(1, encodeModifierKeys(event)); 234 | data.setUint32(2, keyBytes.byteLength); 235 | for (let i = 0; i < keyBytes.length; i++) { 236 | data.setUint8(6 + i, keyBytes[i]); 237 | } 238 | webSocket.send(eventMessage); 239 | }; 240 | } 241 | 242 | return handlers; 243 | } 244 | 245 | function removeEventListeners(canvas, handlers) { 246 | Object.keys(handlers).forEach(function (type) { 247 | const target = (type.indexOf("key") !== 0) ? canvas : document; 248 | target.removeEventListener(type, handlers[type]); 249 | }); 250 | } 251 | 252 | function disableContextMenu(canvas) { 253 | canvas.addEventListener("contextmenu", function (e) { 254 | e.preventDefault(); 255 | }, false); 256 | } 257 | 258 | function encodeModifierKeys(event) { 259 | let modifiers = 0; 260 | if (event.altKey) { 261 | modifiers |= 1; 262 | } 263 | if (event.shiftKey) { 264 | modifiers |= 2; 265 | } 266 | if (event.ctrlKey) { 267 | modifiers |= 4; 268 | } 269 | if (event.metaKey) { 270 | modifiers |= 8; 271 | } 272 | return modifiers; 273 | } 274 | 275 | function draw(ctx, data) { 276 | switch (data.getUint8(0)) { 277 | case 1: 278 | ctx.arc( 279 | data.getFloat64(1), data.getFloat64(9), data.getFloat64(17), 280 | data.getFloat64(25), data.getFloat64(33), !!data.getUint8(41)); 281 | return 42; 282 | case 2: 283 | ctx.arcTo( 284 | data.getFloat64(1), data.getFloat64(9), 285 | data.getFloat64(17), data.getFloat64(25), 286 | data.getFloat64(33)); 287 | return 41; 288 | case 3: 289 | ctx.beginPath(); 290 | return 1; 291 | case 4: 292 | ctx.bezierCurveTo( 293 | data.getFloat64(1), data.getFloat64(9), 294 | data.getFloat64(17), data.getFloat64(25), 295 | data.getFloat64(33), data.getFloat64(41)); 296 | return 49; 297 | case 5: 298 | ctx.clearRect( 299 | data.getFloat64(1), data.getFloat64(9), 300 | data.getFloat64(17), data.getFloat64(25)); 301 | return 33; 302 | case 6: 303 | ctx.clip(); 304 | return 1; 305 | case 8: { 306 | const id = data.getUint32(1); 307 | const width = data.getUint32(5); 308 | const height = data.getUint32(9); 309 | const len = width * height * 4; 310 | const bufferOffset = data.byteOffset + 13; 311 | const buffer = data.buffer.slice(bufferOffset, bufferOffset + len); 312 | const array = new Uint8ClampedArray(buffer); 313 | const imageData = new ImageData(array, width, height); 314 | allocImageData[id] = imageData; 315 | const offCanvas = document.createElement("canvas"); 316 | offCanvas.width = width; 317 | offCanvas.height = height; 318 | offCanvas.getContext("2d").putImageData(imageData, 0, 0); 319 | allocOffscreenCanvas[id] = offCanvas; 320 | return 13 + len; 321 | } 322 | case 9: { 323 | const id = data.getUint32(1); 324 | const x0 = data.getFloat64(5); 325 | const y0 = data.getFloat64(13); 326 | const x1 = data.getFloat64(21); 327 | const y1 = data.getFloat64(29); 328 | allocGradient[id] = ctx.createLinearGradient(x0, y0, x1, y1); 329 | return 37; 330 | } 331 | case 10: { 332 | const id = data.getUint32(1); 333 | const image = allocOffscreenCanvas[data.getUint32(5)]; 334 | const repetition = enumRepetition[data.getUint8(9)]; 335 | allocPattern[id] = ctx.createPattern(image, repetition); 336 | return 10; 337 | } 338 | case 11: { 339 | const id = data.getUint32(1); 340 | const x0 = data.getFloat64(5); 341 | const y0 = data.getFloat64(13); 342 | const r0 = data.getFloat64(21); 343 | const x1 = data.getFloat64(29); 344 | const y1 = data.getFloat64(37); 345 | const r1 = data.getFloat64(45); 346 | allocGradient[id] = ctx.createRadialGradient(x0, y0, r0, x1, y1, r1); 347 | return 53; 348 | } 349 | case 13: 350 | ctx.drawImage(allocOffscreenCanvas[data.getUint32(1)], 351 | data.getFloat64(5), data.getFloat64(13)); 352 | return 21; 353 | case 14: 354 | ctx.ellipse( 355 | data.getFloat64(1), data.getFloat64(9), data.getFloat64(17), 356 | data.getFloat64(25), data.getFloat64(33), data.getFloat64(41), 357 | data.getFloat64(49), !!data.getUint8(57)); 358 | return 58; 359 | case 15: 360 | ctx.fill(); 361 | return 1; 362 | case 16: 363 | ctx.fillRect( 364 | data.getFloat64(1), data.getFloat64(9), 365 | data.getFloat64(17), data.getFloat64(25)); 366 | return 33; 367 | case 17: { 368 | ctx.fillStyle = getRGBA(data, 1); 369 | return 5; 370 | } 371 | case 18: { 372 | const text = getString(data, 17); 373 | ctx.fillText(text.value, data.getFloat64(1), data.getFloat64(9)); 374 | return 17 + text.byteLen; 375 | } 376 | case 19: { 377 | const font = getString(data, 1); 378 | ctx.font = font.value; 379 | return 1 + font.byteLen; 380 | } 381 | case 20: { 382 | const id = data.getUint32(1); 383 | const gradient = allocGradient[id]; 384 | gradient.addColorStop(data.getFloat64(5), getRGBA(data, 13)); 385 | return 17; 386 | } 387 | case 21: { 388 | const id = data.getUint32(1); 389 | const offset = data.getFloat64(5); 390 | const color = getString(data, 13); 391 | const gradient = allocGradient[id]; 392 | gradient.addColorStop(offset, color.value); 393 | return 13 + color.byteLen; 394 | } 395 | case 22: 396 | ctx.fillStyle = allocGradient[data.getUint32(1)]; 397 | return 5; 398 | case 23: 399 | ctx.globalAlpha = data.getFloat64(1); 400 | return 9; 401 | case 24: 402 | ctx.globalCompositeOperation = enumCompositeOperation[data.getUint8(1)]; 403 | return 2; 404 | case 25: 405 | ctx.imageSmoothingEnabled = !!data.getUint8(1); 406 | return 2; 407 | case 26: 408 | ctx.strokeStyle = allocGradient[data.getUint32(1)]; 409 | return 5; 410 | case 27: { 411 | const id = data.getUint32(1); 412 | allocPattern[id] = null; 413 | return 5; 414 | } 415 | case 28: 416 | ctx.lineCap = enumLineCap[data.getUint8(1)]; 417 | return 2; 418 | case 29: 419 | ctx.lineDashOffset = data.getFloat64(1); 420 | return 9; 421 | case 30: 422 | ctx.lineJoin = enumLineJoin[data.getUint8(1)]; 423 | return 2; 424 | case 31: 425 | ctx.lineTo(data.getFloat64(1), data.getFloat64(9)); 426 | return 17; 427 | case 32: 428 | ctx.lineWidth = data.getFloat64(1); 429 | return 9; 430 | case 33: { 431 | const id = data.getUint32(1); 432 | allocGradient[id] = null; 433 | return 5; 434 | } 435 | case 34: 436 | ctx.miterLimit = data.getFloat64(1); 437 | return 9; 438 | case 35: 439 | ctx.moveTo(data.getFloat64(1), data.getFloat64(9)); 440 | return 17; 441 | case 36: 442 | ctx.putImageData(allocImageData[data.getUint32(1)], 443 | data.getFloat64(5), data.getFloat64(13)); 444 | return 21; 445 | case 37: 446 | ctx.quadraticCurveTo( 447 | data.getFloat64(1), data.getFloat64(9), 448 | data.getFloat64(17), data.getFloat64(25)); 449 | return 33; 450 | case 38: 451 | ctx.rect( 452 | data.getFloat64(1), data.getFloat64(9), 453 | data.getFloat64(17), data.getFloat64(25)); 454 | return 33; 455 | case 39: 456 | ctx.restore(); 457 | return 1; 458 | case 40: 459 | ctx.rotate(data.getFloat64(1)); 460 | return 9; 461 | case 41: 462 | ctx.save(); 463 | return 1; 464 | case 42: 465 | ctx.scale(data.getFloat64(1), data.getFloat64(9)); 466 | return 17; 467 | case 43: { 468 | const segments = []; 469 | const len = data.getUint32(1); 470 | for (let i = 0; i < len; i++) { 471 | segments.push(data.getFloat64(5 + i * 8)); 472 | } 473 | ctx.setLineDash(segments); 474 | return 5 + (len * 8); 475 | } 476 | case 44: 477 | ctx.setTransform( 478 | data.getFloat64(1), data.getFloat64(9), 479 | data.getFloat64(17), data.getFloat64(25), 480 | data.getFloat64(33), data.getFloat64(41)); 481 | return 49; 482 | case 45: 483 | ctx.shadowBlur = data.getFloat64(1); 484 | return 9; 485 | case 46: { 486 | ctx.shadowColor = getRGBA(data, 1); 487 | return 5; 488 | } 489 | case 47: 490 | ctx.shadowOffsetX = data.getFloat64(1); 491 | return 9; 492 | case 48: 493 | ctx.shadowOffsetY = data.getFloat64(1); 494 | return 9; 495 | case 49: 496 | ctx.stroke(); 497 | return 1; 498 | case 50: 499 | ctx.strokeRect( 500 | data.getFloat64(1), data.getFloat64(9), 501 | data.getFloat64(17), data.getFloat64(25)); 502 | return 33; 503 | case 51: { 504 | ctx.strokeStyle = getRGBA(data, 1); 505 | return 5; 506 | } 507 | case 52: { 508 | const text = getString(data, 17); 509 | ctx.strokeText(text.value, data.getFloat64(1), data.getFloat64(9)); 510 | return 17 + text.byteLen; 511 | } 512 | case 53: 513 | ctx.textAlign = enumTextAlign[data.getUint8(1)]; 514 | return 2; 515 | case 54: 516 | ctx.textBaseline = enumTextBaseline[data.getUint8(1)]; 517 | return 2; 518 | case 55: 519 | ctx.transform( 520 | data.getFloat64(1), data.getFloat64(9), 521 | data.getFloat64(17), data.getFloat64(25), 522 | data.getFloat64(33), data.getFloat64(41)); 523 | return 49; 524 | case 56: 525 | ctx.translate(data.getFloat64(1), data.getFloat64(9)); 526 | return 17; 527 | case 57: { 528 | const text = getString(data, 25); 529 | ctx.fillText(text.value, data.getFloat64(1), data.getFloat64(9), data.getFloat64(17)); 530 | return 25 + text.byteLen; 531 | } 532 | case 58: { 533 | const text = getString(data, 25); 534 | ctx.strokeText(text.value, data.getFloat64(1), data.getFloat64(9), data.getFloat64(17)); 535 | return 25 + text.byteLen; 536 | } 537 | case 59: { 538 | const color = getString(data, 1); 539 | ctx.fillStyle = color.value; 540 | return 1 + color.byteLen; 541 | } 542 | case 60: { 543 | const color = getString(data, 1); 544 | ctx.strokeStyle = color.value; 545 | return 1 + color.byteLen; 546 | } 547 | case 61: { 548 | const color = getString(data, 1); 549 | ctx.shadowColor = color.value; 550 | return 1 + color.byteLen; 551 | } 552 | case 62: 553 | ctx.putImageData(allocImageData[data.getUint32(1)], 554 | data.getFloat64(5), data.getFloat64(13), 555 | data.getFloat64(21), data.getFloat64(29), 556 | data.getFloat64(37), data.getFloat64(45)); 557 | return 53; 558 | case 63: 559 | ctx.drawImage(allocOffscreenCanvas[data.getUint32(1)], 560 | data.getFloat64(5), data.getFloat64(13), 561 | data.getFloat64(21), data.getFloat64(29)); 562 | return 37; 563 | case 64: 564 | ctx.drawImage(allocOffscreenCanvas[data.getUint32(1)], 565 | data.getFloat64(5), data.getFloat64(13), 566 | data.getFloat64(21), data.getFloat64(29), 567 | data.getFloat64(37), data.getFloat64(45), 568 | data.getFloat64(53), data.getFloat64(61)); 569 | return 69; 570 | case 65: { 571 | const id = data.getUint32(1); 572 | allocImageData[id] = null; 573 | allocOffscreenCanvas[id] = null; 574 | return 5; 575 | } 576 | case 66: 577 | ctx.fillStyle = allocPattern[data.getUint32(1)]; 578 | return 5; 579 | case 67: 580 | ctx.strokeStyle = allocPattern[data.getUint32(1)]; 581 | return 5; 582 | case 68: { 583 | const id = data.getUint32(1); 584 | allocImageData[id] = ctx.getImageData( 585 | data.getFloat64(5), data.getFloat64(13), 586 | data.getFloat64(21), data.getFloat64(29)); 587 | return 37; 588 | } 589 | } 590 | return 1; 591 | } 592 | 593 | function getString(data, offset) { 594 | const stringLen = data.getUint32(offset); 595 | const stringBegin = data.byteOffset + offset + 4; 596 | const stringEnd = stringBegin + stringLen; 597 | return { 598 | value: new TextDecoder().decode(data.buffer.slice(stringBegin, stringEnd)), 599 | byteLen: 4 + stringLen 600 | }; 601 | } 602 | 603 | function getRGBA(data, offset) { 604 | return "rgba(" + 605 | data.getUint8(offset) + ", " + 606 | data.getUint8(offset + 1) + ", " + 607 | data.getUint8(offset + 2) + ", " + 608 | data.getUint8(offset + 3) / 255 + ")"; 609 | } 610 | }); 611 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package canvas 6 | 7 | import ( 8 | "image" 9 | "image/color" 10 | "math" 11 | "testing" 12 | 13 | "github.com/google/go-cmp/cmp" 14 | ) 15 | 16 | func TestContextDrawing(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | draw func(*Context) 20 | want []byte 21 | }{ 22 | { 23 | "Arc", 24 | func(ctx *Context) { ctx.Arc(5, 10, 15, 0, 0.75, true) }, 25 | []byte{ 26 | 0x01, 27 | 0x40, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 28 | 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 29 | 0x40, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 30 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x3f, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 32 | 0x01, 33 | }, 34 | }, 35 | { 36 | "ArcTo", 37 | func(ctx *Context) { ctx.ArcTo(100, 50, 80, 60, 25) }, 38 | []byte{ 39 | 0x02, 40 | 0x40, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 41 | 0x40, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 42 | 0x40, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43 | 0x40, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 44 | 0x40, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 45 | }, 46 | }, 47 | { 48 | "BeginPath", 49 | func(ctx *Context) { ctx.BeginPath() }, 50 | []byte{0x03}, 51 | }, 52 | { 53 | "BezierCurveTo", 54 | func(ctx *Context) { ctx.BezierCurveTo(230, 30, 150, 80, 250, 100) }, 55 | []byte{ 56 | 0x04, 57 | 0x40, 0x6c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 58 | 0x40, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x40, 0x62, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 60 | 0x40, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 61 | 0x40, 0x6f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 62 | 0x40, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 63 | }, 64 | }, 65 | { 66 | "ClearRect", 67 | func(ctx *Context) { ctx.ClearRect(10, 20, 120, 80) }, 68 | []byte{ 69 | 0x05, 70 | 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 71 | 0x40, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 72 | 0x40, 0x5e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 73 | 0x40, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 74 | }, 75 | }, 76 | { 77 | "Clip", 78 | func(ctx *Context) { ctx.Clip() }, 79 | []byte{0x06}, 80 | }, 81 | { 82 | "ClosePath", 83 | func(ctx *Context) { ctx.ClosePath() }, 84 | []byte{0x07}, 85 | }, 86 | { 87 | "CreateImageData", 88 | func(ctx *Context) { 89 | img := image.NewRGBA(image.Rect(0, 0, 4, 3)) 90 | for i := range img.Pix { 91 | img.Pix[i] = byte(i) 92 | } 93 | ctx.CreateImageData(img) 94 | }, 95 | []byte{ 96 | 0x08, 97 | 0x00, 0x00, 0x00, 0x00, // ID 98 | 0x00, 0x00, 0x00, 0x04, // Width 99 | 0x00, 0x00, 0x00, 0x03, // Height 100 | // Pixels 101 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 102 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 103 | 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 104 | }, 105 | }, 106 | { 107 | "CreateImageData: ID generation", 108 | func(ctx *Context) { 109 | ctx.CreateImageData(image.NewRGBA(image.Rect(0, 0, 0, 0))) 110 | ctx.CreateImageData(image.NewRGBA(image.Rect(0, 0, 0, 0))) 111 | ctx.CreateImageData(image.NewRGBA(image.Rect(0, 0, 0, 0))) 112 | }, 113 | []byte{ 114 | 0x08, // CreateImageData 115 | 0x00, 0x00, 0x00, 0x00, // ID = 0 116 | 0x00, 0x00, 0x00, 0x00, // Width 117 | 0x00, 0x00, 0x00, 0x00, // Height 118 | 0x08, // CreateImageData 119 | 0x00, 0x00, 0x00, 0x01, // ID = 1 120 | 0x00, 0x00, 0x00, 0x00, // Width 121 | 0x00, 0x00, 0x00, 0x00, // Height 122 | 0x08, // CreateImageData 123 | 0x00, 0x00, 0x00, 0x02, // ID = 2 124 | 0x00, 0x00, 0x00, 0x00, // Width 125 | 0x00, 0x00, 0x00, 0x00, // Height 126 | }, 127 | }, 128 | { 129 | "Ellipse", 130 | func(ctx *Context) { ctx.Ellipse(100, 80, 50, 75, math.Pi/4, 0, 2*math.Pi, true) }, 131 | []byte{ 132 | 0x0e, 133 | 0x40, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 134 | 0x40, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 135 | 0x40, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 136 | 0x40, 0x52, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 137 | 0x3f, 0xe9, 0x21, 0xfb, 0x54, 0x44, 0x2d, 0x18, 138 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 139 | 0x40, 0x19, 0x21, 0xfb, 0x54, 0x44, 0x2d, 0x18, 140 | 0x01, 141 | }, 142 | }, 143 | { 144 | "Fill", 145 | func(ctx *Context) { ctx.Fill() }, 146 | []byte{0x0f}, 147 | }, 148 | { 149 | "FillRect", 150 | func(ctx *Context) { ctx.FillRect(250, 120, 70, 65) }, 151 | []byte{ 152 | 0x10, 153 | 0x40, 0x6f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 154 | 0x40, 0x5e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 155 | 0x40, 0x51, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 156 | 0x40, 0x50, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 157 | }, 158 | }, 159 | { 160 | "SetFillStyle", 161 | func(ctx *Context) { ctx.SetFillStyle(color.RGBA{R: 10, G: 20, B: 30, A: 255}) }, 162 | []byte{ 163 | 0x11, 164 | 0x0a, 0x14, 0x1e, 0xff, 165 | }, 166 | }, 167 | { 168 | "SetFillStyleString", 169 | func(ctx *Context) { ctx.SetFillStyleString("#0A141E") }, 170 | []byte{ 171 | 0x3b, 172 | 0x00, 0x00, 0x00, 0x07, // len(color) 173 | 0x23, 0x30, 0x41, 0x31, 0x34, 0x31, 0x45, // color 174 | }, 175 | }, 176 | { 177 | "SetFillStyleGradient", 178 | func(ctx *Context) { ctx.SetFillStyleGradient(&Gradient{id: 5}) }, 179 | []byte{ 180 | 0x16, 181 | 0x00, 0x00, 0x00, 0x05, 182 | }, 183 | }, 184 | { 185 | "SetFillStylePattern", 186 | func(ctx *Context) { ctx.SetFillStylePattern(&Pattern{id: 16}) }, 187 | []byte{ 188 | 0x42, 189 | 0x00, 0x00, 0x00, 0x10, 190 | }, 191 | }, 192 | { 193 | "FillText", 194 | func(ctx *Context) { ctx.FillText("test äöü", 22, 38) }, 195 | []byte{ 196 | 0x12, 197 | 0x40, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x 198 | 0x40, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y 199 | 0x00, 0x00, 0x00, 0x0b, // len(text) 200 | 0x74, 0x65, 0x73, 0x74, 0x20, 0xc3, 0xa4, 0xc3, 0xb6, 0xc3, 0xbc, // text 201 | }, 202 | }, 203 | { 204 | "FillTextMaxWidth", 205 | func(ctx *Context) { ctx.FillTextMaxWidth("Hello, 世界", 45, 52, 100) }, 206 | []byte{ 207 | 0x39, 208 | 0x40, 0x46, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, // x 209 | 0x40, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y 210 | 0x40, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // maxWidth 211 | 0x00, 0x00, 0x00, 0x0d, // len(text) 212 | 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, // text "Hello, " 213 | 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c, // text "世界" 214 | }, 215 | }, 216 | { 217 | "SetFont", 218 | func(ctx *Context) { ctx.SetFont("Helvetica") }, 219 | []byte{ 220 | 0x13, 221 | 0x00, 0x00, 0x00, 0x09, // len(font) 222 | 0x48, 0x65, 0x6c, 0x76, 0x65, 0x74, 0x69, 0x63, 0x61, // font 223 | }, 224 | }, 225 | { 226 | "SetGlobalAlpha", 227 | func(ctx *Context) { ctx.SetGlobalAlpha(0.5) }, 228 | []byte{ 229 | 0x17, 230 | 0x3f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 231 | }, 232 | }, 233 | { 234 | "SetGlobalCompositeOperation", 235 | func(ctx *Context) { 236 | ctx.SetGlobalCompositeOperation(OpSourceOver) 237 | ctx.SetGlobalCompositeOperation(OpSourceIn) 238 | ctx.SetGlobalCompositeOperation(OpSourceOut) 239 | ctx.SetGlobalCompositeOperation(OpSourceAtop) 240 | ctx.SetGlobalCompositeOperation(OpDestinationOver) 241 | ctx.SetGlobalCompositeOperation(OpDestinationIn) 242 | ctx.SetGlobalCompositeOperation(OpDestinationOut) 243 | ctx.SetGlobalCompositeOperation(OpDestinationAtop) 244 | ctx.SetGlobalCompositeOperation(OpLighter) 245 | ctx.SetGlobalCompositeOperation(OpCopy) 246 | ctx.SetGlobalCompositeOperation(OpXOR) 247 | ctx.SetGlobalCompositeOperation(OpMultiply) 248 | ctx.SetGlobalCompositeOperation(OpScreen) 249 | ctx.SetGlobalCompositeOperation(OpOverlay) 250 | ctx.SetGlobalCompositeOperation(OpDarken) 251 | ctx.SetGlobalCompositeOperation(OpLighten) 252 | ctx.SetGlobalCompositeOperation(OpColorDodge) 253 | ctx.SetGlobalCompositeOperation(OpColorBurn) 254 | ctx.SetGlobalCompositeOperation(OpHardLight) 255 | ctx.SetGlobalCompositeOperation(OpSoftLight) 256 | ctx.SetGlobalCompositeOperation(OpDifference) 257 | ctx.SetGlobalCompositeOperation(OpExclusion) 258 | ctx.SetGlobalCompositeOperation(OpHue) 259 | ctx.SetGlobalCompositeOperation(OpSaturation) 260 | ctx.SetGlobalCompositeOperation(OpColor) 261 | ctx.SetGlobalCompositeOperation(OpLuminosity) 262 | }, 263 | []byte{ 264 | 0x18, 0x00, 265 | 0x18, 0x01, 266 | 0x18, 0x02, 267 | 0x18, 0x03, 268 | 0x18, 0x04, 269 | 0x18, 0x05, 270 | 0x18, 0x06, 271 | 0x18, 0x07, 272 | 0x18, 0x08, 273 | 0x18, 0x09, 274 | 0x18, 0x0a, 275 | 0x18, 0x0b, 276 | 0x18, 0x0c, 277 | 0x18, 0x0d, 278 | 0x18, 0x0e, 279 | 0x18, 0x0f, 280 | 0x18, 0x10, 281 | 0x18, 0x11, 282 | 0x18, 0x12, 283 | 0x18, 0x13, 284 | 0x18, 0x14, 285 | 0x18, 0x15, 286 | 0x18, 0x16, 287 | 0x18, 0x17, 288 | 0x18, 0x18, 289 | 0x18, 0x19, 290 | }, 291 | }, 292 | { 293 | "SetImageSmoothingEnabled", 294 | func(ctx *Context) { 295 | ctx.SetImageSmoothingEnabled(false) 296 | ctx.SetImageSmoothingEnabled(true) 297 | }, 298 | []byte{ 299 | 0x19, 0x00, 300 | 0x19, 0x01, 301 | }, 302 | }, 303 | { 304 | "SetLineCap", 305 | func(ctx *Context) { 306 | ctx.SetLineCap(CapButt) 307 | ctx.SetLineCap(CapRound) 308 | ctx.SetLineCap(CapSquare) 309 | }, 310 | []byte{ 311 | 0x1c, 0x00, 312 | 0x1c, 0x01, 313 | 0x1c, 0x02, 314 | }, 315 | }, 316 | { 317 | "SetLineDashOffset", 318 | func(ctx *Context) { ctx.SetLineDashOffset(0.3) }, 319 | []byte{ 320 | 0x1d, 321 | 0x3f, 0xd3, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 322 | }, 323 | }, 324 | { 325 | "SetLineJoin", 326 | func(ctx *Context) { 327 | ctx.SetLineJoin(JoinMiter) 328 | ctx.SetLineJoin(JoinRound) 329 | ctx.SetLineJoin(JoinBevel) 330 | }, 331 | []byte{ 332 | 0x1e, 0x00, 333 | 0x1e, 0x01, 334 | 0x1e, 0x02, 335 | }, 336 | }, 337 | { 338 | "LineTo", 339 | func(ctx *Context) { ctx.LineTo(10, 20) }, 340 | []byte{ 341 | 0x1f, 342 | 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 343 | 0x40, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 344 | }, 345 | }, 346 | { 347 | "SetLineWidth", 348 | func(ctx *Context) { ctx.SetLineWidth(2.5) }, 349 | []byte{ 350 | 0x20, 351 | 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 352 | }, 353 | }, 354 | { 355 | "SetMiterLimit", 356 | func(ctx *Context) { ctx.SetMiterLimit(5) }, 357 | []byte{ 358 | 0x22, 359 | 0x40, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 360 | }, 361 | }, 362 | { 363 | "SetShadowBlur", 364 | func(ctx *Context) { ctx.SetShadowBlur(15) }, 365 | []byte{ 366 | 0x2d, 367 | 0x40, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 368 | }, 369 | }, 370 | { 371 | "SetShadowColor", 372 | func(ctx *Context) { ctx.SetShadowColor(color.RGBA{R: 10, G: 20, B: 30, A: 255}) }, 373 | []byte{ 374 | 0x2e, 375 | 0x0a, 0x14, 0x1e, 0xff, 376 | }, 377 | }, 378 | { 379 | "SetShadowColorString", 380 | func(ctx *Context) { ctx.SetShadowColorString("#a0b3ff") }, 381 | []byte{ 382 | 0x3d, 383 | 0x00, 0x00, 0x00, 0x07, // len(color) 384 | 0x23, 0x61, 0x30, 0x62, 0x33, 0x66, 0x66, // color 385 | }, 386 | }, 387 | { 388 | "SetShadowOffsetX", 389 | func(ctx *Context) { ctx.SetShadowOffsetX(32) }, 390 | []byte{ 391 | 0x2f, 392 | 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 393 | }, 394 | }, 395 | { 396 | "SetShadowOffsetY", 397 | func(ctx *Context) { ctx.SetShadowOffsetY(25) }, 398 | []byte{ 399 | 0x30, 400 | 0x40, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 401 | }, 402 | }, 403 | { 404 | "SetStrokeStyle", 405 | func(ctx *Context) { ctx.SetStrokeStyle(color.RGBA{R: 120, G: 80, B: 110, A: 255}) }, 406 | []byte{ 407 | 0x33, 408 | 0x78, 0x50, 0x6e, 0xff, 409 | }, 410 | }, 411 | { 412 | "SetStrokeStyleString", 413 | func(ctx *Context) { ctx.SetStrokeStyleString("LightGreen") }, 414 | []byte{ 415 | 0x3c, 416 | 0x00, 0x00, 0x00, 0x0a, // len(color) 417 | 0x4c, 0x69, 0x67, 0x68, 0x74, 0x47, 0x72, 0x65, 0x65, 0x6e, // color 418 | }, 419 | }, 420 | { 421 | "SetStrokeStyleGradient", 422 | func(ctx *Context) { ctx.SetStrokeStyleGradient(&Gradient{id: 333333}) }, 423 | []byte{ 424 | 0x1a, 425 | 0x00, 0x05, 0x16, 0x15, 426 | }, 427 | }, 428 | { 429 | "SetStrokeStylePattern", 430 | func(ctx *Context) { ctx.SetStrokeStylePattern(&Pattern{id: 54321}) }, 431 | []byte{ 432 | 0x43, 433 | 0x00, 0x00, 0xd4, 0x31, 434 | }, 435 | }, 436 | { 437 | "SetTextAlign", 438 | func(ctx *Context) { 439 | ctx.SetTextAlign(AlignStart) 440 | ctx.SetTextAlign(AlignEnd) 441 | ctx.SetTextAlign(AlignLeft) 442 | ctx.SetTextAlign(AlignRight) 443 | ctx.SetTextAlign(AlignCenter) 444 | }, 445 | []byte{ 446 | 0x35, 0x00, 447 | 0x35, 0x01, 448 | 0x35, 0x02, 449 | 0x35, 0x03, 450 | 0x35, 0x04, 451 | }, 452 | }, 453 | { 454 | "SetTextBaseline", 455 | func(ctx *Context) { 456 | ctx.SetTextBaseline(BaselineAlphabetic) 457 | ctx.SetTextBaseline(BaselineIdeographic) 458 | ctx.SetTextBaseline(BaselineTop) 459 | ctx.SetTextBaseline(BaselineBottom) 460 | ctx.SetTextBaseline(BaselineHanging) 461 | ctx.SetTextBaseline(BaselineMiddle) 462 | }, 463 | []byte{ 464 | 0x36, 0x00, 465 | 0x36, 0x01, 466 | 0x36, 0x02, 467 | 0x36, 0x03, 468 | 0x36, 0x04, 469 | 0x36, 0x05, 470 | }, 471 | }, 472 | { 473 | "MoveTo", 474 | func(ctx *Context) { ctx.MoveTo(300, 200) }, 475 | []byte{ 476 | 0x23, 477 | 0x40, 0x72, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 478 | 0x40, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 479 | }, 480 | }, 481 | { 482 | "QuadraticCurveTo", 483 | func(ctx *Context) { ctx.QuadraticCurveTo(130, 215, 155, 330) }, 484 | []byte{ 485 | 0x25, 486 | 0x40, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 487 | 0x40, 0x6a, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 488 | 0x40, 0x63, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 489 | 0x40, 0x74, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 490 | }, 491 | }, 492 | { 493 | "Rect", 494 | func(ctx *Context) { ctx.Rect(52.5, 82.5, 70.2, 120.8) }, 495 | []byte{ 496 | 0x26, 497 | 0x40, 0x4a, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 498 | 0x40, 0x54, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 499 | 0x40, 0x51, 0x8c, 0xcc, 0xcc, 0xcc, 0xcc, 0xcd, 500 | 0x40, 0x5e, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 501 | }, 502 | }, 503 | { 504 | "Restore", 505 | func(ctx *Context) { ctx.Restore() }, 506 | []byte{0x27}, 507 | }, 508 | { 509 | "Rotate", 510 | func(ctx *Context) { ctx.Rotate(3.1415) }, 511 | []byte{ 512 | 0x28, 513 | 0x40, 0x09, 0x21, 0xca, 0xc0, 0x83, 0x12, 0x6f, 514 | }, 515 | }, 516 | { 517 | "Save", 518 | func(ctx *Context) { ctx.Save() }, 519 | []byte{0x29}, 520 | }, 521 | { 522 | "Scale", 523 | func(ctx *Context) { ctx.Scale(2.5, 3) }, 524 | []byte{ 525 | 0x2a, 526 | 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 527 | 0x40, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 528 | }, 529 | }, 530 | { 531 | "Stroke", 532 | func(ctx *Context) { ctx.Stroke() }, 533 | []byte{0x31}, 534 | }, 535 | { 536 | "StrokeText", 537 | func(ctx *Context) { ctx.StrokeText("Test", 32, 42) }, 538 | []byte{ 539 | 0x34, 540 | 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x 541 | 0x40, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y 542 | 0x00, 0x00, 0x00, 0x04, // len(text) 543 | 0x54, 0x65, 0x73, 0x74, // text 544 | }, 545 | }, 546 | { 547 | "StrokeTextMaxWidth", 548 | func(ctx *Context) { ctx.StrokeTextMaxWidth("Test", 32, 42, 80) }, 549 | []byte{ 550 | 0x3a, 551 | 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x 552 | 0x40, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y 553 | 0x40, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // maxWidth 554 | 0x00, 0x00, 0x00, 0x04, // len(text) 555 | 0x54, 0x65, 0x73, 0x74, // text 556 | }, 557 | }, 558 | { 559 | "StrokeRect", 560 | func(ctx *Context) { ctx.StrokeRect(93, 105, 60, 50) }, 561 | []byte{ 562 | 0x32, 563 | 0x40, 0x57, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 564 | 0x40, 0x5a, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 565 | 0x40, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 566 | 0x40, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 567 | }, 568 | }, 569 | { 570 | "Translate", 571 | func(ctx *Context) { ctx.Translate(14.5, 23.9) }, 572 | []byte{ 573 | 0x38, 574 | 0x40, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 575 | 0x40, 0x37, 0xe6, 0x66, 0x66, 0x66, 0x66, 0x66, 576 | }, 577 | }, 578 | { 579 | "Transform", 580 | func(ctx *Context) { ctx.Transform(0.5, 3.2, 1.7, 5.2, 11, 21) }, 581 | []byte{ 582 | 0x37, 583 | 0x3f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 584 | 0x40, 0x09, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a, 585 | 0x3f, 0xfb, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 586 | 0x40, 0x14, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcd, 587 | 0x40, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 588 | 0x40, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 589 | }, 590 | }, 591 | { 592 | "SetTransform", 593 | func(ctx *Context) { ctx.SetTransform(0.5, 3.2, 1.7, 5.2, 11, 21) }, 594 | []byte{ 595 | 0x2c, 596 | 0x3f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 597 | 0x40, 0x09, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a, 598 | 0x3f, 0xfb, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 599 | 0x40, 0x14, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcd, 600 | 0x40, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 601 | 0x40, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 602 | }, 603 | }, 604 | { 605 | "SetLineDash", 606 | func(ctx *Context) { 607 | ctx.SetLineDash([]float64{ 608 | 0.5, 1.5, 0.25, 2, 609 | }) 610 | }, 611 | []byte{ 612 | 0x2b, 613 | 0x00, 0x00, 0x00, 0x04, // len(segments) 614 | 0x3f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 615 | 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 616 | 0x3f, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 617 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 618 | }, 619 | }, 620 | { 621 | "PutImageData", 622 | func(ctx *Context) { 623 | img := &ImageData{id: 255, ctx: ctx} 624 | ctx.PutImageData(img, 30, 45) 625 | }, 626 | []byte{ 627 | 0x24, // PutImageData 628 | 0x00, 0x00, 0x00, 0xff, // ID 629 | 0x40, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dx 630 | 0x40, 0x46, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, // dy 631 | }, 632 | }, 633 | { 634 | "PutImageDataDirty", 635 | func(ctx *Context) { 636 | img := &ImageData{id: 256, ctx: ctx} 637 | ctx.PutImageDataDirty(img, 320, 200, 110, 85, 50, 40) 638 | }, 639 | []byte{ 640 | 0x3e, // PutImageDataDirty 641 | 0x00, 0x00, 0x01, 0x00, // ID 642 | 0x40, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dx 643 | 0x40, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dy 644 | 0x40, 0x5b, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, // dirtyX 645 | 0x40, 0x55, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // dirtyY 646 | 0x40, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dirtyWidth 647 | 0x40, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dirtyHeight 648 | }, 649 | }, 650 | { 651 | "DrawImage", 652 | func(ctx *Context) { 653 | img := &ImageData{id: 3, ctx: ctx} 654 | ctx.DrawImage(img, 80, 90) 655 | }, 656 | []byte{ 657 | 0x0d, // DrawImage 658 | 0x00, 0x00, 0x00, 0x03, // ID 659 | 0x40, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dx 660 | 0x40, 0x56, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, // dy 661 | }, 662 | }, 663 | { 664 | "DrawImageScaled", 665 | func(ctx *Context) { 666 | img := &ImageData{id: 1000000, ctx: ctx} 667 | ctx.DrawImageScaled(img, 400, 500, 2.5, 3.2) 668 | }, 669 | []byte{ 670 | 0x3f, // DrawImageScaled 671 | 0x00, 0x0f, 0x42, 0x40, // ID 672 | 0x40, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dx 673 | 0x40, 0x7f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // dy 674 | 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dWidth 675 | 0x40, 0x09, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a, // dHeight 676 | }, 677 | }, 678 | { 679 | "DrawImageSubRectangle", 680 | func(ctx *Context) { 681 | img := &ImageData{id: 65535, ctx: ctx} 682 | ctx.DrawImageSubRectangle(img, 42, 32, 30, 24, 10, 15, 40, 34) 683 | }, 684 | []byte{ 685 | 0x40, // DrawImageSubRectangle 686 | 0x00, 0x00, 0xff, 0xff, // ID 687 | 0x40, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sx 688 | 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sy 689 | 0x40, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sWidth 690 | 0x40, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sHeight 691 | 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dx 692 | 0x40, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dy 693 | 0x40, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dWidth 694 | 0x40, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dHeight 695 | }, 696 | }, 697 | { 698 | "ImageData.Release", 699 | func(ctx *Context) { 700 | img := &ImageData{id: 999, ctx: ctx} 701 | img.Release() 702 | }, 703 | []byte{ 704 | 0x41, // ReleaseImage 705 | 0x00, 0x00, 0x03, 0xe7, // ID 706 | }, 707 | }, 708 | { 709 | "ImageData.Release idempotency", 710 | func(ctx *Context) { 711 | img := &ImageData{id: 2, ctx: ctx} 712 | img.Release() 713 | img.Release() 714 | }, 715 | []byte{ 716 | 0x41, // ReleaseImage 717 | 0x00, 0x00, 0x00, 0x02, // ID 718 | }, 719 | }, 720 | { 721 | "CreateLinearGradient", 722 | func(ctx *Context) { 723 | ctx.CreateLinearGradient(70, 80, 100, 120) 724 | }, 725 | []byte{ 726 | 0x09, // CreateLinearGradient 727 | 0x00, 0x00, 0x00, 0x00, // ID 728 | 0x40, 0x51, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, // x0 729 | 0x40, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y0 730 | 0x40, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x1 731 | 0x40, 0x5e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y1 732 | }, 733 | }, 734 | { 735 | "CreateLinearGradient: ID generation", 736 | func(ctx *Context) { 737 | ctx.CreateLinearGradient(0, 0, 0, 0) 738 | ctx.CreateLinearGradient(0, 0, 0, 0) 739 | ctx.CreateLinearGradient(0, 0, 0, 0) 740 | }, 741 | []byte{ 742 | 0x09, // CreateLinearGradient 743 | 0x00, 0x00, 0x00, 0x00, // ID = 0 744 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x0 745 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y0 746 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x1 747 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y1 748 | 0x09, // CreateLinearGradient 749 | 0x00, 0x00, 0x00, 0x01, // ID = 1 750 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x0 751 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y0 752 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x1 753 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y1 754 | 0x09, // CreateLinearGradient 755 | 0x00, 0x00, 0x00, 0x02, // ID = 2 756 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x0 757 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y0 758 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x1 759 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y1 760 | }, 761 | }, 762 | { 763 | "CreateRadialGradient", 764 | func(ctx *Context) { 765 | ctx.CreateRadialGradient(70, 80, 30, 100, 120, 20) 766 | }, 767 | []byte{ 768 | 0x0b, // CreateRadialGradient 769 | 0x00, 0x00, 0x00, 0x00, // ID 770 | 0x40, 0x51, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, // x0 771 | 0x40, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y0 772 | 0x40, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // r0 773 | 0x40, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // x1 774 | 0x40, 0x5e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // y1 775 | 0x40, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // r1 776 | }, 777 | }, 778 | { 779 | "Gradient.AddColorStop", 780 | func(ctx *Context) { 781 | g := &Gradient{id: 100000, ctx: ctx} 782 | g.AddColorStop(4.6, color.RGBA{R: 25, G: 110, B: 48, A: 255}) 783 | }, 784 | []byte{ 785 | 0x14, // GradientAddColorStop 786 | 0x00, 0x01, 0x86, 0xa0, // ID 787 | 0x40, 0x12, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, // offset 788 | 0x19, 0x6e, 0x30, 0xff, // R, G, B, A 789 | }, 790 | }, 791 | { 792 | "Gradient.AddColorStopString", 793 | func(ctx *Context) { 794 | g := &Gradient{id: 12345, ctx: ctx} 795 | g.AddColorStopString(23.4, "yellow") 796 | }, 797 | []byte{ 798 | 0x15, // GradientAddColorStopString 799 | 0x00, 0x00, 0x30, 0x39, // ID 800 | 0x40, 0x37, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 801 | 0x00, 0x00, 0x00, 0x06, // len(color) 802 | 0x79, 0x65, 0x6c, 0x6c, 0x6f, 0x77, // color 803 | }, 804 | }, 805 | { 806 | "Gradient.Release", 807 | func(ctx *Context) { 808 | g := &Gradient{id: 132, ctx: ctx} 809 | g.Release() 810 | }, 811 | []byte{ 812 | 0x21, // ReleaseGradient 813 | 0x00, 0x00, 0x00, 0x84, // ID 814 | }, 815 | }, 816 | { 817 | "Gradient.Release idempotency", 818 | func(ctx *Context) { 819 | g := &Gradient{id: 4, ctx: ctx} 820 | g.Release() 821 | g.Release() 822 | }, 823 | []byte{ 824 | 0x21, // ReleaseGradient 825 | 0x00, 0x00, 0x00, 0x04, // ID 826 | }, 827 | }, 828 | { 829 | "CreatePattern", 830 | func(ctx *Context) { 831 | img := &ImageData{id: 16} 832 | ctx.CreatePattern(img, PatternRepeatX) 833 | ctx.CreatePattern(img, PatternRepeatY) 834 | ctx.CreatePattern(img, PatternNoRepeat) 835 | }, 836 | []byte{ 837 | 0x0a, // CreatePattern 838 | 0x00, 0x00, 0x00, 0x00, // ID 839 | 0x00, 0x00, 0x00, 0x10, // ImageData ID 840 | 0x01, 841 | 0x0a, // CreatePattern 842 | 0x00, 0x00, 0x00, 0x01, // ID 843 | 0x00, 0x00, 0x00, 0x10, // ImageData ID 844 | 0x02, 845 | 0x0a, // CreatePattern 846 | 0x00, 0x00, 0x00, 0x02, // ID 847 | 0x00, 0x00, 0x00, 0x10, // ImageData ID 848 | 0x03, 849 | }, 850 | }, 851 | { 852 | "Pattern.Release", 853 | func(ctx *Context) { 854 | p := &Pattern{id: 1023, ctx: ctx} 855 | p.Release() 856 | }, 857 | []byte{ 858 | 0x1b, // ReleasePattern 859 | 0x00, 0x00, 0x03, 0xff, // ID 860 | }, 861 | }, 862 | { 863 | "Pattern.Release idempotency", 864 | func(ctx *Context) { 865 | p := &Pattern{id: 10, ctx: ctx} 866 | p.Release() 867 | p.Release() 868 | }, 869 | []byte{ 870 | 0x1b, // ReleasePattern 871 | 0x00, 0x00, 0x00, 0x0a, // ID 872 | }, 873 | }, 874 | { 875 | "GetImageData", 876 | func(ctx *Context) { 877 | ctx.GetImageData(10, 10, 320, 200) 878 | }, 879 | []byte{ 880 | 0x44, // GetImageData 881 | 0x00, 0x00, 0x00, 0x00, // ID 882 | 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sx 883 | 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sy 884 | 0x40, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sw 885 | 0x40, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sh 886 | }, 887 | }, 888 | } 889 | for _, tt := range tests { 890 | t.Run(tt.name, func(t *testing.T) { 891 | draws := make(chan []byte) 892 | ctx := newContext(draws, nil, nil) 893 | go func(draw func(*Context)) { 894 | draw(ctx) 895 | ctx.Flush() 896 | }(tt.draw) 897 | got := <-draws 898 | if diff := cmp.Diff(tt.want, got); diff != "" { 899 | t.Errorf("mismatch (-want, +got)\n%s", diff) 900 | } 901 | }) 902 | } 903 | } 904 | 905 | func TestImageDataSize(t *testing.T) { 906 | tests := []struct { 907 | width int 908 | height int 909 | }{ 910 | {12, 32}, 911 | {320, 200}, 912 | {1350, 875}, 913 | } 914 | ctx := newContext(nil, nil, nil) 915 | for _, tt := range tests { 916 | got := ctx.CreateImageData(image.NewRGBA(image.Rect(0, 0, tt.width, tt.height))) 917 | if got.Width() != tt.width || got.Height() != tt.height { 918 | t.Errorf("got: W %d H %d, want: W %d H %d", 919 | got.Width(), got.Height(), 920 | tt.width, tt.height) 921 | } 922 | } 923 | } 924 | 925 | func TestCanvasSize(t *testing.T) { 926 | tests := []struct { 927 | width, height int 928 | }{ 929 | {width: 0, height: 0}, 930 | {width: 12, height: 36}, 931 | {width: 840, height: 900}, 932 | {width: 42314, height: 42355}, 933 | } 934 | for _, tt := range tests { 935 | ctx := newContext(nil, nil, &Options{ 936 | Width: tt.width, 937 | Height: tt.height, 938 | }) 939 | gotWidth := ctx.CanvasWidth() 940 | gotHeight := ctx.CanvasHeight() 941 | if gotWidth != tt.width || gotHeight != tt.height { 942 | t.Errorf("got: W %d H %d, want: W %d H %d", 943 | gotWidth, gotHeight, tt.width, tt.height) 944 | } 945 | } 946 | } 947 | 948 | func TestUseAfterRelease(t *testing.T) { 949 | tests := []struct { 950 | name string 951 | typeName string 952 | draw func(ctx *Context) 953 | }{ 954 | { 955 | name: "ImageData", 956 | typeName: "ImageData", 957 | draw: func(ctx *Context) { 958 | img := ctx.CreateImageData(image.NewRGBA(image.Rect(0, 0, 0, 0))) 959 | img.Release() 960 | ctx.DrawImage(img, 0, 0) 961 | }, 962 | }, 963 | { 964 | name: "Gradient", 965 | typeName: "Gradient", 966 | draw: func(ctx *Context) { 967 | g := ctx.CreateLinearGradient(0, 0, 0, 0) 968 | g.Release() 969 | g.AddColorStop(0, color.Black) 970 | }, 971 | }, 972 | { 973 | name: "Pattern", 974 | typeName: "Pattern", 975 | draw: func(ctx *Context) { 976 | img := ctx.CreateImageData(image.NewRGBA(image.Rect(0, 0, 0, 0))) 977 | p := ctx.CreatePattern(img, PatternRepeat) 978 | p.Release() 979 | ctx.SetFillStylePattern(p) 980 | }, 981 | }, 982 | } 983 | 984 | for _, tt := range tests { 985 | t.Run(tt.name, func(t *testing.T) { 986 | defer func() { 987 | r := recover() 988 | if r == nil { 989 | t.Errorf("expected panic, but did not panic") 990 | return 991 | } 992 | want := tt.typeName + ": use after release" 993 | if r != want { 994 | t.Errorf("expected panic message %q, but was: %q", want, r) 995 | } 996 | }() 997 | ctx := newContext(nil, nil, nil) 998 | tt.draw(ctx) 999 | }) 1000 | } 1001 | } 1002 | 1003 | func TestEvents(t *testing.T) { 1004 | tests := []struct { 1005 | name string 1006 | want []Event 1007 | }{ 1008 | { 1009 | "multiple events", []Event{ 1010 | MouseMoveEvent{}, 1011 | KeyDownEvent{}, 1012 | KeyUpEvent{}, 1013 | }, 1014 | }, 1015 | } 1016 | for _, tt := range tests { 1017 | t.Run(tt.name, func(t *testing.T) { 1018 | eventsIn := make(chan Event) 1019 | ctx := newContext(nil, eventsIn, nil) 1020 | go func() { 1021 | for _, ev := range tt.want { 1022 | eventsIn <- ev 1023 | } 1024 | close(eventsIn) 1025 | }() 1026 | var got []Event 1027 | for event := range ctx.Events() { 1028 | got = append(got, event) 1029 | } 1030 | if diff := cmp.Diff(tt.want, got); diff != "" { 1031 | t.Errorf("mismatch (-want, +got)\n%s", diff) 1032 | } 1033 | }) 1034 | } 1035 | } 1036 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // The API doc comments are based on the MDN Web Docs for the [Canvas API] 6 | // by Mozilla Contributors and are licensed under [CC-BY-SA 2.5]. 7 | // 8 | // [Canvas API]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D 9 | // [CC-BY-SA 2.5]: https://creativecommons.org/licenses/by-sa/2.5/ 10 | 11 | package canvas 12 | 13 | import ( 14 | "image" 15 | "image/color" 16 | ) 17 | 18 | // Context is the server-side drawing context for a client-side canvas. It 19 | // buffers all drawing operations until the Flush method is called. The Flush 20 | // method then sends the buffered operations to the client. 21 | // 22 | // The Context for a client-server connection is obtained from the parameter 23 | // of the run function that was passed to ListenAndServe, ListenAndServeTLS, 24 | // or NewServeMux. 25 | type Context struct { 26 | opts *Options 27 | draws chan<- []byte 28 | events <-chan Event 29 | buf buffer 30 | 31 | imageDataIDs idGenerator 32 | gradientIDs idGenerator 33 | patternIDs idGenerator 34 | } 35 | 36 | func newContext(draws chan<- []byte, events <-chan Event, opts *Options) *Context { 37 | return &Context{ 38 | opts: opts, 39 | draws: draws, 40 | events: events, 41 | } 42 | } 43 | 44 | // Events returns a channel of events sent by the client. 45 | // 46 | // A type switch on the received Event values can differentiate between the 47 | // concrete event types such as MouseDownEvent or KeyUpEvent. 48 | func (ctx *Context) Events() <-chan Event { 49 | return ctx.events 50 | } 51 | 52 | // CanvasWidth returns the width of the canvas in pixels. 53 | func (ctx *Context) CanvasWidth() int { 54 | return ctx.opts.Width 55 | } 56 | 57 | // CanvasHeight returns the height of the canvas in pixels. 58 | func (ctx *Context) CanvasHeight() int { 59 | return ctx.opts.Height 60 | } 61 | 62 | // SetFillStyle sets the color to use inside shapes. 63 | // The default color is black. 64 | func (ctx *Context) SetFillStyle(c color.Color) { 65 | ctx.buf.addByte(bFillStyle) 66 | ctx.buf.addColor(c) 67 | } 68 | 69 | // SetFillStyleString sets the color to use inside shapes. 70 | // The color is parsed as a CSS color value like "#a100cb", "#ccc", 71 | // "darkgreen", "rgba(0.5, 0.2, 0.7, 1.0)", etc. 72 | // The default color is "#000" (black). 73 | func (ctx *Context) SetFillStyleString(color string) { 74 | ctx.buf.addByte(bFillStyleString) 75 | ctx.buf.addString(color) 76 | } 77 | 78 | // SetFillStyleGradient sets the gradient (a linear or radial gradient) to 79 | // use inside shapes. 80 | func (ctx *Context) SetFillStyleGradient(g *Gradient) { 81 | g.checkUseAfterRelease() 82 | ctx.buf.addByte(bFillStyleGradient) 83 | ctx.buf.addUint32(g.id) 84 | } 85 | 86 | // SetFillStylePattern sets the pattern (a repeating image) to use inside 87 | // shapes. 88 | func (ctx *Context) SetFillStylePattern(p *Pattern) { 89 | p.checkUseAfterRelease() 90 | ctx.buf.addByte(bFillStylePattern) 91 | ctx.buf.addUint32(p.id) 92 | } 93 | 94 | // SetFont sets the current text style to use when drawing text. This string 95 | // uses the same syntax as the CSS font specifier. The default font is 96 | // "10px sans-serif". 97 | func (ctx *Context) SetFont(font string) { 98 | ctx.buf.addByte(bFont) 99 | ctx.buf.addString(font) 100 | } 101 | 102 | // SetGlobalAlpha sets the alpha (transparency) value that is applied to 103 | // shapes and images before they are drawn onto the canvas. 104 | // The alpha value is a number between 0.0 (fully transparent) and 1.0 105 | // (fully opaque), inclusive. The default value is 1.0. 106 | // Values outside that range, including ±Inf and NaN, will not be set. 107 | func (ctx *Context) SetGlobalAlpha(alpha float64) { 108 | ctx.buf.addByte(bGlobalAlpha) 109 | ctx.buf.addFloat64(alpha) 110 | } 111 | 112 | // SetGlobalCompositeOperation sets the type of compositing operation to 113 | // apply when drawing new shapes. 114 | // 115 | // The default mode is OpSourceOver. 116 | func (ctx *Context) SetGlobalCompositeOperation(mode CompositeOperation) { 117 | ctx.buf.addByte(bGlobalCompositeOperation) 118 | ctx.buf.addByte(byte(mode)) 119 | } 120 | 121 | // SetImageSmoothingEnabled determines whether scaled images are smoothed 122 | // (true, default) or not (false). 123 | // 124 | // This property is useful for games and other apps that use pixel art. When 125 | // enlarging images, the default resizing algorithm will blur the pixels. Set 126 | // this property to false to retain the pixels' sharpness. 127 | func (ctx *Context) SetImageSmoothingEnabled(enabled bool) { 128 | ctx.buf.addByte(bImageSmoothingEnabled) 129 | ctx.buf.addBool(enabled) 130 | } 131 | 132 | // SetLineCap sets the shape used to draw the end points of lines. 133 | // The default value is CapButt. 134 | // 135 | // Note: Lines can be drawn with the Stroke, StrokeRect, and StrokeText 136 | // methods. 137 | func (ctx *Context) SetLineCap(cap LineCap) { 138 | ctx.buf.addByte(bLineCap) 139 | ctx.buf.addByte(byte(cap)) 140 | } 141 | 142 | // SetLineDashOffset sets the line dash offset, or "phase." 143 | // The default value is 0.0. 144 | // 145 | // Note: Lines are drawn by calling the Stroke method. 146 | func (ctx *Context) SetLineDashOffset(offset float64) { 147 | ctx.buf.addByte(bLineDashOffset) 148 | ctx.buf.addFloat64(offset) 149 | } 150 | 151 | // SetLineJoin sets the shape used to join two line segments where they meet. 152 | // The default is JoinMiter. 153 | // 154 | // This property has no effect wherever two connected segments have the same 155 | // direction, because no joining area will be added in this case. Degenerate 156 | // segments with a length of zero (i.e., with all endpoints and control points 157 | // at the exact same position) are also ignored. 158 | // 159 | // Note: Lines can be drawn with the Stroke, StrokeRect, and StrokeText 160 | // methods. 161 | func (ctx *Context) SetLineJoin(join LineJoin) { 162 | ctx.buf.addByte(bLineJoin) 163 | ctx.buf.addByte(byte(join)) 164 | } 165 | 166 | // SetLineWidth sets the thickness of lines. 167 | // The width is a number in coordinate space units. 168 | // Zero, negative, ±Inf, and NaN values are ignored. 169 | // This value is 1.0 by default. 170 | // 171 | // Note: Lines can be drawn with the Stroke, StrokeRect, and StrokeText 172 | // methods. 173 | func (ctx *Context) SetLineWidth(width float64) { 174 | ctx.buf.addByte(bLineWidth) 175 | ctx.buf.addFloat64(width) 176 | } 177 | 178 | // SetMiterLimit sets the miter limit ratio. 179 | // The miter limit ratio is a number in coordinate space units. 180 | // Zero, negative, ±Inf, and NaN values are ignored. 181 | // The default value is 10.0. 182 | func (ctx *Context) SetMiterLimit(value float64) { 183 | ctx.buf.addByte(bMiterLimit) 184 | ctx.buf.addFloat64(value) 185 | } 186 | 187 | // SetShadowBlur sets the amount of blur applied to shadows. 188 | // The default is 0 (no blur). 189 | // 190 | // The blur level is a non-negative float specifying the level of shadow blur, 191 | // where 0 represents no blur and larger numbers represent increasingly more 192 | // blur. This value doesn't correspond to a number of pixels, and is not 193 | // affected by the current transformation matrix. 194 | // Negative, ±Inf, and NaN values are ignored. 195 | // 196 | // Note: Shadows are only drawn if the SetShadowColor / SetShadowColorString 197 | // property is set to a non-transparent value. One of the SetShadowBlur, 198 | // SetShadowOffsetX, or SetShadowOffsetY properties must be non-zero, as well. 199 | func (ctx *Context) SetShadowBlur(level float64) { 200 | ctx.buf.addByte(bShadowBlur) 201 | ctx.buf.addFloat64(level) 202 | } 203 | 204 | // SetShadowColor sets the color of shadows. 205 | // The default value is fully-transparent black. 206 | // 207 | // Be aware that the shadow's rendered opacity will be affected by the opacity 208 | // of the SetFillStyle color when filling, and of the SetStrokeStyle color 209 | // when stroking. 210 | // 211 | // Note: Shadows are only drawn if the SetShadowColor / SetShadowColorString 212 | // property is set to a non-transparent value. One of the SetShadowBlur, 213 | // SetShadowOffsetX, or SetShadowOffsetY properties must be non-zero, as well. 214 | func (ctx *Context) SetShadowColor(c color.Color) { 215 | ctx.buf.addByte(bShadowColor) 216 | ctx.buf.addColor(c) 217 | } 218 | 219 | // SetShadowColorString sets the color of shadows. 220 | // The default value is fully-transparent black. 221 | // 222 | // The color is parsed as a CSS color value like "#a100cb", "#ccc", 223 | // "darkgreen", "rgba(0.5, 0.2, 0.7, 1.0)", etc. 224 | // 225 | // Be aware that the shadow's rendered opacity will be affected by the opacity 226 | // of the SetFillStyle color when filling, and of the SetStrokeStyle color 227 | // when stroking. 228 | // 229 | // Note: Shadows are only drawn if the SetShadowColor / SetShadowColorString 230 | // property is set to a non-transparent value. One of the SetShadowBlur, 231 | // SetShadowOffsetX, or SetShadowOffsetY properties must be non-zero, as well. 232 | func (ctx *Context) SetShadowColorString(color string) { 233 | ctx.buf.addByte(bShadowColorString) 234 | ctx.buf.addString(color) 235 | } 236 | 237 | // SetShadowOffsetX sets the distance that shadows will be offset horizontally. 238 | // 239 | // The offset is a float specifying the distance that shadows will be offset 240 | // horizontally. Positive values are to the right, and negative to the left. 241 | // The default value is 0 (no horizontal offset). ±Inf and NaN values are 242 | // ignored. 243 | // 244 | // Note: Shadows are only drawn if the SetShadowColor / SetShadowColorString 245 | // property is set to a non-transparent value. One of the SetShadowBlur, 246 | // SetShadowOffsetX, or SetShadowOffsetY properties must be non-zero, as well. 247 | func (ctx *Context) SetShadowOffsetX(offset float64) { 248 | ctx.buf.addByte(bShadowOffsetX) 249 | ctx.buf.addFloat64(offset) 250 | } 251 | 252 | // SetShadowOffsetY sets the distance that shadows will be offset vertically. 253 | // 254 | // The offset is a float specifying the distance that shadows will be offset 255 | // vertically. Positive values are down, and negative are up. 256 | // The default value is 0 (no vertical offset). ±Inf and NaN values are 257 | // ignored. 258 | // 259 | // Note: Shadows are only drawn if the SetShadowColor / SetShadowColorString 260 | // property is set to a non-transparent value. One of the SetShadowBlur, 261 | // SetShadowOffsetX, or SetShadowOffsetY properties must be non-zero, as well. 262 | func (ctx *Context) SetShadowOffsetY(offset float64) { 263 | ctx.buf.addByte(bShadowOffsetY) 264 | ctx.buf.addFloat64(offset) 265 | } 266 | 267 | // SetStrokeStyle sets the color to use for the strokes (outlines) around 268 | // shapes. The default color is black. 269 | // 270 | // The color is parsed as a CSS color value like "#a100cb", "#ccc", 271 | // "darkgreen", "rgba(0.5, 0.2, 0.7, 1.0)", etc. 272 | func (ctx *Context) SetStrokeStyle(c color.Color) { 273 | ctx.buf.addByte(bStrokeStyle) 274 | ctx.buf.addColor(c) 275 | } 276 | 277 | // SetStrokeStyleString sets the color to use for the strokes (outlines) around 278 | // shapes. The default color is black. 279 | func (ctx *Context) SetStrokeStyleString(color string) { 280 | ctx.buf.addByte(bStrokeStyleString) 281 | ctx.buf.addString(color) 282 | } 283 | 284 | // SetStrokeStyleGradient sets the gradient (a linear or radial gradient) to 285 | // use for the strokes (outlines) around shapes. 286 | func (ctx *Context) SetStrokeStyleGradient(g *Gradient) { 287 | g.checkUseAfterRelease() 288 | ctx.buf.addByte(bStrokeStyleGradient) 289 | ctx.buf.addUint32(g.id) 290 | } 291 | 292 | // SetStrokeStylePattern sets the pattern (a repeating image) to use for the 293 | // strokes (outlines) around shapes. 294 | func (ctx *Context) SetStrokeStylePattern(p *Pattern) { 295 | p.checkUseAfterRelease() 296 | ctx.buf.addByte(bStrokeStylePattern) 297 | ctx.buf.addUint32(p.id) 298 | } 299 | 300 | // SetTextAlign sets the current text alignment used when drawing text. 301 | // 302 | // The alignment is relative to the x value of the FillText method. For 303 | // example, if the text alignment is set to AlignCenter, then the text's left 304 | // edge will be at x - (textWidth / 2). 305 | // 306 | // The default value is AlignStart. 307 | func (ctx *Context) SetTextAlign(align TextAlign) { 308 | ctx.buf.addByte(bTextAlign) 309 | ctx.buf.addByte(byte(align)) 310 | } 311 | 312 | // SetTextBaseline sets the current text baseline used when drawing text. 313 | // 314 | // The default value is BaselineAlphabetic. 315 | func (ctx *Context) SetTextBaseline(baseline TextBaseline) { 316 | ctx.buf.addByte(bTextBaseline) 317 | ctx.buf.addByte(byte(baseline)) 318 | } 319 | 320 | // Arc adds a circular arc to the current sub-path. 321 | // 322 | // It creates a circular arc centered at (x, y) with a radius of radius, 323 | // which must be positive. The path starts at startAngle, ends at endAngle, 324 | // and travels in the direction given by anticlockwise. The angles are 325 | // in radians, measured from the positive x-axis. 326 | func (ctx *Context) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) { 327 | ctx.buf.addByte(bArc) 328 | ctx.buf.addFloat64(x) 329 | ctx.buf.addFloat64(y) 330 | ctx.buf.addFloat64(radius) 331 | ctx.buf.addFloat64(startAngle) 332 | ctx.buf.addFloat64(endAngle) 333 | ctx.buf.addBool(anticlockwise) 334 | } 335 | 336 | // ArcTo adds a circular arc to the current sub-path, using the given control 337 | // points and radius. The arc is automatically connected to the path's latest 338 | // point with a straight line, if necessary for the specified parameters. 339 | // 340 | // This method is commonly used for making rounded corners. 341 | // 342 | // (x1, y1) are the coordinates of the first control point, 343 | // (x2, y2) are the coordinates of the second control point. 344 | // The radius must be non-negative. 345 | // 346 | // Note: Be aware that you may get unexpected results when using a relatively 347 | // large radius: the arc's connecting line will go in whatever direction it 348 | // must to meet the specified radius. 349 | func (ctx *Context) ArcTo(x1, y1, x2, y2, radius float64) { 350 | ctx.buf.addByte(bArcTo) 351 | ctx.buf.addFloat64(x1) 352 | ctx.buf.addFloat64(y1) 353 | ctx.buf.addFloat64(x2) 354 | ctx.buf.addFloat64(y2) 355 | ctx.buf.addFloat64(radius) 356 | } 357 | 358 | // BeginPath starts a new path by emptying the list of sub-paths. Call this 359 | // method when you want to create a new path. 360 | // 361 | // Note: To create a new sub-path, i.e., one matching the current canvas state, 362 | // you can use MoveTo. 363 | func (ctx *Context) BeginPath() { 364 | ctx.buf.addByte(bBeginPath) 365 | } 366 | 367 | // BezierCurveTo adds a cubic Bézier curve to the current sub-path. It requires 368 | // three points: the first two are control points and the third one is the end 369 | // point. The starting point is the latest point in the current path, which can 370 | // be changed using MoveTo before creating the Bézier curve. 371 | // 372 | // (cp1x, cp1y) are the coordinates of the first control point, (cp2x, cp2y) 373 | // the coordinates of the second control point, and (x, y) the coordinates 374 | // of the end point. 375 | func (ctx *Context) BezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y float64) { 376 | ctx.buf.addByte(bBezierCurveTo) 377 | ctx.buf.addFloat64(cp1x) 378 | ctx.buf.addFloat64(cp1y) 379 | ctx.buf.addFloat64(cp2x) 380 | ctx.buf.addFloat64(cp2y) 381 | ctx.buf.addFloat64(x) 382 | ctx.buf.addFloat64(y) 383 | } 384 | 385 | // ClearRect erases the pixels in a rectangular area by setting them to 386 | // transparent black. 387 | // 388 | // It sets the pixels in a rectangular area to transparent black 389 | // (rgba(0,0,0,0)). The rectangle's corner is at (x, y), and its size is 390 | // specified by width and height. 391 | // 392 | // Note: Be aware that ClearRect may cause unintended side effects if you're 393 | // not using paths properly. Make sure to call BeginPath before starting to 394 | // draw new items after calling ClearRect. 395 | func (ctx *Context) ClearRect(x, y, width, height float64) { 396 | ctx.buf.addByte(bClearRect) 397 | ctx.buf.addFloat64(x) 398 | ctx.buf.addFloat64(y) 399 | ctx.buf.addFloat64(width) 400 | ctx.buf.addFloat64(height) 401 | } 402 | 403 | // Clip turns the current or given path into the current clipping region. 404 | // The previous clipping region, if any, is intersected with the current or 405 | // given path to create the new clipping region. 406 | // 407 | // Note: Be aware that the clipping region is only constructed from shapes 408 | // added to the path. It doesn't work with shape primitives drawn directly to 409 | // the canvas, such as FillRect. Instead, you'd have to use Rect to add a 410 | // rectangular shape to the path before calling Clip. 411 | func (ctx *Context) Clip() { 412 | ctx.buf.addByte(bClip) 413 | } 414 | 415 | // ClosePath attempts to add a straight line from the current point to the 416 | // start of the current sub-path. If the shape has already been closed or has 417 | // only one point, this function does nothing. 418 | // 419 | // This method doesn't draw anything to the canvas directly. You can render the 420 | // path using the Stroke or Fill methods. 421 | func (ctx *Context) ClosePath() { 422 | ctx.buf.addByte(bClosePath) 423 | } 424 | 425 | // Ellipse adds an elliptical arc to the current sub-path. 426 | // 427 | // It creates an elliptical arc centered at (x, y) with the radii radiusX and 428 | // radiusY. The path starts at startAngle and ends at endAngle, and travels in 429 | // the direction given by anticlockwise. 430 | // 431 | // The radii must be non-negative. The rotation and the angles are expressed 432 | // in radians. The angles are measured clockwise from the positive x-axis. 433 | func (ctx *Context) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, anticlockwise bool) { 434 | ctx.buf.addByte(bEllipse) 435 | ctx.buf.addFloat64(x) 436 | ctx.buf.addFloat64(y) 437 | ctx.buf.addFloat64(radiusX) 438 | ctx.buf.addFloat64(radiusY) 439 | ctx.buf.addFloat64(rotation) 440 | ctx.buf.addFloat64(startAngle) 441 | ctx.buf.addFloat64(endAngle) 442 | ctx.buf.addBool(anticlockwise) 443 | } 444 | 445 | // Fill fills the current path with the current fill style (see SetFillStyle, 446 | // SetFillStyleString, SetFillStyleGradient, SetFillStylePattern). 447 | func (ctx *Context) Fill() { 448 | ctx.buf.addByte(bFill) 449 | } 450 | 451 | // FillRect draws a rectangle that is filled according to the current fill 452 | // style. 453 | // 454 | // It draws a filled rectangle whose starting point is at (x, y) and whose 455 | // size is specified by width and height. The fill style is determined by the 456 | // current fill style (see SetFillStyle, SetFillStyleString, 457 | // SetFillStyleGradient, SetFillStylePattern). 458 | // 459 | // This method draws directly to the canvas without modifying the current path, 460 | // so any subsequent Fill or Stroke calls will have no effect on it. 461 | func (ctx *Context) FillRect(x, y, width, height float64) { 462 | ctx.buf.addByte(bFillRect) 463 | ctx.buf.addFloat64(x) 464 | ctx.buf.addFloat64(y) 465 | ctx.buf.addFloat64(width) 466 | ctx.buf.addFloat64(height) 467 | } 468 | 469 | // FillText draws a text string at the specified coordinates, filling the 470 | // string's characters with the current fill style (see SetFillStyle, 471 | // SetFillStyleString, SetFillStyleGradient, SetFillStylePattern). 472 | // 473 | // This method draws directly to the canvas without modifying the current path, 474 | // so any subsequent Fill or Stroke calls will have no effect on it. 475 | // 476 | // The text is rendered using the font and text layout configuration as defined 477 | // by the SetFont, SetTextAlign, and SetTextBaseline properties. 478 | func (ctx *Context) FillText(text string, x, y float64) { 479 | ctx.buf.addByte(bFillText) 480 | ctx.buf.addFloat64(x) 481 | ctx.buf.addFloat64(y) 482 | ctx.buf.addString(text) 483 | } 484 | 485 | // FillTextMaxWidth draws a text string at the specified coordinates, filling 486 | // the string's characters with the current fill style (see SetFillStyle, 487 | // SetFillStyleString, SetFillStyleGradient, SetFillStylePattern). 488 | // 489 | // The maxWidth parameter specifies the maximum number of pixels wide the text 490 | // may be once rendered. The user agent will adjust the kerning, select a more 491 | // horizontally condensed font (if one is available or can be generated without 492 | // loss of quality), or scale down to a smaller font size in order to fit the 493 | // text in the specified width. 494 | // 495 | // This method draws directly to the canvas without modifying the current path, 496 | // so any subsequent Fill or Stroke calls will have no effect on it. 497 | // 498 | // The text is rendered using the font and text layout configuration as defined 499 | // by the SetFont, SetTextAlign, and SetTextBaseline properties. 500 | func (ctx *Context) FillTextMaxWidth(text string, x, y, maxWidth float64) { 501 | ctx.buf.addByte(bFillTextMaxWidth) 502 | ctx.buf.addFloat64(x) 503 | ctx.buf.addFloat64(y) 504 | ctx.buf.addFloat64(maxWidth) 505 | ctx.buf.addString(text) 506 | } 507 | 508 | // LineTo adds a straight line to the current sub-path by connecting the 509 | // sub-path's last point to the specified (x, y) coordinates. 510 | // 511 | // Like other methods that modify the current path, this method does not 512 | // directly render anything. To draw the path onto a canvas, you can use the 513 | // Fill or Stroke methods. 514 | func (ctx *Context) LineTo(x, y float64) { 515 | ctx.buf.addByte(bLineTo) 516 | ctx.buf.addFloat64(x) 517 | ctx.buf.addFloat64(y) 518 | } 519 | 520 | // MoveTo begins a new sub-path at the point specified by the given (x, y) 521 | // coordinates. 522 | func (ctx *Context) MoveTo(x, y float64) { 523 | ctx.buf.addByte(bMoveTo) 524 | ctx.buf.addFloat64(x) 525 | ctx.buf.addFloat64(y) 526 | } 527 | 528 | // QuadraticCurveTo adds a quadratic Bézier curve to the current sub-path. 529 | // It requires two points: the first one is a control point and the second one 530 | // is the end point. The starting point is the latest point in the current 531 | // path, which can be changed using MoveTo before creating the quadratic 532 | // Bézier curve. 533 | // 534 | // (cpx, cpy) is the coordinate of the control point, and (x, y) is the 535 | // coordinate of the end point. 536 | func (ctx *Context) QuadraticCurveTo(cpx, cpy, x, y float64) { 537 | ctx.buf.addByte(bQuadraticCurveTo) 538 | ctx.buf.addFloat64(cpx) 539 | ctx.buf.addFloat64(cpy) 540 | ctx.buf.addFloat64(x) 541 | ctx.buf.addFloat64(y) 542 | } 543 | 544 | // Rect adds a rectangle to the current path. 545 | // 546 | // It creates a rectangular path whose starting point is at (x, y) and whose 547 | // size is specified by width and height. 548 | // Like other methods that modify the current path, this method does not 549 | // directly render anything. To draw the rectangle onto a canvas, you can use 550 | // the Fill or Stroke methods. 551 | // 552 | // Note: To both create and render a rectangle in one step, use the FillRect 553 | // or StrokeRect methods. 554 | func (ctx *Context) Rect(x, y, width, height float64) { 555 | ctx.buf.addByte(bRect) 556 | ctx.buf.addFloat64(x) 557 | ctx.buf.addFloat64(y) 558 | ctx.buf.addFloat64(width) 559 | ctx.buf.addFloat64(height) 560 | } 561 | 562 | // Restore restores the most recently saved canvas state by popping the top 563 | // entry in the drawing state stack. If there is no saved state, this method 564 | // does nothing. 565 | // 566 | // For more information about the drawing state, see Save. 567 | func (ctx *Context) Restore() { 568 | ctx.buf.addByte(bRestore) 569 | } 570 | 571 | // Rotate adds a rotation to the transformation matrix. 572 | // 573 | // The rotation angle is expressed in radians (clockwise). You can use 574 | // degree * math.Pi / 180 to calculate a radian from a degree. 575 | // 576 | // The rotation center point is always the canvas origin. To change the center 577 | // point, you will need to move the canvas by using the Translate method. 578 | func (ctx *Context) Rotate(angle float64) { 579 | ctx.buf.addByte(bRotate) 580 | ctx.buf.addFloat64(angle) 581 | } 582 | 583 | // Save saves the entire state of the canvas by pushing the current state onto 584 | // a stack. 585 | // 586 | // The drawing state that gets saved onto a stack consists of: 587 | // - The current transformation matrix. 588 | // - The current clipping region. 589 | // - The current dash list. 590 | // - The current values of the attributes set via the following methods: 591 | // SetStrokeStyle*, SetFillStyle*, SetGlobalAlpha, SetLineWidth, 592 | // SetLineCap, SetLineJoin, SetMiterLimit, SetLineDashOffset, 593 | // SetShadowOffsetX, SetShadowOffsetY, SetShadowBlur, SetShadowColor*, 594 | // SetGlobalCompositeOperation, SetFont, SetTextAlign, SetTextBaseline, 595 | // SetImageSmoothingEnabled. 596 | func (ctx *Context) Save() { 597 | ctx.buf.addByte(bSave) 598 | } 599 | 600 | // Scale adds a scaling transformation to the canvas units horizontally 601 | // and/or vertically. 602 | // 603 | // By default, one unit on the canvas is exactly one pixel. A scaling 604 | // transformation modifies this behavior. For instance, a scaling factor of 0.5 605 | // results in a unit size of 0.5 pixels; shapes are thus drawn at half the 606 | // normal size. Similarly, a scaling factor of 2.0 increases the unit size so 607 | // that one unit becomes two pixels; shapes are thus drawn at twice the normal 608 | // size. 609 | // 610 | // x is the scaling factor in the horizontal direction. A negative value flips 611 | // pixels across the vertical axis. A value of 1 results in no horizontal 612 | // scaling. 613 | // 614 | // y is the scaling factor in the vertical direction. A negative value flips 615 | // pixels across the horizontal axis. A value of 1 results in no vertical 616 | // scaling. 617 | func (ctx *Context) Scale(x, y float64) { 618 | ctx.buf.addByte(bScale) 619 | ctx.buf.addFloat64(x) 620 | ctx.buf.addFloat64(y) 621 | } 622 | 623 | // Stroke outlines the current or given path with the current stroke style. 624 | // 625 | // Strokes are aligned to the center of a path; in other words, half of the 626 | // stroke is drawn on the inner side, and half on the outer side. 627 | // 628 | // The stroke is drawn using the [non-zero winding rule], which means that path 629 | // intersections will still get filled. 630 | // 631 | // [non-zero winding rule]: https://en.wikipedia.org/wiki/Nonzero-rule 632 | func (ctx *Context) Stroke() { 633 | ctx.buf.addByte(bStroke) 634 | } 635 | 636 | // StrokeText strokes - that is, draws the outlines of - the characters of a 637 | // text string at the specified coordinates. 638 | // 639 | // The text is rendered using the settings specified by SetFont, SetTextAlign, 640 | // and SetTextBaseline. (x, y) is the coordinate of the point at which to begin 641 | // drawing the text. 642 | // 643 | // This method draws directly to the canvas without modifying the current path, 644 | // so any subsequent Fill or Stroke calls will have no effect on it. 645 | // 646 | // Use the FillText method to fill the text characters rather than having just 647 | // their outlines drawn. 648 | func (ctx *Context) StrokeText(text string, x, y float64) { 649 | ctx.buf.addByte(bStrokeText) 650 | ctx.buf.addFloat64(x) 651 | ctx.buf.addFloat64(y) 652 | ctx.buf.addString(text) 653 | } 654 | 655 | // StrokeTextMaxWidth strokes - that is, draws the outlines of - the characters 656 | // of a text string at the specified coordinates. A parameter allows 657 | // specifying a maximum width for the rendered text, which the user agent will 658 | // achieve by condensing the text or by using a lower font size. 659 | // 660 | // The text is rendered using the settings specified by SetFont, SetTextAlign, 661 | // and SetTextBaseline. (x, y) is the coordinate of the point at which to begin 662 | // drawing the text. 663 | // 664 | // The user agent will adjust the kerning, select a more horizontally condensed 665 | // font (if one is available or can be generated without loss of quality), or 666 | // scale down to a smaller font size in order to fit the text in the specified 667 | // maxWidth. 668 | // 669 | // This method draws directly to the canvas without modifying the current path, 670 | // so any subsequent Fill or Stroke calls will have no effect on it. 671 | // 672 | // Use the FillText method to fill the text characters rather than having just 673 | // their outlines drawn. 674 | func (ctx *Context) StrokeTextMaxWidth(text string, x, y, maxWidth float64) { 675 | ctx.buf.addByte(bStrokeTextMaxWidth) 676 | ctx.buf.addFloat64(x) 677 | ctx.buf.addFloat64(y) 678 | ctx.buf.addFloat64(maxWidth) 679 | ctx.buf.addString(text) 680 | } 681 | 682 | // StrokeRect draws a rectangle that is stroked (outlined) according to the 683 | // current stroke style and other context settings. 684 | // 685 | // It draws a stroked rectangle whose starting point is at (x, y) and whose 686 | // size is specified by width and height. 687 | // 688 | // This method draws directly to the canvas without modifying the current path 689 | // so any subsequent Fill or Stroke calls will have no effect on it. 690 | func (ctx *Context) StrokeRect(x, y, width, height float64) { 691 | ctx.buf.addByte(bStrokeRect) 692 | ctx.buf.addFloat64(x) 693 | ctx.buf.addFloat64(y) 694 | ctx.buf.addFloat64(width) 695 | ctx.buf.addFloat64(height) 696 | } 697 | 698 | // Translate adds a translation transformation to the current matrix by moving 699 | // the canvas and its origin x units horizontally and y units vertically on the 700 | // grid. 701 | // 702 | // Positive x values are to the right, and negative to the left. 703 | // Positive y values are down, and negative are up. 704 | func (ctx *Context) Translate(x, y float64) { 705 | ctx.buf.addByte(bTranslate) 706 | ctx.buf.addFloat64(x) 707 | ctx.buf.addFloat64(y) 708 | } 709 | 710 | // Transform multiplies the current transformation with the matrix described 711 | // by the arguments of this method. This lets you scale, rotate, translate 712 | // (move), and skew the context. The transformation matrix is described by: 713 | // 714 | // [ a b 0 ] 715 | // [ c d 0 ] 716 | // [ e f 1 ] 717 | // 718 | // a: Horizontal scaling. A value of 1 results in no scaling. 719 | // b: Vertical skewing. 720 | // c: Horizontal skewing. 721 | // d: Vertical scaling. A value of 1 results in no scaling. 722 | // e: Horizontal translation (moving). 723 | // f: Vertical translation (moving). 724 | // 725 | // Note: See also the SetTransform method, which resets the current transform 726 | // to the identity matrix and then invokes Transform. 727 | func (ctx *Context) Transform(a, b, c, d, e, f float64) { 728 | ctx.buf.addByte(bTransform) 729 | ctx.buf.addFloat64(a) 730 | ctx.buf.addFloat64(b) 731 | ctx.buf.addFloat64(c) 732 | ctx.buf.addFloat64(d) 733 | ctx.buf.addFloat64(e) 734 | ctx.buf.addFloat64(f) 735 | } 736 | 737 | // SetTransform resets (overrides) the current transformation to the identity 738 | // matrix, and then invokes a transformation described by the arguments of this 739 | // method. This lets you scale, rotate, translate (move), and skew the context. 740 | // The transformation matrix is described by: 741 | // 742 | // [ a b 0 ] 743 | // [ c d 0 ] 744 | // [ e f 1 ] 745 | // 746 | // a: Horizontal scaling. A value of 1 results in no scaling. 747 | // b: Vertical skewing. 748 | // c: Horizontal skewing. 749 | // d: Vertical scaling. A value of 1 results in no scaling. 750 | // e: Horizontal translation (moving). 751 | // f: Vertical translation (moving). 752 | // 753 | // Note: See also the Transform method; instead of overriding the current 754 | // transform matrix, it multiplies it with a given one. 755 | func (ctx *Context) SetTransform(a, b, c, d, e, f float64) { 756 | ctx.buf.addByte(bSetTransform) 757 | ctx.buf.addFloat64(a) 758 | ctx.buf.addFloat64(b) 759 | ctx.buf.addFloat64(c) 760 | ctx.buf.addFloat64(d) 761 | ctx.buf.addFloat64(e) 762 | ctx.buf.addFloat64(f) 763 | } 764 | 765 | // SetLineDash sets the line dash pattern used when stroking lines. It uses a 766 | // slice of values that specify alternating lengths of lines and gaps which 767 | // describe the pattern. 768 | // 769 | // The segments are a slice of numbers that specify distances to alternately 770 | // draw a line and a gap (in coordinate space units). If the number of elements 771 | // in the slice is odd, the elements of the slice get copied and concatenated. 772 | // For example, {5, 15, 25} will become {5, 15, 25, 5, 15, 25}. If the slice 773 | // is empty, the line dash list is cleared and line strokes return to being 774 | // solid. 775 | func (ctx *Context) SetLineDash(segments []float64) { 776 | ctx.buf.addByte(bSetLineDash) 777 | ctx.buf.addUint32(uint32(len(segments))) 778 | for _, seg := range segments { 779 | ctx.buf.addFloat64(seg) 780 | } 781 | } 782 | 783 | // CreateImageData creates a new, blank ImageData object on the client with the 784 | // specified dimensions. All of the pixels in the new object are transparent 785 | // black. The ImageData object should be released with the ImageData.Release 786 | // method when it is no longer needed. 787 | func (ctx *Context) CreateImageData(m image.Image) *ImageData { 788 | rgba := ensureRGBA(m) 789 | bounds := m.Bounds() 790 | id := ctx.imageDataIDs.generateID() 791 | ctx.buf.addByte(bCreateImageData) 792 | ctx.buf.addUint32(id) 793 | ctx.buf.addUint32(uint32(bounds.Dx())) 794 | ctx.buf.addUint32(uint32(bounds.Dy())) 795 | ctx.buf.addBytes(rgba.Pix) 796 | return &ImageData{id: id, ctx: ctx, width: bounds.Dx(), height: bounds.Dy()} 797 | } 798 | 799 | // PutImageData paints data from the given ImageData object onto the 800 | // canvas. If a dirty rectangle is provided, only the pixels from that 801 | // rectangle are painted. This method is not affected by the canvas 802 | // transformation matrix. 803 | // 804 | // (dx, dy) is the position at which to place the image data in the destination 805 | // canvas. 806 | // 807 | // Note: Image data can be retrieved from a canvas using the GetImageData 808 | // method. 809 | func (ctx *Context) PutImageData(src *ImageData, dx, dy float64) { 810 | src.checkUseAfterRelease() 811 | ctx.buf.addByte(bPutImageData) 812 | ctx.buf.addUint32(src.id) 813 | ctx.buf.addFloat64(dx) 814 | ctx.buf.addFloat64(dy) 815 | } 816 | 817 | // PutImageDataDirty paints data from the given ImageData object onto the 818 | // canvas. If a dirty rectangle is provided, only the pixels from that 819 | // rectangle are painted. This method is not affected by the canvas 820 | // transformation matrix. 821 | // 822 | // (dx, dy) is the position at which to place the image data in the destination 823 | // canvas. 824 | // 825 | // (dirtyX, dirtyY) is the position of the top-left corner from which the image 826 | // data will be extracted; dirtyWidth and dirtyHeight are the width and height 827 | // of the rectangle to be painted. 828 | // 829 | // Note: Image data can be retrieved from a canvas using the GetImageData 830 | // method. 831 | func (ctx *Context) PutImageDataDirty(src *ImageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight float64) { 832 | src.checkUseAfterRelease() 833 | ctx.buf.addByte(bPutImageDataDirty) 834 | ctx.buf.addUint32(src.id) 835 | ctx.buf.addFloat64(dx) 836 | ctx.buf.addFloat64(dy) 837 | ctx.buf.addFloat64(dirtyX) 838 | ctx.buf.addFloat64(dirtyY) 839 | ctx.buf.addFloat64(dirtyWidth) 840 | ctx.buf.addFloat64(dirtyHeight) 841 | } 842 | 843 | // DrawImage draws an image onto the canvas. 844 | // 845 | // (dx, dy) is the position in the destination canvas at which to place the 846 | // top-left corner of the source image. 847 | func (ctx *Context) DrawImage(src *ImageData, dx, dy float64) { 848 | src.checkUseAfterRelease() 849 | ctx.buf.addByte(bDrawImage) 850 | ctx.buf.addUint32(src.id) 851 | ctx.buf.addFloat64(dx) 852 | ctx.buf.addFloat64(dy) 853 | } 854 | 855 | // DrawImageScaled draws an image onto the canvas. 856 | // 857 | // (dx, dy) is the position in the destination canvas at which to place the 858 | // top-left corner of the source image; dWidth and dHeight are the width to 859 | // draw the image in the destination canvas. This allows scaling of the drawn 860 | // image. 861 | func (ctx *Context) DrawImageScaled(src *ImageData, dx, dy, dWidth, dHeight float64) { 862 | src.checkUseAfterRelease() 863 | ctx.buf.addByte(bDrawImageScaled) 864 | ctx.buf.addUint32(src.id) 865 | ctx.buf.addFloat64(dx) 866 | ctx.buf.addFloat64(dy) 867 | ctx.buf.addFloat64(dWidth) 868 | ctx.buf.addFloat64(dHeight) 869 | } 870 | 871 | // DrawImageSubRectangle draws an image onto the canvas. 872 | // 873 | // (sx, sy) is the position of the top left corner of the sub-rectangle of the 874 | // source image to draw into the destination context; sWidth and sHeight are 875 | // the width and height of the sub-rectangle of the source image to draw into 876 | // the destination context. 877 | // 878 | // (dx, dy) is the position in the destination canvas at which to place the 879 | // top-left corner of the source image; dWidth and dHeight are the width to 880 | // draw the image in the destination canvas. This allows scaling of the drawn 881 | // image. 882 | func (ctx *Context) DrawImageSubRectangle(src *ImageData, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight float64) { 883 | src.checkUseAfterRelease() 884 | ctx.buf.addByte(bDrawImageSubRectangle) 885 | ctx.buf.addUint32(src.id) 886 | ctx.buf.addFloat64(sx) 887 | ctx.buf.addFloat64(sy) 888 | ctx.buf.addFloat64(sWidth) 889 | ctx.buf.addFloat64(sHeight) 890 | ctx.buf.addFloat64(dx) 891 | ctx.buf.addFloat64(dy) 892 | ctx.buf.addFloat64(dWidth) 893 | ctx.buf.addFloat64(dHeight) 894 | } 895 | 896 | // CreateLinearGradient creates a gradient along the line connecting two given 897 | // coordinates. To be applied to a shape, the gradient must first be set via 898 | // the SetFillStyleGradient or SetStrokeStyleGradient methods. 899 | // 900 | // (x0, y0) defines the start point, and (x1, y1) defines the end point of the 901 | // gradient line. 902 | // 903 | // Note: Gradient coordinates are global, i.e., relative to the current 904 | // coordinate space. When applied to a shape, the coordinates are NOT relative 905 | // to the shape's coordinates. 906 | func (ctx *Context) CreateLinearGradient(x0, y0, x1, y1 float64) *Gradient { 907 | id := ctx.gradientIDs.generateID() 908 | ctx.buf.addByte(bCreateLinearGradient) 909 | ctx.buf.addUint32(id) 910 | ctx.buf.addFloat64(x0) 911 | ctx.buf.addFloat64(y0) 912 | ctx.buf.addFloat64(x1) 913 | ctx.buf.addFloat64(y1) 914 | return &Gradient{id: id, ctx: ctx} 915 | } 916 | 917 | // CreateRadialGradient creates a radial gradient using the size and 918 | // coordinates of two circles. To be applied to a shape, the gradient must 919 | // first be set via the SetFillStyleGradient or SetStrokeStyleGradient methods. 920 | // 921 | // (x0, y0) defines the center, and r0 the radius of the start circle. 922 | // (x1, y1) defines the center, and r1 the radius of the end circle. 923 | // Each radius must be non-negative and finite. 924 | // 925 | // Note: Gradient coordinates are global, i.e., relative to the current 926 | // coordinate space. When applied to a shape, the coordinates are NOT relative 927 | // to the shape's coordinates. 928 | func (ctx *Context) CreateRadialGradient(x0, y0, r0, x1, y1, r1 float64) *Gradient { 929 | id := ctx.gradientIDs.generateID() 930 | ctx.buf.addByte(bCreateRadialGradient) 931 | ctx.buf.addUint32(id) 932 | ctx.buf.addFloat64(x0) 933 | ctx.buf.addFloat64(y0) 934 | ctx.buf.addFloat64(r0) 935 | ctx.buf.addFloat64(x1) 936 | ctx.buf.addFloat64(y1) 937 | ctx.buf.addFloat64(r1) 938 | return &Gradient{id: id, ctx: ctx} 939 | } 940 | 941 | // CreatePattern creates a pattern using the specified image and repetition. 942 | // The repetition indicates how to repeat the pattern's image. 943 | // 944 | // This method doesn't draw anything to the canvas directly. The pattern it 945 | // creates must be set via the SetFillStylePattern or SetStrokeStylePattern 946 | // methods, after which it is applied to any subsequent drawing. 947 | func (ctx *Context) CreatePattern(src *ImageData, repetition PatternRepetition) *Pattern { 948 | src.checkUseAfterRelease() 949 | id := ctx.patternIDs.generateID() 950 | ctx.buf.addByte(bCreatePattern) 951 | ctx.buf.addUint32(id) 952 | ctx.buf.addUint32(src.id) 953 | ctx.buf.addByte(byte(repetition)) 954 | return &Pattern{id: id, ctx: ctx} 955 | } 956 | 957 | // GetImageData returns an ImageData object representing the underlying pixel 958 | // data for a specified portion of the canvas. 959 | // 960 | // (sx, sy) is the position of the top-left corner of the rectangle from which 961 | // the ImageData will be extracted; sw and sh are the width and height of the 962 | // rectangle from which the ImageData will be extracted. 963 | // 964 | // This method is not affected by the canvas's transformation matrix. If the 965 | // specified rectangle extends outside the bounds of the canvas, the pixels 966 | // outside the canvas are transparent black in the returned ImageData object. 967 | // 968 | // Note: Image data can be painted onto a canvas using the PutImageData method. 969 | func (ctx *Context) GetImageData(sx, sy, sw, sh float64) *ImageData { 970 | id := ctx.imageDataIDs.generateID() 971 | ctx.buf.addByte(bGetImageData) 972 | ctx.buf.addUint32(id) 973 | ctx.buf.addFloat64(sx) 974 | ctx.buf.addFloat64(sy) 975 | ctx.buf.addFloat64(sw) 976 | ctx.buf.addFloat64(sh) 977 | return &ImageData{id: id, ctx: ctx, width: int(sw), height: int(sh)} 978 | } 979 | 980 | // Flush sends the buffered drawing operations of the context from the server 981 | // to the client. 982 | // 983 | // Nothing is displayed on the client canvas until Flush is called. 984 | // An animation loop usually has one flush per animation frame. 985 | func (ctx *Context) Flush() { 986 | ctx.draws <- ctx.buf.bytes 987 | ctx.buf.reset() 988 | } 989 | 990 | type idGenerator struct { 991 | next uint32 992 | } 993 | 994 | func (g *idGenerator) generateID() uint32 { 995 | id := g.next 996 | g.next++ 997 | return id 998 | } 999 | --------------------------------------------------------------------------------