Seamless, repeating pattern mode [WIP]

So I had this helper function that wraps values into a range and I was wondering, if it works for repeated patterns, too. And indeed, it does:

It’s extremely similar to the symmetry implementation. But first no matter where the user draws, the position gets wrapped into a range from 0.0 to patternsize. Then comes a for loop and draws the dab nine times at once with an offset of patternsize (surprise: this impacts performance)…

mypaint-tiled-surface.c, line 606:

// Pattern pass
if(self->surface_do_pattern){
    const int p_Xsize = self->surface_pattern_X_size;
    const int p_Ysize = self->surface_pattern_Y_size;
    // no matter where the user draws, the dabs will land in a range of 0.0 to size		
    x = circular_wrap(x,p_Xsize); // circular_wrap() in helpers.c from pigmentblending-test branch
    y = circular_wrap(y,p_Ysize);		
    for (int i = 0; i <= p_Xsize*2; i= i + p_Xsize) {
        for (int j = 0; j <= p_Ysize*2; j= j + p_Ysize) {
            if(draw_dab_internal(self, x + i, y + j, radius, color_r, color_g, color_b,
            opaque, hardness, color_a, aspect_ratio, angle,
            lock_alpha, colorize)) {
                surface_modified = TRUE;
            }			
        }
    }
} else
// Normal pass
if (draw_dab_internal(self, x, y, radius, color_r, color_g, color_b,
opaque, hardness, color_a, aspect_ratio, angle,
lock_alpha, colorize)) {
    surface_modified = TRUE;
}
// Symmetry pass
if(self->surface_do_symmetry) {
    const float symm_x = self->surface_center_x + (self->surface_center_x - x);
    if (draw_dab_internal(self, symm_x, y, radius, color_r, color_g, color_b,
    opaque, hardness, color_a, aspect_ratio, -angle,
    lock_alpha, colorize)) {
        surface_modified = TRUE;
    }
}

Plus a few more lines to set it all up, just like symmetry. See git:

This already works “okay” for development with hardcoded values and manual export using the frame with the same values as patternsize. But what it really needs is a GUI. I did have the simple, minimalistic idea to connect it to the frame. However I’m completely lost in that whole GUI code, tried to figure out how symmetry or the frame work, how the values get pulled and pushed from gui to brushengine. I’m still confused… :dizzy_face:

This may not be the most intuitive solution, but at least it doesn’t need extra menus/windows/overlays. Three new elements in the frame options: Headline “Seamless Pattern”, Active Checkbox, Button to Set patternsize from Frame. When the Button is hit, two things should happen: First the pattern x & y sizes get set to the frame size, second the frame moves in the middle of the pattern, which should be x = x-size (of frame&pattern), y = y-size (of frame&pattern)… Top left corner of the top left pattern “tile” sits at 0,0.

Things that suck: Performance could be better, maybe if the 9x drawing of the same dab is moved further into draw_dab_internal or something. Drawing next to the covered area picks up transparency when smudging. And it would be a lot more awesome if the pattern covers a bigger area - Maybe instead of actually drawing multiple dabs, it might be possible to just render the same area multiple times…

3 Likes

I really wished for a mode like this a few weeks ago. :slight_smile:

Hope you manage to fix the performance issues and that this becomes a standard feature of MyPaint!

A bigger issue is the GUI. I’m still trying to figure it out. Just adding a simple checkbox looks sooooo complex.

The performance is not extremely bad. At least I was able to draw the test patterns on a low end Cintiq Companion 2.

It’s in a Git branch:

I have some python + GTK experience. If you’re talking about the code in mypaint itself, rather than libmypaint, this breakdown might help. It covers what you need to do for any widget:

  • know what container you are going to add it into (usually a VBox or HBox that already exists).
  • Actually create it (this should be a single line, like mycheck = Gtk.CheckButton("foo")
  • Add it to the container by calling the pack_start() or pack_end() method of the container widget. eg `container.pack_end()
  • Make it visible, eg mycheck.show(). If there is a later call to container.show_all(), then this isn’t needed (as show_all() shows all widgets inside of a container widget)

After that, you probably want to define a function and connect it to the toggled signal (reference), so that it affects MyPaint internal state instead of just … sitting there being a checkbox. The function should probably just check the state of the box (widget.get_active() IIRC) and set the internal state of MyPaint accordingly.

Another tip that’s really essential for me: look up the GTK docs as needed . For example, pack_start. As that is the C API documentation, you can generally ignore the first parameter – this is equivalent to the object you are calling the method on in Python. So in this case you can see it expects:

  1. the child widget you want to pack (mycheck in my example)
  2. True or False indicating whether the widget should get extra space allocated to it if the parent widget has space beyond what is needed.
  3. True or False indicating whether the actual drawn size of the widget should encompass any extra space allocated to it. Otherwise, the extra space only pads it. False is what you usually want here IME.
1 Like

This option should really be added to the symmetry tool since you are essentially mirroring the edges of the pattern instead of the center.

Plus it could have its own framing tool.

Wrapping != Mirroring , although I agree the option belongs in symmetry tool.

Your comment does remind me though that the number of dabs drawn here can probably be reduced to 4/2/1, given the premise that all tiles are identical and no dabs fill an entire tile.

Reminding that we are working with a 3x3 grid of tiles here, there are three cases:

  1. The dab won’t overlap any tile edges. Do one dab and copy this tile onto the other tiles.
  2. The dab will overlap tile edges on 1 axis only. Do a dab on both edges of that tile. 3 tiles are affected. Take the tile that was affected by both dabs and copy it over all other tiles.
  3. The dab will overlap tile edges on 2 axes (ie. it’s on or near a corner). Do four dabs. take the tile and copy it over all other tiles.

AFAICS this is minimal for MyPaint’s system. In a system like GIMP’s, where paint is rendered into a buffer, you’d have the option of rendering a single dab and then chopping it up into 4 bits which you can place at the other edges.

One challenge here is that you would need to place the exact same dab down. That means probably at minimum some RNG-manipulation to make sure that any randomization used by the dabs was identical. (actually it occurs to me that the current 9dab implementation probably has this issue too…)

This will be very welcome - especially by game artists!
I hope you manage to get it merged to trunk. Please keep going :smiley: