diff --git a/README.md b/README.md index 49e764e..796bb02 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,15 @@ gc.FillGlyph(c, fontID, glyphID, outline, m, paint) `Canvas.ClipStroke` clips to the area a stroke paints, for strokes and stroked text painted with a shader. +**CMYK.** `Canvas.ResetPair(cmy, k, region)` draws into two images at +once, rasterizing once: (C, M, Y, A) and (K, K, K, A), premultiplied ink +amounts, make a CMYK destination with alpha; `Paint.Second` is what goes +into the second. Compositing treats channels alike and shaders interpolate +each on its own, so one shader per image paints CMYK. `LayerShader` blends +such layers with `Subtractive` (on the complements of the inks, PDF 2.0, +11.3.5) and, for the K image, `Black` (K of the backdrop for Hue, +Saturation and Color, of the source for Luminosity). + ## How it is fast - **Band accumulation**: edges are accumulated as area/cover into a diff --git a/blend.go b/blend.go index fa6b73d..6ff9735 100644 --- a/blend.go +++ b/blend.go @@ -82,6 +82,34 @@ type LayerShader struct { // and Alone the same group drawn onto a transparent image, of which // only the alpha is read. Both cover Src's rectangle. Initial, Alone *image.RGBA + + // Subtractive says that Src and Dst hold amounts of ink, 0 for none, + // as the images of a CMYK canvas do (Canvas.ResetPair): blend modes + // other than Normal blend the complements, 1 − c, and the result is + // complemented again (PDF 2.0, 11.3.5). In premultiplied colours the + // complement of c is α − c. The non-separable modes then blend + // (C, M, Y) as they blend (R, G, B). + Subtractive bool + // Black says that the layer is the black image of such a pair, + // (K, K, K, A): the non-separable modes take K from the backdrop + // (Hue, Saturation, Color) or from the source (Luminosity), as PDF + // 2.0, 11.3.5.3 has it; the separable ones blend K on its own. + Black bool +} + +// blendBackdrop is the blend function B(cb, cs) = cb, what the black +// image of a CMYK pair blends with in Hue, Saturation and Color. +const blendBackdrop BlendMode = 255 + +// mode returns the blend mode the shader blends with. +func (c *LayerShader) mode() BlendMode { + if c.Black && c.Blend >= BlendHue { + if c.Blend == BlendLuminosity { + return BlendNormal + } + return blendBackdrop + } + return c.Blend } // ShadeSpan implements Shader. @@ -97,7 +125,8 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) { c.removeBackdrop(y, x, sp, out) sp = unsafe.Slice((*byte)(unsafe.Pointer(&out[0])), 4*n) } - if c.Blend == BlendNormal && c.Mask == nil { + bm := c.mode() + if bm == BlendNormal && c.Mask == nil { // The layer's pixels are in the layout of out already. src := unsafe.Slice((*uint32)(unsafe.Pointer(&sp[0])), n) if c.Alpha == 255 { @@ -111,7 +140,7 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) { return } var dp []byte - if c.Blend != BlendNormal { + if bm != BlendNormal { dp = c.Dst.Pix[c.Dst.PixOffset(x, y):][: 4*n : 4*n] } var mrow []uint8 @@ -123,7 +152,7 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) { } mx = mr.Min.X } - if c.Blend == BlendNormal { + if bm == BlendNormal { // Masked Normal: scale the packed pixels by the combined alpha. src := unsafe.Slice((*uint32)(unsafe.Pointer(&sp[0])), n) k0 := uint32(c.Alpha) @@ -140,15 +169,15 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) { } return } - if c.Blend == BlendMultiply || c.Blend == BlendScreen { + if (bm == BlendMultiply || bm == BlendScreen) && !c.Subtractive { c.shadeSeparable(x, out, sp, dp, mrow, mx) return } // The separable modes look opaque pixels up in a table, taken once // per span. var tab *[1 << 16]uint8 - if c.Blend < BlendHue { - tab = blendTable(c.Blend) + if bm < BlendHue { + tab = blendTable(bm) } for i := range out { p := sp[4*i : 4*i+4 : 4*i+4] @@ -170,10 +199,24 @@ func (c *LayerShader) ShadeSpan(y, x int, out []uint32) { } if dp != nil && dp[4*i+3] != 0 { q := dp[4*i : 4*i+4 : 4*i+4] - if a == 255 && q[3] == 255 && tab != nil { - r, g, b = tab[int(q[0])<<8|int(r)], tab[int(q[1])<<8|int(g)], tab[int(q[2])<<8|int(b)] + qr, qg, qb, qa := q[0], q[1], q[2], q[3] + switch { + case bm == blendBackdrop: + // s·(1−αb) + αs·αb·cb = s·(1−αb) + αs·d. + r, g, b = backdropInt(r, a, qr, qa), backdropInt(g, a, qg, qa), backdropInt(b, a, qb, qa) + out[i] = pack(r, g, b, a) + continue + case c.Subtractive: + r, g, b = sub8(a, r), sub8(a, g), sub8(a, b) + qr, qg, qb = sub8(qa, qr), sub8(qa, qg), sub8(qa, qb) + } + if a == 255 && qa == 255 && tab != nil { + r, g, b = tab[int(qr)<<8|int(r)], tab[int(qg)<<8|int(g)], tab[int(qb)<<8|int(b)] } else { - r, g, b = blendPixel(c.Blend, r, g, b, a, q[0], q[1], q[2], q[3]) + r, g, b = blendPixel(bm, r, g, b, a, qr, qg, qb, qa) + } + if c.Subtractive { + r, g, b = sub8(a, r), sub8(a, g), sub8(a, b) } } out[i] = pack(r, g, b, a) @@ -287,6 +330,22 @@ func (c *LayerShader) shadeSeparable(x int, out []uint32, sp, dp, mrow []uint8, } } +// sub8 returns a − c, the complement of a premultiplied colour channel c +// of alpha a, 0 where c exceeds a. +func sub8(a, c uint8) uint8 { + if c > a { + return 0 + } + return a - c +} + +// backdropInt is one channel of s·(1−αb) + αs·d, the colour a source s +// of alpha sa takes the backdrop d of alpha da with. +func backdropInt(s, sa, d, da uint8) uint8 { + v := div255(uint32(s)*(255-uint32(da)) + uint32(sa)*uint32(d)) + return uint8(min(v, uint32(sa))) +} + // overAlpha is negative if a colour channel exceeds alpha. It does not // branch: on varied colours, the branches of max(r, g, b) > a mispredict. func overAlpha(r, g, b, a uint8) int32 { diff --git a/canvas.go b/canvas.go index a57e27b..495ab28 100644 --- a/canvas.go +++ b/canvas.go @@ -13,8 +13,26 @@ import ( type Paint struct { Color color.RGBA // premultiplied Shader Shader + // Second is what is painted into the second image of a canvas that + // draws into two (Canvas.ResetPair); nil paints Color or Shader there + // too. + Second *Paint } +// second returns what p paints into a second image. +func (p *Paint) second() *Paint { + if p.Second != nil { + return p.Second + } + return p +} + +// invisible reports that p paints nothing. +func (p *Paint) invisible() bool { return p.Shader == nil && p.Color.A == 0 } + +// opaque reports that p is an opaque solid colour. +func (p *Paint) opaque() bool { return p.Shader == nil && p.Color.A == 255 } + // Errors reported by Canvas.Err. var ( ErrEdgeBudget = errors.New("stilus: edge budget exceeded, path truncated") @@ -35,6 +53,13 @@ const DefaultMaxClipDepth = 256 // band is rendered by passing an image whose Rect is that tile, or by // restricting Reset's region. A Canvas is not safe for concurrent use; use // one per worker. After warm-up a Canvas does not allocate. +// +// A canvas reset with ResetPair draws into two images at once: what it +// paints is rasterized once and composited into both, each with its paint +// (Paint.Second). Two images of premultiplied ink amounts, (C, M, Y, A) and +// (K, K, K, A), make a CMYK destination with alpha: compositing treats +// the channels alike, and gradients and images interpolate each on its +// own, so that a shader per image paints a CMYK one. type Canvas struct { // MaxClipDepth overrides DefaultMaxClipDepth when non-zero. MaxClipDepth int @@ -46,6 +71,14 @@ type Canvas struct { s Stroker solid SolidBlitter shader ShaderBlitter + + // second is the second image of ResetPair, nil for none, and what + // paints into it. + second *image.RGBA + solid2 SolidBlitter + shader2 ShaderBlitter + pair pairBlitter + mread maskBlitter frac fracBlitter scale scaleBlitter @@ -73,7 +106,7 @@ func NewCanvas(dst *image.RGBA) *Canvas { // Reset retargets the canvas to dst, limited to region (in dst's // coordinates), clears the clip stack and the error. func (c *Canvas) Reset(dst *image.RGBA, region image.Rectangle) { - c.dst = dst + c.dst, c.second = dst, nil c.solid.t.set(dst) c.shader.t.set(dst) c.stack = append(c.stack[:0], newClipState(region.Intersect(dst.Bounds()))) @@ -81,6 +114,39 @@ func (c *Canvas) Reset(dst *image.RGBA, region image.Rectangle) { c.err = nil } +// ResetPair is Reset with a second image that everything is drawn into +// as well, with the second paint of each Paint; region is limited to both +// images. +func (c *Canvas) ResetPair(dst, second *image.RGBA, region image.Rectangle) { + c.Reset(dst, region.Intersect(second.Bounds())) + c.second = second + c.solid2.t.set(second) + c.shader2.t.set(second) +} + +// pairBlitter blits the same coverage through two blitters. +type pairBlitter struct{ a, b Blitter } + +func (p *pairBlitter) BlitRun(y, x0, x1 int, alpha uint8) { + p.a.BlitRun(y, x0, x1, alpha) + p.b.BlitRun(y, x0, x1, alpha) +} + +func (p *pairBlitter) BlitCoverage(y, x int, cov []uint8) { + p.a.BlitCoverage(y, x, cov) + p.b.BlitCoverage(y, x, cov) +} + +// invisible reports that p paints nothing into the canvas's images. +func (c *Canvas) invisible(p *Paint) bool { + return p.invisible() && (c.second == nil || p.second().invisible()) +} + +// opaque reports that p is an opaque solid colour in every image. +func (c *Canvas) opaque(p *Paint) bool { + return p.opaque() && (c.second == nil || p.second().opaque()) +} + // Err returns the first error since Reset, if any. Drawing continues after // an error; the error only reports that output may be incomplete. func (c *Canvas) Err() error { return c.err } @@ -136,12 +202,22 @@ func (c *Canvas) chain(st *clipState, b Blitter) Blitter { } func (c *Canvas) paint(p *Paint) Blitter { + b := paintWith(p, &c.solid, &c.shader) + if c.second == nil { + return b + } + c.pair.a, c.pair.b = b, paintWith(p.second(), &c.solid2, &c.shader2) + return &c.pair +} + +// paintWith returns the blitter of p, of the two given. +func paintWith(p *Paint, solid *SolidBlitter, shader *ShaderBlitter) Blitter { if p.Shader != nil { - c.shader.setShader(p.Shader) - return &c.shader + shader.setShader(p.Shader) + return shader } - c.solid.SetColor(p.Color) - return &c.solid + solid.SetColor(p.Color) + return solid } func (c *Canvas) setClip(b image.Rectangle) { @@ -160,7 +236,7 @@ func (c *Canvas) checkBudget() { func (c *Canvas) Fill(p *Path, m Matrix, rule FillRule, paint *Paint) { defer c.guard() st := c.top() - if st.bounds.Empty() || (paint.Shader == nil && paint.Color.A == 0) { + if st.bounds.Empty() || c.invisible(paint) { return } c.setClip(st.bounds) @@ -201,7 +277,7 @@ func (c *Canvas) fillRect(p *Path, m Matrix, st *clipState, paint *Paint) bool { func (c *Canvas) Stroke(p *Path, m Matrix, st *StrokeStyle, paint *Paint) { defer c.guard() cs := c.top() - if cs.bounds.Empty() || (paint.Shader == nil && paint.Color.A == 0) || len(p.Points) == 0 { + if cs.bounds.Empty() || c.invisible(paint) || len(p.Points) == 0 { return } pr := prepStroke(m, st) @@ -266,7 +342,7 @@ func (c *Canvas) strokePaint(cs *clipState, paint *Paint, pr strokePrep) (Blitte // sets c.seg up for it: b is the stroke's blitter chain, overlap and single // are mayOverlap and singleSegment of its path and style. func (c *Canvas) strokeFast(cs *clipState, b Blitter, paint *Paint, pr strokePrep, overlap, single bool) bool { - opaque := paint.Shader == nil && paint.Color.A == 255 + opaque := c.opaque(paint) if pr.dense || !(opaque && (cs.mask == nil || !overlap) || single) { return false } @@ -282,7 +358,7 @@ func (c *Canvas) strokeFast(cs *clipState, b Blitter, paint *Paint, pr strokePre c.seg.r, c.seg.b = &c.r, b c.seg.solid = nil c.seg.frac = cs.frac - if cs.mask == nil && opaque { + if cs.mask == nil && opaque && c.second == nil { c.seg.solid = &c.solid } c.seg.setBorder(cs, overlap) diff --git a/cmyk_test.go b/cmyk_test.go new file mode 100644 index 0000000..484b61a --- /dev/null +++ b/cmyk_test.go @@ -0,0 +1,221 @@ +package stilus + +import ( + "bytes" + "image" + "image/color" + "math" + "math/rand/v2" + "testing" +) + +// rampShader paints a premultiplied colour that changes with x and y, its +// channels scaled by k so that two of them differ. +type rampShader struct{ k uint32 } + +func (s rampShader) ShadeSpan(y, x int, dst []uint32) { + for i := range dst { + a := uint32(128 + (x+i+y)%128) + v := (uint32(x+i) * s.k) % (a + 1) + dst[i] = PackRGBA(color.RGBA{uint8(v), uint8(v / 2), uint8(a - v/3), uint8(a)}) + } +} + +// pairScene draws the same scene with the given canvas and a source of +// paints: fills of curves, opaque and translucent strokes (the analytic +// path among them), a dashed stroke, a pixel-aligned rectangle, shader +// fills, glyphs from the cache (blitted and through the mask shader), all +// under a fractional clip rectangle and a clip path. +func pairScene(c *Canvas, gc *GlyphCache, paint func(i int) *Paint) { + var p Path + p.MoveTo(10, 10) + p.CubicTo(60, 0, 90, 80, 30, 70) + p.Close() + var clip Path + clip.MoveTo(5, 5) + clip.LineTo(115, 12) + clip.LineTo(100, 95) + clip.LineTo(8, 88) + clip.Close() + c.ClipRect(Rect{X0: 2.5, Y0: 3.25, X1: 117.75, Y1: 96.5}, Identity) + c.ClipPath(&clip, Identity, NonZero) + c.Fill(&p, Identity, NonZero, paint(0)) + var line Path + line.MoveTo(5, 50) + line.LineTo(115, 60) + c.Stroke(&line, Identity, &StrokeStyle{Width: 6, MiterLimit: 10}, paint(1)) + c.Stroke(&line, Matrix{1, 0, 0, 1, 0, 15}, &StrokeStyle{Width: 3, MiterLimit: 10}, paint(2)) + c.Stroke(&p, Identity, &StrokeStyle{Width: 2, MiterLimit: 10, Dash: []float64{4, 3}}, paint(3)) + var r Path + r.Rect(70, 20, 30, 20) + c.Fill(&r, Identity, NonZero, paint(4)) + c.Fill(&r, Matrix{1, 0, 0, 1, -50, 40}, NonZero, paint(5)) + var g Path + g.MoveTo(0, 0) + g.LineTo(0.6, 0) + g.LineTo(0.3, -0.7) + g.Close() + for i := range 6 { + gc.FillGlyph(c, 1, 7, &g, Matrix{14, 0, 0, 14, float64(15 + 16*i), 90}, paint(i%3)) + } + c.PopClip() + c.PopClip() + // Without a clip: the opaque stroke's analytic rows and the glyph + // mask's direct blit. + c.Stroke(&line, Matrix{1, 0, 0, 1, 0, -30}, &StrokeStyle{Width: 5, MiterLimit: 10}, paint(1)) + c.Stroke(&line, Matrix{1, 0, 0, 1, 0, -40}, &StrokeStyle{Width: 4, MiterLimit: 10}, paint(0)) + for i := range 6 { + gc.FillGlyph(c, 1, 7, &g, Matrix{14, 0, 0, 14, float64(10 + 18*i), 30}, paint([3]int{0, 4, 2}[i%3])) + } +} + +// TestCanvasPair draws into two images at once, each with its paint, and +// gets the bytes of drawing them one by one. +func TestCanvasPair(t *testing.T) { + rect := image.Rect(0, 0, 120, 100) + first := []*Paint{ + {Color: color.RGBA{200, 10, 30, 255}}, {Color: color.RGBA{0, 90, 0, 255}}, {Color: color.RGBA{40, 40, 0, 128}}, + {Color: color.RGBA{0, 0, 255, 255}}, {Color: color.RGBA{9, 8, 7, 255}}, {Shader: rampShader{3}}, + } + second := []*Paint{ + {Color: color.RGBA{60, 60, 60, 255}}, {Shader: rampShader{5}}, {Color: color.RGBA{20, 20, 20, 128}}, + {Color: color.RGBA{255, 255, 255, 255}}, {Color: color.RGBA{1, 1, 1, 255}}, {Color: color.RGBA{0, 0, 0, 0}}, + } + one := func(ps []*Paint) *image.RGBA { + img := image.NewRGBA(rect) + c := NewCanvas(img) + gc := GlyphCache{MaxBytes: 1 << 20} + pairScene(c, &gc, func(i int) *Paint { return ps[i] }) + return img + } + want1, want2 := one(first), one(second) + + a, b := image.NewRGBA(rect), image.NewRGBA(rect) + c := NewCanvas(a) + c.ResetPair(a, b, rect) + gc := GlyphCache{MaxBytes: 1 << 20} + pairScene(c, &gc, func(i int) *Paint { + p := *first[i] + p.Second = second[i] + return &p + }) + if !bytes.Equal(a.Pix, want1.Pix) { + t.Error("the first image is not what it is drawn alone") + } + if !bytes.Equal(b.Pix, want2.Pix) { + t.Error("the second image is not what it is drawn alone") + } + if c.Err() != nil { + t.Error(c.Err()) + } + + // Without a second paint the same is painted into both; after Reset + // the canvas draws into one image again. + a, b = image.NewRGBA(rect), image.NewRGBA(rect) + c.ResetPair(a, b, rect) + pairScene(c, &gc, func(i int) *Paint { return first[i] }) + if !bytes.Equal(a.Pix, want1.Pix) || !bytes.Equal(b.Pix, want1.Pix) { + t.Error("one paint for both images") + } + c.Reset(a, rect) + if c.second != nil { + t.Error("Reset keeps the second image") + } +} + +// randomPremul returns a premultiplied pixel, opaque one time in four. +func randomPremul(rng *rand.Rand) [4]uint8 { + a := uint8(rng.IntN(256)) + if rng.IntN(4) == 0 { + a = 255 + } + var p [4]uint8 + for i := range 3 { + p[i] = uint8(rng.IntN(int(a) + 1)) + } + p[3] = a + return p +} + +func layerImages(px [][4]uint8) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, len(px), 1)) + for i, p := range px { + copy(img.Pix[4*i:], p[:]) + } + return img +} + +// TestLayerShaderSubtractive blends ink amounts on their complements: each +// mode within a level of B(cb, cs) = 1 − B(1 − cb, 1 − cs) computed in +// floating point, Normal as it is, and the black image of a pair with K of +// the backdrop (Hue, Saturation, Color) or the source (Luminosity). +func TestLayerShaderSubtractive(t *testing.T) { + rng := rand.New(rand.NewPCG(9, 10)) + const n = 400 + src, dst := make([][4]uint8, n), make([][4]uint8, n) + for i := range n { + src[i], dst[i] = randomPremul(rng), randomPremul(rng) + } + s, d := layerImages(src), layerImages(dst) + out := make([]uint32, n) + for bm := BlendNormal; bm <= BlendLuminosity; bm++ { + for _, black := range []bool{false, true} { + if black && bm < BlendHue { + continue + } + ls := LayerShader{Src: s, Dst: d, Alpha: 255, Blend: bm, Subtractive: true, Black: black} + ls.ShadeSpan(0, 0, out) + for i := range n { + got := UnpackRGBA(out[i]) + sp, dp := src[i], dst[i] + want := subtractiveRef(bm, black, sp, dp) + g := [3]uint8{got.R, got.G, got.B} + for k := range 3 { + if math.Abs(float64(g[k])-want[k]) > 1 || got.A != sp[3] { + t.Fatalf("%v black %v: pixel %d: %v over %v gives %v, want %v", bm, black, i, sp, dp, got, want) + } + } + } + } + } +} + +// subtractiveRef is the colour the shader returns, s·(1−αb) + αs·αb·B, of +// premultiplied ink amounts, in floating point. +func subtractiveRef(bm BlendMode, black bool, sp, dp [4]uint8) [3]float64 { + as, ab := float64(sp[3])/255, float64(dp[3])/255 + var cs, cb [3]float64 + for k := range 3 { + if sp[3] > 0 { + cs[k] = min(float64(sp[k])/float64(sp[3]), 1) + } + if dp[3] > 0 { + cb[k] = min(float64(dp[k])/float64(dp[3]), 1) + } + } + var bl [3]float64 + switch { + case bm == BlendNormal || black && bm == BlendLuminosity: + bl = cs + case black: + bl = cb + default: + var ics, icb [3]float64 + for k := range 3 { + ics[k], icb[k] = 1-cs[k], 1-cb[k] + } + r := Blend(bm, icb, ics) + for k := range 3 { + bl[k] = 1 - r[k] + } + } + var o [3]float64 + for k := range 3 { + if dp[3] == 0 { + o[k] = float64(sp[k]) + continue + } + o[k] = float64(sp[k])*(1-ab) + as*ab*bl[k]*255 + } + return o +} diff --git a/glyphcache.go b/glyphcache.go index 7128406..102027a 100644 --- a/glyphcache.go +++ b/glyphcache.go @@ -75,6 +75,10 @@ type GlyphCache struct { shader MaskShader rect Path paint Paint + // shader2 and paint2 paint the second image of a canvas that draws + // into two. + shader2 MaskShader + paint2 Paint } // FillGlyph fills outline (nonzero), transformed by m into device space, @@ -95,7 +99,7 @@ func (gc *GlyphCache) FillGlyph(c *Canvas, font uint64, glyph int32, outline *Pa return } ox, oy := math.Floor(m[4]), math.Floor(m[5]) - if paint.Shader != nil || !(sigmaMax(m) <= MaxCachedEm) || + if paint.Shader != nil || c.second != nil && paint.second().Shader != nil || !(sigmaMax(m) <= MaxCachedEm) || !(math.Abs(ox) < 1<<24 && math.Abs(oy) < 1<<24) { c.Fill(outline, m, NonZero, paint) return @@ -164,24 +168,33 @@ func (gc *GlyphCache) FillGlyph(c *Canvas, font uint64, glyph int32, outline *Pa const lim = 1 << 23 if st := c.top(); st.mask == nil && st.clearOfFrac(r.Intersect(clip)) && (c.r.MaxEdges == 0 || c.r.MaxEdges >= 2) && r.Min.X >= -lim && r.Min.Y >= -lim && r.Max.X <= lim && r.Max.Y <= lim { - c.blitMask(mask, origin, PackRGBA(paint.Color), r.Intersect(clip)) + c.blitMask(&c.solid.t, mask, origin, PackRGBA(paint.Color), r.Intersect(clip)) + if c.second != nil { + c.blitMask(&c.solid2.t, mask, origin, PackRGBA(paint.second().Color), r.Intersect(clip)) + } return } gc.shader = MaskShader{Mask: mask, X: origin.X, Y: origin.Y, Color: PackRGBA(paint.Color)} gc.paint.Shader = &gc.shader + gc.paint.Second = nil + if c.second != nil { + gc.shader2 = MaskShader{Mask: mask, X: origin.X, Y: origin.Y, Color: PackRGBA(paint.second().Color)} + gc.paint2.Shader = &gc.shader2 + gc.paint.Second = &gc.paint2 + } gc.rect.Reset() gc.rect.Rect(float32(r.Min.X), float32(r.Min.Y), float32(r.Dx()), float32(r.Dy())) c.Fill(&gc.rect, Identity, NonZero, &gc.paint) - gc.shader.Mask = nil + gc.shader.Mask, gc.shader2.Mask = nil, nil } // blitMask composites the premultiplied colour col through mask, moved by -// origin, over the pixels of r, which must lie within the clip bounds and -// the moved mask. It rounds as the MaskShader path does (the colour scaled +// origin, over the pixels of r of t, which must lie within the clip bounds +// and the moved mask. It rounds as the MaskShader path does (the colour scaled // by coverage, then composited over), so an opaque colour does not go // through covOpaque, whose single blend can round differently: only full // coverage, where both agree, stores the colour. -func (c *Canvas) blitMask(mask *image.Alpha, origin image.Point, col uint32, r image.Rectangle) { +func (c *Canvas) blitMask(t *target, mask *image.Alpha, origin image.Point, col uint32, r image.Rectangle) { defer c.guard() if col == 0 { return @@ -191,7 +204,7 @@ func (c *Canvas) blitMask(mask *image.Alpha, origin image.Point, col uint32, r i for y := r.Min.Y; y < r.Max.Y; y++ { o := mask.PixOffset(r.Min.X-origin.X, y-origin.Y) cov := mask.Pix[o : o+r.Dx()] - d := c.solid.t.row(y, r.Min.X, r.Max.X) + d := t.row(y, r.Min.X, r.Max.X) if !opaque { covOver(d, cov, cx) continue diff --git a/shape.go b/shape.go index 0b24a32..d6417d3 100644 --- a/shape.go +++ b/shape.go @@ -558,7 +558,7 @@ func (s *Shape) Bounds() image.Rectangle { return s.bounds } func (c *Canvas) FillShape(s *Shape, paint *Paint) { defer c.guard() cs := c.top() - if cs.bounds.Empty() || (paint.Shader == nil && paint.Color.A == 0) { + if cs.bounds.Empty() || c.invisible(paint) { return } switch s.kind { diff --git a/union.go b/union.go index 436268c..a65c571 100644 --- a/union.go +++ b/union.go @@ -191,7 +191,7 @@ func signedArea(p *Path) float64 { func (c *Canvas) FillUnion(u *Union, paint *Paint) { defer c.guard() cs := c.top() - if cs.bounds.Empty() || len(u.ops) == 0 || (paint.Shader == nil && paint.Color.A == 0) { + if cs.bounds.Empty() || len(u.ops) == 0 || c.invisible(paint) { return } c.setClip(cs.bounds)