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

Initial support for sweep gradients in Cairo and CoreGraphics backends #16

Merged
merged 5 commits into from
May 12, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions Lib/blackrenderer/backends/cairo.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from contextlib import contextmanager
import os
from math import sqrt
from fontTools.pens.basePen import BasePen
from fontTools.pens.recordingPen import RecordingPen
from fontTools.ttLib.tables.otTables import ExtendMode
import cairo
from .base import Canvas, Surface

from .sweepGradient import buildSweepGradientPatches

_extendModeMap = {
ExtendMode.PAD: cairo.Extend.PAD,
Expand Down Expand Up @@ -106,9 +107,32 @@ def drawPathSweepGradient(
extendMode,
gradientTransform,
):
self.drawPathSolid(path, colorLine[0][1])

# TODO: blendMode for PaintComposite
# alloc the mesh pattern
pat = cairo.MeshPattern()
# find current path' extent
x1, y1, x2, y2 = self.context.clip_extents()
maxX = max(d * d for d in (x1 - center[0], x2 - center[0]))
maxY = max(d * d for d in (y1 - center[1], y2 - center[1]))
R = sqrt(maxX + maxY)
patches = buildSweepGradientPatches(
colorLine, center, R, startAngle, endAngle, useGouraudShading=False
)
for (P0, color0), C0, C1, (P1, color1) in patches:
# draw patch
pat.begin_patch()
pat.move_to(center[0], center[1])
pat.line_to(P0[0], P0[1])
pat.curve_to(C0[0], C0[1], C1[0], C1[1], P1[0], P1[1])
pat.line_to(center[0], center[1])
pat.set_corner_color_rgba(0, *color0)
pat.set_corner_color_rgba(1, *color0)
pat.set_corner_color_rgba(2, *color1)
pat.set_corner_color_rgba(3, *color1)
pat.end_patch()
self.context.set_source(pat)
self.context.paint()

# TODO: blendMode for PaintComposite)

def _drawGradient(self, path, gradient, gradientTransform):
self.context.new_path()
Expand Down
31 changes: 30 additions & 1 deletion Lib/blackrenderer/backends/coregraphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from fontTools.ttLib.tables.otTables import ExtendMode
import Quartz as CG
from .base import Canvas, Surface
from .sweepGradient import buildSweepGradientPatches


class CoreGraphicsPathPen(BasePen):
Expand Down Expand Up @@ -125,7 +126,35 @@ def drawPathSweepGradient(
extendMode,
gradientTransform,
):
self.drawPathSolid(path, colorLine[0][1])
from math import sqrt

if self.clipIsEmpty or CG.CGPathGetBoundingBox(path.path) == CG.CGRectNull:
return
with self.savedState():
CG.CGContextAddPath(self.context, path.path)
CG.CGContextClip(self.context)
self.transform(gradientTransform)
# find current path' extent
bb = CG.CGContextGetClipBoundingBox(self.context)
x1, y1 = bb.origin.x, bb.origin.y
x2 = x1 + bb.size.width
y2 = y1 + bb.size.height
maxX = max(d * d for d in (x1 - center[0], x2 - center[0]))
maxY = max(d * d for d in (y1 - center[1], y2 - center[1]))
R = sqrt(maxX + maxY)
# compute the triangle fan approximating the sweep gradient
patches = buildSweepGradientPatches(
colorLine, center, R, startAngle, endAngle, useGouraudShading=True
)
CG.CGContextSetAllowsAntialiasing(self.context, False)
for (P0, color0), (P1, color1) in patches:
color = 0.5 * (color0 + color1)
CG.CGContextMoveToPoint(self.context, center[0], center[1])
CG.CGContextAddLineToPoint(self.context, P0[0], P0[1])
CG.CGContextAddLineToPoint(self.context, P1[0], P1[1])
CG.CGContextSetRGBFillColor(self.context, *color)
CG.CGContextFillPath(self.context)
CG.CGContextSetAllowsAntialiasing(self.context, True)

# TODO: blendMode for PaintComposite

Expand Down
58 changes: 58 additions & 0 deletions Lib/blackrenderer/backends/sweepGradient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from math import pi, ceil, sin, cos
from fontTools.misc.vector import Vector


def buildSweepGradientPatches(
colorLine,
center,
radius,
startAngle,
endAngle,
useGouraudShading,
):
"""Provides colorful patches that mimic a sweep gradient, for use, in
particular, in the Cairo and CoreGraphics backends, since these libraries
lack the sweep gradient feature."""
patches = []
# generate a fan of 'triangular' bezier patches, with center 'center' and radius 'radius'
degToRad = pi / 180.0
horasio marked this conversation as resolved.
Show resolved Hide resolved
if useGouraudShading:
maxAngle = pi / 360.0
justvanrossum marked this conversation as resolved.
Show resolved Hide resolved
radius = 1.05 * radius # we will use straight-edged triangles
justvanrossum marked this conversation as resolved.
Show resolved Hide resolved
else:
maxAngle = pi / 10.0
n = len(colorLine)
center = Vector(center)
for i in range(n - 1):
a0, col0 = colorLine[i + 0]
a1, col1 = colorLine[i + 1]
col0 = Vector(col0)
col1 = Vector(col1)
a0 = degToRad * (startAngle + a0 * (endAngle - startAngle))
a1 = degToRad * (startAngle + a1 * (endAngle - startAngle))
numSplits = int(ceil((a1 - a0) / maxAngle))
p0 = Vector((cos(a0), sin(a0)))
color0 = col0
for a in range(numSplits):
k = (a + 1.0) / numSplits
angle1 = a0 + k * (a1 - a0)
color1 = col0 + k * (col1 - col0)
p1 = Vector((cos(angle1), sin(angle1)))
P0 = center[0] + radius * p0[0], center[1] + radius * p0[1]
P1 = center[0] + radius * p1[0], center[1] + radius * p1[1]
# draw patch
if useGouraudShading:
patches.append(((P0, color0), (P1, color1)))
else:
# compute cubic Bezier antennas (control points) so as to approximate the circular arc p0-p1
A = (p0 + p1).normalized()
U = Vector((-A[1], A[0])) # tangent to circle at A
C0 = A + ((p0 - A).dot(p0) / U.dot(p0)) * U
C1 = A + ((p1 - A).dot(p1) / U.dot(p1)) * U
C0 = center + radius * (C0 + 0.33333 * (C0 - p0))
C1 = center + radius * (C1 + 0.33333 * (C1 - p1))
patches.append(((P0, color0), C0, C1, (P1, color1)))
# move to next patch
p0 = p1
color0 = color1
return patches
20 changes: 12 additions & 8 deletions Tests/test_canvas_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,22 @@ def test_colorStops(backendName, surfaceFactory, stopOffsets, extend):
@pytest.mark.parametrize("extend", test_extendModes)
@pytest.mark.parametrize("backendName, surfaceFactory", backends)
def test_sweepGradient(backendName, surfaceFactory, extend):
surface = surfaceFactory(0, 0, 200, 200)
H, W = 400, 400
surface = surfaceFactory(0, 0, H, W)
canvas = surface.canvas
center = (100, 100)
center = (H / 2, W / 2)
startAngle = 45
endAngle = 315
color1 = (1, 0, 0, 1)
color2 = (0, 0, 1, 1)
stopOffsets = [0, 1]
stop1, stop2 = stopOffsets
colorLine = [(stop1, color1), (stop2, color2)]
colors = [
(1, 0, 0, 1),
(0, 1, 0, 1),
(1, 1, 0, 1),
(0, 0, 1, 1),
]
stopOffsets = [0, 0.5, 0.6, 1]
colorLine = list(zip(stopOffsets, colors))
canvas.drawRectSweepGradient(
(0, 0, 200, 200), colorLine, center, startAngle, endAngle, extend, Identity
(0, 0, H, W), colorLine, center, startAngle, endAngle, extend, Identity
)

ext = surface.fileExtension
Expand Down