Are Mesh Gradients possible in Python?

I was roaming a bit and noticed that krita 4.4.2 is able to do mesh gradients now, which is very cool indeed.

I was curious if it is possible to do it in Python too or it uses some odd library that is not open to Python? This was a topic I was researching alot a while ago and had deemed it impossible with Qt at the time.

my objective for this was to make the YUV color picker GUI that has this kind of look.
yuv

I know the colors on any point but I cant place it without messing up the others on the side.
And I had no luck with the blending modes up till now.

Hi

Using gradient mesh might not be possible in python:

  • I’m not sure this is available through API
  • In Python, to draw something you’re working with QPainter connected to a device and the only method you have to switch from a Krita paint to QPainter is through Node.pixelData()methods

From my point of view it would be easier to use gradient from Qt to build your color picker.
The thing I’m not sure about is what you mean by “I know the colors on any point but I cant place it without messing up the others on the side”

Looking just the given example

from PyQt5.Qt import *
from PyQt5 import QtCore
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

size = 400

def gradient(size, ctl, ctr, cbl, cbr):
    
    # gradient on left side, color top to color bottom
    gradientL=QLinearGradient(0,0,0,size)
    gradientL.setColorAt(0, ctl)
    gradientL.setColorAt(1, cbl)

    # gradient on right side, color top to color bottom
    gradientR=QLinearGradient(0,0,0,size)
    gradientR.setColorAt(0, ctr)
    gradientR.setColorAt(1, cbr)
    
    # gradient to define transparency, from left (opaque) to right (transparent)
    gradientTransparencyLR=QLinearGradient(0,0,size,0)
    gradientTransparencyLR.setColorAt(0, QColor(Qt.black))
    gradientTransparencyLR.setColorAt(1, QColor(Qt.transparent))

    # create a first pixmap, color left, set it as transparent (because composition mode works on transparent pixels only)
    pixmapLeft = QPixmap(size, size)
    pixmapLeft.fill(Qt.transparent)

    canvas = QPainter()
    canvas.begin(pixmapLeft)
    
    # first, prepare pixmap with opaque>transparent gradient
    canvas.setPen(Qt.NoPen)
    canvas.setBrush(gradientTransparencyLR)
    canvas.drawRect(QRect(0,0,size,size))
        
    # and paint left color gradient on it:
    # result: left side is opaque with gradient from top left to bottom left
    canvas.setCompositionMode(QPainter.CompositionMode_SourceIn)
    canvas.setBrush(gradientL)
    canvas.drawRect(QRect(0,0,size,size))

    canvas.end()    
    
    # create a new pixmap    
    pixmap= QPixmap(size, size)
    canvas = QPainter()
    canvas.begin(pixmap)
    
    # paint right top to bottom gradient (fill all area)
    canvas.setPen(Qt.NoPen)
    canvas.setBrush(gradientR)
    canvas.drawRect(QRect(0,0,size,size))
    
    # draw pixmap of left side gradient
    canvas.drawPixmap(0,0,pixmapLeft)
    
    canvas.end()    

    return pixmap
    

dlg = QDialog(Application.activeWindow().qwindow())
dlg.setModal(True)
layout = QHBoxLayout(dlg)

lbl0 = QLabel("")
lbl0.setFixedHeight(size)
lbl0.setFixedWidth(size)
lbl0.setPixmap(gradient(size, QColor("#ff6800"), QColor("#ff00ff"), QColor("#00ff00"), QColor("#0099ff")))

layout.addWidget(lbl0)

dlg.exec()

Result:

Seems to be what you want?

Grum999

A solution with a gradient would be the best for sure, pixeldata is pretty slow to update if I recall.

I think I have tried a similar solution at the time and it did not work out very well once you placed in another values only the gradients coded in worked at those points.


I think my gradients were A to C and A to G and trying to blend them together somehow to no success.
Your way I am curious to see the results on B and H I wonder if a RGB interpolation does it or not. But it it is close it is good for me.

Assuming:

  • B/H values only move on X axis,
  • D/F values only move on Y axis

I got this:

As you can see, there’s 2 examples, because I’m not sure about how to calculate intermediate color, so I’ve implemented 2 method.

Considering P position for B (or D) is 0 to 100%
On left side, intermediate B color is always P% of A/C, whatever to position
On right side, intermediate B color is always 50% of A/C, whatever to position

If color calculation is different, it might not be too difficult to implement I think

Here is the code (not really optimized but already as fast enough in Linux :yum:):

from PyQt5.Qt import *
from PyQt5 import QtCore
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

size = 400


class WColorPickerYUV(QWidget):
    positionChanged = pyqtSignal(float, float)
    
    def __init__(self, method=1, parent=None):
        super(WColorPickerYUV, self).__init__(parent)
        
        self.__method=method
        
        
        self.__ctl=QColor("#ff6600")
        self.__ctr=QColor("#ff00ff")
        self.__cbl=QColor("#00ff00")
        self.__cbr=QColor("#0099ff")
        self.__posB=0.5
        self.__posD=0.5
        
        self.__cursorPen=QPen(QColor(255,255,255,128))
        self.__cursorPen.setWidth(1)
        self.__cursorSize=25
        
    def mouseMoveEvent(self, event):
        if Qt.LeftButton and event.buttons() == Qt.LeftButton:
            if (event.localPos().x()>=0 and event.localPos().x()<=self.width() and
                event.localPos().y()>=0 and event.localPos().y()<=self.height()):
                self.setPosition(event.localPos().x()/self.width(), event.localPos().y()/self.height())
                self.positionChanged.emit(self.__posB, self.__posD)

    def paintEvent(self, event):
        painter = QPainter(self)
        
        self.__drawGradient(painter)
        self.__drawCursor(painter)
        

    def __drawCursor(self, targetCanvas):
        x=round(self.__posB * self.width())
        y=round(self.__posD * self.height())
        
        targetCanvas.setPen(self.__cursorPen)
        targetCanvas.drawLine(0, y, self.__cursorSize, y)
        targetCanvas.drawLine(self.width(), y, self.width()-self.__cursorSize, y)

        targetCanvas.drawLine(x, 0, x, self.__cursorSize)
        targetCanvas.drawLine(x, self.height(), x, self.height()-self.__cursorSize)


    def __drawGradient(self, targetCanvas):
        
        
        # method=1
        if self.__method==1:
            posD1m=1-self.__posD
            colorD=QColor(round((self.__posD*self.__ctl.red() + posD1m*self.__cbl.red())), 
                          round((self.__posD*self.__ctl.green() + posD1m*self.__cbl.green())), 
                          round((self.__posD*self.__ctl.blue() + posD1m*self.__cbl.blue())))
            colorF=QColor(round((self.__posD*self.__ctr.red() + posD1m*self.__cbr.red())), 
                          round((self.__posD*self.__ctr.green() + posD1m*self.__cbr.green())), 
                          round((self.__posD*self.__ctr.blue() + posD1m*self.__cbr.blue())))
            colorB=QColor(0,0,0,round(255*self.__posB))
        else:
            colorD=QColor((self.__ctl.red() + self.__cbl.red())>>1, 
                          (self.__ctl.green() + self.__cbl.green())>>1, 
                          (self.__ctl.blue() + self.__cbl.blue())>>1)
            colorF=QColor((self.__ctr.red() + self.__cbr.red())>>1, 
                          (self.__ctr.green() + self.__cbr.green())>>1, 
                          (self.__ctr.blue() + self.__cbr.blue())>>1)
            colorB=QColor(0,0,0,127)
            
                                                               
        # gradient on left side, color top to color bottom
        gradientL=QLinearGradient(0,0,0,self.height())
        gradientL.setColorAt(0, self.__ctl)
        gradientL.setColorAt(self.__posD, colorD)
        gradientL.setColorAt(1, self.__cbl)

        # gradient on right side, color top to color bottom
        gradientR=QLinearGradient(0,0,0, self.height())
        gradientR.setColorAt(0, self.__ctr)
        gradientR.setColorAt(self.__posD, colorF)
        gradientR.setColorAt(1, self.__cbr)
    
        # gradient to define transparency, from left (opaque) to right (transparent)
        gradientTransparencyLR=QLinearGradient(0,0,self.width(),0)
        gradientTransparencyLR.setColorAt(0, QColor(Qt.black))
        gradientTransparencyLR.setColorAt(self.__posB, colorB)
        gradientTransparencyLR.setColorAt(1, QColor(Qt.transparent))

        # create a first pixmap, color left, set it as transparent (because composition mode works on transparent pixels only)
        pixmapLeft = QPixmap(self.size())
        pixmapLeft.fill(Qt.transparent)

        canvas = QPainter()
        canvas.begin(pixmapLeft)
    
        # first, prepare pixmap with opaque>transparent gradient
        canvas.setPen(Qt.NoPen)
        canvas.setBrush(gradientTransparencyLR)
        canvas.drawRect(QRect(0,0,size,size))
        
        # and paint left color gradient on it:
        # result: left side is opaque with gradient from top left to bottom left
        canvas.setCompositionMode(QPainter.CompositionMode_SourceIn)
        canvas.setBrush(gradientL)
        canvas.drawRect(QRect(0,0,size,size))

        canvas.end()    
    
        # paint target canvas
        # paint right top to bottom gradient (fill all area)
        targetCanvas.setPen(Qt.NoPen)
        targetCanvas.setBrush(gradientR)
        targetCanvas.drawRect(QRect(0,0,size,size))
    
        # draw pixmap of left side gradient
        targetCanvas.drawPixmap(0,0,pixmapLeft)

    def setPosition(self, pb, pd):
        self.__posB=pb
        self.__posD=pd
        self.update()
        
    



dlg = QDialog(Application.activeWindow().qwindow())
dlg.setModal(True)
layout = QHBoxLayout(dlg)

cp1 = WColorPickerYUV(1)
cp1.setFixedHeight(size)
cp1.setFixedWidth(size)

cp2 = WColorPickerYUV(2)
cp2.setFixedHeight(size)
cp2.setFixedWidth(size)


cp1.positionChanged.connect(cp2.setPosition)
cp2.positionChanged.connect(cp1.setPosition)


layout.addWidget(cp1)
layout.addWidget(cp2)

dlg.exec()

Now, if B/H/D/F/E points can move in any axis, using Qt gradient is not possible, and only home made gradient through pixeldata can do the job (but python execution script is too slow for that and you might need to code it in C/C++)

Grum999

this is weird I don’t get the same result. I have been trying and I am not quite sure what I am doing wrong. I have been trying to set it up but the Pixmap does not seem to want to work fully. Also I noticed a difference that I am not sure if it has influence and that is I am building on a widget and not on a label.

I tweaked it a bit but is essencially the same:

########################################################################

# gradient on left side, color top to color bottom
gradientL = QLinearGradient(0,0, 0,self.panel_height)
gradientL.setColorAt(0, self.cor1)
gradientL.setColorAt(0.5, self.cor2)
gradientL.setColorAt(1, self.cor3)

# gradient on right side, color top to color bottom
gradientR = QLinearGradient(0,0, 0,self.panel_height)
gradientR.setColorAt(0, self.cor4)
gradientR.setColorAt(0.5, self.cor5)
gradientR.setColorAt(1, self.cor6)

# gradient to define transparency, from left (opaque) to right (transparent)
gradientTransparencyLR = QLinearGradient(0,0, self.panel_width,0)
gradientTransparencyLR.setColorAt(0, QColor(Qt.black))
gradientTransparencyLR.setColorAt(1, QColor(Qt.transparent))


# create a first pixmap, color left, set it as transparent (because composition mode works on transparent pixels only)
pixmapLeft = QPixmap(self.panel_width, self.panel_height)
pixmapLeft.fill(Qt.transparent)

canvas = QPainter(self)
canvas.begin(pixmapLeft)

# first, prepare pixmap with opaque>transparent gradient
canvas.setPen(Qt.NoPen)
canvas.setBrush(gradientTransparencyLR)
canvas.drawRect(QRect(0,0, self.panel_width, self.panel_height))

# and paint left color gradient on it:
# result: left side is opaque with gradient from top left to bottom left
canvas.setCompositionMode(QPainter.CompositionMode_SourceIn)
canvas.setBrush(gradientL)
canvas.drawRect(QRect(0,0, self.panel_width, self.panel_height))

canvas.end()


# create a new pixmap
pixmap= QPixmap(self.panel_width, self.panel_height)
canvas = QPainter(self)
canvas.begin(pixmap)

# paint right top to bottom gradient (fill all area)
canvas.setPen(Qt.NoPen)
canvas.setBrush(gradientR)
canvas.drawRect(QRect(0,0, self.panel_width, self.panel_height))

# draw pixmap of left side gradient
canvas.drawPixmap(0,0,pixmapLeft)

canvas.end()



########################################################################
# Start Qpainter
painter = QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.setPen(QtCore.Qt.NoPen)

# Lines
painter.setPen(QPen(QColor(self.color_light), 2, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin))
lines = QPainterPath()
lines.moveTo(self.panel_width*0.5, 0)
lines.lineTo(self.panel_width*0.5, self.panel_height)
lines.moveTo(0, self.panel_height*0.5)
lines.lineTo(self.panel_width, self.panel_height*0.5)
painter.drawPath(lines)

the result I get is this.

My issue seems to be the masking of one gradient to the other.

My first example I use a label because I was lazy to build more :slight_smile:
My second example (the animated one) I create a widget from a QWidget

You’re applying a qpainter initialisation twice:

  • One on QWidget
  • One on pixmap
    => the second one might be ignored (you should have a warning in console log) then you’re drawing directly on widget; I don’t have the full code but I suppose you’re returning pixmap and then draw it on widget canvas

Just do

canvas = QPainter()
canvas.begin(pixmap)

And i think it should be better

Grum999

it worked =0! Sometimes I gasp at how everything works.

I was comparing the results to the sliders output with the yuv gui and the colors seem right at naked eye.