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 // geom.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)) 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 TestAffine(t *testing.T) { 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) ) geom.PixelsPerPt = float32(pixW) / float32(ptW) geom.Width = ptW geom.Height = ptH 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, geom.PixelsPerPt, geom.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()), int(ptTopLeft.Y.Px())) drawCross(got, int(ptBottomRight.X.Px()), int(ptBottomRight.Y.Px())) 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) } }