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

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

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

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

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

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

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

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

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

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

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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 2

      0

      Надо будет на nodejs попробовать нарисовать

        0
        Я с вашего позволения сделал всё это в своем макросе для Corel (само собой, название давать не буду, чтобы не сочли за рекламу). Сперва тоже думал что проблема со множеством точек будет напрягать и даже сделал алгоритм который уменьшает точки вдвое, без существенной потери качества, но в целом — оказалось не особо нужным. При современных возможностях компьютера, легкие гильоши строятся так или иначе быстро. Удаление же половины существенно увеличивает время постобработки и не приводит к особым выгодам по качеству или лёгкости конечного файла. А сложные розетки и бордюры — всё равно очень трудозатратны и разница точек в каждой кривой вдвое больше или меньше, не столь уж и очевидна в плане времени тратящегося на создание. По крайней мере, если судить по возможностям в CorelDraw.
        И отдельно хочу сказать Вам спасибо, так как Ваша статья очень существенно помогла продвинуться в понимании как строятся подобные сетки!

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое