Гильоши другим манером

—

от автора

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

Довольно бесполезный, надо заметить, если мы рисуем их не просто для развлечения, а в практических целях — например, чтобы добавить в дизайн тех самых ценных бумаг. Тысячи точек будут только тормозить редактор, а нормально вывести результат все равно не получится — вместо настоящих непрерывных линий там будет какой-то результат усреднения точек, сделанный как бог на душу положит.

Поэтому пришло время подумать о другом алгоритме — который давал бы сразу вектора. Поскольку в распространенных редакторах для кривых линий предлагается только интерполяция кривыми Безье, на них и будем ориентироваться.

Алгоритм, собственно, несложен — и куда проще описанного в первой части. Берем две огибающие кривые, задаем количество волн, которые должны уложиться в 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); 

Самый понятный случай — когда синусоиды располагаются между двух кругов.

image

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

image

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

image

Тут результат, правда, не идеальный. Во-первых, пришлось поправить функцию tens, чтобы она всегда возвращала константное «натяжение» 0.5. А во-вторых, кривые лежат не сильно симметрично, а в левой части возле оси X и вовсе как-то нехорошо путаются. Конечно, это всё можно поправить руками, особенно если вы делаете банкноты для государства и располагаете очень квалифицированными художниками, но можно попробовать и увеличить точность расчетов, потому что они явно сбиваются в каких-то точках, где у огибающих резко меняется кривизна.

Поскольку гильоши интерполируются, возникает вопрос: совпадают ли они с, так сказать, «настоящими», то есть нарисованными по точкам. Будучи плохо знаком с дифференциальной геометрией, затрудняюсь сказать, но скорее «нет», чем «да».

Но кто реально заметит разницу?

А практическая польза несомненна — с парой десятков кривых Безье работать куда проще, чем с тысячей точек, и возможностей для дизайна они открывают куда больше.

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

а) задавать разное количество точек на внешней и внутренней кривой, тогда не будет создаваться эффекта измельчения узора ближе к центру как в примерах с кругом и эллипсом.

б) размещать точки на огибающих не равномерно, а, например, делая их то чаще, то реже, что добавит в узор новое измерение.

ссылка на оригинал статьи https://habr.com/ru/post/502412/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *