diff --git a/README.md b/README.md index ac347e9..e1553de 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ covering several topics of Pixel. Here's the content of the tutorial parts so fa - [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse) - [Drawing efficiently with Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch) - [Drawing shapes with IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw) +- [Typing text on the screen](https://github.com/faiface/pixel/wiki/Typing-text-on-the-screen) ## Examples diff --git a/data.go b/data.go index 4e6c528..c941241 100644 --- a/data.go +++ b/data.go @@ -45,7 +45,7 @@ func (td *TrianglesData) SetLen(len int) { Color RGBA Picture Vec Intensity float64 - }{ZV, Alpha(1), ZV, 0}) + }{Color: RGBA{1, 1, 1, 1}}) } } if len < td.Len() { diff --git a/examples/community/maze/LICENSE b/examples/community/maze/LICENSE new file mode 100644 index 0000000..1326e36 --- /dev/null +++ b/examples/community/maze/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 stephen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/community/maze/README.md b/examples/community/maze/README.md new file mode 100644 index 0000000..27b344a --- /dev/null +++ b/examples/community/maze/README.md @@ -0,0 +1,19 @@ +# Maze generator in Go + +Created by [Stephen Chavez](https://github.com/redragonx) + +This uses the game engine: Pixel. Install it here: https://github.com/faiface/pixel + +I made this to improve my understanding of Go and some game concepts with some basic maze generating algorithms. + +Controls: Press 'R' to restart the maze. + +Optional command-line arguments: `go run ./maze-generator.go` + - `-w` sets the maze's width in pixels. + - `-h` sets the maze's height in pixels. + - `-c` sets the maze cell's size in pixels. + +Code based on the Recursive backtracker algorithm. +- https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker + +![Screenshot](screenshot.png) \ No newline at end of file diff --git a/examples/community/maze/maze-generator.go b/examples/community/maze/maze-generator.go new file mode 100644 index 0000000..a8b09f8 --- /dev/null +++ b/examples/community/maze/maze-generator.go @@ -0,0 +1,317 @@ +package main + +// Code based on the Recursive backtracker algorithm. +// https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker +// See https://youtu.be/HyK_Q5rrcr4 as an example +// YouTube example ported to Go for the Pixel library. + +// Created by Stephen Chavez + +import ( + "crypto/rand" + "errors" + "flag" + "fmt" + "math/big" + "time" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/examples/community/maze/stack" + "github.com/faiface/pixel/imdraw" + "github.com/faiface/pixel/pixelgl" + + "github.com/pkg/profile" + "golang.org/x/image/colornames" +) + +var visitedColor = pixel.RGB(0.5, 0, 1).Mul(pixel.Alpha(0.35)) +var hightlightColor = pixel.RGB(0.3, 0, 0).Mul(pixel.Alpha(0.45)) +var debug = false + +type cell struct { + walls [4]bool // Wall order: top, right, bottom, left + + row int + col int + visited bool +} + +func (c *cell) Draw(imd *imdraw.IMDraw, wallSize int) { + drawCol := c.col * wallSize // x + drawRow := c.row * wallSize // y + + imd.Color = colornames.White + if c.walls[0] { + // top line + imd.Push(pixel.V(float64(drawCol), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow))) + imd.Line(3) + } + if c.walls[1] { + // right Line + imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize))) + imd.Line(3) + } + if c.walls[2] { + // bottom line + imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow+wallSize))) + imd.Line(3) + } + if c.walls[3] { + // left line + imd.Push(pixel.V(float64(drawCol), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow))) + imd.Line(3) + } + imd.EndShape = imdraw.SharpEndShape + + if c.visited { + imd.Color = visitedColor + imd.Push(pixel.V(float64(drawCol), (float64(drawRow))), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize))) + imd.Rectangle(0) + } +} + +func (c *cell) GetNeighbors(grid []*cell, cols int, rows int) ([]*cell, error) { + neighbors := []*cell{} + j := c.row + i := c.col + + top, _ := getCellAt(i, j-1, cols, rows, grid) + right, _ := getCellAt(i+1, j, cols, rows, grid) + bottom, _ := getCellAt(i, j+1, cols, rows, grid) + left, _ := getCellAt(i-1, j, cols, rows, grid) + + if top != nil && !top.visited { + neighbors = append(neighbors, top) + } + if right != nil && !right.visited { + neighbors = append(neighbors, right) + } + if bottom != nil && !bottom.visited { + neighbors = append(neighbors, bottom) + } + if left != nil && !left.visited { + neighbors = append(neighbors, left) + } + + if len(neighbors) == 0 { + return nil, errors.New("We checked all cells...") + } + return neighbors, nil +} + +func (c *cell) GetRandomNeighbor(grid []*cell, cols int, rows int) (*cell, error) { + neighbors, err := c.GetNeighbors(grid, cols, rows) + if neighbors == nil { + return nil, err + } + nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(neighbors)))) + if err != nil { + panic(err) + } + randomIndex := nBig.Int64() + return neighbors[randomIndex], nil +} + +func (c *cell) hightlight(imd *imdraw.IMDraw, wallSize int) { + x := c.col * wallSize + y := c.row * wallSize + + imd.Color = hightlightColor + imd.Push(pixel.V(float64(x), float64(y)), pixel.V(float64(x+wallSize), float64(y+wallSize))) + imd.Rectangle(0) +} + +func newCell(col int, row int) *cell { + newCell := new(cell) + newCell.row = row + newCell.col = col + + for i := range newCell.walls { + newCell.walls[i] = true + } + return newCell +} + +// Creates the inital maze slice for use. +func initGrid(cols, rows int) []*cell { + grid := []*cell{} + for j := 0; j < rows; j++ { + for i := 0; i < cols; i++ { + newCell := newCell(i, j) + grid = append(grid, newCell) + } + } + return grid +} + +func setupMaze(cols, rows int) ([]*cell, *stack.Stack, *cell) { + // Make an empty grid + grid := initGrid(cols, rows) + backTrackStack := stack.NewStack(len(grid)) + currentCell := grid[0] + + return grid, backTrackStack, currentCell +} + +func cellIndex(i, j, cols, rows int) int { + if i < 0 || j < 0 || i > cols-1 || j > rows-1 { + return -1 + } + return i + j*cols +} + +func getCellAt(i int, j int, cols int, rows int, grid []*cell) (*cell, error) { + possibleIndex := cellIndex(i, j, cols, rows) + + if possibleIndex == -1 { + return nil, fmt.Errorf("cellIndex: CellIndex is a negative number %d", possibleIndex) + } + return grid[possibleIndex], nil +} + +func removeWalls(a *cell, b *cell) { + x := a.col - b.col + + if x == 1 { + a.walls[3] = false + b.walls[1] = false + } else if x == -1 { + a.walls[1] = false + b.walls[3] = false + } + + y := a.row - b.row + + if y == 1 { + a.walls[0] = false + b.walls[2] = false + } else if y == -1 { + a.walls[2] = false + b.walls[0] = false + } +} + +func run() { + // unsiged integers, because easier parsing error checks. + // We must convert these to intergers, as done below... + uScreenWidth, uScreenHeight, uWallSize := parseArgs() + + var ( + // In pixels + // Defualt is 800x800x40 = 20x20 wallgrid + screenWidth = int(uScreenWidth) + screenHeight = int(uScreenHeight) + wallSize = int(uWallSize) + + frames = 0 + second = time.Tick(time.Second) + + grid = []*cell{} + cols = screenWidth / wallSize + rows = screenHeight / wallSize + currentCell = new(cell) + backTrackStack = stack.NewStack(1) + ) + + // Set game FPS manually + fps := time.Tick(time.Second / 60) + + cfg := pixelgl.WindowConfig{ + Title: "Pixel Rocks! - Maze example", + Bounds: pixel.R(0, 0, float64(screenHeight), float64(screenWidth)), + } + + win, err := pixelgl.NewWindow(cfg) + if err != nil { + panic(err) + } + + grid, backTrackStack, currentCell = setupMaze(cols, rows) + + gridIMDraw := imdraw.New(nil) + + for !win.Closed() { + if win.JustReleased(pixelgl.KeyR) { + fmt.Println("R pressed") + grid, backTrackStack, currentCell = setupMaze(cols, rows) + } + + win.Clear(colornames.Gray) + gridIMDraw.Clear() + + for i := range grid { + grid[i].Draw(gridIMDraw, wallSize) + } + + // step 1 + // Make the initial cell the current cell and mark it as visited + currentCell.visited = true + currentCell.hightlight(gridIMDraw, wallSize) + + // step 2.1 + // If the current cell has any neighbours which have not been visited + // Choose a random unvisited cell + nextCell, _ := currentCell.GetRandomNeighbor(grid, cols, rows) + if nextCell != nil && !nextCell.visited { + // step 2.2 + // Push the current cell to the stack + backTrackStack.Push(currentCell) + + // step 2.3 + // Remove the wall between the current cell and the chosen cell + + removeWalls(currentCell, nextCell) + + // step 2.4 + // Make the chosen cell the current cell and mark it as visited + nextCell.visited = true + currentCell = nextCell + } else if backTrackStack.Len() > 0 { + currentCell = backTrackStack.Pop().(*cell) + } + + gridIMDraw.Draw(win) + win.Update() + <-fps + updateFPSDisplay(win, &cfg, &frames, grid, second) + } +} + +// Parses the maze arguments, all of them are optional. +// Uses uint as implicit error checking :) +func parseArgs() (uint, uint, uint) { + var mazeWidthPtr = flag.Uint("w", 800, "w sets the maze's width in pixels.") + var mazeHeightPtr = flag.Uint("h", 800, "h sets the maze's height in pixels.") + var wallSizePtr = flag.Uint("c", 40, "c sets the maze cell's size in pixels.") + + flag.Parse() + + // If these aren't default values AND if they're not the same values. + // We should warn the user that the maze will look funny. + if *mazeWidthPtr != 800 || *mazeHeightPtr != 800 { + if *mazeWidthPtr != *mazeHeightPtr { + fmt.Printf("WARNING: maze width: %d and maze height: %d don't match. \n", *mazeWidthPtr, *mazeHeightPtr) + fmt.Println("Maze will look funny because the maze size is bond to the window size!") + } + } + + return *mazeWidthPtr, *mazeHeightPtr, *wallSizePtr +} + +func updateFPSDisplay(win *pixelgl.Window, cfg *pixelgl.WindowConfig, frames *int, grid []*cell, second <-chan time.Time) { + *frames++ + select { + case <-second: + win.SetTitle(fmt.Sprintf("%s | FPS: %d with %d Cells", cfg.Title, *frames, len(grid))) + *frames = 0 + default: + } + +} + +func main() { + if debug { + defer profile.Start().Stop() + } + pixelgl.Run(run) +} diff --git a/examples/community/maze/screenshot.png b/examples/community/maze/screenshot.png new file mode 100644 index 0000000..ce5bfd2 Binary files /dev/null and b/examples/community/maze/screenshot.png differ diff --git a/examples/community/maze/stack/stack.go b/examples/community/maze/stack/stack.go new file mode 100644 index 0000000..50a7a46 --- /dev/null +++ b/examples/community/maze/stack/stack.go @@ -0,0 +1,86 @@ +package stack + +type Stack struct { + top *Element + size int + max int +} + +type Element struct { + value interface{} + next *Element +} + +func NewStack(max int) *Stack { + return &Stack{max: max} +} + +// Return the stack's length +func (s *Stack) Len() int { + return s.size +} + +// Return the stack's max +func (s *Stack) Max() int { + return s.max +} + +// Push a new element onto the stack +func (s *Stack) Push(value interface{}) { + if s.size+1 > s.max { + if last := s.PopLast(); last == nil { + panic("Unexpected nil in stack") + } + } + s.top = &Element{value, s.top} + s.size++ +} + +// Remove the top element from the stack and return it's value +// If the stack is empty, return nil +func (s *Stack) Pop() (value interface{}) { + if s.size > 0 { + value, s.top = s.top.value, s.top.next + s.size-- + return + } + return nil +} + +func (s *Stack) PopLast() (value interface{}) { + if lastElem := s.popLast(s.top); lastElem != nil { + return lastElem.value + } + return nil +} + +//Peek returns a top without removing it from list +func (s *Stack) Peek() (value interface{}, exists bool) { + exists = false + if s.size > 0 { + value = s.top.value + exists = true + } + + return +} + +func (s *Stack) popLast(elem *Element) *Element { + if elem == nil { + return nil + } + // not last because it has next and a grandchild + if elem.next != nil && elem.next.next != nil { + return s.popLast(elem.next) + } + + // current elem is second from bottom, as next elem has no child + if elem.next != nil && elem.next.next == nil { + last := elem.next + // make current elem bottom of stack by removing its next element + elem.next = nil + s.size-- + return last + } + return nil +} diff --git a/examples/guide/07_typing_text_on_the_screen/intuitive.ttf b/examples/guide/07_typing_text_on_the_screen/intuitive.ttf new file mode 100644 index 0000000..9039d7b Binary files /dev/null and b/examples/guide/07_typing_text_on_the_screen/intuitive.ttf differ diff --git a/examples/guide/07_typing_text_on_the_screen/main.go b/examples/guide/07_typing_text_on_the_screen/main.go new file mode 100644 index 0000000..caaa0da --- /dev/null +++ b/examples/guide/07_typing_text_on_the_screen/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "io/ioutil" + "os" + "time" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/pixelgl" + "github.com/faiface/pixel/text" + "github.com/golang/freetype/truetype" + "golang.org/x/image/colornames" + "golang.org/x/image/font" +) + +func loadTTF(path string, size float64) (font.Face, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + bytes, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + + font, err := truetype.Parse(bytes) + if err != nil { + return nil, err + } + + return truetype.NewFace(font, &truetype.Options{ + Size: size, + GlyphCacheEntries: 1, + }), nil +} + +func run() { + cfg := pixelgl.WindowConfig{ + Title: "Pixel Rocks!", + Bounds: pixel.R(0, 0, 1024, 768), + } + win, err := pixelgl.NewWindow(cfg) + if err != nil { + panic(err) + } + win.SetSmooth(true) + + face, err := loadTTF("intuitive.ttf", 80) + if err != nil { + panic(err) + } + + atlas := text.NewAtlas(face, text.ASCII) + txt := text.New(pixel.V(50, 500), atlas) + + txt.Color = colornames.Lightgrey + + fps := time.Tick(time.Second / 120) + + for !win.Closed() { + txt.WriteString(win.Typed()) + if win.JustPressed(pixelgl.KeyEnter) || win.Repeated(pixelgl.KeyEnter) { + txt.WriteRune('\n') + } + + win.Clear(colornames.Darkcyan) + txt.Draw(win, pixel.IM.Moved(win.Bounds().Center().Sub(txt.Bounds().Center()))) + win.Update() + + <-fps + } +} + +func main() { + pixelgl.Run(run) +} diff --git a/examples/typewriter/main.go b/examples/typewriter/main.go index d7691ff..24ee972 100644 --- a/examples/typewriter/main.go +++ b/examples/typewriter/main.go @@ -227,10 +227,7 @@ func (dl *dotlight) Draw(t pixel.Target, m pixel.Matrix) { dl.imd.Color = pixel.Alpha(0) for i := 0.0; i <= 32; i++ { angle := i * 2 * math.Pi / 32 - dl.imd.Push(dl.pos.Add(pixel.V( - math.Cos(angle)*dl.radius, - math.Sin(angle)*dl.radius, - ))) + dl.imd.Push(dl.pos.Add(pixel.V(dl.radius, 0).Rotated(angle))) } dl.imd.Polygon(0) dl.imd.Draw(t) diff --git a/geometry.go b/geometry.go index f934839..13a574d 100644 --- a/geometry.go +++ b/geometry.go @@ -3,8 +3,6 @@ package pixel import ( "fmt" "math" - - "github.com/go-gl/mathgl/mgl64" ) // Vec is a 2D vector type with X and Y coordinates. @@ -251,7 +249,7 @@ func (r Rect) Union(s Rect) Rect { ) } -// Matrix is a 3x3 transformation matrix that can be used for all kinds of spacial transforms, such +// Matrix is a 3x2 affine matrix that can be used for all kinds of spatial transforms, such // as movement, scaling and rotations. // // Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For @@ -261,38 +259,41 @@ func (r Rect) Union(s Rect) Rect { // // This code creates a Matrix that first moves everything by 100 units horizontally and 200 units // vertically and then rotates everything by 90 degrees around the origin. -type Matrix [9]float64 +// +// Layout is: +// [0] [2] [4] +// [1] [3] [5] +// 0 0 1 (implicit row) +type Matrix [6]float64 // IM stands for identity matrix. Does nothing, no transformation. -var IM = Matrix(mgl64.Ident3()) +var IM = Matrix{1, 0, 0, 1, 0, 0} // String returns a string representation of the Matrix. // // m := pixel.IM -// fmt.Println(m) // Matrix(1 0 0 | 0 1 0 | 0 0 1) +// fmt.Println(m) // Matrix(1 0 0 | 0 1 0) func (m Matrix) String() string { return fmt.Sprintf( - "Matrix(%v %v %v | %v %v %v | %v %v %v)", - m[0], m[3], m[6], - m[1], m[4], m[7], - m[2], m[5], m[8], + "Matrix(%v %v %v | %v %v %v)", + m[0], m[2], m[4], + m[1], m[3], m[5], ) } // Moved moves everything by the delta vector. func (m Matrix) Moved(delta Vec) Matrix { - m3 := mgl64.Mat3(m) - m3 = mgl64.Translate2D(delta.XY()).Mul3(m3) - return Matrix(m3) + m[4], m[5] = m[4]+delta.X, m[5]+delta.Y + return m } // ScaledXY scales everything around a given point by the scale factor in each axis respectively. func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix { - m3 := mgl64.Mat3(m) - m3 = mgl64.Translate2D(around.Scaled(-1).XY()).Mul3(m3) - m3 = mgl64.Scale2D(scale.XY()).Mul3(m3) - m3 = mgl64.Translate2D(around.XY()).Mul3(m3) - return Matrix(m3) + m[4], m[5] = m[4]-around.X, m[5]-around.Y + m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X + m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y + m[4], m[5] = m[4]+around.X, m[5]+around.Y + return m } // Scaled scales everything around a given point by the scale factor. @@ -302,36 +303,42 @@ func (m Matrix) Scaled(around Vec, scale float64) Matrix { // Rotated rotates everything around a given point by the given angle in radians. func (m Matrix) Rotated(around Vec, angle float64) Matrix { - m3 := mgl64.Mat3(m) - m3 = mgl64.Translate2D(around.Scaled(-1).XY()).Mul3(m3) - m3 = mgl64.Rotate3DZ(angle).Mul3(m3) - m3 = mgl64.Translate2D(around.XY()).Mul3(m3) - return Matrix(m3) + sint, cost := math.Sincos(angle) + m[4], m[5] = m[4]-around.X, m[5]-around.Y + m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0}) + m[4], m[5] = m[4]+around.X, m[5]+around.Y + return m } // Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied // after the transformations of this Matrix. func (m Matrix) Chained(next Matrix) Matrix { - m3 := mgl64.Mat3(m) - m3 = mgl64.Mat3(next).Mul3(m3) - return Matrix(m3) + return Matrix{ + m[0]*next[0] + m[2]*next[1], + m[1]*next[0] + m[3]*next[1], + m[0]*next[2] + m[2]*next[3], + m[1]*next[2] + m[3]*next[3], + m[0]*next[4] + m[2]*next[5] + m[4], + m[1]*next[4] + m[3]*next[5] + m[5], + } } // Project applies all transformations added to the Matrix to a vector u and returns the result. // // Time complexity is O(1). func (m Matrix) Project(u Vec) Vec { - m3 := mgl64.Mat3(m) - proj := m3.Mul3x1(mgl64.Vec3{u.X, u.Y, 1}) - return V(proj.X(), proj.Y()) + return Vec{X: m[0]*u.X + m[2]*u.Y + m[4], Y: m[1]*u.X + m[3]*u.Y + m[5]} } // Unproject does the inverse operation to Project. // +// It turns out that multiplying a vector by the inverse matrix of m can be nearly-accomplished by +// subtracting the translate part of the matrix and multplying by the inverse of the top-left 2x2 +// matrix, and the inverse of a 2x2 matrix is simple enough to just be inlined in the computation. +// // Time complexity is O(1). func (m Matrix) Unproject(u Vec) Vec { - m3 := mgl64.Mat3(m) - inv := m3.Inv() - unproj := inv.Mul3x1(mgl64.Vec3{u.X, u.Y, 1}) - return V(unproj.X(), unproj.Y()) + d := (m[0] * m[3]) - (m[1] * m[2]) + u.X, u.Y = (u.X-m[4])/d, (u.Y-m[5])/d + return Vec{u.X*m[3] - u.Y*m[1], u.Y*m[0] - u.X*m[2]} } diff --git a/imdraw/imdraw.go b/imdraw/imdraw.go index 626cb2f..f7ce769 100644 --- a/imdraw/imdraw.go +++ b/imdraw/imdraw.go @@ -52,6 +52,7 @@ type IMDraw struct { EndShape EndShape points []point + pool [][]point matrix pixel.Matrix mask pixel.RGBA @@ -109,7 +110,7 @@ func (imd *IMDraw) Clear() { // // This does not affect matrix and color mask set by SetMatrix and SetColorMask. func (imd *IMDraw) Reset() { - imd.points = nil + imd.points = imd.points[:0] imd.Color = pixel.Alpha(1) imd.Picture = pixel.ZV imd.Intensity = 0 @@ -256,10 +257,22 @@ func (imd *IMDraw) EllipseArc(radius pixel.Vec, low, high, thickness float64) { func (imd *IMDraw) getAndClearPoints() []point { points := imd.points - imd.points = nil + // use one of the existing pools so we don't reallocate as often + if len(imd.pool) > 0 { + pos := len(imd.pool) - 1 + imd.points = imd.pool[pos][:0] + imd.pool = imd.pool[:pos] + } else { + imd.points = nil + } return points } +func (imd *IMDraw) restorePoints(points []point) { + imd.pool = append(imd.pool, imd.points) + imd.points = points[:0] +} + func (imd *IMDraw) applyMatrixAndMask(off int) { for i := range (*imd.tri)[off:] { (*imd.tri)[off+i].Position = imd.matrix.Project((*imd.tri)[off+i].Position) @@ -271,6 +284,7 @@ func (imd *IMDraw) fillRectangle() { points := imd.getAndClearPoints() if len(points) < 2 { + imd.restorePoints(points) return } @@ -292,7 +306,7 @@ func (imd *IMDraw) fillRectangle() { in: (a.in + b.in) / 2, } - for k, p := range []point{a, b, c, a, b, d} { + for k, p := range [...]point{a, b, c, a, b, d} { (*imd.tri)[j+k].Position = p.pos (*imd.tri)[j+k].Color = p.col (*imd.tri)[j+k].Picture = p.pic @@ -302,12 +316,15 @@ func (imd *IMDraw) fillRectangle() { imd.applyMatrixAndMask(off) imd.batch.Dirty() + + imd.restorePoints(points) } func (imd *IMDraw) outlineRectangle(thickness float64) { points := imd.getAndClearPoints() if len(points) < 2 { + imd.restorePoints(points) return } @@ -323,12 +340,15 @@ func (imd *IMDraw) outlineRectangle(thickness float64) { imd.pushPt(pixel.V(b.pos.X, a.pos.Y), mid) imd.polyline(thickness, true) } + + imd.restorePoints(points) } func (imd *IMDraw) fillPolygon() { points := imd.getAndClearPoints() if len(points) < 3 { + imd.restorePoints(points) return } @@ -336,16 +356,19 @@ func (imd *IMDraw) fillPolygon() { imd.tri.SetLen(imd.tri.Len() + 3*(len(points)-2)) for i, j := 1, off; i+1 < len(points); i, j = i+1, j+3 { - for k, p := range []point{points[0], points[i], points[i+1]} { - (*imd.tri)[j+k].Position = p.pos - (*imd.tri)[j+k].Color = p.col - (*imd.tri)[j+k].Picture = p.pic - (*imd.tri)[j+k].Intensity = p.in + for k, p := range [...]int{0, i, i + 1} { + tri := &(*imd.tri)[j+k] + tri.Position = points[p].pos + tri.Color = points[p].col + tri.Picture = points[p].pic + tri.Intensity = points[p].in } } imd.applyMatrixAndMask(off) imd.batch.Dirty() + + imd.restorePoints(points) } func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) { @@ -387,6 +410,8 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) { imd.applyMatrixAndMask(off) imd.batch.Dirty() } + + imd.restorePoints(points) } func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness float64, doEndShape bool) { @@ -485,12 +510,15 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa } } } + + imd.restorePoints(points) } func (imd *IMDraw) polyline(thickness float64, closed bool) { points := imd.getAndClearPoints() if len(points) == 0 { + imd.restorePoints(points) return } if len(points) == 1 { @@ -521,6 +549,8 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { imd.pushPt(points[j].pos.Sub(normal), points[j]) // middle points + // compute "previous" normal: + ijNormal := points[1].pos.Sub(points[0].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) for i := 0; i < len(points); i++ { j, k := i+1, i+2 @@ -536,7 +566,6 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { k %= len(points) } - ijNormal := points[j].pos.Sub(points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) jkNormal := points[k].pos.Sub(points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) orientation := 1.0 @@ -567,6 +596,8 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { imd.pushPt(points[j].pos.Add(jkNormal), points[j]) imd.pushPt(points[j].pos.Sub(jkNormal), points[j]) } + // "next" normal becomes previous normal + ijNormal = jkNormal } // last point @@ -591,4 +622,6 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normal.Angle(), normal.Angle()-math.Pi) } } + + imd.restorePoints(points) } diff --git a/pixelgl/canvas.go b/pixelgl/canvas.go index 7a6400e..9e1efeb 100644 --- a/pixelgl/canvas.go +++ b/pixelgl/canvas.go @@ -91,8 +91,13 @@ func (c *Canvas) MakePicture(p pixel.Picture) pixel.TargetPicture { // SetMatrix sets a Matrix that every point will be projected by. func (c *Canvas) SetMatrix(m pixel.Matrix) { - for i := range m { - c.mat[i] = float32(m[i]) + // pixel.Matrix is 3x2 with an implicit 0, 0, 1 row after it. So + // [0] [2] [4] [0] [3] [6] + // [1] [3] [5] => [1] [4] [7] + // 0 0 1 0 0 1 + // since all matrix ops are affine, the last row never changes, and we don't need to copy it + for i, j := range [...]int{0, 1, 3, 4, 6, 7} { + c.mat[j] = float32(m[i]) } } @@ -218,6 +223,11 @@ func (c *Canvas) Texture() *glhf.Texture { return c.gf.Texture() } +// Frame returns the underlying OpenGL Frame of this Canvas. +func (c *Canvas) Frame() *glhf.Frame { + return c.gf.frame +} + // SetPixels replaces the content of the Canvas with the provided pixels. The provided slice must be // an alpha-premultiplied RGBA sequence of correct length (4 * width * height). func (c *Canvas) SetPixels(pixels []uint8) { diff --git a/pixelgl/gltriangles.go b/pixelgl/gltriangles.go index 64b7355..bf7a895 100644 --- a/pixelgl/gltriangles.go +++ b/pixelgl/gltriangles.go @@ -103,15 +103,17 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) { tx, ty = (*t)[i].Picture.XY() in = (*t)[i].Intensity ) - gt.data[i*gt.vs.Stride()+0] = float32(px) - gt.data[i*gt.vs.Stride()+1] = float32(py) - gt.data[i*gt.vs.Stride()+2] = float32(col.R) - gt.data[i*gt.vs.Stride()+3] = float32(col.G) - gt.data[i*gt.vs.Stride()+4] = float32(col.B) - gt.data[i*gt.vs.Stride()+5] = float32(col.A) - gt.data[i*gt.vs.Stride()+6] = float32(tx) - gt.data[i*gt.vs.Stride()+7] = float32(ty) - gt.data[i*gt.vs.Stride()+8] = float32(in) + s := gt.vs.Stride() + d := gt.data[i*s : i*s+9] + d[0] = float32(px) + d[1] = float32(py) + d[2] = float32(col.R) + d[3] = float32(col.G) + d[4] = float32(col.B) + d[5] = float32(col.A) + d[6] = float32(tx) + d[7] = float32(ty) + d[8] = float32(in) } return }