Adding upstream version 0.0~git20250520.a1d9079+dfsg.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
590ac7ff5f
commit
20149b7f3a
456 changed files with 70406 additions and 0 deletions
26
exp/sprite/clock/clock.go
Normal file
26
exp/sprite/clock/clock.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package clock provides a clock and time functions for a sprite engine.
|
||||
package clock
|
||||
|
||||
// A Time represents an instant in sprite time.
|
||||
//
|
||||
// The application using the sprite engine is responsible for
|
||||
// determining sprite time.
|
||||
//
|
||||
// Typically time 0 is when the app is initialized and time is
|
||||
// quantized at the intended frame rate. For example, an app may
|
||||
// record wall time when it is initialized
|
||||
//
|
||||
// var start = time.Now()
|
||||
//
|
||||
// and then compute the current instant in time for 60 FPS:
|
||||
//
|
||||
// now := clock.Time(time.Since(start) * 60 / time.Second)
|
||||
//
|
||||
// An application can pause or reset sprite time, but it must be aware
|
||||
// of any stateful sprite.Arranger instances that expect time to
|
||||
// continue.
|
||||
type Time int32
|
83
exp/sprite/clock/tween.go
Normal file
83
exp/sprite/clock/tween.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package clock
|
||||
|
||||
// Standard tween functions.
|
||||
//
|
||||
// Easing means a slowing near the timing boundary, as defined by
|
||||
// a cubic bezier curve. Exact parameters match the CSS properties.
|
||||
var (
|
||||
EaseIn = CubicBezier(0.42, 0, 1, 1)
|
||||
EaseOut = CubicBezier(0, 0, 0.58, 1)
|
||||
EaseInOut = CubicBezier(0.42, 0, 0.58, 1)
|
||||
)
|
||||
|
||||
// Linear computes the fraction [0,1] that t lies between [t0,t1].
|
||||
func Linear(t0, t1, t Time) float32 {
|
||||
if t >= t1 {
|
||||
return 1
|
||||
}
|
||||
if t <= t0 {
|
||||
return 0
|
||||
}
|
||||
return float32(t-t0) / float32(t1-t0)
|
||||
}
|
||||
|
||||
// CubicBezier generates a tween function determined by a Cubic Bézier curve.
|
||||
//
|
||||
// The parameters are cubic control parameters. The curve starts at (0,0)
|
||||
// going toward (x0,y0), and arrives at (1,1) coming from (x1,y1).
|
||||
func CubicBezier(x0, y0, x1, y1 float32) func(t0, t1, t Time) float32 {
|
||||
return func(start, end, now Time) float32 {
|
||||
// A Cubic-Bezier curve restricted to starting at (0,0) and
|
||||
// ending at (1,1) is defined as
|
||||
//
|
||||
// B(t) = 3*(1-t)^2*t*P0 + 3*(1-t)*t^2*P1 + t^3
|
||||
//
|
||||
// with derivative
|
||||
//
|
||||
// B'(t) = 3*(1-t)^2*P0 + 6*(1-t)*t*(P1-P0) + 3*t^2*(1-P1)
|
||||
//
|
||||
// Given a value x ∈ [0,1], we solve for t using Newton's
|
||||
// method and solve for y using t.
|
||||
|
||||
x := Linear(start, end, now)
|
||||
|
||||
// Solve for t using x.
|
||||
t := x
|
||||
for i := 0; i < 5; i++ {
|
||||
t2 := t * t
|
||||
t3 := t2 * t
|
||||
d := 1 - t
|
||||
d2 := d * d
|
||||
|
||||
nx := 3*d2*t*x0 + 3*d*t2*x1 + t3
|
||||
dxdt := 3*d2*x0 + 6*d*t*(x1-x0) + 3*t2*(1-x1)
|
||||
if dxdt == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
t -= (nx - x) / dxdt
|
||||
if t <= 0 || t >= 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if t < 0 {
|
||||
t = 0
|
||||
}
|
||||
if t > 1 {
|
||||
t = 1
|
||||
}
|
||||
|
||||
// Solve for y using t.
|
||||
t2 := t * t
|
||||
t3 := t2 * t
|
||||
d := 1 - t
|
||||
d2 := d * d
|
||||
y := 3*d2*t*y0 + 3*d*t2*y1 + t3
|
||||
|
||||
return y
|
||||
}
|
||||
}
|
53
exp/sprite/clock/tween_test.go
Normal file
53
exp/sprite/clock/tween_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package clock
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLinear(t *testing.T) {
|
||||
t0 := Time(0)
|
||||
t1 := Time(6 * 60)
|
||||
now := Time(3 * 60)
|
||||
|
||||
if c := Linear(t0, t1, now); c != 0.5 {
|
||||
t.Errorf("c=%.2f, want 0.5", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCubicBezier(t *testing.T) {
|
||||
t0 := Time(0)
|
||||
t1 := Time(1e6)
|
||||
|
||||
tests := []struct {
|
||||
x0, y0, x1, y1 float32
|
||||
x, y float32
|
||||
}{
|
||||
{0.00, 0.1, 0.4, 1.00, 0.0, 0.00},
|
||||
{0.00, 0.1, 0.4, 1.00, 0.1, 0.26},
|
||||
{0.00, 0.1, 0.4, 1.00, 0.5, 0.79},
|
||||
{0.00, 0.1, 0.4, 1.00, 0.9, 0.99},
|
||||
{0.00, 0.1, 0.4, 1.00, 1.0, 1.00},
|
||||
{0.36, 0.2, 0.3, 0.85, 0.0, 0.0},
|
||||
{0.36, 0.2, 0.3, 0.85, 0.3059, 0.3952},
|
||||
{0.36, 0.2, 0.3, 0.85, 0.4493, 0.6408},
|
||||
{0.36, 0.2, 0.3, 0.85, 0.8116, 0.9410},
|
||||
{0.00, 0.0, 1.0, 1.00, 0.1, 0.1},
|
||||
{0.00, 0.0, 1.0, 1.00, 0.5, 0.5},
|
||||
{0.00, 0.0, 1.0, 1.00, 0.9, 0.9},
|
||||
{0.42, 0.0, 1.0, 1.00, 0.0, 0.0},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
cb := CubicBezier(test.x0, test.y0, test.x1, test.y1)
|
||||
now := t0 + Time(float32(t1-t0)*test.x)
|
||||
y := cb(t0, t1, now)
|
||||
|
||||
const epsilon = 0.01
|
||||
diff := y - test.y
|
||||
if diff < -epsilon || +epsilon < diff {
|
||||
t.Errorf("CubicBezier(%.2f,%.2f,%.2f,%.2f): for x=%.2f got y=%.2f, want %.2f", test.x0, test.y0, test.x1, test.y1, test.x, y, test.y)
|
||||
}
|
||||
}
|
||||
}
|
161
exp/sprite/glsprite/glsprite.go
Normal file
161
exp/sprite/glsprite/glsprite.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin || linux || windows
|
||||
|
||||
// Package glsprite implements a sprite Engine using OpenGL ES 2.
|
||||
//
|
||||
// Each sprite.Texture is loaded as a GL texture object and drawn
|
||||
// to the screen via an affine transform done in a simple shader.
|
||||
package glsprite
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/draw"
|
||||
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/exp/f32"
|
||||
"golang.org/x/mobile/exp/gl/glutil"
|
||||
"golang.org/x/mobile/exp/sprite"
|
||||
"golang.org/x/mobile/exp/sprite/clock"
|
||||
"golang.org/x/mobile/geom"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
// TODO: move this into package sprite as Node.EngineFields.RelTransform??
|
||||
relTransform f32.Affine
|
||||
}
|
||||
|
||||
type texture struct {
|
||||
e *engine
|
||||
glImage *glutil.Image
|
||||
b image.Rectangle
|
||||
}
|
||||
|
||||
func (t *texture) Bounds() (w, h int) { return t.b.Dx(), t.b.Dy() }
|
||||
|
||||
func (t *texture) Download(r image.Rectangle, dst draw.Image) {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
func (t *texture) Upload(r image.Rectangle, src image.Image) {
|
||||
draw.Draw(t.glImage.RGBA, r, src, src.Bounds().Min, draw.Src)
|
||||
t.glImage.Upload()
|
||||
}
|
||||
|
||||
func (t *texture) Release() {
|
||||
t.glImage.Release()
|
||||
delete(t.e.textures, t)
|
||||
}
|
||||
|
||||
// Engine creates an OpenGL-based sprite.Engine.
|
||||
func Engine(images *glutil.Images) sprite.Engine {
|
||||
return &engine{
|
||||
nodes: []*node{nil},
|
||||
images: images,
|
||||
textures: make(map[*texture]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
type engine struct {
|
||||
images *glutil.Images
|
||||
textures map[*texture]struct{}
|
||||
nodes []*node
|
||||
|
||||
absTransforms []f32.Affine
|
||||
}
|
||||
|
||||
func (e *engine) Register(n *sprite.Node) {
|
||||
if n.EngineFields.Index != 0 {
|
||||
panic("glsprite: sprite.Node already registered")
|
||||
}
|
||||
o := &node{}
|
||||
o.relTransform.Identity()
|
||||
|
||||
e.nodes = append(e.nodes, o)
|
||||
n.EngineFields.Index = int32(len(e.nodes) - 1)
|
||||
}
|
||||
|
||||
func (e *engine) Unregister(n *sprite.Node) {
|
||||
panic("todo")
|
||||
}
|
||||
|
||||
func (e *engine) LoadTexture(src image.Image) (sprite.Texture, error) {
|
||||
b := src.Bounds()
|
||||
t := &texture{
|
||||
e: e,
|
||||
glImage: e.images.NewImage(b.Dx(), b.Dy()),
|
||||
b: b,
|
||||
}
|
||||
e.textures[t] = struct{}{}
|
||||
t.Upload(b, src)
|
||||
// TODO: set "glImage.Pix = nil"?? We don't need the CPU-side image any more.
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (e *engine) SetSubTex(n *sprite.Node, x sprite.SubTex) {
|
||||
n.EngineFields.Dirty = true // TODO: do we need to propagate dirtiness up/down the tree?
|
||||
n.EngineFields.SubTex = x
|
||||
}
|
||||
|
||||
func (e *engine) SetTransform(n *sprite.Node, m f32.Affine) {
|
||||
n.EngineFields.Dirty = true // TODO: do we need to propagate dirtiness up/down the tree?
|
||||
e.nodes[n.EngineFields.Index].relTransform = m
|
||||
}
|
||||
|
||||
func (e *engine) Render(scene *sprite.Node, t clock.Time, sz size.Event) {
|
||||
e.absTransforms = append(e.absTransforms[:0], f32.Affine{
|
||||
{1, 0, 0},
|
||||
{0, 1, 0},
|
||||
})
|
||||
e.render(scene, t, sz)
|
||||
}
|
||||
|
||||
func (e *engine) render(n *sprite.Node, t clock.Time, sz size.Event) {
|
||||
if n.EngineFields.Index == 0 {
|
||||
panic("glsprite: sprite.Node not registered")
|
||||
}
|
||||
if n.Arranger != nil {
|
||||
n.Arranger.Arrange(e, n, t)
|
||||
}
|
||||
|
||||
// Push absTransforms.
|
||||
// TODO: cache absolute transforms and use EngineFields.Dirty?
|
||||
rel := &e.nodes[n.EngineFields.Index].relTransform
|
||||
m := f32.Affine{}
|
||||
m.Mul(&e.absTransforms[len(e.absTransforms)-1], rel)
|
||||
e.absTransforms = append(e.absTransforms, m)
|
||||
|
||||
if x := n.EngineFields.SubTex; x.T != nil {
|
||||
x.T.(*texture).glImage.Draw(
|
||||
sz,
|
||||
geom.Point{
|
||||
geom.Pt(m[0][2]),
|
||||
geom.Pt(m[1][2]),
|
||||
},
|
||||
geom.Point{
|
||||
geom.Pt(m[0][2] + m[0][0]),
|
||||
geom.Pt(m[1][2] + m[1][0]),
|
||||
},
|
||||
geom.Point{
|
||||
geom.Pt(m[0][2] + m[0][1]),
|
||||
geom.Pt(m[1][2] + m[1][1]),
|
||||
},
|
||||
x.R,
|
||||
)
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
e.render(c, t, sz)
|
||||
}
|
||||
|
||||
// Pop absTransforms.
|
||||
e.absTransforms = e.absTransforms[:len(e.absTransforms)-1]
|
||||
}
|
||||
|
||||
func (e *engine) Release() {
|
||||
for img := range e.textures {
|
||||
img.Release()
|
||||
}
|
||||
}
|
205
exp/sprite/portable/affine_test.go
Normal file
205
exp/sprite/portable/affine_test.go
Normal file
|
@ -0,0 +1,205 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package portable
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"math"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/exp/f32"
|
||||
"golang.org/x/mobile/geom"
|
||||
)
|
||||
|
||||
func TestAffine(t *testing.T) {
|
||||
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
|
||||
t.Skipf("testdata not available on %s", runtime.GOOS)
|
||||
}
|
||||
f, err := os.Open("../../../testdata/testpattern.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
srcOrig, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := image.NewRGBA(srcOrig.Bounds())
|
||||
draw.Draw(src, src.Rect, srcOrig, srcOrig.Bounds().Min, draw.Src)
|
||||
|
||||
const (
|
||||
pixW = 100
|
||||
pixH = 100
|
||||
ptW = geom.Pt(50)
|
||||
ptH = geom.Pt(50)
|
||||
)
|
||||
sz := size.Event{
|
||||
WidthPx: pixW,
|
||||
HeightPx: pixH,
|
||||
WidthPt: ptW,
|
||||
HeightPt: ptH,
|
||||
PixelsPerPt: float32(pixW) / float32(ptW),
|
||||
}
|
||||
|
||||
got := image.NewRGBA(image.Rect(0, 0, pixW, pixH))
|
||||
blue := image.NewUniform(color.RGBA{B: 0xff, A: 0xff})
|
||||
draw.Draw(got, got.Bounds(), blue, image.Point{}, draw.Src)
|
||||
|
||||
b := src.Bounds()
|
||||
b.Min.X += 10
|
||||
b.Max.Y /= 2
|
||||
|
||||
var a f32.Affine
|
||||
a.Identity()
|
||||
a.Scale(&a, sz.PixelsPerPt, sz.PixelsPerPt)
|
||||
a.Translate(&a, 0, 24)
|
||||
a.Rotate(&a, float32(math.Asin(12./20)))
|
||||
// See commentary in the render method defined in portable.go.
|
||||
a.Scale(&a, 40/float32(b.Dx()), 20/float32(b.Dy()))
|
||||
a.Inverse(&a)
|
||||
|
||||
affine(got, src, b, nil, &a, draw.Over)
|
||||
|
||||
ptTopLeft := geom.Point{0, 24}
|
||||
ptBottomRight := geom.Point{12 + 32, 16}
|
||||
|
||||
drawCross(got, 0, 0)
|
||||
drawCross(got, int(ptTopLeft.X.Px(sz.PixelsPerPt)), int(ptTopLeft.Y.Px(sz.PixelsPerPt)))
|
||||
drawCross(got, int(ptBottomRight.X.Px(sz.PixelsPerPt)), int(ptBottomRight.Y.Px(sz.PixelsPerPt)))
|
||||
drawCross(got, pixW-1, pixH-1)
|
||||
|
||||
const wantPath = "../../../testdata/testpattern-window.png"
|
||||
f, err = os.Open(wantPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
wantSrc, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want, ok := wantSrc.(*image.RGBA)
|
||||
if !ok {
|
||||
b := wantSrc.Bounds()
|
||||
want = image.NewRGBA(b)
|
||||
draw.Draw(want, b, wantSrc, b.Min, draw.Src)
|
||||
}
|
||||
|
||||
if !imageEq(got, want) {
|
||||
gotPath, err := writeTempPNG("testpattern-window-got", got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Errorf("got\n%s\nwant\n%s", gotPath, wantPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAffineMask(t *testing.T) {
|
||||
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
|
||||
t.Skipf("testdata not available on %s", runtime.GOOS)
|
||||
}
|
||||
f, err := os.Open("../../../testdata/testpattern.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
srcOrig, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := srcOrig.Bounds()
|
||||
src := image.NewRGBA(b)
|
||||
draw.Draw(src, src.Rect, srcOrig, b.Min, draw.Src)
|
||||
mask := image.NewAlpha(b)
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
mask.Set(x, y, color.Alpha{A: uint8(x - b.Min.X)})
|
||||
}
|
||||
}
|
||||
want := image.NewRGBA(b)
|
||||
draw.DrawMask(want, want.Rect, src, b.Min, mask, b.Min, draw.Src)
|
||||
|
||||
a := new(f32.Affine)
|
||||
a.Identity()
|
||||
got := image.NewRGBA(b)
|
||||
affine(got, src, b, mask, a, draw.Src)
|
||||
|
||||
if !imageEq(got, want) {
|
||||
gotPath, err := writeTempPNG("testpattern-mask-got", got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPath, err := writeTempPNG("testpattern-mask-want", want)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Errorf("got\n%s\nwant\n%s", gotPath, wantPath)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTempPNG(prefix string, m image.Image) (string, error) {
|
||||
f, err := os.CreateTemp("", prefix+"-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
f.Close()
|
||||
path := f.Name() + ".png"
|
||||
f, err = os.Create(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := png.Encode(f, m); err != nil {
|
||||
f.Close()
|
||||
return "", err
|
||||
}
|
||||
return path, f.Close()
|
||||
}
|
||||
|
||||
func drawCross(m *image.RGBA, x, y int) {
|
||||
c := color.RGBA{0xff, 0, 0, 0xff} // red
|
||||
m.SetRGBA(x+0, y-2, c)
|
||||
m.SetRGBA(x+0, y-1, c)
|
||||
m.SetRGBA(x-2, y+0, c)
|
||||
m.SetRGBA(x-1, y+0, c)
|
||||
m.SetRGBA(x+0, y+0, c)
|
||||
m.SetRGBA(x+1, y+0, c)
|
||||
m.SetRGBA(x+2, y+0, c)
|
||||
m.SetRGBA(x+0, y+1, c)
|
||||
m.SetRGBA(x+0, y+2, c)
|
||||
}
|
||||
|
||||
func eqEpsilon(x, y uint8) bool {
|
||||
const epsilon = 9
|
||||
return x-y < epsilon || y-x < epsilon
|
||||
}
|
||||
|
||||
func colorEq(c0, c1 color.RGBA) bool {
|
||||
return eqEpsilon(c0.R, c1.R) && eqEpsilon(c0.G, c1.G) && eqEpsilon(c0.B, c1.B) && eqEpsilon(c0.A, c1.A)
|
||||
}
|
||||
|
||||
func imageEq(m0, m1 *image.RGBA) bool {
|
||||
b0 := m0.Bounds()
|
||||
b1 := m1.Bounds()
|
||||
if b0 != b1 {
|
||||
return false
|
||||
}
|
||||
badPx := 0
|
||||
for y := b0.Min.Y; y < b0.Max.Y; y++ {
|
||||
for x := b0.Min.X; x < b0.Max.X; x++ {
|
||||
c0, c1 := m0.At(x, y).(color.RGBA), m1.At(x, y).(color.RGBA)
|
||||
if !colorEq(c0, c1) {
|
||||
badPx++
|
||||
}
|
||||
}
|
||||
}
|
||||
badFrac := float64(badPx) / float64(b0.Dx()*b0.Dy())
|
||||
return badFrac < 0.01
|
||||
}
|
193
exp/sprite/portable/portable.go
Normal file
193
exp/sprite/portable/portable.go
Normal file
|
@ -0,0 +1,193 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package portable implements a sprite Engine using the image package.
|
||||
//
|
||||
// It is intended to serve as a reference implementation for testing
|
||||
// other sprite Engines written against OpenGL, or other more exotic
|
||||
// modern hardware interfaces.
|
||||
package portable
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/draw"
|
||||
|
||||
xdraw "golang.org/x/image/draw"
|
||||
"golang.org/x/image/math/f64"
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/exp/f32"
|
||||
"golang.org/x/mobile/exp/sprite"
|
||||
"golang.org/x/mobile/exp/sprite/clock"
|
||||
)
|
||||
|
||||
// Engine builds a sprite Engine that renders onto dst.
|
||||
func Engine(dst *image.RGBA) sprite.Engine {
|
||||
return &engine{
|
||||
dst: dst,
|
||||
nodes: []*node{nil},
|
||||
}
|
||||
}
|
||||
|
||||
type node struct {
|
||||
// TODO: move this into package sprite as Node.EngineFields.RelTransform??
|
||||
relTransform f32.Affine
|
||||
}
|
||||
|
||||
type texture struct {
|
||||
m *image.RGBA
|
||||
}
|
||||
|
||||
func (t *texture) Bounds() (w, h int) {
|
||||
b := t.m.Bounds()
|
||||
return b.Dx(), b.Dy()
|
||||
}
|
||||
|
||||
func (t *texture) Download(r image.Rectangle, dst draw.Image) {
|
||||
draw.Draw(dst, r, t.m, t.m.Bounds().Min, draw.Src)
|
||||
}
|
||||
|
||||
func (t *texture) Upload(r image.Rectangle, src image.Image) {
|
||||
draw.Draw(t.m, r, src, src.Bounds().Min, draw.Src)
|
||||
}
|
||||
|
||||
func (t *texture) Release() {}
|
||||
|
||||
type engine struct {
|
||||
dst *image.RGBA
|
||||
nodes []*node
|
||||
absTransforms []f32.Affine
|
||||
}
|
||||
|
||||
func (e *engine) Register(n *sprite.Node) {
|
||||
if n.EngineFields.Index != 0 {
|
||||
panic("portable: sprite.Node already registered")
|
||||
}
|
||||
|
||||
o := &node{}
|
||||
o.relTransform.Identity()
|
||||
|
||||
e.nodes = append(e.nodes, o)
|
||||
n.EngineFields.Index = int32(len(e.nodes) - 1)
|
||||
}
|
||||
|
||||
func (e *engine) Unregister(n *sprite.Node) {
|
||||
panic("todo")
|
||||
}
|
||||
|
||||
func (e *engine) LoadTexture(m image.Image) (sprite.Texture, error) {
|
||||
b := m.Bounds()
|
||||
w, h := b.Dx(), b.Dy()
|
||||
|
||||
t := &texture{m: image.NewRGBA(image.Rect(0, 0, w, h))}
|
||||
t.Upload(b, m)
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (e *engine) SetSubTex(n *sprite.Node, x sprite.SubTex) {
|
||||
n.EngineFields.Dirty = true // TODO: do we need to propagate dirtiness up/down the tree?
|
||||
n.EngineFields.SubTex = x
|
||||
}
|
||||
|
||||
func (e *engine) SetTransform(n *sprite.Node, m f32.Affine) {
|
||||
n.EngineFields.Dirty = true // TODO: do we need to propagate dirtiness up/down the tree?
|
||||
e.nodes[n.EngineFields.Index].relTransform = m
|
||||
}
|
||||
|
||||
func (e *engine) Render(scene *sprite.Node, t clock.Time, sz size.Event) {
|
||||
// Affine transforms are done in geom.Pt. When finally drawing
|
||||
// the geom.Pt onto an image.Image we need to convert to system
|
||||
// pixels. We scale by sz.PixelsPerPt to do this.
|
||||
e.absTransforms = append(e.absTransforms[:0], f32.Affine{
|
||||
{sz.PixelsPerPt, 0, 0},
|
||||
{0, sz.PixelsPerPt, 0},
|
||||
})
|
||||
e.render(scene, t)
|
||||
}
|
||||
|
||||
func (e *engine) render(n *sprite.Node, t clock.Time) {
|
||||
if n.EngineFields.Index == 0 {
|
||||
panic("portable: sprite.Node not registered")
|
||||
}
|
||||
if n.Arranger != nil {
|
||||
n.Arranger.Arrange(e, n, t)
|
||||
}
|
||||
|
||||
// Push absTransforms.
|
||||
// TODO: cache absolute transforms and use EngineFields.Dirty?
|
||||
rel := &e.nodes[n.EngineFields.Index].relTransform
|
||||
m := f32.Affine{}
|
||||
m.Mul(&e.absTransforms[len(e.absTransforms)-1], rel)
|
||||
e.absTransforms = append(e.absTransforms, m)
|
||||
|
||||
if x := n.EngineFields.SubTex; x.T != nil {
|
||||
// Affine transforms work in geom.Pt, which is entirely
|
||||
// independent of the number of pixels in a texture. A texture
|
||||
// of any image.Rectangle bounds rendered with
|
||||
//
|
||||
// Affine{{1, 0, 0}, {0, 1, 0}}
|
||||
//
|
||||
// should have the dimensions (1pt, 1pt). To do this we divide
|
||||
// by the pixel width and height, reducing the texture to
|
||||
// (1px, 1px) of the destination image. Multiplying by
|
||||
// sz.PixelsPerPt, done in Render above, makes it (1pt, 1pt).
|
||||
dx, dy := x.R.Dx(), x.R.Dy()
|
||||
if dx > 0 && dy > 0 {
|
||||
m.Scale(&m, 1/float32(dx), 1/float32(dy))
|
||||
// TODO(nigeltao): delete the double-inverse: one here and one
|
||||
// inside func affine.
|
||||
m.Inverse(&m) // See the documentation on the affine function.
|
||||
affine(e.dst, x.T.(*texture).m, x.R, nil, &m, draw.Over)
|
||||
}
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
e.render(c, t)
|
||||
}
|
||||
|
||||
// Pop absTransforms.
|
||||
e.absTransforms = e.absTransforms[:len(e.absTransforms)-1]
|
||||
}
|
||||
|
||||
func (e *engine) Release() {}
|
||||
|
||||
// affine draws each pixel of dst using bilinear interpolation of the
|
||||
// affine-transformed position in src. This is equivalent to:
|
||||
//
|
||||
// for each (x,y) in dst:
|
||||
// dst(x,y) = bilinear interpolation of src(a*(x,y))
|
||||
//
|
||||
// While this is the simpler implementation, it can be counter-
|
||||
// intuitive as an affine transformation is usually described in terms
|
||||
// of the source, not the destination. For example, a scale transform
|
||||
//
|
||||
// Affine{{2, 0, 0}, {0, 2, 0}}
|
||||
//
|
||||
// will produce a dst that is half the size of src. To perform a
|
||||
// traditional affine transform, use the inverse of the affine matrix.
|
||||
func affine(dst *image.RGBA, src image.Image, srcb image.Rectangle, mask image.Image, a *f32.Affine, op draw.Op) {
|
||||
// For legacy compatibility reasons, the matrix a transforms from dst-space
|
||||
// to src-space. The golang.org/x/image/draw package's matrices transform
|
||||
// from src-space to dst-space, so we invert (and adjust for different
|
||||
// origins).
|
||||
i := *a
|
||||
i[0][2] += float32(srcb.Min.X)
|
||||
i[1][2] += float32(srcb.Min.Y)
|
||||
i.Inverse(&i)
|
||||
i[0][2] += float32(dst.Rect.Min.X)
|
||||
i[1][2] += float32(dst.Rect.Min.Y)
|
||||
m := f64.Aff3{
|
||||
float64(i[0][0]),
|
||||
float64(i[0][1]),
|
||||
float64(i[0][2]),
|
||||
float64(i[1][0]),
|
||||
float64(i[1][1]),
|
||||
float64(i[1][2]),
|
||||
}
|
||||
// TODO(nigeltao): is the caller or callee responsible for detecting
|
||||
// transforms that are simple copies or scales, for which there are faster
|
||||
// implementations in the xdraw package.
|
||||
xdraw.ApproxBiLinear.Transform(dst, m, src, srcb, op, &xdraw.Options{
|
||||
SrcMask: mask,
|
||||
})
|
||||
}
|
126
exp/sprite/sprite.go
Normal file
126
exp/sprite/sprite.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package sprite provides a 2D scene graph for rendering and animation.
|
||||
//
|
||||
// A tree of nodes is drawn by a rendering Engine, provided by another
|
||||
// package. The OS-independent Go version based on the image package is:
|
||||
//
|
||||
// golang.org/x/mobile/exp/sprite/portable
|
||||
//
|
||||
// An Engine draws a screen starting at a root Node. The tree is walked
|
||||
// depth-first, with affine transformations applied at each level.
|
||||
//
|
||||
// Nodes are rendered relative to their parent.
|
||||
//
|
||||
// Typical main loop:
|
||||
//
|
||||
// for each frame {
|
||||
// quantize time.Now() to a clock.Time
|
||||
// process UI events
|
||||
// modify the scene's nodes and animations (Arranger values)
|
||||
// e.Render(scene, t, sz)
|
||||
// }
|
||||
package sprite
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/draw"
|
||||
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/exp/f32"
|
||||
"golang.org/x/mobile/exp/sprite/clock"
|
||||
)
|
||||
|
||||
type Arranger interface {
|
||||
Arrange(e Engine, n *Node, t clock.Time)
|
||||
}
|
||||
|
||||
type Texture interface {
|
||||
Bounds() (w, h int)
|
||||
Download(r image.Rectangle, dst draw.Image)
|
||||
Upload(r image.Rectangle, src image.Image)
|
||||
Release()
|
||||
}
|
||||
|
||||
type SubTex struct {
|
||||
T Texture
|
||||
R image.Rectangle
|
||||
}
|
||||
|
||||
type Engine interface {
|
||||
Register(n *Node)
|
||||
Unregister(n *Node)
|
||||
|
||||
LoadTexture(a image.Image) (Texture, error)
|
||||
|
||||
SetSubTex(n *Node, x SubTex)
|
||||
SetTransform(n *Node, m f32.Affine) // sets transform relative to parent.
|
||||
|
||||
// Render renders the scene arranged at the given time, for the given
|
||||
// window configuration (dimensions and resolution).
|
||||
Render(scene *Node, t clock.Time, sz size.Event)
|
||||
|
||||
Release()
|
||||
}
|
||||
|
||||
// A Node is a renderable element and forms a tree of Nodes.
|
||||
type Node struct {
|
||||
Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node
|
||||
|
||||
Arranger Arranger
|
||||
|
||||
// EngineFields contains fields that should only be accessed by Engine
|
||||
// implementations. It is exported because such implementations can be
|
||||
// in other packages.
|
||||
EngineFields struct {
|
||||
// TODO: separate TexDirty and TransformDirty bits?
|
||||
Dirty bool
|
||||
Index int32
|
||||
SubTex SubTex
|
||||
}
|
||||
}
|
||||
|
||||
// AppendChild adds a node c as a child of n.
|
||||
//
|
||||
// It will panic if c already has a parent or siblings.
|
||||
func (n *Node) AppendChild(c *Node) {
|
||||
if c.Parent != nil || c.PrevSibling != nil || c.NextSibling != nil {
|
||||
panic("sprite: AppendChild called for an attached child Node")
|
||||
}
|
||||
last := n.LastChild
|
||||
if last != nil {
|
||||
last.NextSibling = c
|
||||
} else {
|
||||
n.FirstChild = c
|
||||
}
|
||||
n.LastChild = c
|
||||
c.Parent = n
|
||||
c.PrevSibling = last
|
||||
}
|
||||
|
||||
// RemoveChild removes a node c that is a child of n. Afterwards, c will have
|
||||
// no parent and no siblings.
|
||||
//
|
||||
// It will panic if c's parent is not n.
|
||||
func (n *Node) RemoveChild(c *Node) {
|
||||
if c.Parent != n {
|
||||
panic("sprite: RemoveChild called for a non-child Node")
|
||||
}
|
||||
if n.FirstChild == c {
|
||||
n.FirstChild = c.NextSibling
|
||||
}
|
||||
if c.NextSibling != nil {
|
||||
c.NextSibling.PrevSibling = c.PrevSibling
|
||||
}
|
||||
if n.LastChild == c {
|
||||
n.LastChild = c.PrevSibling
|
||||
}
|
||||
if c.PrevSibling != nil {
|
||||
c.PrevSibling.NextSibling = c.NextSibling
|
||||
}
|
||||
c.Parent = nil
|
||||
c.PrevSibling = nil
|
||||
c.NextSibling = nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue