Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

color() not working with the hex notation when used before setup() #348

Closed
villares opened this issue Sep 10, 2023 · 20 comments · Fixed by #353
Closed

color() not working with the hex notation when used before setup() #348

villares opened this issue Sep 10, 2023 · 20 comments · Fixed by #353

Comments

@villares
Copy link
Contributor

image

import py5

ca = py5.color('#ffcc00')
cb = py5.color(255, 204, 0)

def setup():
    py5.size(300, 200)
    py5.fill(ca)
    py5.rect(0, 0, 100, 200)
    py5.fill(cb)
    py5.rect(100, 0, 100, 200)
    cc = py5.color('#ffcc00')
    py5.fill(cc)
    py5.rect(200, 0, 100, 200)
    print(ca, cb, cc)

py5.run_sketch()

ca, created before setup(), should match cb and cc.

@hx2A
Copy link
Collaborator

hx2A commented Sep 10, 2023

Thanks for reporting this!

Incidentally, the "hex converter" code correctly converts the hex code to the proper value -13312, but then that value is passed to the real Processing color() method. That single negative integer argument gets routed to this Java code:

  public final int color(float fgray) {
    if (g == null) {
      int gray = (int) fgray;
      if (gray > 255) gray = 255; else if (gray < 0) gray = 0;
      return 0xff000000 | (gray << 16) | (gray << 8) | gray;
    }
    return g.color(fgray);
  }

When this is called before the Sketch starts, the g variable is null. The if (gray < 0) gray = 0; bit causes the output to be the same as the output of py5.color(0, 0, 0, 255).

The fix here is for color(), if the hex converter can parse the user input, there is no need to call Processing's color() method.

@hx2A
Copy link
Collaborator

hx2A commented Sep 10, 2023

Here's where I am now. This code:

ca = py5.color(255, 204, 0)
cb = py5.color("#FFCC00")


def setup():
    py5.size(200, 200)
    cc = py5.color(255, 204, 0)
    cd = py5.color("#ffcc00")

    py5.println(ca)
    py5.println(cb)
    py5.println(cc)
    py5.println(cd)

    py5.color_mode(py5.HSB, 360, 100, 100)

    py5.println(ca)
    py5.println(cb)
    py5.println(cc)
    py5.println(cd)

    py5.println(ca.to_hex())
    py5.println(cb.to_hex())
    py5.println(cc.to_hex())
    py5.println(cd.to_hex())

yields this output:

(red=255, green=204, blue=0, alpha=255)
(red=255, green=204, blue=0, alpha=255)
(red=255, green=204, blue=0, alpha=255)
(red=255, green=204, blue=0, alpha=255)
(hue=48.000004, saturation=100.000000, brightness=100.000000, alpha=255.000000)
(hue=48.000004, saturation=100.000000, brightness=100.000000, alpha=255.000000)
(hue=48.000004, saturation=100.000000, brightness=100.000000, alpha=255.000000)
(hue=48.000004, saturation=100.000000, brightness=100.000000, alpha=255.000000)
#FFFFCC00
#FFFFCC00
#FFFFCC00
#FFFFCC00

Each Py5Color object needs to know the color mode settings and is therefore eternally linked to its creator. I didn't format the output to be something like Py5Color(red=255, green=204, blue=0, alpha=255) because that syntax would suggest that that is py5 valid code, which theoretically could be true, but without a link to a creator with color mode settings this would cause a lot of complications and doesn't add any value over py5.color(255, 204, 0).

I need to add intelligent rounding to those floats. The .to_hex() piece is an extra idea I had and perhaps I can add some others like .red(), .hue(), etc. Maybe these could be properties instead of method calls? I think that would be a good idea.

@hx2A
Copy link
Collaborator

hx2A commented Sep 10, 2023

#FFFFCC00
#FFFFCC00
#FFFFCC00
#FFFFCC00

I just noticed these 8 character hex values are wrong. It should be #FFCC00FF because the alpha value goes last.

@villares
Copy link
Contributor Author

villares commented Sep 10, 2023

Looks wonderful!

What if the color objects displayed always the same representation, irrespective of the color mode?
Something like:
Py5Color «red=255, green=204, blue=0, hue=48, saturation=100, brightness=100, alpha=255»

I admit I kind of like the idea of having valid Python on the __repr__, but I'm afraid it isn't worth the trouble.

@hx2A
Copy link
Collaborator

hx2A commented Sep 10, 2023

I didn't format the output to be something like Py5Color(red=255, green=204, blue=0, alpha=255) because that syntax would suggest that that is py5 valid code

I admit I kind of like the idea of having valid Python on the repr, but I'm afraid it isn't worth the trouble.

After thinking about it some more, I see my original logic is flawed; currently the Py5Shape and Py5Graphics classes have repr strings with Py5Shape and Py5Graphics in them, but you create those instances with calls to create_shape() and create_graphics(), etc. It is totally fine to have Py5Color(red=255..., and people create it with calls to py5.color(). I'll make it like that.

What if the color objects displayed always the same representation, irrespective of the color mode?

At first I thought, yes of course, let's do it, but then I realized it can't work that way because it would have to guess at the value ranges. If it is set to py5.color_mode(py5.RGB, 255, 255, 255), what ranges should it use for hue, saturation, or brightness? It can't assume py5.color_mode(py5.HSB, 360, 100, 100), or anything else. Or maybe it can make that assumption?

@hx2A
Copy link
Collaborator

hx2A commented Sep 10, 2023

Or maybe it can make that assumption?

Here's what Processing does:

colorMode(RGB, 255, 255, 255);

color c = color(255, 0, 1);

println(hue(c), saturation(c), brightness(c));
println(red(c), green(c), blue(c));

colorMode(HSB, 360, 100, 100);

println(hue(c), saturation(c), brightness(c));
println(red(c), green(c), blue(c));
254.83333 255.0 255.0
255.0 0.0 1.0
359.7647 100.0 100.0
360.0 0.0 0.3921569

So Processing assumes the value ranges for the other color mode are identical to the active color mode...I don't like that all, but from looking at the code I understand why this happens. I'd much prefer for assumptions about the other color mode to not jump around like that.

@villares
Copy link
Contributor Author

Silly idea but, let's see... how about adding a ranges=(255, 255, 255) keyword argument for the constructor that would guarantee both the reader knows the range used in the repr as well as it would make the code "work". I'd fix it in the repr to (255, 255, 255).

I was even wondering if to make the repr shorter it could be like: Py5Color(255, 204, 0, mode=py5.RGB, ranges=(255, 255, 255)) and that's it...

@hx2A
Copy link
Collaborator

hx2A commented Sep 11, 2023

No, I can't do that. The Py5Color instances need to be created from the py5.color() method with the color parameter ranges managed by the py5.color_mode() method. Trying to let users instantiate a Py5Color class directly, without py5.color() and with parameters for the value ranges is complicated and would become a second parallel method for managing colors that provides the same functionality as the first.

For the __repr__() method, the main goal is to provide users with something useful and not large and opaque positive or negative numbers. I see 3 options:

  1. Color values only measured with the current color mode setting:
Py5Color(red=255, green=204, blue=0, alpha=255)
Py5Color(hue=300, saturation=80, brightness=80, alpha=255)

Maybe we could make it like this to include the ranges:

Py5Color(red=255/255, green=204/255, blue=0/255, alpha=255/255)
Py5Color(hue=300/360, saturation=80/100, brightness=80/100, alpha=255/255)

That seems a bit cluttered to me though, but it is an idea.

  1. Color values measured with the current color mode setting, plus percentages for the "other" color mode. (And degrees for hue, since hue is best thought of as a circle). Percentages would make it clear these values are fractions of some unspecified range. Something like:
Py5Color(red=255, green=204, blue=0, alpha=255, hue=300°, saturation=80%, brightness=80%)
Py5Color(hue=300, saturation=80, brightness=80, alpha=255, red=100%, green=80%, blue=0%)

To me this doesn't have any miscommunication about what is happening and it provides a complete set of information.

  1. 8 character hex values:
"#FFCC00FF"

or

Py5Color("#FFCC00FF")

This seems a bit boring and also assumes the users are familiar with hex values. Graphic designers and people with html dev experience would be comfortable with this but many will not.

Which do you like best? I kind of like the second one the best. It is easy to implement and would be helpful for folks debugging their code. That's the most important goal here.

I do like your other ideas though about providing users with more functionality for working with colors. Earlier today I was thinking of adding other properties and methods to Py5Color but then I felt like I might be re-inventing the wheel and that there might already be a good Python color library out there. Turns out, there is: colour. Check it out, I think you might like it. It can already do everything we have been talking about. The color ranges are always [0, 1] so there is no ambiguity there.

It would be easy for py5 to accept the colour library's Color class objects in addition to the current range of options (integers, hex strings, etc).

@villares
Copy link
Contributor Author

Yeah! 1 or 2 would be great. I think I like 1 best, with the ranges.

@hx2A
Copy link
Collaborator

hx2A commented Sep 14, 2023

Here's where this is now.

For RGB color mode with the ranges all set to the default and most common values 0-255, the Py5Color int will display this:

Py5Color(red=255, green=204, blue=0, alpha=255, hue=48°, saturation=100%, brightness=100%)

Observe that the RGB values come first and that there is no need for decimals. The HSB values have no user specified ranges so it will use 0-360° (degrees) for hue and 0-100% for saturation and brightness. This seemed to be the most common way to communicate HSB color params and it is also the most clear, with the ° and % symbols adding meaning to the numbers.

For HSB color modes with the ranges set to 0-360 for hue and 0-100 for the others, the HSB values come first. The RGB values use percentages, indicating that is is some fraction of an unspecified range.

Py5Color(hue=48°, saturation=100%, brightness=100%, alpha=100%, red=100%, green=80%, blue=0%)

For non-traditional color ranges it will display color values as floats with 2 decimal places. I don't think it is really necessary to specifically include what the ranges are because the user will know what the color ranges are from their call to color_mode().

Py5Color(red=100.00, green=80.00, blue=0.00, alpha=100.00, hue=48°, saturation=100%, brightness=100%)
Py5Color(hue=6.67, saturation=10.00, brightness=10.00, alpha=10.00, red=100%, green=80%, blue=0%)

There's also the .hex attribute to return the color as an 8 character hex.

All of this seems straightforward to me and great for debugging. Certainly easier than those large negative numbers folks have somehow gotten used to.

In addition, I have the Python colour library now working great with py5. I can use the colour library's Color class and seamlessly pass it to py5 color methods:

from colour import Color


color1 = Color("violet")


def setup():
    py5.size(200, 200)

    py5.rect(0, 0, 100, 100)
    py5.fill(color1, 128)
    py5.rect(50, 50, 100, 100)

py5.run_sketch()

@villares
Copy link
Contributor Author

villares commented Sep 14, 2023

Looks great!

Two things to consider...

  1. I liked the "number / range" notation... don't assume the user knows the range, they might be debugging a rogue call to color_mode...

  2. I like the HSB degrees thing, but mind you that the default py5 HSB mode is 255, 255, 255, as in Processing Java (but not in p5js) and I wouldn't change that.

@hx2A
Copy link
Collaborator

hx2A commented Sep 15, 2023

liked the "number / range" notation... don't assume the user knows the range, they might be debugging a rogue call to color_mode...

Good point, including the range is useful for debugging purposes. And since this feature's only real purpose is for debugging color related code, I added it back in. The strings are getting a bit long for my taste, but that's fine, it does look neatly organized.

Py5Color(RGB, red=126/255, green=129/255, blue=6/255, alpha=255/255, hue=61.46/360, saturation=95.35/100, brightness=50.59/100)
Py5Color(HSB, hue=61.46/360, saturation=95.35/100, brightness=50.59/100, alpha=100.00/100, red=126/255, green=129/255, blue=6/255)

I got rid of the percentages and degrees stuff for HSB because it looked bad when along side other value/range pairs. This is consistent and clear.

For RGB values, when the ranges are all [0, 255] it does not use decimals because they are not necessary. For all other cases, there are 2 decimals for the values.

In RGB color mode, the HSB values are presented with ranges are 360, 100, 100. In HSB color mode, the RGB values are presented with ranges 255, 255, 255.

the default py5 HSB mode is 255, 255, 255, as in Processing Java (but not in p5js) and I wouldn't change that.

All image or drawing programs like Inkscape, Krita, Gimp, etc that I've seen always use a 360 range for hue and 100 range for saturation and brightness. This also makes the most sense if you think about hue as a color wheel. Processing uses 255 for HSB because both HSB and RGB use the same shared color mode variables. There's no need for py5 to duplicate that design choice.

@hx2A
Copy link
Collaborator

hx2A commented Sep 17, 2023

I made some minor cosmetic improvements and made the code more robust. Now the printed colors look like this:

Py5Color(RGB, red=165/255, green=42/255, blue=42/255, alpha=255/255, hue=0/360, saturation=74.55/100, brightness=64.71/100)
Py5Color(HSB, hue=0/360, saturation=74.55/100, brightness=64.71/100, alpha=255/255, red=165/255, green=42/255, blue=42/255)

I think it looks neatly organized and is certainly much better than the large negative numbers.

I got rid of the .hex property to convert a color to hex because I remembered there already is a hex_color() method. The Py5Color class is just a simple class that looks nice when printed.

The Python colour library works great with py5 and is a full-featured Color class library. You can pass it to any method that requires a color of some kind.

In addition, py5 already supports all named colors known to matplotlib. That was great but it required you to know the names of the colors, which was too much for my brain to handle. I added the names to a submodule so you can now write code like this:

py5.fill(py5.xkcd_colors.WEIRD_GREEN)
py5.stroke(py5.css4_colors.PALEGOLDENROD)

And as always, Imported mode users can omit the py5. prefixes.

If you have an editor that supports tab completion, you will no longer need to remember the names of any colors.

@hx2A
Copy link
Collaborator

hx2A commented Sep 18, 2023

I discovered the "creating colors before the Sketch starts" code wasn't as robust as I needed it to be but I think I have it working now. Things were getting a bit messy but I am straightening it out now.

In addition, since I am working on the color related code, I am taking the opportunity to add a new feature that I had in mind for a while now. Have a look at this code:

import py5


def setup():
    py5.size(400, 600)

    py5.color_mode(py5.CMAP, "plasma", py5.width, py5.height)
    py5.frame_rate(3)


def draw():
    py5.background("#FFFFFF")

    for _ in range(500):
        x = py5.random(py5.width)
        y = py5.random(py5.height)
        py5.fill(x, y)
        py5.rect(x, y, 10, 10)


py5.run_sketch()

CMAP is a new color mode, short for "colormap". "plasma" is the name of a matplotlib colormap. Basically what is happening here is colors are specified with grayscale value and an alpha value, with the grayscale value passing through the colormap to pick the actual color used. I am setting the ranges to be the width and height of the Sketch, resulting in this:

image

There's some parameter validation stuff I need to add and some other details but this feature basically works.

One of the neat things about matplotlib colormaps is they can have special colors for values that are over or under the given range or bad values (ie np.nan). All of those things work with this feature as well.

@villares
Copy link
Contributor Author

WOW! This colormap feature seems amazing! I was fiddling with some "color indexation" a while ago, this might make things much easier!!!

@hx2A
Copy link
Collaborator

hx2A commented Sep 20, 2023

Thanks! This is very different from the features in other tools that are available, and maybe it would even be a great way to teach color maps to students.

@hx2A
Copy link
Collaborator

hx2A commented Sep 22, 2023

I think I'm done implementing the code changes for this. The new CMAP colormode stuff will only be available to Sketch.color_mode() and not Py5Graphics.color_mode() or Py5Shape.color_mode(). That's fine though, and anyone could always just as easily do something like shape.fill(py5.color(0.5)) or something to map the value (0.5) to the colormap color via py5.color() which would then be passed to shape.fill().

@villares
Copy link
Contributor Author

Fantastic. We should add this information about availability, or a link to it, at the Sketch.color_mode(), Py5Graphics.color_mode() and Py5Shape.color_mode() documentation pages!

@hx2A hx2A reopened this Sep 22, 2023
@hx2A
Copy link
Collaborator

hx2A commented Sep 22, 2023

I'm still working on this, but about to make a PR and close it.

This is now working code:

import matplotlib as mpl
from colour import Color

import py5
from py5 import css4_colors, mpl_cmaps, xkcd_colors

# color palette, all of these values can be passed to any py5 method that needs a color
violet = py5.color("violet")
bisque = py5.color(css4_colors.BISQUE)
green = Color("green")
purple = py5.color("#FF00FF")

print(violet)
print(bisque)
print(green)
print(purple)


def setup():
    py5.size(400, 600, py5.P2D)

    py5.color_mode(py5.CMAP, mpl_cmaps.OCEAN, py5.width, py5.height)


def draw():
    # make the background color oscillate over time
    py5.background(py5.remap(py5.sin(py5.frame_count * 0.02), -1, 1, 0, py5.width))

    for _ in range(250):
        x = py5.random(py5.width)
        y = py5.random(py5.height)
        py5.fill(x, y)
        py5.rect(x, y, 10, 10)


py5.run_sketch()

I added mpl_cmaps this morning so nobody has to search around for the names of the colormaps. In addition you can also pass matplotlib Colormap instances. That way py5 will support the matplotlib 3rd party libraries that provide additional Colormap palettes. And of course, you can make your own if you like.

While testing this I discovered it is now easy to make colors oscillate.

We should add this information about availability, or a link to it, at the Sketch.color_mode(), Py5Graphics.color_mode() and Py5Shape.color_mode() documentation pages!

Yes, documenting it is another can of worms. There is another open issue about gaps in the color documentation and I will bundle that with this.

@hx2A
Copy link
Collaborator

hx2A commented Sep 22, 2023

Now this is closed. Thank you, @villares , for the conversation that sparked all these good ideas!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants