2023-11-15 17:19:59 -07:00
|
|
|
|
from fontTools.pens.recordingPen import RecordingPen
|
2023-11-15 16:11:31 -07:00
|
|
|
|
from fontTools.pens.boundsPen import ControlBoundsPen
|
|
|
|
|
from fontTools.pens.cairoPen import CairoPen
|
2023-11-16 17:09:24 -07:00
|
|
|
|
from fontTools.pens.pointPen import SegmentToPointPen
|
|
|
|
|
from fontTools.varLib.interpolatable import PerContourOrComponentPen, RecordingPointPen
|
2023-11-15 19:52:51 -07:00
|
|
|
|
from itertools import cycle
|
2023-11-16 17:32:27 -07:00
|
|
|
|
from functools import wraps
|
2023-11-15 16:11:31 -07:00
|
|
|
|
import cairo
|
2023-11-16 14:58:48 -07:00
|
|
|
|
import math
|
2023-11-15 16:11:31 -07:00
|
|
|
|
|
|
|
|
|
|
2023-11-16 17:32:27 -07:00
|
|
|
|
class InterpolatablePlot:
|
2023-11-15 16:11:31 -07:00
|
|
|
|
width = 640
|
|
|
|
|
height = 480
|
|
|
|
|
pad = 16
|
|
|
|
|
line_height = 36
|
|
|
|
|
head_color = (0.3, 0.3, 0.3)
|
|
|
|
|
label_color = (0.2, 0.2, 0.2)
|
|
|
|
|
border_color = (0.9, 0.9, 0.9)
|
|
|
|
|
border_width = 1
|
|
|
|
|
fill_color = (0.8, 0.8, 0.8)
|
|
|
|
|
stroke_color = (0.1, 0.1, 0.1)
|
|
|
|
|
stroke_width = 2
|
2023-11-15 19:56:17 -07:00
|
|
|
|
oncurve_node_color = (0, 0.8, 0)
|
2023-11-15 19:34:19 -07:00
|
|
|
|
oncurve_node_diameter = 10
|
2023-11-15 19:56:17 -07:00
|
|
|
|
offcurve_node_color = (0, 0.5, 0)
|
2023-11-15 19:34:19 -07:00
|
|
|
|
offcurve_node_diameter = 8
|
2023-11-15 19:56:17 -07:00
|
|
|
|
handle_color = (0.2, 1, 0.2)
|
2023-11-15 19:34:19 -07:00
|
|
|
|
handle_width = 1
|
2023-11-16 17:09:24 -07:00
|
|
|
|
other_start_point_color = (0, 0, 1)
|
2023-11-16 17:12:21 -07:00
|
|
|
|
reversed_start_point_color = (0, 1, 0)
|
2023-11-15 20:34:41 -07:00
|
|
|
|
start_point_color = (1, 0, 0)
|
2023-11-15 19:43:59 -07:00
|
|
|
|
start_point_width = 15
|
|
|
|
|
start_handle_width = 5
|
2023-11-16 14:58:48 -07:00
|
|
|
|
start_handle_length = 100
|
|
|
|
|
start_handle_arrow_length = 5
|
2023-11-15 19:52:51 -07:00
|
|
|
|
contour_colors = ((1, 0, 0), (0, 0, 1), (0, 1, 0), (1, 1, 0), (1, 0, 1), (0, 1, 1))
|
|
|
|
|
contour_alpha = 0.5
|
2023-11-16 14:31:37 -07:00
|
|
|
|
cupcake_color = (0.3, 0, 0.3)
|
|
|
|
|
cupcake = r"""
|
|
|
|
|
,@.
|
|
|
|
|
,@.@@,.
|
|
|
|
|
,@@,.@@@. @.@@@,.
|
|
|
|
|
,@@. @@@. @@. @@,.
|
|
|
|
|
,@@@.@,.@. @. @@@@,.@.@@,.
|
|
|
|
|
,@@.@. @@.@@. @,. .@’ @’ @@,
|
|
|
|
|
,@@. @. .@@.@@@. @@’ @,
|
|
|
|
|
,@. @@. @,
|
|
|
|
|
@. @,@@,. , .@@,
|
|
|
|
|
@,. .@,@@,. .@@,. , .@@, @, @,
|
|
|
|
|
@. .@. @ @@,. , @
|
|
|
|
|
@,.@@. @,. @@,. @. @,. @’
|
|
|
|
|
@@||@,. @’@,. @@,. @@ @,. @’@@, @’
|
|
|
|
|
\\@@@@’ @,. @’@@@@’ @@,. @@@’ //@@@’
|
|
|
|
|
|||||||| @@,. @@’ ||||||| |@@@|@|| ||
|
|
|
|
|
\\\\\\\ ||@@@|| ||||||| ||||||| //
|
|
|
|
|
||||||| |||||| |||||| |||||| ||
|
|
|
|
|
\\\\\\ |||||| |||||| |||||| //
|
|
|
|
|
|||||| ||||| ||||| ||||| ||
|
|
|
|
|
\\\\\ ||||| ||||| ||||| //
|
|
|
|
|
||||| |||| ||||| |||| ||
|
|
|
|
|
\\\\ |||| |||| |||| //
|
|
|
|
|
||||||||||||||||||||||||
|
|
|
|
|
"""
|
2023-11-16 15:40:25 -07:00
|
|
|
|
shrug_color = (0, 0.3, 0.3)
|
|
|
|
|
shrug = r"""¯\_(")_/¯"""
|
2023-11-15 16:11:31 -07:00
|
|
|
|
|
2023-11-16 17:32:27 -07:00
|
|
|
|
def __init__(self, out, glyphsets, names=None, **kwargs):
|
|
|
|
|
self.out = out
|
2023-11-15 16:11:31 -07:00
|
|
|
|
self.glyphsets = glyphsets
|
|
|
|
|
self.names = names or [repr(g) for g in glyphsets]
|
|
|
|
|
|
|
|
|
|
for k, v in kwargs.items():
|
|
|
|
|
if not hasattr(self, k):
|
|
|
|
|
raise TypeError("Unknown keyword argument: %s" % k)
|
|
|
|
|
setattr(self, k, v)
|
|
|
|
|
|
|
|
|
|
def __enter__(self):
|
2023-11-16 17:32:27 -07:00
|
|
|
|
raise NotImplementedError
|
2023-11-15 16:11:31 -07:00
|
|
|
|
|
|
|
|
|
def __exit__(self, type, value, traceback):
|
2023-11-16 17:32:27 -07:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
def set_size(self, width, height):
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
def show_page(self):
|
|
|
|
|
raise NotImplementedError
|
2023-11-15 16:11:31 -07:00
|
|
|
|
|
|
|
|
|
def add_problems(self, problems):
|
|
|
|
|
for glyph, glyph_problems in problems.items():
|
|
|
|
|
for p in glyph_problems:
|
|
|
|
|
self.add_problem(glyph, p)
|
2023-11-16 17:32:27 -07:00
|
|
|
|
self.show_page()
|
2023-11-15 16:11:31 -07:00
|
|
|
|
|
|
|
|
|
def add_problem(self, glyphname, p):
|
|
|
|
|
master_keys = ("master",) if "master" in p else ("master_1", "master_2")
|
|
|
|
|
master_indices = [self.names.index(p[k]) for k in master_keys]
|
|
|
|
|
|
2023-11-16 15:47:21 -07:00
|
|
|
|
if p["type"] == "missing":
|
|
|
|
|
sample_glyph = next(
|
|
|
|
|
i for i, m in enumerate(self.glyphsets) if m[glyphname] is not None
|
|
|
|
|
)
|
|
|
|
|
master_indices.insert(0, sample_glyph)
|
|
|
|
|
|
2023-11-15 16:11:31 -07:00
|
|
|
|
total_width = self.width + 2 * self.pad
|
|
|
|
|
total_height = (
|
|
|
|
|
self.pad
|
|
|
|
|
+ self.line_height
|
|
|
|
|
+ self.pad
|
|
|
|
|
+ len(master_indices) * (self.height + self.pad * 2 + self.line_height)
|
|
|
|
|
+ self.pad
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-16 17:32:27 -07:00
|
|
|
|
self.set_size(total_width, total_height)
|
2023-11-15 16:11:31 -07:00
|
|
|
|
|
|
|
|
|
x = self.pad
|
|
|
|
|
y = self.pad
|
|
|
|
|
|
|
|
|
|
self.draw_label(glyphname, y=y, color=self.head_color, align=0)
|
|
|
|
|
self.draw_label(p["type"], y=y, color=self.head_color, align=1)
|
|
|
|
|
y += self.line_height + self.pad
|
|
|
|
|
|
2023-11-16 17:09:24 -07:00
|
|
|
|
for which, master_idx in enumerate(master_indices):
|
2023-11-15 16:11:31 -07:00
|
|
|
|
glyphset = self.glyphsets[master_idx]
|
|
|
|
|
name = self.names[master_idx]
|
|
|
|
|
|
|
|
|
|
self.draw_label(name, y=y, color=self.label_color, align=0.5)
|
|
|
|
|
y += self.line_height + self.pad
|
|
|
|
|
|
2023-11-16 14:11:56 -07:00
|
|
|
|
if glyphset[glyphname] is not None:
|
2023-11-16 17:09:24 -07:00
|
|
|
|
self.draw_glyph(glyphset, glyphname, p, which, x=x, y=y)
|
2023-11-16 15:40:25 -07:00
|
|
|
|
else:
|
2023-11-16 15:47:21 -07:00
|
|
|
|
self.draw_shrug(x=x, y=y)
|
2023-11-15 16:11:31 -07:00
|
|
|
|
|
|
|
|
|
y += self.height + self.pad
|
|
|
|
|
|
|
|
|
|
def draw_label(self, label, *, y=0, color=(0, 0, 0), align=0):
|
|
|
|
|
cr = cairo.Context(self.surface)
|
|
|
|
|
cr.select_font_face("@cairo", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
|
|
|
|
|
cr.set_font_size(self.line_height)
|
|
|
|
|
font_extents = cr.font_extents()
|
|
|
|
|
font_size = self.line_height * self.line_height / font_extents[2]
|
|
|
|
|
cr.set_font_size(font_size)
|
|
|
|
|
font_extents = cr.font_extents()
|
|
|
|
|
|
|
|
|
|
cr.set_source_rgb(*color)
|
|
|
|
|
|
|
|
|
|
extents = cr.text_extents(label)
|
|
|
|
|
if extents.width > self.width:
|
|
|
|
|
# Shrink
|
|
|
|
|
font_size *= self.width / extents.width
|
|
|
|
|
cr.set_font_size(font_size)
|
|
|
|
|
font_extents = cr.font_extents()
|
|
|
|
|
extents = cr.text_extents(label)
|
|
|
|
|
|
|
|
|
|
# Center
|
|
|
|
|
label_x = (self.width - extents.width) * align + self.pad
|
|
|
|
|
label_y = y + font_extents[0]
|
|
|
|
|
cr.move_to(label_x, label_y)
|
|
|
|
|
cr.show_text(label)
|
|
|
|
|
|
2023-11-16 17:09:24 -07:00
|
|
|
|
def draw_glyph(self, glyphset, glyphname, problem, which, *, x=0, y=0):
|
2023-11-15 20:11:34 -07:00
|
|
|
|
problem_type = problem["type"]
|
2023-11-15 16:11:31 -07:00
|
|
|
|
glyph = glyphset[glyphname]
|
|
|
|
|
|
2023-11-15 17:19:59 -07:00
|
|
|
|
recording = RecordingPen()
|
|
|
|
|
glyph.draw(recording)
|
|
|
|
|
|
2023-11-15 16:11:31 -07:00
|
|
|
|
boundsPen = ControlBoundsPen(glyphset)
|
2023-11-15 17:19:59 -07:00
|
|
|
|
recording.replay(boundsPen)
|
2023-11-15 16:11:31 -07:00
|
|
|
|
|
|
|
|
|
glyph_width = boundsPen.bounds[2] - boundsPen.bounds[0]
|
|
|
|
|
glyph_height = boundsPen.bounds[3] - boundsPen.bounds[1]
|
|
|
|
|
|
|
|
|
|
scale = min(self.width / glyph_width, self.height / glyph_height)
|
|
|
|
|
|
|
|
|
|
cr = cairo.Context(self.surface)
|
|
|
|
|
cr.translate(x, y)
|
|
|
|
|
# Center
|
|
|
|
|
cr.translate(
|
|
|
|
|
(self.width - glyph_width * scale) / 2,
|
|
|
|
|
(self.height - glyph_height * scale) / 2,
|
|
|
|
|
)
|
|
|
|
|
cr.scale(scale, -scale)
|
|
|
|
|
cr.translate(-boundsPen.bounds[0], -boundsPen.bounds[3])
|
|
|
|
|
|
|
|
|
|
if self.border_color:
|
|
|
|
|
cr.set_source_rgb(*self.border_color)
|
|
|
|
|
cr.rectangle(
|
|
|
|
|
boundsPen.bounds[0], boundsPen.bounds[1], glyph_width, glyph_height
|
|
|
|
|
)
|
|
|
|
|
cr.set_line_width(self.border_width / scale)
|
|
|
|
|
cr.stroke()
|
|
|
|
|
|
2023-11-15 19:34:19 -07:00
|
|
|
|
if self.fill_color and problem_type != "open_path":
|
2023-11-15 16:11:31 -07:00
|
|
|
|
pen = CairoPen(glyphset, cr)
|
2023-11-15 17:19:59 -07:00
|
|
|
|
recording.replay(pen)
|
2023-11-15 16:11:31 -07:00
|
|
|
|
cr.set_source_rgb(*self.fill_color)
|
|
|
|
|
cr.fill()
|
|
|
|
|
|
|
|
|
|
if self.stroke_color:
|
|
|
|
|
pen = CairoPen(glyphset, cr)
|
2023-11-15 17:19:59 -07:00
|
|
|
|
recording.replay(pen)
|
2023-11-15 16:11:31 -07:00
|
|
|
|
cr.set_source_rgb(*self.stroke_color)
|
|
|
|
|
cr.set_line_width(self.stroke_width / scale)
|
|
|
|
|
cr.stroke()
|
2023-11-15 19:34:19 -07:00
|
|
|
|
|
|
|
|
|
if problem_type in ("node_count", "node_incompatibility"):
|
|
|
|
|
cr.set_line_cap(cairo.LINE_CAP_ROUND)
|
|
|
|
|
|
|
|
|
|
# Oncurve nodes
|
|
|
|
|
for segment, args in recording.value:
|
|
|
|
|
if not args:
|
|
|
|
|
continue
|
|
|
|
|
x, y = args[-1]
|
|
|
|
|
cr.move_to(x, y)
|
|
|
|
|
cr.line_to(x, y)
|
|
|
|
|
cr.set_source_rgb(*self.oncurve_node_color)
|
|
|
|
|
cr.set_line_width(self.oncurve_node_diameter / scale)
|
|
|
|
|
cr.stroke()
|
|
|
|
|
|
|
|
|
|
# Offcurve nodes
|
|
|
|
|
for segment, args in recording.value:
|
|
|
|
|
for x, y in args[:-1]:
|
|
|
|
|
cr.move_to(x, y)
|
|
|
|
|
cr.line_to(x, y)
|
|
|
|
|
cr.set_source_rgb(*self.offcurve_node_color)
|
|
|
|
|
cr.set_line_width(self.offcurve_node_diameter / scale)
|
|
|
|
|
cr.stroke()
|
|
|
|
|
|
|
|
|
|
# Handles
|
|
|
|
|
for segment, args in recording.value:
|
|
|
|
|
if not args:
|
|
|
|
|
pass
|
2023-11-15 19:56:17 -07:00
|
|
|
|
elif segment in ("moveTo", "lineTo"):
|
|
|
|
|
cr.move_to(*args[0])
|
|
|
|
|
elif segment == "qCurveTo":
|
2023-11-15 19:34:19 -07:00
|
|
|
|
for x, y in args:
|
|
|
|
|
cr.line_to(x, y)
|
|
|
|
|
cr.new_sub_path()
|
2023-11-15 19:56:17 -07:00
|
|
|
|
cr.move_to(*args[-1])
|
|
|
|
|
elif segment == "curveTo":
|
2023-11-15 19:34:19 -07:00
|
|
|
|
cr.line_to(*args[0])
|
|
|
|
|
cr.new_sub_path()
|
|
|
|
|
cr.move_to(*args[1])
|
|
|
|
|
cr.line_to(*args[2])
|
|
|
|
|
cr.new_sub_path()
|
2023-11-15 19:56:17 -07:00
|
|
|
|
cr.move_to(*args[-1])
|
2023-11-15 19:34:19 -07:00
|
|
|
|
else:
|
|
|
|
|
assert False
|
|
|
|
|
|
|
|
|
|
cr.set_source_rgb(*self.handle_color)
|
|
|
|
|
cr.set_line_width(self.handle_width / scale)
|
|
|
|
|
cr.stroke()
|
2023-11-15 19:43:59 -07:00
|
|
|
|
|
|
|
|
|
if problem_type == "wrong_start_point":
|
2023-11-15 20:11:34 -07:00
|
|
|
|
idx = problem["contour"]
|
2023-11-15 20:34:41 -07:00
|
|
|
|
|
2023-11-16 17:09:24 -07:00
|
|
|
|
# Draw suggested point
|
|
|
|
|
if which == 0:
|
|
|
|
|
perContourPen = PerContourOrComponentPen(
|
|
|
|
|
RecordingPen, glyphset=glyphset
|
|
|
|
|
)
|
|
|
|
|
recording.replay(perContourPen)
|
|
|
|
|
points = RecordingPointPen()
|
|
|
|
|
converter = SegmentToPointPen(points, False)
|
|
|
|
|
perContourPen.value[idx].replay(converter)
|
|
|
|
|
targetPoint = points.value[problem["value_2"]][0]
|
|
|
|
|
cr.move_to(*targetPoint)
|
|
|
|
|
cr.line_to(*targetPoint)
|
|
|
|
|
cr.set_line_cap(cairo.LINE_CAP_ROUND)
|
|
|
|
|
cr.set_source_rgb(*self.other_start_point_color)
|
|
|
|
|
cr.set_line_width(self.start_point_width / scale)
|
|
|
|
|
cr.stroke()
|
|
|
|
|
|
2023-11-16 14:58:48 -07:00
|
|
|
|
# Draw start point
|
2023-11-15 20:34:41 -07:00
|
|
|
|
cr.set_line_cap(cairo.LINE_CAP_ROUND)
|
|
|
|
|
i = 0
|
|
|
|
|
for segment, args in recording.value:
|
|
|
|
|
if segment == "moveTo":
|
|
|
|
|
if i == idx:
|
|
|
|
|
cr.move_to(*args[0])
|
|
|
|
|
cr.line_to(*args[0])
|
|
|
|
|
i += 1
|
|
|
|
|
|
2023-11-16 17:12:21 -07:00
|
|
|
|
if which == 0 or not problem["reversed"]:
|
|
|
|
|
cr.set_source_rgb(*self.start_point_color)
|
|
|
|
|
else:
|
|
|
|
|
cr.set_source_rgb(*self.reversed_start_point_color)
|
2023-11-15 20:34:41 -07:00
|
|
|
|
cr.set_line_width(self.start_point_width / scale)
|
|
|
|
|
cr.stroke()
|
|
|
|
|
|
2023-11-16 14:58:48 -07:00
|
|
|
|
# Draw arrow
|
2023-11-15 19:43:59 -07:00
|
|
|
|
cr.set_line_cap(cairo.LINE_CAP_SQUARE)
|
|
|
|
|
first_pt = None
|
2023-11-15 20:11:34 -07:00
|
|
|
|
i = 0
|
2023-11-15 19:43:59 -07:00
|
|
|
|
for segment, args in recording.value:
|
2023-11-15 19:56:17 -07:00
|
|
|
|
if segment == "moveTo":
|
2023-11-15 19:43:59 -07:00
|
|
|
|
first_pt = args[0]
|
|
|
|
|
continue
|
|
|
|
|
if first_pt is None:
|
|
|
|
|
continue
|
|
|
|
|
second_pt = args[0]
|
|
|
|
|
|
2023-11-15 20:11:34 -07:00
|
|
|
|
if i == idx:
|
2023-11-16 14:58:48 -07:00
|
|
|
|
first_pt = complex(*first_pt)
|
|
|
|
|
second_pt = complex(*second_pt)
|
|
|
|
|
length = abs(second_pt - first_pt)
|
|
|
|
|
if length:
|
|
|
|
|
# Draw handle
|
|
|
|
|
length *= scale
|
|
|
|
|
second_pt = (
|
|
|
|
|
first_pt
|
|
|
|
|
+ (second_pt - first_pt) / length * self.start_handle_length
|
|
|
|
|
)
|
|
|
|
|
cr.move_to(first_pt.real, first_pt.imag)
|
|
|
|
|
cr.line_to(second_pt.real, second_pt.imag)
|
|
|
|
|
# Draw arrowhead
|
|
|
|
|
cr.save()
|
|
|
|
|
cr.translate(second_pt.real, second_pt.imag)
|
|
|
|
|
cr.rotate(
|
|
|
|
|
math.atan2(
|
|
|
|
|
second_pt.imag - first_pt.imag,
|
|
|
|
|
second_pt.real - first_pt.real,
|
|
|
|
|
)
|
|
|
|
|
)
|
2023-11-16 15:04:13 -07:00
|
|
|
|
cr.translate(self.start_handle_width, 0)
|
2023-11-16 14:58:48 -07:00
|
|
|
|
cr.move_to(0, 0)
|
|
|
|
|
cr.scale(1 / scale, 1 / scale)
|
|
|
|
|
cr.line_to(
|
|
|
|
|
-self.start_handle_arrow_length,
|
|
|
|
|
-self.start_handle_arrow_length,
|
|
|
|
|
)
|
|
|
|
|
cr.line_to(
|
|
|
|
|
-self.start_handle_arrow_length,
|
|
|
|
|
self.start_handle_arrow_length,
|
|
|
|
|
)
|
|
|
|
|
cr.close_path()
|
|
|
|
|
cr.restore()
|
2023-11-15 19:43:59 -07:00
|
|
|
|
|
|
|
|
|
first_pt = None
|
2023-11-15 20:11:34 -07:00
|
|
|
|
i += 1
|
2023-11-15 19:43:59 -07:00
|
|
|
|
|
|
|
|
|
cr.set_line_width(self.start_handle_width / scale)
|
|
|
|
|
cr.stroke()
|
|
|
|
|
|
2023-11-15 19:52:51 -07:00
|
|
|
|
if problem_type == "contour_order":
|
2023-11-16 11:12:40 -07:00
|
|
|
|
matching = problem["value_2"]
|
|
|
|
|
colors = cycle(self.contour_colors)
|
2023-11-15 19:56:17 -07:00
|
|
|
|
perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset)
|
2023-11-15 19:52:51 -07:00
|
|
|
|
recording.replay(perContourPen)
|
2023-11-16 11:12:40 -07:00
|
|
|
|
for i, contour in enumerate(perContourPen.value):
|
|
|
|
|
if matching[i] == i:
|
|
|
|
|
continue
|
|
|
|
|
color = next(colors)
|
2023-11-15 19:52:51 -07:00
|
|
|
|
contour.replay(CairoPen(glyphset, cr))
|
|
|
|
|
cr.set_source_rgba(*color, self.contour_alpha)
|
|
|
|
|
cr.fill()
|
2023-11-16 14:24:47 -07:00
|
|
|
|
|
|
|
|
|
def draw_cupcake(self):
|
2023-11-16 14:31:37 -07:00
|
|
|
|
cupcake = self.cupcake.splitlines()
|
2023-11-16 14:24:47 -07:00
|
|
|
|
cr = cairo.Context(self.surface)
|
2023-11-16 14:31:37 -07:00
|
|
|
|
cr.set_source_rgb(*self.cupcake_color)
|
2023-11-16 14:24:47 -07:00
|
|
|
|
cr.set_font_size(self.line_height)
|
|
|
|
|
cr.select_font_face(
|
|
|
|
|
"monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL
|
|
|
|
|
)
|
|
|
|
|
width = 0
|
|
|
|
|
height = 0
|
|
|
|
|
for line in cupcake:
|
|
|
|
|
extents = cr.text_extents(line)
|
|
|
|
|
width = max(width, extents.width)
|
|
|
|
|
height += extents.height
|
2023-11-16 15:51:51 -07:00
|
|
|
|
if not width:
|
|
|
|
|
return
|
2023-11-16 14:24:47 -07:00
|
|
|
|
cr.scale(self.width / width, self.height / height)
|
|
|
|
|
for line in cupcake:
|
|
|
|
|
cr.translate(0, cr.text_extents(line).height)
|
|
|
|
|
cr.move_to(0, 0)
|
|
|
|
|
cr.show_text(line)
|
2023-11-16 15:40:25 -07:00
|
|
|
|
|
2023-11-16 15:47:21 -07:00
|
|
|
|
def draw_shrug(self, x=0, y=0):
|
2023-11-16 15:40:25 -07:00
|
|
|
|
cr = cairo.Context(self.surface)
|
2023-11-16 15:47:21 -07:00
|
|
|
|
cr.translate(x, y)
|
2023-11-16 15:40:25 -07:00
|
|
|
|
cr.set_source_rgb(*self.shrug_color)
|
|
|
|
|
cr.set_font_size(self.line_height)
|
|
|
|
|
cr.select_font_face(
|
|
|
|
|
"monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL
|
|
|
|
|
)
|
|
|
|
|
extents = cr.text_extents(self.shrug)
|
2023-11-16 15:51:51 -07:00
|
|
|
|
if not extents.width:
|
|
|
|
|
return
|
2023-11-16 15:47:21 -07:00
|
|
|
|
cr.translate(0, self.height * 0.6)
|
|
|
|
|
scale = self.width / extents.width
|
2023-11-16 15:40:25 -07:00
|
|
|
|
cr.scale(scale, scale)
|
2023-11-16 15:47:21 -07:00
|
|
|
|
cr.move_to(-extents.x_bearing, 0)
|
2023-11-16 15:40:25 -07:00
|
|
|
|
cr.show_text(self.shrug)
|
2023-11-16 17:32:27 -07:00
|
|
|
|
|
|
|
|
|
|
2023-11-16 17:36:37 -07:00
|
|
|
|
class InterpolatablePostscriptLike(InterpolatablePlot):
|
2023-11-16 17:32:27 -07:00
|
|
|
|
@wraps(InterpolatablePlot.__init__)
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
|
|
|
self.surface.finish()
|
|
|
|
|
|
|
|
|
|
def set_size(self, width, height):
|
|
|
|
|
self.surface.set_size(width, height)
|
|
|
|
|
|
|
|
|
|
def show_page(self):
|
|
|
|
|
self.surface.show_page()
|
2023-11-16 17:34:49 -07:00
|
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
self.surface = cairo.PSSurface(self.out, self.width, self.height)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
2023-11-16 17:36:37 -07:00
|
|
|
|
class InterpolatablePS(InterpolatablePostscriptLike):
|
2023-11-16 17:34:49 -07:00
|
|
|
|
def __enter__(self):
|
|
|
|
|
self.surface = cairo.PSSurface(self.out, self.width, self.height)
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
2023-11-16 17:36:37 -07:00
|
|
|
|
class InterpolatablePDF(InterpolatablePostscriptLike):
|
2023-11-16 17:34:49 -07:00
|
|
|
|
def __enter__(self):
|
|
|
|
|
self.surface = cairo.PDFSurface(self.out, self.width, self.height)
|
|
|
|
|
return self
|