├── 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 | [](https://pkg.go.dev/github.com/fzipp/canvas)
4 | 
5 | [](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 | 
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 |
--------------------------------------------------------------------------------