Issues

Issue #496 wontfix

love.graphics.setFill. No wait, scratch that, more functions instead! :D

hahawoo
created an issue

So I was thinking...

Perhaps the draw mode ('fill' or 'line') of primitive shapes should be a state, like color, line width, etc.

Is there anything which makes it that much more suited to being an argument instead of a state than color/line width/line style is?

As well as being consistent with other states that aren't arguments, it would also be a bit more consistent with other things LÖVE can draw, such as lines and points. Lines and points only have coordinate parameters, instead of drawing mode and coordinates.

The interface could look like love.graphics.setDrawMode('fill'), or maybe even love.graphics.setFill(true).

Comments (11)

  1. Bart van Strien

    I don't know, that's a lot of function noise, and it's something you change a lot anyway. While I know this can be said for colors as well, those are 4 arguments, and a lot more annoying to specify for everything.

  2. hahawoo reporter
    • changed status to open

    Yeah, it's possible that this could create a lot of function noise. The worst case scenario would be something like this:

    love.graphics.setFill(true)
    love.graphics.rectangle(10, 10, 200, 100)
    
    love.graphics.setFill(false)
    love.graphics.polygon(50, 100, 200, 250, 300, 100)
    
    love.graphics.setFill(true)
    love.graphics.circle(300, 300, 200)
    

    Or you could imagine a project where different functions were drawing things with different fill settings, and you couldn't be sure of the what the state is so you always set it before hand, just in case, like what can happen with colors.

    On the other hand, the best case scenario is this:

    love.graphics.rectangle(10, 10, 200, 100)
    love.graphics.polygon(50, 100, 200, 250, 300, 100)
    love.graphics.circle(300, 300, 200)
    

    And the second best case scenario would be to have to change the fill setting only once at the start, with all of the shapes sharing the non-default fill setting.

    The medium-case scenario would be something like this, where you have to change the fill setting at some point, but there are many shapes which use the same one close together:

    love.graphics.setFill(true)
    love.graphics.rectangle(10, 10, 10, 10)
    love.graphics.rectangle(10, 30, 10, 10)
    love.graphics.rectangle(10, 50, 10, 10)
    love.graphics.rectangle(10, 70, 10, 10)
    
    love.graphics.setFill(false)
    love.graphics.circle(300, 300, 200)
    

    I don't really know how common the worst case scenario or scenarios like it would be, but I'm going to suggest, and I'm basing this off completely nothing here, that many cases would at least be in the medium-case zone, if not in the best-case zone. Not that it means anything, but everything I've made recently has been completely in the best-case zone... because I mean, like, filled shapes... they're just so aesthetically pleasing. :P

    So I guess with that I'm just trying to say, yes, from a noise standpoint it could be worse, or it could be better, or it could be just different, and I'd say it wouldn't be too much worse in most cases, but I'm not really sure.

    But, I think having this be a state rather than a function argument makes things cleaner conceptually. I'll try to describe how I see it...

    So, LÖVE uses a lot of state for graphics things.

    love.graphics.circle('line', 20, 20, 20)
    

    What does this draw? You can't know without knowing the state of the:

    • Color
    • Blend mode
    • Line width
    • Line style
    • Coordinate system

    And I think the draw mode probably belongs on that list.

    The draw mode as an argument, it's pretty innocuous, it's easy to type and it can only be one of two things. But, does it belong there?

    Conceptually, I think it would be cleaner if the function just took the information for the geometry, and left everything else to the global state. Currently it's like...

    "The arguments are the geometry information and the draw mode. The color, line width and style, and blend mode are global state."

    But it could be...

    "The arguments are the geometry information. The draw mode, color, line width and style, and blend mode are global state."

    Like I mentioned before, lines are already all geometry information, so in a sense it would be consistent with that. I dunno, it just seems so nice to me that the information-about-the-shape and the how-it-looks are separated.

    "But the draw mode does affect the geometry, an unfilled circle isn't the same as a filled circle, it's a donut!" I said to myself.

    "Ah yes, but they are the same on a conceptual level, and the line width also affects the shape on that very literal level, and that is a state!" I responded.

    "Huh, right." I mumbled.

    And, yeah, I dunno. I just think it would be nicer to use.

    And on a kind of related note...

    Drawing shapes might be the first thing that someone does with LÖVE. Hand-waving the function love.draw() a bit for a while, you could say "hey, this draws a rectangle at the x and y position and width and height you tell it":

    function love.draw()
        love.graphics.rectangle(20, 30, 200, 150)
    end
    

    "And you can change the color..."

    function love.draw()
        love.graphics.setColor(165, 255, 80)
        love.graphics.rectangle(20, 30, 200, 150)
    end
    

    "And you can say if the rectangle is filled or not..."

    function love.draw()    
        love.graphics.setColor(165, 255, 80)
        love.graphics.setFill(false)
        love.graphics.rectangle(20, 30, 200, 150)
    end
    

    "And you can say what the width of the line is..."

    function love.draw()    
        love.graphics.setColor(165, 255, 80)
        love.graphics.setFill(false)
        love.graphics.setLineWidth(3)
        love.graphics.rectangle(20, 30, 200, 150)
    end
    

    You know what I mean? :D It's the same thing conceptually as setting the color or the line width. With love.graphics.rectangle('fill', 20, 30, 200, 150), there's a slightly different thing there. And like, what else could "fill" be?

    I now think that setFill(boolean) is way more descriptive than setDrawMode(DrawMode), the name is more obvious, and if you saw setDrawMode('fill'), you know, again, "what else could 'fill' be?" With setFill(true), it's pretty obvious.

    So yeah, I think it's nicer, and even on a "does this save any characters?" level, I assume it wouldn't that bad in most cases, does the fill mode actually change that often in practice? (I am unsure. I'm all about filled shapes. :P)

  3. hahawoo reporter

    A couple of other ideas:

    Filled and line shapes could have different functions. There are four more functions, but the functions are less complex without the draw mode enum. It also saves characters:

    love.graphics.circle(x...
    love.graphics.circleLine(x...
    love.graphics.circle('line', x...
    

    Another idea is having the draw mode at the end, with a default of "fill":

    love.graphics.circle(x, y, radius, 'line')
    

    A problem with that could be that with polygons, circles and arcs, there are a varying number of arguments before the draw mode. I don't think this would be a problem technically, but I don't think there's anything like that currently in LÖVE's API.

    I still think that global state for fill mode might be the best though. Not having global state affect the shape of primitives would be nice, but since there's already setLineWidth which does do this, I think setFill also makes sense.

    Edit: Actually... I dunno. Maybe it might be a lot of function noise? I'm actually liking the additional functions right now. It doesn't add any global state, and it reduces complexity, at the cost of more functions. But I'm actually liking the idea now... y'know, you've got rectangle for a solid rectangle, rectangleLine for the outline of a rectangle, and line for an arbitrary line. And most importantly, it solves the real issues which made me create this issue in the first place, which are that you don't have to specify the same draw mode all the time, and the draw mode isn't in with the coordinate information.

  4. josefnpat

    I actually kinda think these might be really good. As a user, you could always;

    love.graphics._rectangle = love.graphics.rectangle
    love.graphics.rectangle = function(fill,x,y,w,h)
      local old_fill = love.graphics.getFill()
      love.graphics.setFill(fill)
      local ret = love.graphics._rectangle(x,y,w,h)
      love.graphics.setFill(old_fill)
      return ret
    end
    

    With that in mind, I would be for making the entire API like this, with the idea that you could overload the functions yourself.

  5. hahawoo reporter

    To note, I don't think there are any other functions which have varying styles and don't have a default (not including state setting functions). For example, newSource can be either of type "stream" or "static". You could have two different functions for the different types, but there is a default, "stream", which makes this function nice to use. It's the same sort of thing with Source:seek, newFileData, etc.

    So basically, what I'm suggesting is just moving the type from the first argument to the function name. And some cool things about that are that there are less characters to type, and you can have a "default" in the sense that "fill" (or "line", but I think "fill" is a better "default") doesn't need to be specified. And maybe it's kinda easier to find a function than it is to find an enum.

    And it kind of divides things nicely too. For example, you can say that setLineWidth affects the *Line functions, and polygon has a limitation that polygonLine doesn't have.

  6. josefnpat

    Talking on IRC with bartbes and slime, it's hard to be objective about arguments in the love API.

    It boils down to; in what cases should love;

    • Use arguments (is often used, and not for particularly many functions)
    • Use environment setters (setting is not commonly used, or is used with many other functions)
    • pass objects into the functions (extra overhead, not newb friendly, not very pretty)

    It's really hard to be objective about it, and the only thing that the devs can go on (from what I gather) is;

    • Consistency (does this argument / [g|s]etter match the rest of the API)
    • How often the setting is used (is this something that would be used a lot; e.g. l.g.rectangle("line",...) I use for debugging and l.g.rectangle("fill",... I use for sidebars)
    • Would this change make porting from previous versions harder. (how much work does it create for developers who want to change from 0.8.0 and below to 0.9.x? is the software at a stable enough state that we should avoid making these changes, or should we be a bit more volatile, and go for more changes?)

    If you can come up with a good reason on all three of these points, I think there might be something to talk about.

  7. Log in to comment