CGShading Demo

A PyObjC Example without documentation

Sources

MyQuartzView.py

"""
Example for using CGShading and CGFunction. This code is directly translated
from procedural C code and is definitely not good Python style.
"""
import array
import math
import random
import sys

import Cocoa
import objc
import Quartz
from objc import super  # noqa: A004

# Global variables
frequency = [0.0, 0.0, 0.0, 0.0]
startPoint = Quartz.CGPoint(0.0, 0.0)
startRadius = 0.0
startExtend = False
endPoint = Quartz.CGPoint(0.0, 0.0)
endRadius = 0.0
endExtend = False

shading = None
function = None
getFunction = None
getShading = None
colorspace = None

DEFAULT_WIDTH = 256
DEFAULT_HEIGHT = 256

MAX_WIDTH = 1000
MAX_HEIGHT = 1000


def randomPoint():
    return Quartz.CGPoint(random.random(), random.random())


def evaluate1(components, input_value, output_value):
    out = []
    for k in range(components - 1):
        out.append(1 + (math.sin(input[0] * frequency[k])) / 2)
    out.append(1)
    return out


def getFunction1(colorspace):
    if sys.maxsize > 2**32:
        a_type = "d"
    else:
        a_type = "f"
    domain = array.array(a_type, [-2 * math.pi, 2 * math.pi])
    function_range = array.array(a_type, [0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
    components = 1 + Quartz.CGColorSpaceGetNumberOfComponents(colorspace)

    return Quartz.CGFunctionCreate(
        components, 1, domain, components, function_range, evaluate1
    )


def evaluate2(components, input_value, output_value):
    c = [0.510, 0.188, 0.910, 0.122]

    v = input_value[0]
    out = []
    for k in range(components - 1):
        if v < 0.5:
            out.append(c[k] * 2 * (0.5 - v))
        else:
            out.append(c[k] * 2 * (v - 0.5))
    out.append(1)
    return out


def getFunction2(colorspace):
    domain = [0, 1]
    function_range = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]

    components = 1 + Quartz.CGColorSpaceGetNumberOfComponents(colorspace)
    return Quartz.CGFunctionCreate(
        components, 1, domain, components, function_range, evaluate2
    )


def evaluate3(components, input_value, output_value):
    c = [0.3, 0, 0, 0]

    out = []
    for k in range(components - 1):
        out.append(c[k] * input_value[0])
    out.append(1)
    return out


def getFunction3(colorspace):
    domain = [0, 1]
    function_range = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]

    components = 1 + Quartz.CGColorSpaceGetNumberOfComponents(colorspace)
    return Quartz.CGFunctionCreate(
        components, 1, domain, components, function_range, evaluate3
    )


def getAxialShading(colorspace, function):
    return Quartz.CGShadingCreateAxial(
        colorspace, startPoint, endPoint, function, startExtend, endExtend
    )


def getRadialShading(colorspace, function):
    return Quartz.CGShadingCreateRadial(
        colorspace,
        startPoint,
        startRadius,
        endPoint,
        endRadius,
        function,
        startExtend,
        endExtend,
    )


class MyQuartzView(Cocoa.NSView):
    def initWithFrame_(self, frameRect):
        global startPoint, startRadius, startExtend
        global endPoint, endRadius, endExtend

        super().initWithFrame_(frameRect)

        startPoint = Quartz.CGPoint(0, 0)
        startRadius = 0
        startExtend = False

        endPoint = Quartz.CGPointMake(0, 0)
        endRadius = 0
        endExtend = False

        return self

    def drawRect_(self, rect):
        currentContext = Cocoa.NSGraphicsContext.currentContext().graphicsPort()

        # Note that at this point the current context CTM is set up such
        # that the context size corresponds to the size of the view
        # i.e. one unit in the context == one pixel
        # Also, the origin is in the bottom left of the view with +y pointing up
        global getFunction

        bounds = self.bounds()

        angle = 0
        sx = sy = 1
        width = bounds.size.width
        height = bounds.size.height

        if getFunction is None:
            self.randomize_(self)

        m = Quartz.CGAffineTransformIdentity
        m = Quartz.CGAffineTransformRotate(m, angle)
        m = Quartz.CGAffineTransformScale(m, width, height)
        m = Quartz.CGAffineTransformScale(m, sx, sy)

        Quartz.CGContextBeginPage(currentContext, bounds)

        Quartz.CGContextTranslateCTM(
            currentContext, bounds.size.width / 2, bounds.size.height / 2
        )
        Quartz.CGContextConcatCTM(currentContext, m)
        Quartz.CGContextTranslateCTM(currentContext, -0.5, -0.5)

        Quartz.CGContextSaveGState(currentContext)

        Quartz.CGContextClipToRect(currentContext, Quartz.CGRectMake(0, 0, 1, 1))
        Quartz.CGContextSetRGBFillColor(currentContext, 0.7, 0.7, 0.9, 1)
        Quartz.CGContextFillRect(currentContext, Quartz.CGRectMake(0, 0, 1, 1))

        Quartz.CGContextDrawShading(currentContext, shading)

        Quartz.CGContextRestoreGState(currentContext)

        Quartz.CGContextSaveGState(currentContext)
        Quartz.CGContextClipToRect(currentContext, Quartz.CGRectMake(0, 0, 1, 1))
        Quartz.CGContextSetRGBStrokeColor(currentContext, 1, 0, 0, 1)

        if getShading == getRadialShading:
            Quartz.CGContextAddArc(
                currentContext,
                startPoint.x,
                startPoint.y,
                startRadius,
                math.radians(0),
                math.radians(360),
                True,
            )
            Quartz.CGContextClosePath(currentContext)
            Quartz.CGContextMoveToPoint(
                currentContext, endPoint.x + endRadius, endPoint.y
            )
            Quartz.CGContextAddArc(
                currentContext,
                endPoint.x,
                endPoint.y,
                endRadius,
                math.radians(0),
                math.radians(360),
                True,
            )
            Quartz.CGContextClosePath(currentContext)

        Quartz.CGContextMoveToPoint(currentContext, startPoint.x + 0.01, startPoint.y)
        Quartz.CGContextAddArc(
            currentContext,
            startPoint.x,
            startPoint.y,
            0.01,
            math.radians(0),
            math.radians(360),
            True,
        )
        Quartz.CGContextClosePath(currentContext)
        Quartz.CGContextMoveToPoint(currentContext, startPoint.x, startPoint.y)
        Quartz.CGContextAddLineToPoint(currentContext, endPoint.x, endPoint.y)

        ctm = Quartz.CGContextGetCTM(currentContext)
        Quartz.CGContextConcatCTM(currentContext, Quartz.CGAffineTransformInvert(ctm))
        Quartz.CGContextStrokePath(currentContext)
        Quartz.CGContextRestoreGState(currentContext)

        Quartz.CGContextSaveGState(currentContext)
        Quartz.CGContextSetGrayStrokeColor(currentContext, 0, 1)
        Quartz.CGContextAddRect(currentContext, Quartz.CGRectMake(0, 0, 1, 1))
        ctm = Quartz.CGContextGetCTM(currentContext)
        Quartz.CGContextConcatCTM(currentContext, Quartz.CGAffineTransformInvert(ctm))
        Quartz.CGContextStrokePath(currentContext)
        Quartz.CGContextRestoreGState(currentContext)

        Quartz.CGContextEndPage(currentContext)

        Quartz.CGContextFlush(currentContext)

    @objc.IBAction
    def randomize_(self, sender):
        global colorspace, getFunction, getShading
        global function, shading
        global startPoint, startRadius, endPoint, endRadius

        if colorspace is None:
            colorspace = Quartz.CGColorSpaceCreateDeviceRGB()

        for k in range(len(frequency)):
            frequency[k] = random.random()

        startPoint = randomPoint()
        startRadius = random.random() / 2
        endPoint = randomPoint()
        endRadius = random.random() / 2

        if getFunction == getFunction1:
            getFunction = getFunction2

        elif getFunction == getFunction2:
            getFunction = getFunction3

        else:
            getFunction = getFunction1

        if getShading == getAxialShading:
            getShading = getRadialShading
        else:
            getShading = getAxialShading

        function = getFunction(colorspace)
        shading = getShading(colorspace, function)

        self.setNeedsDisplay_(True)

    @objc.IBAction
    def toggleStartExtend_(self, sender):
        global startExtend, shading

        startExtend = not startExtend
        shading = getShading(colorspace, function)

        self.setNeedsDisplay_(True)

    @objc.IBAction
    def toggleEndExtend_(self, sender):
        global endExtend, shading

        endExtend = not endExtend
        shading = getShading(colorspace, function)

        self.setNeedsDisplay_(True)

main.py

import sys

# AppHelper.runEventLoop()
import AppKit  # noqa: F401
import MyQuartzView  # noqa: F401

AppKit.NSApplicationMain(sys.argv)

setup.py

"""
Script for building the example.

Usage:
    python3 setup.py py2app
"""
from setuptools import setup

setup(
    name="CGShadingDemo",
    app=["main.py"],
    data_files=["English.lproj"],
    setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"],
)