Гильоши другим манером
Гильоши — это характерные узоры на бумажных деньгах и других ценных бумагах. Подробный рассказ о них с отступлением в историю можно найти в предыдущей статье. Там же приводился и алгоритм рисования, строящий гильоши по точкам.
Довольно бесполезный, надо заметить, если мы рисуем их не просто для развлечения, а в практических целях — например, чтобы добавить в дизайн тех самых ценных бумаг. Тысячи точек будут только тормозить редактор, а нормально вывести результат все равно не получится — вместо настоящих непрерывных линий там будет какой-то результат усреднения точек, сделанный как бог на душу положит.
Поэтому пришло время подумать о другом алгоритме — который давал бы сразу вектора. Поскольку в распространенных редакторах для кривых линий предлагается только интерполяция кривыми Безье, на них и будем ориентироваться.
Алгоритм, собственно, несложен — и куда проще описанного в первой части. Берем две огибающие кривые, задаем количество волн, которые должны уложиться в 360 градусов, прикидываем, под каким углом и с какой кривизной должен идти от текущей точки до следующей настоящий гильош, и интерполируем его четырьмя кривыми Безье.
Вот программа на языке Asymptote, крайне удобном для подобных вещей.
import graph;
size(1000,1000);
xaxis(ticks=Ticks);
yaxis(ticks=Ticks);
defaultpen(2);
var zero = (0,0);
/////////////////////////////
// натяжение кривой безье зависит от угла в этой точке
// 0..180 -> 0..1
real tens(bool at_top, real angle)
{
return angle/180;
}
guide wave(path top, path bottom, int parts, real offset)
{
guide w;
real step = 1/parts;
real half = step/2;
pair[] top_pt;
pair[] bot_pt;
pair[] top_dir;
pair[] bot_dir;
// Углы в точках
real[] top_angle;
real[] bot_angle;
for(int i: sequence(0,parts-1))
{
real rel = i*step + step*offset;
real top_time = reltime(top, rel);
real bot_time = reltime(bottom, rel+half);
// точки соединения кривыми
top_pt[i] = point(top, top_time);
bot_pt[i] = point(bottom, bot_time);
// направление производной в точке относительной длины rel
top_dir[i] = dir(top, top_time);
bot_dir[i] = dir(bottom, bot_time);
}
for(int i: sequence(0,parts-1))
{
int prev = i == 0 ? parts-1 : i-1;
int next = i == parts-1 ? 0 : i+1;
// t: t[i]--b[i] /\ t[i]--b[prev]
var v1 = bot_pt[i] - top_pt[i];
var v2 = bot_pt[prev] - top_pt[i];
var a = degrees(v2) - degrees(v1);
top_angle[i] = a<0 ? 360+a : a;
// b: b[i]--t[i] /\ b[i]--t[next]
v1 = top_pt[i] - bot_pt[i];
v2 = top_pt[next] - bot_pt[i];
a = degrees(v2) - degrees(v1);
bot_angle[i] = a<0 ? 360+a : a;
}
for(int i: sequence(0,parts-1))
{
int next = i == parts-1 ? 0 : i+1;
var l1 = length(top_pt[i]--bot_pt[i]);
pair ctl1 = top_pt[i] + top_dir[i] * tens(true, top_angle[i]) * l1;
pair ctl2 = bot_pt[i] - bot_dir[i] * tens(false, bot_angle[i]) * l1;
w = w .. top_pt[i] .. controls ctl1 and ctl2 .. bot_pt[i];
var l2 = length(bot_pt[i]--top_pt[next]);
ctl1 = bot_pt[i] + bot_dir[i] * tens(false, bot_angle[i]) * l2;
ctl2 = top_pt[next] - top_dir[next] * tens(true, top_angle[next]) * l2;
w = w .. bot_pt[i] .. controls ctl1 and ctl2 .. top_pt[next];
}
return w;
}
// Рисуем много кривых, сдвигая каждую
void repeat(int count, path top, path bottom, int parts)
{
real step = 1/count;
for(int i: sequence(0, count-1))
{
draw(wave(top, bottom, parts, step*i));
}
}
// Перемещаем огибающие в центр экрана и подгоняем их под некоторый стандартный размер
// Это чтобы можно было брать готовые кривые из других источников и сильно с ними не возиться
path normalize(path p)
{
var min = min(p);
var max = max(p);
var top_center = min + ((max.x-min.x)/2, (max.y-min.y)/2);
return scale(20*1/(max-min).x)*shift(zero - top_center)*p;
}
/////////////////////////////
// Тест 3 - некая красивая кривая, взятая прямо из графического редактора
path top = (338.499521684,-159.274266483)
..controls (327.252951684,-158.148796483) and (323.448961684,-145.618286483) .. (318.743661684,-137.260595483)
..controls (309.897671684,-123.808725483) and (292.025851684,-123.657732483) .. (278.251471684,-118.807470483)
..controls (272.669581684,-117.510629483) and (268.731931684,-109.221757483) .. (274.571781684,-105.645360483)
..controls (281.545351684,-101.031122483) and (290.488261684,-97.7906864833) .. (293.317871684,-89.0437964838)
..controls (296.611021684,-81.8498064838) and (293.894071684,-73.5853264838) .. (295.556161684,-66.3445764838)
..controls (299.563831684,-59.7686064838) and (308.181311684,-64.5344964838) .. (312.903811684,-67.4344264838)
..controls (325.368171684,-74.9872364838) and (341.157891684,-80.6126364838) .. (355.257331684,-73.9383264838)
..controls (363.506651684,-70.9246164838) and (370.115991684,-63.9703964838) .. (378.731941684,-62.0926264838)
..controls (384.688491684,-61.4010364838) and (389.980631684,-67.6129964838) .. (387.306161684,-73.3211464838)
..controls (385.256921684,-82.8346964838) and (388.441441684,-93.9447564833) .. (397.757331684,-98.3016064833)
..controls (403.144721684,-101.085582483) and (412.671611684,-104.606352483) .. (410.331551684,-112.414892483)
..controls (406.654931684,-119.718595483) and (396.921641684,-119.937732483) .. (390.144051684,-122.524267483)
..controls (378.065751684,-125.483516483) and (364.313841684,-130.717262483) .. (359.884541684,-143.562216483)
..controls (356.731021684,-151.157386483) and (350.818391684,-160.192046483) .. (341.435061684,-159.293796483)
..controls (340.456461684,-159.306096483) and (339.478031684,-159.281196483) .. (338.499521684,-159.274296483)
--cycle;
top = normalize(top);
bottom = scale(0.5)*top;
// Тест 2 - обычные эллипсы
top = ellipse(zero, 4, 6);
bottom = ellipse(zero, 2, 3);
// Тест 1, самый простой, синусы по кругу
top = circle(zero, 5);
bottom = circle(zero, 3);
// 12 кривых, каждая соприкасается с огибающей в 8 точках
// top - внешняя огибающая, bottom - внутренняя
repeat(12, top, bottom, 8);
// Огибающие для наглядности
//draw(top, red);
//draw(bottom, red);
Самый понятный случай — когда синусоиды располагаются между двух кругов.
Случай похитрее — эллипсы вместо кругов.
И картинка, приближенная к промышленному применению: гильоши, образующие некую художественную розетку.
Тут результат, правда, не идеальный. Во-первых, пришлось поправить функцию tens, чтобы она всегда возвращала константное «натяжение» 0.5. А во-вторых, кривые лежат не сильно симметрично, а в левой части возле оси X и вовсе как-то нехорошо путаются. Конечно, это всё можно поправить руками, особенно если вы делаете банкноты для государства и располагаете очень квалифицированными художниками, но можно попробовать и увеличить точность расчетов, потому что они явно сбиваются в каких-то точках, где у огибающих резко меняется кривизна.
Поскольку гильоши интерполируются, возникает вопрос: совпадают ли они с, так сказать, «настоящими», то есть нарисованными по точкам. Будучи плохо знаком с дифференциальной геометрией, затрудняюсь сказать, но скорее «нет», чем «да».
Но кто реально заметит разницу?
А практическая польза несомненна — с парой десятков кривых Безье работать куда проще, чем с тысячей точек, и возможностей для дизайна они открывают куда больше.
Кроме того, этот алгоритм можно еще и совершенствовать. Сразу напрашиваются два варианта:
а) задавать разное количество точек на внешней и внутренней кривой, тогда не будет создаваться эффекта измельчения узора ближе к центру как в примерах с кругом и эллипсом.
б) размещать точки на огибающих не равномерно, а, например, делая их то чаще, то реже, что добавит в узор новое измерение.