Luxor

  • Tutorial

Сегодня мы рассмотрим графический пакет для языка Julia, который называется Luxor. Это один из тех инструментов, которые превращают процесс создания векторных изображений в решение логических задачек с сопутствующей бурей эмоций.


Осторожно! Под катом 8.5 Мб легковесных картинок и гифок изображающих психоделические яйца и четырехмерные объекты, просмотр которых может вызвать лёгкое помутнение рассудка!


Установка


https://julialang.org — скачиваем дистрибутив Джулии с официального сайта. Затем, запустив интерпретатор, вбиваем в его консоль команды:


using Pkg
Pkg.add("Colors")
Pkg.add("ColorSchemes")
Pkg.add("Luxor")

что установит пакеты для расширенной работы с цветами и сам Luxor.


Возможные проблемы


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


Так, ImageMagick.jl не хотел загружать и сохранять файлы. Решение нашлось на странице оригинала — оказалось, он не любит кириллицу в путях.
Проблема номер два возникла с пакетом низкоуровневой графики Cairo на Windows 7. Решение упрячу здесь:


Танцы с бубном
  1. Набираем в интерпретаторе ]add Gtk — начнет устанавливаться пакет для работы с gui и скорее всего он упадет во время построения
  2. Далее качаем gtk+-bundle_3.6.4-20130513_win64
  3. В папке с пакетами Джулии во время установки накидалось всё необходимое, но во время выполнения пункта один, gtk не достроился, поэтому мы и скачали готовую версию для нашей машины — кидаем содержимое скачанного архива в директорию C:\Users\User.julia\packages\WinRPM\Y9QdZ\deps\usr\x86_64-w64-mingw32\sys-root\mingw (Ваш путь может отличаться)
  4. Запустите julia и вбейте ]build Gtk и после построения using Gtk, и, для пущей верности, перестроим Люксор: ]build Luxor
  5. Перезапускаем julia, и можем смело использовать всё что нужно: using Luxor

В случае иных проблем стараемся найти свой случай


Если хочется пробовать анимацию


Пакет Luxor создает анимацию средствами ffmpeg при условии, что он присутствует на вашем компьютере. ffmpeg — это кроссплатформенная open-source библиотека для обработки видео- и аудиофайлов, очень полезная штука (есть хороший экскурс на хабре). Установим ее:


  • Качаем ffmpeg с оффсайта. В моем случае это загрузка для windows
  • Распаковываем и прописываем путь к ffmpeg.exe в переменную Path.

Подробней про забивание в Path


Компьютер/Свойства сиситемы/Дополнительные параметры системы/Переменные среды/Path (Создать если нет) и добавить туда путь к Вашему ffmpeg.exe
Пример C:\Program Files\ffmpeg-4.1.3-win64-static\bin
если в Path уже есть значения, то отделяем их точкой с запятой.
Теперь если в командную консоль (cmd) вбить ffmpeg с нужными параметрами, оно запустится и отработает, а Julia будет с ним общаться только так.


Hello world


Начнем с маленького подводного камня — при построении изображения создается графический файл и сохраняется в рабочей директории. То есть, при работе в REPL корневая папка julia будет забиваться картинками, а если рисовать в Jupyter — то картинки накапливаются рядом с блокнотом-проектом, поэтому, будет хорошей привычкой перед началом работы задавать рабочую директории в отдельно отведенном месте:


using Luxor
cd("C:\\Users\\User\\Desktop\\mycop")

Создадим первый рисунок


Drawing(220, 220, "hw.png")
origin()

background("white")
sethue("black")
text("Hello world")
circle(Point(0, 0), 100, :stroke)

finish()
preview()


Drawing() создает рисунок, по умолчанию в формате PNG, имя файла по умолчанию 'luxor-drawing.png', размер по умолчанию 800x800, для всех форматов кроме png можно задавать нецелочисленные размеры, а также, можно использовать размеры листа бумаги ("A0", "A1", "A2", "A3", "A4"...)
finish() — завершает рисование и закрывает файл. Вы можете открыть его во внешнем приложении просмотра с помощью preview(), который при работе в Jupyter (IJulia) отобразит файл PNG или SVG в блокноте. При работе в Juno отобразит файл PNG или SVG на панели «График». В Repl же вызовется средство для работы с изображениями, которое вы задали для данного формата в своей ОС.
То же самое можно записать в короткой форме используя макросы


@png begin
    text("Hello world")
    circle(Point(0, 0), 100, :stroke)
    end

Для векторных форматов EPS, SVG, PDF всё работает аналогично.


Евклидово яйцо



Это довольно интересный способ рисования яйца, а если соединить ключевые точки и разрезать по полученным линиям, выйдет отличный танграм



Начнем с окружности:


@png begin

    radius=80

    setdash("dot")
    sethue("gray30")

    A, B = [Point(x, 0) for x in [-radius, radius]]

    line(A, B, :stroke)

    circle(O, radius, :stroke)
    end 200 200 "egg0" # размеры и название файла


Всё предельно просто: setdash("dot") — рисуем точками, sethue("gray30") — цвет линии: чем меньше, тем темнее, чем ближе к 100 тем белее. Класс точки определен и без нас, а центр координат (0,0) можно задавать буквой O. Добавляем две окружности и подписываем точки:


@png begin
    radius=80
    setdash("dot")
    sethue("gray30")
    A, B = [Point(x, 0) for x in [-radius, radius]]
    line(A, B, :stroke)
    circle(O, radius, :stroke)

    label("A", :NW, A)
    label("O", :N,  O)
    label("B", :NE, B)

    circle.([A, O, B], 2, :fill)
    circle.([A, B], 2radius, :stroke)
    end 600 400 "egg2"


Для поиска точек пересечения есть функция, которая называется intersectionlinecircle(), которая находит точку или точки, где линия пересекает окружность. Таким образом, мы можем найти две точки, где один из кругов пересекает воображаемую вертикальную линию, проведенную через O. Из-за симметрии нам можно обработать только круг A.


@png begin

    radius=80
    setdash("dot")
    sethue("gray30")
    A, B = [Point(x, 0) for x in [-radius, radius]]
    line(A, B, :stroke)
    circle(O, radius, :stroke)

    label("A", :NW, A)
    label("O", :N,  O)
    label("B", :NE, B)

    circle.([A, O, B], 2, :fill)
    circle.([A, B], 2radius, :stroke)

    # >>>>
    nints, C, D =
        intersectionlinecircle(Point(0, -2radius), Point(0, 2radius), A, 2radius)

    if nints == 2
        circle.([C, D], 2, :fill)
        label.(["D", "C"], :N, [D, C])
    end

    end 600 400 "egg3"


Чтоб определить центр верхней окружность найдем пересечение OD


Код
@png begin

    radius=80
    setdash("dot")
    sethue("gray30")
    A, B = [Point(x, 0) for x in [-radius, radius]]
    line(A, B, :stroke)
    circle(O, radius, :stroke)

    label("A", :NW, A)
    label("O", :N,  O)
    label("B", :NE, B)

    circle.([A, O, B], 2, :fill)
    circle.([A, B], 2radius, :stroke)

    # >>>>    
    nints, C1, C2 = intersectionlinecircle(O, D, O, radius)
    if nints == 2
        circle(C1, 3, :fill)
        label("C1", :N, C1)
    end

    end 600 400 "egg4"


Радиус придаточной окружности определяется ограничением двумя большими окружностями:


Код
@png begin

    radius=80
    setdash("dot")
    sethue("gray30")
    A, B = [Point(x, 0) for x in [-radius, radius]]
    line(A, B, :stroke)
    circle(O, radius, :stroke)

    label("A", :NW, A)
    label("O", :N,  O)
    label("B", :NE, B)

    circle.([A, O, B], 2, :fill)
    circle.([A, B], 2radius, :stroke)

    # >>>>    
    nints, C1, C2 = intersectionlinecircle(O, D, O, radius)
    if nints == 2
        circle(C1, 3, :fill)
        label("C1", :N, C1)
    end

    # >>>> 
    nints, I3, I4 = intersectionlinecircle(A, C1, A, 2radius)
    nints, I1, I2 = intersectionlinecircle(B, C1, B, 2radius)

    circle.([I1, I2, I3, I4], 2, :fill)

    # >>>>
    if distance(C1, I1) < distance(C1, I2)
       ip1 = I1
    else
       ip1 = I2
    end
    if distance(C1, I3) < distance(C1, I4)
       ip2 = I3
    else
       ip2 = I4
    end

    label("ip1", :N, ip1)
    label("ip2", :N, ip2)
    circle(C1, distance(C1, ip1), :stroke)

    end 600 400 "egg5"


Яйцо готово! Осталось его собрать из четырех дуг, задаваемых функцией arc2r() и залить площадь:


Код
@png begin

    radius=80
    setdash("dot")
    sethue("gray30")
    A, B = [Point(x, 0) for x in [-radius, radius]]
    line(A, B, :stroke)
    circle(O, radius, :stroke)

    label("A", :NW, A)
    label("O", :N,  O)
    label("B", :NE, B)

    circle.([A, O, B], 2, :fill)
    circle.([A, B], 2radius, :stroke)

    # >>>>    
    nints, C1, C2 = intersectionlinecircle(O, D, O, radius)
    if nints == 2
        circle(C1, 3, :fill)
        label("C1", :N, C1)
    end

    # >>>> 
    nints, I3, I4 = intersectionlinecircle(A, C1, A, 2radius)
    nints, I1, I2 = intersectionlinecircle(B, C1, B, 2radius)

    circle.([I1, I2, I3, I4], 2, :fill)

    # >>>>
    if distance(C1, I1) < distance(C1, I2)
       ip1 = I1
    else
       ip1 = I2
    end
    if distance(C1, I3) < distance(C1, I4)
       ip2 = I3
    else
       ip2 = I4
    end

    label("ip1", :N, ip1)
    label("ip2", :N, ip2)
    circle(C1, distance(C1, ip1), :stroke)

    # >>>>

    setline(5)
    setdash("solid")

    arc2r(B,    A,  ip1, :path) # centered at B, from A to ip1
    arc2r(C1, ip1,  ip2, :path)
    arc2r(A,  ip2,    B, :path)
    arc2r(O,    B,    A, :path)

    strokepreserve()
    setopacity(0.8)
    sethue("ivory")
    fillpath()

    end 600 400 "egg6"


А теперь, чтоб как следует побаловаться занесем свои наработки в


функцию
function egg(radius, action=:none)
    A, B = [Point(x, 0) for x in [-radius, radius]]
    nints, C, D =
        intersectionlinecircle(Point(0, -2radius), Point(0, 2radius), A, 2radius)

    flag, C1 = intersectionlinecircle(C, D, O, radius)
    nints, I3, I4 = intersectionlinecircle(A, C1, A, 2radius)
    nints, I1, I2 = intersectionlinecircle(B, C1, B, 2radius)

    if distance(C1, I1) < distance(C1, I2)
        ip1 = I1
    else
        ip1 = I2
    end
    if distance(C1, I3) < distance(C1, I4)
        ip2 = I3
    else
        ip2 = I4
    end

    newpath()
    arc2r(B, A, ip1, :path)
    arc2r(C1, ip1, ip2, :path)
    arc2r(A, ip2, B, :path)
    arc2r(O, B, A, :path)
    closepath()

    do_action(action)
end

Используем рандомные цвета, рисование слоями и различные начальные условия:


@png begin
    setopacity(0.7)
    for θ in range(0, step=π/6, length=12)
        @layer begin
            rotate(θ)
            translate(100, 50)
            # translate(0, -150)
            #rulers()
            egg(50, :path)
            setline(10)
            randomhue()
            fillpreserve()

            randomhue()
            strokepath()
        end
    end
    end 400 400 "eggs2"



Помимо обводки и заливки, вы можете использовать контур в качестве области отсечения (обрезать другое изображение в форму яйца) или в качестве основы для различных конструкторов. Функция egg() создает контур и позволяет применить к нему действие. Также возможно преобразовать наше творение в многоугольник (массив точек). Следующий код преобразует контур яйца в многоугольник, а затем перемещает каждую другую точку многоугольника на полпути к центроиду.


@png begin
    egg(160, :path)
    pgon = first(pathtopoly())

    pc = polycentroid(pgon)
    circle(pc, 5, :fill)

    for pt in 1:2:length(pgon)
        pgon[pt] = between(pc, pgon[pt], 0.5)
    end
    poly(pgon, :stroke)
    end 350 500 "polyegg"


Неравномерный внешний вид внутренних точек здесь выходит как результат настроек соединения линий по умолчанию. Поэкспериментируйте с setlinejoin("round"), чтобы увидеть, не изменит ли это геометрию. Ну а теперь попробуем offsetpoly() создающую многоугольный контур вне или внутри существующего многоугольника..


@png begin
    egg(80, :path)
    pgon = first(pathtopoly())
    pc = polycentroid(pgon)

    for pt in 1:2:length(pgon)
        pgon[pt] = between(pc, pgon[pt], 0.9)
    end

    for i in 30:-3:-8
        randomhue()
        op = offsetpoly(pgon, i)
        poly(op, :stroke, close=true)
    end
    end 350 500 "polyeggs"


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


Анимация


Для начала зададим функции реализующие фон и отрисовку яйца в зависимости от номера кадра:


Код
using Colors

demo = Movie(400, 400, "test")

function backdrop(scene, framenumber)
    background("black")
end

function frame(scene, framenumber)

    setopacity(0.7)
    θ = framenumber * π/6
    @layer begin
        rotate(θ)
        translate(100, 50)
        egg(50, :path)
        setline(10)
        randomhue()
        fillpreserve()
        randomhue()
        strokepath()
        end
end

Анимация реализуется простым набором команд:


animate(demo, [
    Scene(demo, backdrop, 0:12),
    Scene(demo, frame, 0:12,
        easingfunction=easeinoutcubic,
        optarg="made with Julia")
    ],
    framerate=10,
    tempdirectory="C:\\Users\\User\\Desktop\\mycop",
    creategif=true)

Что на самом деле вызывает наш ffmpeg


run(`ffmpeg -f image2 -i $(tempdirectory)/%10d.png -vf palettegen
    -y $(seq.stitle)-palette.png`)

run(`ffmpeg -framerate 30 -f image2 -i $(tempdirectory)/%10d.png
    -i $(seq.stitle)-palette.png -lavfi paletteuse -y /tmp/$(seq.stitle).gif`)

То есть, создается серия изображений, а потом из этих фрэймов собирается гифка:



Пентахор


Он же пятиячейник — правильный четырехмерный симплекс. Чтобы рисовать и манипулировать на двумерных картинках 4-мерные объекты, для начала определим


класс 4-мерная точка
struct Point4D <: AbstractArray{Float64, 1}
    x::Float64
    y::Float64
    z::Float64
    w::Float64
end

Point4D(a::Array{Float64, 1}) = Point4D(a...)

Base.size(pt::Point4D) = (4, )

Base.getindex(pt::Point4D, i) = [pt.x, pt.y, pt.z, pt.w][i]

struct Point3D <: AbstractArray{Float64, 1}
    x::Float64
    y::Float64
    z::Float64
end

Base.size(pt::Point3D) = (3, )

Вместо того, чтобы определять множество операций вручную, мы можем задать нашу структуру как подтип AbstractArray (Подробней про классы как интерфейсы)


Основная задача, которую мы должны решить, — это как преобразовать 4D точку в 2D точку. Давайте начнем с более простой задачи: как преобразовать 3D-точку в 2D-точку, т.е. как мы можем нарисовать 3D-фигуру на плоской поверхности? Рассмотрим простой куб. Передняя и задняя поверхности могут иметь одинаковые координаты X и Y и изменяться только по своим значениям Z.


Код
@png begin
    fontface("Menlo")
    fontsize(8)
    setblend(blend(
        boxtopcenter(BoundingBox()),
        boxmiddlecenter(BoundingBox()),
        "skyblue",
        "white"))
    box(boxtopleft(BoundingBox()),
        boxmiddleright(BoundingBox()), :fill)

    setblend(blend(
        boxmiddlecenter(BoundingBox()),
        boxbottomcenter(BoundingBox()),
        "grey95",
        "grey45"
        ))
    box(boxmiddleleft(BoundingBox()),
        boxbottomright(BoundingBox()), :fill)

    sethue("black")
    setline(2)
    bx1 = box(O, 250, 250, vertices=true)
    poly(bx1, :stroke, close=true)
    label.(["-1 1 1", "-1 -1 1", "1 -1 1", "1 1 1"],
        slope.(O, bx1), bx1)

    setline(1)
    bx2 = box(O, 150, 150, vertices=true)
    poly(bx2, :stroke, close=true)
    label.(["-1 1 0", "-1 -1 0", "1 -1 0", "1 1 0"],
        slope.(O, bx2), bx2, offset=-45)

    map((x, y) -> line(x, y, :stroke), bx1, bx2)
    end 400 400 "cube.png"


Поэтому идея состоит в том, чтобы спроецировать куб из 3D в 2D, сохранив первые два значения и умножив или изменив их на третье значение. Проверим


как оно работает
const K = 4.0
function convert(Point, pt3::Point3D)
    k = 1/(K - pt3.z)
    return Point(pt3.x * k, pt3.y * k)
end

@png begin
    cube = Point3D[
        Point3D(-1, -1,  1),
        Point3D(-1,  1,  1),
        Point3D( 1, -1,  1),
        Point3D( 1,  1,  1),
        Point3D(-1, -1, -1),
        Point3D(-1,  1, -1),
        Point3D( 1, -1, -1),
        Point3D( 1,  1, -1),
    ]
    circle.(convert.(Point, cube) * 300, 5, :fill)
    end 220 220 "points"


Используя тот же принцип, давайте создадим метод для преобразования 4D-точки и функцию, которая берет список четырехмерных точек и дважды отображает их в список двухмерных точек, подходящих для рисования.


function convert(Point3D, pt4::Point4D)
    k = 1/(K - pt4.w)
    return Point3D(pt4.x * k, pt4.y * k, pt4.z * k)
end

function flatten(shape4)
    return map(pt3 -> convert(Point, pt3), map(pt4 -> convert(Point3D, pt4), shape4))
end

Далее задаем вершины и грани и проверяем, как оно работает в цвете


ТЫК!
const n = -1/√5

const pentachoron = [Point4D(vertex...) for vertex in [
    [ 1.0,  1.0,  1.0, n],
    [ 1.0, -1.0, -1.0, n],
    [-1.0,  1.0, -1.0, n],
    [-1.0, -1.0,  1.0, n],
    [ 0.0,  0.0,  0.0, n + √5]]];

const pentachoronfaces = [
    [1, 2, 3],
    [1, 2, 4],
    [1, 2, 5],
    [1, 3, 4],
    [1, 3, 5],
    [1, 4, 5],
    [2, 3, 4],
    [2, 3, 5],
    [2, 4, 5],
    [3, 4, 5]];

@png begin
    setopacity(0.2)
    pentachoron2D = flatten(pentachoron)
    for (n, face) in enumerate(pentachoronfaces)
        randomhue()
        poly(1500 * pentachoron2D[face], :fillpreserve, close=true)
        sethue("black")
        strokepath()
    end
    end 300 250 "5ceil"


Каждый уважающий себя разработчик игр должен знать Математические основы машинной графики. Если же вы никогда не пытались сжимать, вращать, отражать чайники в OpenGL — не пугайтесь, всё довольно просто. Чтоб отразить точку относительно прямой, или чтобы повернуть плоскость вокруг определенной оси, нужно домножить координаты на специальную матрицу. Собственно далее мы и определим нужные нам матрицы преобразований:


наберем побольше
function XY(θ)
    [cos(θ)   -sin(θ)  0        0;
    sin(θ)    cos(θ)   0        0;
    0         0        1        0;
    0         0        0        1]
end

function XW(θ)
    [cos(θ)   0        0        -sin(θ);
    0         1        0        0;
    0         0        1        0;
    sin(θ)    0        0        cos(θ)]
end

function XZ(θ)
    [cos(θ)   0        -sin(θ)  0;
    0         1        0        0;
    sin(θ)    0        cos(θ)   0;
    0         0        0        1]
end

function YZ(θ)
    [1        0        0        0;
    0         cos(θ)   -sin(θ)  0;
    0         sin(θ)   cos(θ)   0;
    0         0        0        1]
end

function YW(θ)
    [1        0        0        0;
    0         cos(θ)   0        -sin(θ);
    0         0        1        0;
    0         sin(θ)   0        cos(θ)]
end

function ZW(θ)
    [1        0        0        0;
    0         1        0        0;
    0         0        cos(θ)   -sin(θ);
    0         0        sin(θ)   cos(θ)];
end

function rotate4(A, matrixfunction)
    return map(A) do pt4
        Point4D(matrixfunction * pt4)
    end
end

Обычно вы поворачиваете точки на плоскости относительно одномерного объекта. 3D-точки — вокруг 2D-линии (часто это одна из осей XYZ). Таким образом, логично что 4D точки поворачиваются относительно 3D-плоскости. Мы определили матрицы, которые выполняют четырехмерное вращение относительно плоскости, определяемой двумя осями X, Y, Z и W. Плоскость XY обычно является плоскостью поверхности рисования. Если вы воспринимаете плоскость XY как экран компьютера то, плоскость XZ параллельна вашему столу или полу, а плоскость YZ — это стены рядом с вашим столом справа или слева. А как же XW, YW и ZW? Это тайна четырехмерных фигур: мы не можем видеть эти плоскости, мы можем только представить их существование, наблюдая, как формы движутся сквозь них и вокруг них.


Теперь задаем функции для фрэймов и сшиваем анимацию:


спойлер
using ColorSchemes

function frame(scene, framenumber, scalefactor=1000)
    background("white") # antiquewhite
    setlinejoin("bevel")
    setline(1.0)
    sethue("black")
    eased_n       = scene.easingfunction(framenumber, 0, 1, scene.framerange.stop)
    pentachoron′  = rotate4(pentachoron, XZ(eased_n * 2π))
    pentachoron2D = flatten(pentachoron′)

    setopacity(0.2)
    for (n, face) in enumerate(pentachoronfaces)
        sethue(get(ColorSchemes.diverging_rainbow_bgymr_45_85_c67_n256,
            n/length(pentachoronfaces)))
        poly(scalefactor * pentachoron2D[face], :fillpreserve, close=true)
        sethue("black")
        strokepath()
    end
end

function makemovie(w, h, fname;
        scalefactor=1000)
    movie1 = Movie(w, h, "4D movie")
    animate(movie1,
        Scene(movie1, (s, f)  -> frame(s, f, scalefactor),
            1:300,
            easingfunction=easeinoutsine),
        #framerate=10,
        tempdirectory="C:\\Users\\User\\Desktop\\mycop",
        creategif=true,
    pathname="C:\\Users\\User\\Desktop\\mycop\\$(fname)")
end

makemovie(320, 320, "pentachoron-xz.gif", scalefactor=2000)


Ну, и еще ракурс:


Код
function frame(scene, framenumber, scalefactor=1000)
    background("antiquewhite")
    setlinejoin("bevel")
    setline(1.0)
    setopacity(0.2)

    eased_n       = scene.easingfunction(framenumber, 0, 1, scene.framerange.stop)
    pentachoron2D = flatten(
        rotate4(
            pentachoron,
                XZ(eased_n * 2π) *
                YW(eased_n * 2π)))
    for (n, face) in enumerate(pentachoronfaces)
        sethue(get(ColorSchemes.diverging_rainbow_bgymr_45_85_c67_n256,
            n/length(pentachoronfaces)))
        poly(scalefactor * pentachoron2D[face], :fillpreserve, close=true)
        sethue("black")
        strokepath()
    end
end

makemovie(500, 500, "pentachoron-xz-yw.gif", scalefactor=2000)


Совершено естественно желание реализовать более популярный четырехмерный объект — Тессеракт


Вершины и грани
const tesseract = [Point4D(vertex...) for vertex in [
    [-1, -1, -1,  1],
    [ 1, -1, -1,  1],
    [ 1,  1, -1,  1],
    [-1,  1, -1,  1],
    [-1, -1,  1,  1],
    [ 1, -1,  1,  1],
    [ 1,  1,  1,  1],
    [-1,  1,  1,  1],
    [-1, -1, -1, -1],
    [ 1, -1, -1, -1],
    [ 1,  1, -1, -1],
    [-1,  1, -1, -1],
    [-1, -1,  1, -1],
    [ 1, -1,  1, -1],
    [ 1,  1,  1, -1],
    [-1,  1,  1, -1]]]

const tesseractfaces = [
    [1,   2,   3,   4],
    [1,   2,  10,   9],
    [1,   4,   8,   5],
    [1,   5,   6,   2],
    [1,   9,  12,   4],
    [2,   3,  11,  10],
    [2,   3,   7,   6],
    [3,   4,   8,   7],
    [5,   6,  14,  13],
    [5,   6,   7,   8],
    [5,   8,  16,  13],
    [6,   7,  15,  14],
    [7,   8,  16,  15],
    [9,  10,  11,  12],
    [9,  10,  14,  13],
    [9,  13,  16,  12],
    [10, 11,  15,  14],
    [13, 14,  15,  16]];

Создание анимации
function frame(scene, framenumber, scalefactor=1000)
    background("black")
    setlinejoin("bevel")
    setline(10.0)
    setopacity(0.7)

    eased_n       = scene.easingfunction(framenumber, 0, 1, scene.framerange.stop)
    tesseract2D = flatten(
        rotate4(
            tesseract,
                XZ(eased_n * 2π) *
                YW(eased_n * 2π)))
    for (n, face) in enumerate(tesseractfaces)
        sethue([Luxor.lighter_blue, Luxor.lighter_green,
            Luxor.lighter_purple, Luxor.lighter_red][mod1(n, 4)]...)
        poly(scalefactor * tesseract2D[face], :fillpreserve, close=true)
        sethue([Luxor.darker_blue, Luxor.darker_green,
            Luxor.darker_purple, Luxor.darker_red][mod1(n, 4)]...)
        strokepath()
    end
end

makemovie(500, 500, "tesseract-xz-yw.gif", scalefactor=1000)


Домашнее задание: автоматизируйте создание массивов координат и номеров вершин (перестановки с повторениями и без повторений соответственно). Также мы использовали не все транслирующие матрицы; каждый новый ракурс вызывает новый "Ух-тыж!", но я решил не перегружать страницу. Ну и можно поэкспериментировать с большим количеством граней и измерений.


Ссылки


  • Luxor — страница на гитхабе
  • Luxor docs — руководство с примерами
  • Cairo — низкоуровневая графическая библиотека; используется Люксором как окружение
  • Блог автора библиотеки — там много всякой крутотени и более расширенных примеров, включая четырехмерные фигуры.

kottke

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

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

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

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

    +2

    Всю статью не покидала мысль что же такое "eag". Надеюсь это не яйцо.

      +2
      Это яйцо
        +1
        Уж очень интересно узнать… На каком языке?
          0

          Спасибо, поправил. Казалось бы, Stardew Valley с полсотни часов наиграл, уток и куриц разводил, а всё равно это "eag" в памяти засело

      +2
      Когда в школе учился, то на уроке информатики как раз занимались подобным. Только тогда это «Turbo basic» называлось. А сейчас, вон оно чо… Luxor…

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

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