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

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

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

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

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

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

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

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

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

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