Иногда покачивание может быть хорошим решением, чтобы подчеркнуть особую связь между двумя точками.

В этом уроке мы собираемся использовать Python 2.7, Drawbot и немного школьной геометрии. Скрипт находится на GitHub, вы можете получить его, если прокрутите вниз.

Любой читающий графический дизайнер наверняка вспомнит тенденцию к покачиванию, появившуюся несколько лет назад. Если нет, загляните в этот раздел на trendlist.org. Помимо модных тенденций, допустим, вам нужно подчеркнуть взаимосвязь между двумя точками на холсте, однако по какой-либо причине цвет и толщина не подходят. Вы можете обратиться только к форме соединения. Я никогда не был большим поклонником пунктирных линий - холст очень быстро становится нечетким - так что, возможно, линия покачивания может быть полезной. Рисование такого пути с помощью редактора Безье занимает очень много времени, поэтому я хотел бы, чтобы компьютер делал за нас всю тяжелую работу. В этом руководстве я расскажу о моей собственной реализации в Drawbot.

Прежде всего, важно определить, какие переменные мы хотим контролировать. Очевидно, у нас будет начальная и конечная точки, а также длина и высота волны. Почему не подсчет волн? Что ж, если мне нужно выделить несколько пар точек, плотность волн будет более значимым визуальным показателем, чем количество волн. В смысле, у кого есть время считать волны?

Мы не должны забывать, что при использовании кривых Безье мы должны заботиться о контрольных точках Безье (далее именуемые «bcp»).

В этой реализации есть одно предостережение. Расстояние между точками pt1 и pt2, деленное на длину волны, не должно давать остатка. Но это большая просьба для использования в реальной жизни. Конечно, вы можете указать скрипту вернуть ошибку (используя оператор assert), но в большинстве случаев это приведет к тому, что линия не будет нарисована. На мой взгляд, лучше всего округлить длину волны до величины, допускающей целочисленное количество волн в покачивании.

Прежде чем погрузиться в код, я хотел бы сказать несколько дополнительных слов о позиции bcp. Дважды на каждой волне отрезок, который идеально соединяет точки pt1 и pt2, делится точкой. Это точка перегиба, при которой кривая Безье меняет направление. Чтобы вычислить положение bcps, выступающего из точки изгиба, нам необходимо определить по порядку: угол наклона и длину bcp. С этими значениями мы можем использовать синус и косинус для вычисления координат.

Значение bcpInclination, конечно, зависит от угла линии покачивания. Оба угла рассчитываются с помощью функции atan2.

Вместо этого максимальная длина bcp вычисляется с использованием теоремы Пифагора. Это длина гипотенузы прямоугольных треугольников, в которую можно разделить равнобедренный треугольник, в который вписана каждая кривая Безье. Катет, выровненный с покачиванием, составляет четверть отрегулированной длины волны, другой катет измеряет половину высоты волны.

Найдя максимальную длину bcp, мы можем вычислить bcpLength как простое умножение. Теперь остается только правильно выполнить итерацию вычислений во время покачивания.
Итак, давайте погрузимся в код, пора открыть Drawbot.

Чтобы понять следующие шаги, вы должны быть знакомы со следующими темами Python: импорт, циклы for и функции. На всякий случай, Think Python Аллена Б. Дауни - хороший ресурс, чтобы их освежить.

Я обычно разбиваю свои скрипты на четыре раздела: модули, константы, функции / процедуры, переменные и инструкции. Нам понадобится ряд функций из математического модуля Python. Я также предпочитаю хранить данные точек в экземплярах namedtuple из модуля коллекций. Это делает код более читабельным при доступе к данным внутри.

from math import radians, atan2, sqrt, sin, cos
from collections import namedtuple

Затем мы можем определить Point namedtuple, который мы собираемся использовать, и объявить две точки, которые будут крайними точками покачивания.

Point = namedtuple('Point', ['x', 'y'])
[…]
pt1 = Point(50, 50)
pt2 = Point(150, 60)

Чтобы рисовать на холсте Drawbot, нам нужно определить размер холста с помощью функции size (). Эта функция всегда должна быть первым вызовом, связанным с контекстом Drawbot, в противном случае Drawbot предоставит стандартный холст (1000x1000).

size(400, 400)

Процедура рисования будет организована в две функции, чтобы сделать код легко переносимым: одна функция (независимая от Drawbot) будет вычислять точки, необходимые для рисования покачивания, а другая функция (специфичная для Drawbot) будет рисовать точки на холсте.

def calcWiggle(pt1, pt2, waveLength, waveHeight, curveSquaring=.57):
    pass
def drawCurveSequence(wigglePoints):
    pass

Чтобы определить контекст вычисления покачивания, мы должны использовать оператор assert для обнаружения возможных ошибок. Значение квадрата должно быть от 0 до 1, а длина волны должна быть больше 0.

assert 0 <= curveSquaring <= 1, 'curveSquaring should be a value between 0 and 1: {}'.format(curveSquaring)
assert waveLength > 0, 'waveLength smaller or equal to zero: {}'.format(waveLength)

Расстояние между точками pt1 и pt2 необходимо для вычисления отрегулированной длины волны. Я бы предпочел сохранить этот расчет в отдельной функции:

def calcDistance(pt1, pt2):
    return sqrt((pt1.x — pt2.x)**2 + (pt1.y — pt2.y)**2)

То же самое для вычисления угла сегмента между двумя точками:

def calcAngle(pt1, pt2):
    return atan2((pt2.y — pt1.y), (pt2.x — pt1.x))

Я использую эти функции повсюду в своем коде, это означает, что их можно скопировать в любой скрипт Python (не забывайте об импорте). Итак, теперь мы можем вычислить расстояние между точками pt1 и pt2, а также наклон покачивания. Имейте в виду, что atan2 возвращает значение угла в радианах.

diagonal = calcDistance(pt1, pt2)
angleRad = calcAngle(pt1, pt2)

Прежде чем перейти к циклу for, который будет итеративно вычислять точки отклонения, нам нужно определить несколько дополнительных локальных переменных. Сначала количество волн, которое будет использоваться для определения цикла for, и скорректированный интервал волн. Обратите внимание на целочисленное деление // в первой инструкции.

howManyWaves = diagonal//int(waveLength)
waveInterval = diagonal/float(howManyWaves)

Следовательно, необходимые для bcps:

maxBcpLength = sqrt((waveInterval/4.)**2+(waveHeight/2.)**2)
bcpLength = maxBcpLength*curveSquaring
bcpInclination = calcAngle(Point(0,0), Point(waveInterval/4., waveHeight/2.))

Перед запуском цикла for нам нужно инициировать список, в котором будет храниться кривая Безье (единственный элемент, уже находящийся внутри, будет pt1 как точка moveTo ()), имя переменной для предыдущей точки гибкости (каждая точка гибкости расположена с использованием предыдущий) и переменную полярности, которая будет переключаться между 1 и -1, чтобы перемещать покачивание вверх и вниз. Знаки минус на следующей диаграмме получены умножением полученного угла на переменную полярности. Учтите, что цикл for должен повторять вдвое большее количество волн, потому что нам нужны две точки изгиба для каждой волны.

wigglePoints = [pt1]
prevFlexPt = pt1
polarity = 1
for waveIndex in range(0, int(howManyWaves*2)):
    […]

На каждой итерации вычисляется тройка точек, необходимых для построения кривой Безье: bcpOut (которая связана с предыдущими точками в последовательности postscript), bcpIn и конечные точки гибкости. Затем тройки будут сохранены как кортежи в списке wigglePoints.

Каждая пара координат определяется с помощью синуса и косинуса; Взгляните на эту диаграмму, если вам нужно освежить свою тригонометрию.

bcpOutAngle = angleRad+bcpInclination*polarity
bcpOut = Point(prevFlexPt.x+cos(bcpOutAngle)*bcpLength, prevFlexPt.y+sin(bcpOutAngle)*bcpLength)

Затем поворот точки перегиба:

flexPt = Point(prevFlexPt.x+cos(angleRad)*waveInterval/2., prevFlexPt.y+sin(angleRad)*waveInterval/2.)

Используя точку изгиба, мы можем вычислить позицию bcpIn:

bcpInAngle = angleRad+(radians(180)-bcpInclination)*polarity
bcpIn = Point(flexPt.x+cos(bcpInAngle)*bcpLength, flexPt.y+sin(bcpInAngle)*bcpLength)

Не забудьте сохранить тройку в списке и обновить переменные prevFlexPt и полярности:

wigglePoints.append((bcpOut, bcpIn, flexPt))
polarity *= -1
prevFlexPt = flexPt

Последняя инструкция функции - это инструкция возврата:

return wigglePoints

Если мы вернемся к разделу инструкций, теперь мы можем вызвать функцию calcWigglePoints () и распечатать содержимое итератора вывода:

print calcWiggle(pt1, pt2, 16, 36, .7)

Теперь, когда функция, не зависящая от Drawbot, определена, нам нужно определить функцию, которая будет рисовать содержимое списка wigglePoints. В контексте Drawbot есть несколько вариантов, я выбрал класс BezierPath (). Первый элемент в списке - это единственная точка, которая будет использоваться для функции moveTo, затем будут следовать триплеты для curveTo ():

def drawCurvesSequence(wigglePoints):
    myBez = BezierPath()
    myBez.moveTo(wigglePoints[0])
    
    for eachBcpOut, eachBcpIn, eachAnchor in wigglePoints[1:]:
        myBez.curveTo(eachBcpOut, eachBcpIn, eachAnchor)
    myBez.endPath()
    
    drawPath(myBez)

Эта функция работает с любой последовательностью кривых. Теперь осталось просто определить ширину, цвет и т. Д. Штриха. Раздел моих инструкций выглядит следующим образом:

size(400, 400)
oval(pt1.x-1, pt1.y-1, 2, 2)
oval(pt2.x-1, pt2.y-1, 2, 2)
stroke(0,0,0)
strokeWidth(.5)
fill(None)
wigglePoints = calcWiggle(pt1, pt2, 16, 36, .7)
drawCurvesSequence(wigglePoints)

Если вы нашли какую-либо часть этого неясной или трудной для понимания, дайте мне знать. Если вы хотите больше уроков, проявите немного любви и поделитесь со мной своими идеями.

Спасибо Дэйву Коулману, Алессии Маццарелла, Грете Кастеллана, Томмазо Зеннаро и Марку Фрёмбергу за вычитку учебника. Спасибо Just van Rossum за неожиданные выходы этого сценария.

Наслаждаться!