py5coding / py5generator

Meta-programming project that creates the py5 library code.
https://py5coding.org/
GNU General Public License v3.0
52 stars 13 forks source link

Hexadecimal color value support #26

Closed tabreturn closed 3 years ago

tabreturn commented 3 years ago

In Processing, you can use hexadecimal color values, like: fill('#FF0000') instead of fill(255, 0, 0)

Are there any plans to support this is py5?

hx2A commented 3 years ago

In py5 you can use hexidecimal numbers, like so:

size(200, 200)
fill(0x7FFF0000)
rect(10, 20, 30, 40)

However, the alpha channel cannot be higher than 7F. The issue has to do with signed and unsigned integers. Processing ignores the sign issue and just uses the first 8 bits for the alpha channel. When JPype converts Python ints to Java Ints it won't ignore the sign issue and won't convert numbers higher than 0x7FFFFFFF. Attempting to do so results in this error:

py5 encountered an error in your code:
    1    
--> 2    fill(0xFFFF0000)
    3    rect(10, 20, 30, 40)

OverflowError: Cannot convert value to Java int

I guess you can say py5 offers partial support for hexidecimal numbers?

I agree this is inadequate. I will think about what I can do to improve this.

Thrameos commented 3 years ago

In order to get a value which would fall outside of the acceptable range for an integer you must use casting.

The following code fails

   java.lang.Integer(0xffffffff)

But this succeeds

   java.lang.Integer(jpype.JInt(0xffffffff))

Thus the solution is to add a customizer to the wrapper for fill and have the customizer use the casting operator.

hx2A commented 3 years ago

@Thrameos , thanks for joining in! @tabreturn , @Thrameos is the main developer of JPype and works very hard to make it the awesome library it is. @Thrameos , switching to jpype for py5 was a good choice; the ability to use numpy is a major selling point. Also, no more seg faults. :)

A workaround using the latest version of py5 would be something like this:

size(200, 200)

from jpype import JInt

fill(JInt(0xFFFF0000))
rect(10, 20, 30, 40)

The real solution would be something that does that JInt the cast for you.

Thus the solution is to add a customizer to the wrapper for fill and have the customizer use the casting operator.

py5generator has a mechanism for generating code with these kinds of customizations. First I will create a decorator that checks the arguments to fill() and intervenes with a cast if necessary. I use this approach in a bunch of other places where the generated Python code isn't quite right and needs to be modified a bit. I can tell py5generator to add the decorator to fill() and every other method that has the same hex color issue.

The one challenge here is that I'm going to have to add the decorator to about ~30 methods and then test it, but that isn't such a big deal either. It will just take some time to make sure I am providing a complete solution and not missing anything.

tabreturn commented 3 years ago

Thanks, @Thrameos. Love your work on JPype.

@hx2A: indecently, Processing supports the 8-digit 0x........ (when you need to specify an alpha value) and the 6-digit '#......'. I tend to use the latter a lot because many color-mixers (including the one in the Processing IDE) provide 6-digit hexadecimal values to copy-paste into your code.

hx2A commented 3 years ago

indecently, Processing supports the 8-digit 0x........ (when you need to specify an alpha value) and the 6-digit '#......'

I just checked and I see Processing supports#FF0000, without the quotes. Is that valid Java code? Or is the PDE doing some kind of pre-processing to allow that syntax? In any case, py5 would need to require quotes, and the user code would look like this:

size(200, 200)
fill("#FF0000")
rect(10, 20, 30, 40)

Adding support for this to the previously mentioned decorator would not be more work. I would just add a check for 7 character strings that start with a # and do the necessary cast.

I agree, supporting this other way of specifying colors would be valuable, especially given how many people are used to picking colors like that.

hx2A commented 3 years ago

And I see that Github supports that color syntax also. I had no idea it would add a color swatch for things like #FFFF00 or #00FFFF. Neat.

Thrameos commented 3 years ago

In order to get the 20 places if is easiest if the Java library that is backing the code has some common type. For example void fill(ColorSpec c). If the Java library also has an overload void fill(int) then it prevents you from just adding an adapter that converts int to Colorspec unfortunately. You can then use reflection to scan and see if all of the methods that have been customized appropriately. When you customize an interface the type of the method in type object changes from JMethod to function or method so it should be easy to write a script that probes all the Java classes and verifies that they have all been cleaned up.

I have for a while wanted to be able to add a customizer method to classes to that it bulk operations can be done like this. For example suppose that you have some class that you really need all the returns as JString to automatically be str (because they are all short and casting them individually is a source of errors in user code). The idea would be rather than having to customize all 20 method, instead you place a filter function in the class customizer. The result being when the class is constructed it passes the proposed method dispatch and overloads to the customizer. The customizer can either accept the method, alter it to add adapters and converters, delete a problematic overload, etc. Unfortunately I haven't gotten far enough on the details to have a working prototype yet.

tabreturn commented 3 years ago

I just checked and I see Processing supports#FF0000, without the quotes. Is that valid Java code? Or is the PDE doing some kind of pre-processing to allow that syntax?

To clarify: PDE with Python Mode / Processing.py requires you to use quotes (single or double) -- so, yeah, it's a string.

hx2A commented 3 years ago

In order to get the 20 places if is easiest if the Java library that is backing the code has some common type. For example void fill(ColorSpec c). If the Java library also has an overload void fill(int) then it prevents you from just adding an adapter that converts int to Colorspec unfortunately.

Right, that would make this much easier, but I can't make those kinds of changes to the Java library. I have to work with the library as it is. :|

You can then use reflection to scan and see if all of the methods that have been customized appropriately

Reflection is the key! And happily I my reference documentation is nicely organized with metadata for all methods and fields. I was able to throw together a script to collect relevant info on every single int parameter in any method:

from pathlib import Path

from generator.docfiles import Documentation

PY5_API_EN = Path('py5_docs/Reference/api_en/')

for docfile in sorted(PY5_API_EN.glob('*.txt')):
    doc = Documentation(docfile)
    stem = docfile.stem
    group = stem.split('_', 1)[0]
    name = doc.meta['name']

    if group in ['Sketch', 'Py5Functions', 'Py5Tools', 'Py5Magics']:
        slug = stem[len(group)+1:].lower()
    else:
        slug = stem.lower()
    url = 'https://py5.ixora.io/reference/' + slug

    for key, vardescription in doc.variables.items():
        if key in ['params', 'args', 'kwargs']:
            continue
        varname, vartype = key.split(': ')
        if vartype == 'int':
            print(', '.join([group, name, varname, f'"{vardescription}"', url]))

Which I then threw into a Google Sheets to review. I highlighted every row that needs this customization.

@tabreturn , can you review what I did and tell me if you think I missed any or included any that shouldn't be included?

I count 46 rows. I'm glad I employed this approach, because my initial attempt using VS Code missed a whole bunch.

I have for a while wanted to be able to add a customizer method to classes to that it bulk operations can be done like this. ... Unfortunately I haven't gotten far enough on the details to have a working prototype yet.

It's a challenging problem to come up with a reasonable and generalizable way to provide this functionality. For my case, having the ability to apply a customization to to a specific variable type and variable name (type == int && name == 'rgb') would cover a lot of them, but that's probably overly specific to my use-case.

tabreturn commented 3 years ago

Sorry, I accidentally closed and reopened this issue 😳

@tabreturn , can you review what I did and tell me if you think I missed any or included any that shouldn't be included?

Sure, @hx2A. Very happy to do this. Interestingly, I discovered that Processing.py isn't as 'complete' as I thought when it comes to implementing Java Processing's color specs. For example, this works fine in Java Mode:

color c = color(#FF0000);
background(c);

But Python Mode complains that the color(): 1st arg can't be coerced to float, int for this code:

color('#FF0000')
background(c)

The 'common' functions (fill(), stroke(), etc.) are fine with hexadecimal.

I'll go through your Google sheet, check if anything is missing, test things in Python and Java Mode, and post my feedback here (maybe in a linked spreadsheet).

hx2A commented 3 years ago

Sorry, I accidentally closed and reopened this issue

Ha! We've all been there.

Sure, @hx2A. Very happy to do this. Interestingly, I discovered that Processing.py isn't as 'complete' as I thought when it comes to implementing Java Processing's color specs. For example, this works fine in Java Mode:

color c = color(#FF0000);
background(c);

But Python Mode complains that the color(): 1st arg can't be coerced to float, int for this code:

color('#FF0000')
background(c)

Thanks, I appreciate the assistance.

Maybe Python Mode figures you wouldn't need color('#FF0000') to work? There's no need for background(color('#FF0000')) to work if background('#FF0000') works.

I think the reason why it works in Java Mode is because the PDE does some special pre-processing with a regex to find code that matches #[0-9A-F]{6} and changes it to something else?

I just tested this hypothesis with the below code in Java Mode:

int x = #FF0000;
print(x);

That works, and prints out -65536. So it has nothing to do with customizing certain functions to accept that input. If I mess with it and change it to #FF00xx the PDE flags it as a syntax error. It seems to recognize that pattern and provide the user with the illusion that that is a real hex color type.

I just built a prototype for this functionality and pushed it to the dev branch of the py5 repo. I only added it to py5.fill(). This code works correctly now:

size(200, 200)
fill(0xFFFF0000)
rect(10, 20, 30, 40)
hx2A commented 3 years ago

I made some improvements to the code and added it to py5.background() and py5.lerp_color(). Here's a simple demo of everything working:

size(200, 200)
background('#CCCCCC')
no_stroke()
fill('#FFFFFF')
rect(10, 10, 180, 180)
for i in range(160):
    stroke(lerp_color('#FF0000', '#00FF00', i / 160, RGB))
    line(20 + i, 20, 20 + i, 95)
    stroke(lerp_color('#FF0000', '#00FF00', i / 160, HSB))
    line(20 + i, 105, 20 + i, 180)

The hex colors are very useful! And I'm finding py5bot to be a super useful tool. It's great for testing and trying out simple ideas. I have an idea for how I can use this to develop unit tests, which I desperately need.

villares commented 3 years ago

I'd like to add that I wish color('#AABBCC') would work in Python Mode, because it is useful for creating hard-coded palette data structures. So py5 could take the lead :D !

# from my clumsy helpers.py
def hex_color(s):
    """
    This function allows you to create color from a string with hex notation in Python mode.
    On "standard" Processing (Java) we can use hexadecimal color notation #AABBCC
    On Python mode one can use this notation between quotes, as a string in fill(),
    stroke() and background(), but, unfortunately, not with color().
    """
    if s.startswith('#'):
        s = s[1:]
    return color(int(s[:2], 16), int(s[2:4], 16), int(s[4:6], 16))

palette = [hex_color('#00CCDD'), hex_color('#DDCCFF'), hex_color('#FF00DD')]   
hx2A commented 3 years ago

I'd like to add that I wish color('#AABBCC') would work in Python Mode, because it is useful for creating hard-coded palette data structures. So py5 could take the lead :D !

That makes sense. I'll make it happen.

hx2A commented 3 years ago

I've finished the coding changes for this feature and have pushed them to the 0.5a1.dev0 branch of the py5 repo. If you'd like to test it, you can install py5 from there.

My next step and final step for this is to update the reference documentation to reflect this change. Some of the docs, such as Py5Shape.set_ambient, have example code that can be made more efficient because of this new feature. Others, such as color, should mention the '#FF0000' style color parameters.

Once I finish this I'll do another release.

hx2A commented 3 years ago

I'm almost done with the documentation changes. I also found a few small unrelated bugs along the way and fixed them.

I discovered that one place the hexadecimal support will not work for is illustrated in the pixels example code:

def setup():
    pink = py5.color(255, 102, 204)
    py5.load_pixels()
    for i in range(0, (py5.width*py5.height//2) - py5.width//2):
        py5.pixels[i] = pink

    py5.update_pixels()

If pink were defined using hex color or web color notation, the above code would throw an overthrow error. The analogous code works fine in Processing but I predict it will not Processing.py.

It is possible for me to get this to work but I'm going to leave that as an open issue for later. I want to do a new release ASAP and will do so once I complete the documentation changes.

tabreturn commented 3 years ago

The analogous code works fine in Processing but I predict it will not Processing.py

Correct. I can confirm that this won't work with hexadecimal in Processing.py :+1:

hx2A commented 3 years ago

I finished the hexadecimal documentation changes, finally. I'm looking to do a release within the next few days.

hx2A commented 3 years ago

It is possible for me to get this to work but I'm going to leave that as an open issue for later. I want to do a new release ASAP and will do so once I complete the documentation changes.

This morning I thought of an easy way to implement this and now have a working prototype. As a bonus, py5 will now provide a clear error message if the user forgets to call load_pixels():

def setup():
    py5.size(200, 200)
    py5.background("#FF0000")
    # py5.load_pixels()
    py5.println(py5.pixels2[5])
    for i in range(5000):
        py5.pixels2[i] = "#00FF00"
    py5.update_pixels()

this is the error message:

File "<ipython-input-8-08d1fbe412bc>", line 5, in _py5_faux_setup
    1    def setup():
    2        py5.size(200, 200)
    3        py5.background("#FF0000")
    4        # py5.load_pixels()
--> 5        py5.println(py5.pixels2[5])
    6        for i in range(5000):
    ..................................................
     py5.pixels2 = <py5.type_decorators.PixelArray object at 0x7f561406b430>
    ..................................................

RuntimeError: Cannot get pixel colors because load_pixels() has not been called

That's a lot better than the current TypeError: 'NoneType' object is not subscriptable message.

A few more coding changes + plus updates to the documentation and this will be in the next release.

tabreturn commented 3 years ago

Nice! Looking forward to the next release :fireworks:

I thought 3-digit hexadecimal values could be a nice addition, over and above the Processing spec. I find myself using them in CSS a lot when I'm prototyping something.

I don't consider this high-priority, but it could be neat if it's a simple job.

hx2A commented 3 years ago

I thought 3-digit hexadecimal values could be a nice addition, over and above the Processing spec. I find myself using them in CSS a lot when I'm prototyping something.

That's a worthwhile idea, and probably only 2 lines of code? I'll see about adding that also.

hx2A commented 3 years ago

OK, now I believe I've made all the coding changes for this. I just need to do more testing and update the documentation.

hx2A commented 3 years ago

Testing and documentation changes are complete