Хорошо, я много искал с помощью Google и думаю, что нашел разумное решение, подходящее для моих целей. Я публикую это здесь - может быть, это будет полезно и кому-то еще.
Сначала давайте начнем с простого Point
класса:
from collections import namedtuple
class Point(namedtuple("Point", "x y")):
__slots__ = ()
def interpolate(self, other, ratio = 0.5):
return Point(x = self.x * (1.0-ratio) + other.x * float(ratio), \
y = self.y * (1.0-ratio) + other.y * float(ratio))
Кубический B-сплайн - не более чем набор Point
объектов:
class CubicBSpline(object):
__slots__ = ("points", )
def __init__(self, points):
self.points = [Point(*coords) for coords in points]
Теперь предположим, что у нас есть открытый равномерный кубический B-сплайн вместо зажатого. Четыре последовательных контрольных точки кубического B-сплайна определяют один сегмент Безье, поэтому контрольные точки с 0 по 3 определяют первый сегмент Безье, контрольные точки с 1 по 4 определяют второй сегмент и так далее. Контрольные точки сплайна Безье могут быть определены путем линейной интерполяции между контрольными точками B-сплайна соответствующим образом. Пусть A, B, C и D будут четырьмя контрольными точками B-сплайна. Рассчитайте следующие вспомогательные точки:
- Найдите точку, которая делит линию A-B в соотношении 2: 1, пусть это будет A '.
- Найдите точку, которая делит линию C-D в соотношении 1: 2, пусть это будет D '.
- Разделите линию B-C на три равные части, пусть две точки будут F и G.
- Найдите точку на полпути между A 'и F, это будет E.
- Найдите точку на полпути между G и D ', это будет H.
Кривая Безье от E до H с контрольными точками F и G эквивалентна открытому B-сплайну между точками A, B, C и D. См. Разделы 1-5 этого превосходного документа . Между прочим, вышеупомянутый метод называется алгоритмом Бёма, и он намного сложнее, если сформулирован надлежащим математическим способом, который учитывает также неоднородные или не кубические B-сплайны.
Мы должны повторить вышеописанную процедуру для каждой группы из 4 последовательных точек B-сплайна, поэтому в итоге нам понадобятся точки деления 1: 2 и 2: 1 между почти любыми последовательными парами контрольных точек. Вот что делает следующий класс BSplineDrawer
перед рисованием кривых:
class BSplineDrawer(object):
def __init__(self, context):
self.ctx = context
def draw(self, bspline):
pairs = zip(bspline.points[:-1], bspline.points[1:])
one_thirds = [p1.interpolate(p2, 1/3.) for p1, p2 in pairs]
two_thirds = [p2.interpolate(p1, 1/3.) for p1, p2 in pairs]
coords = [None] * 6
for i in xrange(len(bspline.points) - 3):
start = two_thirds[i].interpolate(one_thirds[i+1])
coords[0:2] = one_thirds[i+1]
coords[2:4] = two_thirds[i+1]
coords[4:6] = two_thirds[i+1].interpolate(one_thirds[i+2])
self.context.move_to(*start)
self.context.curve_to(*coords)
self.context.stroke()
Наконец, если мы хотим нарисовать зажатые B-сплайны вместо открытых B-сплайнов, мы просто должны повторить обе конечные точки зажатого B-сплайна еще три раза:
class CubicBSpline(object):
[...]
def clamped(self):
new_points = [self.points[0]] * 3 + self.points + [self.points[-1]] * 3
return CubicBSpline(new_points)
Наконец, вот как должен использоваться код:
import cairo
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 600, 400)
ctx = cairo.Context(surface)
points = [(100,100), (200,100), (200,200), (100,200), (100,400), (300,400)]
spline = CubicBSpline(points).clamped()
ctx.set_source_rgb(0., 0., 1.)
ctx.set_line_width(5)
BSplineDrawer(ctx).draw(spline)