Гильоши — это характерные узоры на бумажных деньгах и других ценных бумагах. Подробный рассказ о них с отступлением в историю можно найти в предыдущей статье. Там же приводился и алгоритм рисования, строящий гильоши по точкам.
Довольно бесполезный, надо заметить, если мы рисуем их не просто для развлечения, а в практических целях — например, чтобы добавить в дизайн тех самых ценных бумаг. Тысячи точек будут только тормозить редактор, а нормально вывести результат все равно не получится — вместо настоящих непрерывных линий там будет какой-то результат усреднения точек, сделанный как бог на душу положит.
Поэтому пришло время подумать о другом алгоритме — который давал бы сразу вектора. Поскольку в распространенных редакторах для кривых линий предлагается только интерполяция кривыми Безье, на них и будем ориентироваться.
Алгоритм, собственно, несложен — и куда проще описанного в первой части. Берем две огибающие кривые, задаем количество волн, которые должны уложиться в 360 градусов, прикидываем, под каким углом и с какой кривизной должен идти от текущей точки до следующей настоящий гильош, и интерполируем его четырьмя кривыми Безье.
Вот программа на языке Asymptote, крайне удобном для подобных вещей.
Самый понятный случай — когда синусоиды располагаются между двух кругов.

Случай похитрее — эллипсы вместо кругов.

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

Тут результат, правда, не идеальный. Во-первых, пришлось поправить функцию tens, чтобы она всегда возвращала константное «натяжение» 0.5. А во-вторых, кривые лежат не сильно симметрично, а в левой части возле оси X и вовсе как-то нехорошо путаются. Конечно, это всё можно поправить руками, особенно если вы делаете банкноты для государства и располагаете очень квалифицированными художниками, но можно попробовать и увеличить точность расчетов, потому что они явно сбиваются в каких-то точках, где у огибающих резко меняется кривизна.
Поскольку гильоши интерполируются, возникает вопрос: совпадают ли они с, так сказать, «настоящими», то есть нарисованными по точкам. Будучи плохо знаком с дифференциальной геометрией, затрудняюсь сказать, но скорее «нет», чем «да».
Но кто реально заметит разницу?
А практическая польза несомненна — с парой десятков кривых Безье работать куда проще, чем с тысячей точек, и возможностей для дизайна они открывают куда больше.
Кроме того, этот алгоритм можно еще и совершенствовать. Сразу напрашиваются два варианта:
а) задавать разное количество точек на внешней и внутренней кривой, тогда не будет создаваться эффекта измельчения узора ближе к центру как в примерах с кругом и эллипсом.
б) размещать точки на огибающих не равномерно, а, например, делая их то чаще, то реже, что добавит в узор новое измерение.
Довольно бесполезный, надо заметить, если мы рисуем их не просто для развлечения, а в практических целях — например, чтобы добавить в дизайн тех самых ценных бумаг. Тысячи точек будут только тормозить редактор, а нормально вывести результат все равно не получится — вместо настоящих непрерывных линий там будет какой-то результат усреднения точек, сделанный как бог на душу положит.
Поэтому пришло время подумать о другом алгоритме — который давал бы сразу вектора. Поскольку в распространенных редакторах для кривых линий предлагается только интерполяция кривыми Безье, на них и будем ориентироваться.
Алгоритм, собственно, несложен — и куда проще описанного в первой части. Берем две огибающие кривые, задаем количество волн, которые должны уложиться в 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 и вовсе как-то нехорошо путаются. Конечно, это всё можно поправить руками, особенно если вы делаете банкноты для государства и располагаете очень квалифицированными художниками, но можно попробовать и увеличить точность расчетов, потому что они явно сбиваются в каких-то точках, где у огибающих резко меняется кривизна.
Поскольку гильоши интерполируются, возникает вопрос: совпадают ли они с, так сказать, «настоящими», то есть нарисованными по точкам. Будучи плохо знаком с дифференциальной геометрией, затрудняюсь сказать, но скорее «нет», чем «да».
Но кто реально заметит разницу?
А практическая польза несомненна — с парой десятков кривых Безье работать куда проще, чем с тысячей точек, и возможностей для дизайна они открывают куда больше.
Кроме того, этот алгоритм можно еще и совершенствовать. Сразу напрашиваются два варианта:
а) задавать разное количество точек на внешней и внутренней кривой, тогда не будет создаваться эффекта измельчения узора ближе к центру как в примерах с кругом и эллипсом.
б) размещать точки на огибающих не равномерно, а, например, делая их то чаще, то реже, что добавит в узор новое измерение.
