tdewolff / canvas

Cairo in Go: vector to raster, SVG, PDF, EPS, WASM, OpenGL, Gio, etc.
MIT License
1.5k stars 102 forks source link

Implement option to use Porter-Duffman src operator #193

Open dkononovGm opened 1 year ago

dkononovGm commented 1 year ago

Hi! Probably this isn't issue, but browsing through your code I coudn't find any solution. The point is that I'm trying to draw a "map" where contours (or layers) have both humps and holes (see figs attached). And in some holes the bottom layer is zero (pixels must not be filled). But when I subsequently overlay the contours and add the zero contour to the top, I see not an empty hole but the last underlying contour with minimal, but non-zero value. Could you please give some advice, how I could solve this problem?

Thanks in advance, Dmitry

frame_0

tdewolff commented 1 year ago

Do you have a small example code that reproduces this?

dkononovGm commented 1 year ago

Yes, here it is.

This is the main loop overlaping layers ordered by the area (larger areas first, smaller atop)

for _, rng := range contourPixelPoints {
        col := rng.Color
        ctx.SetFillColor(col)
        ctx.SetStrokeColor(col)
        ctx.SetStrokeWidth(0.0)

        line := pixelsToLine(rng.Line)
        //ctx.Style.FillRule = canvas.EvenOdd
        ctx.DrawPath(0, 0, line...)
    }

This the function constructing lines:

func pixelsToLine(pixelLine []LinePoint) []*canvas.Path {
    var line []*canvas.Path
    polyline := &canvas.Polyline{}
    for _, l := range pixelLine {
        polyline.Add(l.Px, l.Py)
    }
    line = append(line, polyline.Smoothen())
    return line
}

And this is the palette from which I choose the "col" (color) variable:

precipitationPalette := PaletteRangeColor{
        Ranges: [][]float64{{0.07, 0.1}, {0.1, 0.2}, {0.2, 0.3}, {0.3, 0.4}, {0.4, 0.5}, {0.5, 0.6}, {0.6, 0.7}, {0.7, 0.8}, {0.8, 0.9}, {0.9, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 6}, {6, 7}, {7, 9}, {9, 11}, {11, 13}, {13, 15}, {15, 20}, {20, 50}, {50, 70}, {70, 100}, {100, 120}, {120, 1000}}, // 1000 is an arbitrary high number
        Colors: []color.RGBA{{0x00, 0x00, 0x00, 0x00}, {220, 240, 255, 255}, {174, 220, 255, 255}, {128, 200, 255, 255}, {110, 193, 255, 255}, {91, 186, 255, 255}, {73, 179, 255, 255}, {55, 171, 255, 255}, {37, 164, 255, 255}, {18, 157, 255, 255}, {0, 150, 255, 255}, {0, 144, 245, 255}, {0, 138, 235, 255}, {0, 132, 226, 255}, {0, 127, 217, 255}, {0, 122, 208, 255}, {0, 100, 255, 255}, {0, 50, 255, 255}, {0, 38, 223, 255}, {0, 13, 160, 255}, {0, 0, 128, 255}, {0, 0, 96, 255}, {0, 0, 64, 255}, {0, 0, 32, 255}, {0, 0, 0, 255}, {0, 0, 0, 0}, {0, 0, 0, 0}},
    }

Please notice the first element in the color array. I do know that I have contours with the values {0.07, 0.1} that should correspond to transparent pixels and I know that some of these contours should be atop as shown in the picture in the first message. However I instead see only contours with the color {220, 240, 255, 255} (that is first below). If I substitute the first element in the color array with {0x00, 0x00, 0x00, 0xff}, I see black spots atop.

tdewolff commented 1 year ago

Just a quick idea, the rasterizer gives awkward results for open paths. Are you sure the paths are closed? You can close a polyline by appending the starting point at the end.

On Fri, 9 Dec 2022, 07:59 dkononovGm, @.***> wrote:

Yes, here it is.

This is the main loop overlaping layers ordered by the area (larger areas first, smaller atop)

for _, rng := range contourPixelPoints { col := rng.Color ctx.SetFillColor(col) ctx.SetStrokeColor(col) ctx.SetStrokeWidth(0.0)

  line := pixelsToLine(rng.Line)
  //ctx.Style.FillRule = canvas.EvenOdd
  ctx.DrawPath(0, 0, line...)

}

This the function constructing lines:

func pixelsToLine(pixelLine []LinePoint) []canvas.Path { var line []canvas.Path polyline := &canvas.Polyline{} for _, l := range pixelLine { polyline.Add(l.Px, l.Py) } line = append(line, polyline.Smoothen()) return line }

And this is the palette from which I choose the "col" (color) variable:

precipitationPalette := PaletteRangeColor{ Ranges: [][]float64{{0.07, 0.1}, {0.1, 0.2}, {0.2, 0.3}, {0.3, 0.4}, {0.4, 0.5}, {0.5, 0.6}, {0.6, 0.7}, {0.7, 0.8}, {0.8, 0.9}, {0.9, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 6}, {6, 7}, {7, 9}, {9, 11}, {11, 13}, {13, 15}, {15, 20}, {20, 50}, {50, 70}, {70, 100}, {100, 120}, {120, 1000}}, // 1000 is an arbitrary high number Colors: []color.RGBA{{0x00, 0x00, 0x00, 0x00}, {220, 240, 255, 255}, {174, 220, 255, 255}, {128, 200, 255, 255}, {110, 193, 255, 255}, {91, 186, 255, 255}, {73, 179, 255, 255}, {55, 171, 255, 255}, {37, 164, 255, 255}, {18, 157, 255, 255}, {0, 150, 255, 255}, {0, 144, 245, 255}, {0, 138, 235, 255}, {0, 132, 226, 255}, {0, 127, 217, 255}, {0, 122, 208, 255}, {0, 100, 255, 255}, {0, 50, 255, 255}, {0, 38, 223, 255}, {0, 13, 160, 255}, {0, 0, 128, 255}, {0, 0, 96, 255}, {0, 0, 64, 255}, {0, 0, 32, 255}, {0, 0, 0, 255}, {0, 0, 0, 0}, {0, 0, 0, 0}}, }

Please notice the first element in the color array. I do know that I have contours with the values {0.07, 0.1} that should correspond to transparent pixels and I know that some of these contours should be atop as shown in the picture in the first message. However I instead see only contours with the color {220, 240, 255, 255} (that is first below). If I substitute the first element in the color array with {0x00, 0x00, 0x00, 0xff}, I see black spots atop.

— Reply to this email directly, view it on GitHub https://github.com/tdewolff/canvas/issues/193#issuecomment-1344156834, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABKOGHR6XVY5JMFK224SHC3WMMGKLANCNFSM6AAAAAASWVCYO4 . You are receiving this because you commented.Message ID: @.***>

dkononovGm commented 1 year ago

Just a quick idea, the rasterizer gives awkward results for open paths. Are you sure the paths are closed? You can close a polyline by appending the starting point at the end.

I have just checked the contours and all of them seem to be closed (see debug output attached).

Снимок экрана 2022-12-12 в 13 47 05
dkononovGm commented 1 year ago

I have looked at your "draw" function and have a question:

// DrawPath draws a path at position (x,y) using the current draw state.
func (c *Context) DrawPath(x, y float64, paths ...*Path) {
    if !c.Style.HasFill() && !c.Style.HasStroke() {
        return
    }

    coord := c.coordView.Dot(Point{x, y})
    m := c.view.Translate(coord.X, coord.Y)
    for _, path := range paths {
        var dashes []float64
        path, dashes = path.checkDash(c.Style.DashOffset, c.Style.Dashes)
        if path.Empty() {
            continue
        }
        style := c.Style
        style.Dashes = dashes
        c.RenderPath(path, style, m)
    }
}

In the fist condition you exit from the function if !c.Style.HasFill(). As I understand, if the alpha = 0, HasFill = false and this contour won't be drawn, i.e. pixel values inside this contour won't be set to zero, but will retain the previous values (the values of the previous layer). Am I right? May it be the reason of the issue?

tdewolff commented 1 year ago

Drawing a shape is always blended with previously drawn shapes below. That is, drawing transparent pixels is like doing nothing, which is why HasFill returns false. If you want a pixel to remain transparent, never draw to it.

Would you be able to send me a small piece of working code that reproduces the issue? Then I can take a look at it.

dkononovGm commented 1 year ago

I unfortunately cannot send a small piece of the code, since it's distributed over a number of files and functions. But I can try explaining what I want to have as a result with a simple illustration:

Снимок экрана 2022-12-13 в 15 07 38

I want to draw something like in the picture with the non-filled annulus in the center. Is it possible with your package?

dkononovGm commented 1 year ago

I seem to have figured it out how to illustrate my question with a small piece of code.

Let us imagine that I have three circular contours as above. I draw them in series:

// bottom dark blue path
ctx.SetFillColor(color.Blue)
ctx.SetStrokeColor(color.Blue)
ctx.SetStrokeWidth(0.0)
line := formCircularContour(...)
ctx.DrawPath(0, 0, line...)

//middle cyan path (or light blue, I just do not remember how it is designated in the color list)
ctx.SetFillColor(color.Cyan)
ctx.SetStrokeColor(color.Cyan)
ctx.SetStrokeWidth(0.0)
line := formCircularContour(...)
ctx.DrawPath(0, 0, line...)

//upper path whose "color" I do not know. I want the pixels inside this path to be set default values {0,0,0,0} if it is possible
ctx.SetFillColor(?)
ctx.SetStrokeColor(?)
ctx.SetStrokeWidth(0.0)
line := formCircularContour(...)
ctx.DrawPath(0, 0, line...)

I of course do something like this within a loop. But the main idea is like above. So the question is if this way of drawing contours one on one is appropriate for having the picture above within the paradigm of your package, or I need to invent a different method.

tdewolff commented 1 year ago

In general, the way drawing happens in most libraries (imitating what happens when you literally paint something) is that you can't remove painted pixels below, you can only over-paint with a new color. The rasterizer actually allows the Porter-Duffman operators Over and Src, where the first is used by this library, but it seems that the second would achieve what you need. The problem is that the other renderers (PDF, SVG, ...) would need to generate consistent results which I'm not sure they can and would involve some work.

One way to fix this in your case though would be to paint a subpath in the contrary direction (clockwise/counter clockwise) within the outer path. This only works when you're sure that the higher layer path is contained in the lower layer path. E.g.:

line0 := formCircularContour(...)
line1 := formCircularContour(...)
line2 := formCircularContour(...)

// bottom dark blue path
ctx.SetFillColor(color.Blue)
ctx.SetStrokeColor(color.Blue)
ctx.SetStrokeWidth(0.0)
ctx.DrawPath(0, 0, line0.Append(line1.Reverse()))

//middle cyan path (or light blue, I just do not remember how it is designated in the color list)
ctx.SetFillColor(color.Cyan)
ctx.SetStrokeColor(color.Cyan)
ctx.SetStrokeWidth(0.0)
ctx.DrawPath(0, 0, line1.Append(line2.Reverse()))

// NOT NEEDED
//upper path whose "color" I do not know. I want the pixels inside this path to be set default values {0,0,0,0} if it is possible
//ctx.SetFillColor(?)
//ctx.SetStrokeColor(?)
//ctx.SetStrokeWidth(0.0)
//ctx.DrawPath(0, 0, line2)

I'm working on path boolean operations as we speak, which would allow line1 = line1.Not(line0) instead, which results in the same thing. Anyways, I'll take a look at whether we could implement the Src blending mode for all renderers.

dkononovGm commented 1 year ago

Oh, I see. Am I right that by adding a subpath you in fact form a "complex path" that is, in fact, a ring?

The problem is that I have both ascending and descending embedded contours. Will the idea you proposed for now work in the ascending order of layer values?

Thanks for your help. I'll try to implement this way for now and wait for updates

In general, the way drawing happens in most libraries (imitating what happens when you literally paint something) is that you can't remove painted pixels below, you can only over-paint with a new color. The rasterizer actually allows the Porter-Duffman operators Over and Src, where the first is used by this library, but it seems that the second would achieve what you need. The problem is that the other renderers (PDF, SVG, ...) would need to generate consistent results which I'm not sure they can and would involve some work.

One way to fix this in your case though would be to paint a subpath in the contrary direction (clockwise/counter clockwise) within the outer path. This only works when you're sure that the higher layer path is contained in the lower layer path. E.g.:

line0 := formCircularContour(...)
line1 := formCircularContour(...)
line2 := formCircularContour(...)

// bottom dark blue path
ctx.SetFillColor(color.Blue)
ctx.SetStrokeColor(color.Blue)
ctx.SetStrokeWidth(0.0)
ctx.DrawPath(0, 0, line0.Append(line1.Reverse()))

//middle cyan path (or light blue, I just do not remember how it is designated in the color list)
ctx.SetFillColor(color.Cyan)
ctx.SetStrokeColor(color.Cyan)
ctx.SetStrokeWidth(0.0)
ctx.DrawPath(0, 0, line1.Append(line2.Reverse()))

// NOT NEEDED
//upper path whose "color" I do not know. I want the pixels inside this path to be set default values {0,0,0,0} if it is possible
//ctx.SetFillColor(?)
//ctx.SetStrokeColor(?)
//ctx.SetStrokeWidth(0.0)
//ctx.DrawPath(0, 0, line2)

I'm working on path boolean operations as we speak, which would allow line1 = line1.Not(line0) instead, which results in the same thing. Anyways, I'll take a look at whether we could implement the Src blending mode for all renderers.

dkononovGm commented 1 year ago

Oh. And I have one more question. Do this operation line1.Append(line2.Reverse()) connect the last point of the first contour with the first point of the next contour by MoveTo, or by LineTo?

tdewolff commented 1 year ago

Yes, it subtracts the inner path from the outer path. Appending literally appends the path and does no effort to join both paths, for that we have Join.

I'm not sure what the problem is with ascending/descending. From what I understand, you always need to draw from the bottom up, and from what I see is that each next layer is contained within the previous layer, right? That is, each layer is getting smaller and doesn't intersect with the layers below.

dkononovGm commented 1 year ago

Oh. Almost, but my task is a little more complicated. I can easily have several smaller area contours within one big area contour and those smaller may have both larger and smaller values (see the initial picture in my first message of this issue). Well. I'll try to refactor my code to account for contour embedding, since now I only arrange contours by areas.

Yes, it subtracts the inner path from the outer path. Appending literally appends the path and does no effort to join both paths, for that we have Join.

I'm not sure what the problem is with ascending/descending. From what I understand, you always need to draw from the bottom up, and from what I see is that each next layer is contained within the previous layer, right? That is, each layer is getting smaller and doesn't intersect with the layers below.

tdewolff commented 1 year ago

Well, it depends on whichever value should be on top. I was assuming that darker blue should always be on top (higher value), but if this is not always the case the method above applies equally well. Just with the consideration that higher layers are contained within the lower ones (whether the value is higher/lower i.e. color more white or more blue doesn't matter). I hope that helps. Otherwise you could sort by value first?

tdewolff commented 1 year ago

@dkononovGm Is this still an issue?

dkononovGm commented 1 year ago

@dkononovGm Is this still an issue?

Yes. I still hope that you'll be able to implement that P-D operator you mentioned. :)