Написал тут неожиданно для себя статейку. Вроде получилось симпатично. Даже немного жалко, что её у меня никто не прочитает. Чтобы не потерялась при очередной аварии винчестера, продублирую её тут.
Построение 3D-чертежей
(принято к публикации 2014-10-24)
Довольно часто возникают ситуации, когда требуется сделать некий чертеж на веб-странице. Обычно для этих целей хватает возможностей SVG.
Но иногда появляется необходимость запечатлеть нечто трехмерное. Вовсе необязательно при этом ожидать реалистичной графики - например,
для иллюстрации задач по стереометрии используются схематичные "каркасные" рисунки, состоящие из прямых линий или несложных кривых.
Столкнувшись с такой ситуацией мы быстро выяснили, что нам лень самим придумывать, как будет выглядеть тот или иной параллелепипед на чертеже,
поэтому решили приложить некоторые усилия и научиться задавать и отображать средствами SVG + Javascript несложную трехмерную графику.
В этой ситуации не требуется заботиться о скорости работы алгоритмов - сцены предполагаются статичными. Таким образом, решение поставленной задачи
оказывается под силу даже таким дилетантам, как мы.
Сначала немного теории из проективной геометрии и почти забытого курса компьютерной графики. Физически процесс наблюдения трехмерной схемы
выглядит следующим образом:
Наблюдатель находится в некой точке пространства за экраном и наблюдает на экране проекцию сцены.
Для расчета, как эта самая сцена выглядит на экране, полезно ввести декартову систему координат. Или даже две:
До сих пор мы смотрели картину, что называется, "со стороны" - в системе координат XYZ, связанной со сценой. Попробуем взглянуть на то же самое
в системе координат X'Y'Z', связанной с экраном:
Как видим, с точки зрения понимания, как всё будет выглядеть на экране, обстановка резко улучшилась.
Но не до конца: нам ещё нужно как-то обработать тот факт, что рассматриваемая сцена должна быть спроектирована
на экран. Для этого мы "угоним" наблюдателя влево на "минус бесконечность". В этом, главным образом, и заключается отсылка к проективной геометрии.
При этом справа к нам "приползёт" "плюс бесконечность", наша сцена чудовищно деформируется и станет похожа на то, что мы видим в реальности
(в реальности более далёкие предметы кажутся меньше, чем предметы такого же размера, но расположенные ближе к нам - так называемая перспектива).
Если представить эту операцию в виде некого процесса, то он будет выглядеть примерно так:
Но, разумеется, данная анимация приведена только для наглядности, а нам важен конечный результат.
Короче, после такого преобразования те воображаемые линии, которые тянулись от каждой точки сцены, непонятно где пересекали экран и сходились в глазу
наблюдателя, станут параллельными координатной оси 0Y, в результате чего нам останется останется лишь отбросить у точек сцены координату Y и вывести
на экран получившиеся двумерные точки с координатами (X, Z), отвечающие соответствующим элементам сцены.
Теперь нужно записать эти действия строгим языком математики. Итак, для точек сцены введем четырехмерные векторы (x, y, z, 1), где x, y, z -
обычные трехмерные координаты точки в системе XYZ, а четвертая единица - дань проективной геометрии, о которой было сказано выше и будет сказано дальше.
Кроме того, пусть задана координатная система, связанная с экраном и имеющая в системе XYZ следующие направляющие векторы осей:
Наконец, будем считать, что положение наблюдателя в системе X'Y'Z'1 задано координатами (0, yн, 0, 1)T,
а, в свою очередь, центр системы X'Y'Z'1 в системе XYZ1 имеет координаты (x0, y0, z0, 1)T.
Преобразование, переводящее сцену в координаты, связанные с экраном, является линейным и может быть представлено в виде произведения двух матриц:
ax
bx
cx
0
ay
by
cy
0
az
bz
cz
0
0
0
0
1
*
1
0
0
-x0
0
1
0
-y0
0
0
1
-z0
0
0
0
1
Правая матрица отвечает за параллельный перенос начала координат системы XYZ в начало координат системы X'Y'Z', левая - за переход из системы
XYZ в систему X'Y'Z'.
Проективное же преобразование, которое оставляет 0 на месте, а точку с координатами (0, yн, 0, 1) перегоняет в минус бесконечность, имеет вид:
1
0
0
0
0
1
0
0
0
0
1
0
0
-1/yн
0
1
Таким образом получаем окончательную формулу преобразования вектора (x, y, z, 1) в координаты, связанные с экраном:
xэ
yэ
zэ
tэ
=
1
0
0
0
0
1
0
0
0
0
1
0
0
-1/yн
0
1
*
ax
bx
cx
0
ay
by
cy
0
az
bz
cz
0
0
0
0
1
*
1
0
0
-x0
0
1
0
-y0
0
0
1
-z0
0
0
0
1
*
x
y
z
1
В результате на экране у счастливого пользователя появится точка с координатами (xэ/tэ, zэ/tэ),
возможно, умноженными на некий масштабирующий коэффициент, зависящий от того, насколько далеко от экрана расположена сцена.
Конечно, координата yэ/tэ в реальности тоже эффективно используется - она имеет непосредственное отношение к глубине, по ней
могут быть упорядочены элементы сцены при отрисовке, чтобы дальние элементы не заслоняли ближние.
Теперь уже можно, будучи вооруженными вышеизложенной теорией, приступить к реализации своих амбициозных планов.
Вот, например, простейшая сцена:
Как видим, необязательно ограничивать себя каркасными моделями. Действительно, перейти от отрезков к многоугольникам-граням
достаточно просто. Во-первых, следует упорядочить грани, скажем, по координате Y их
центров (т.е., по среднему, 1/n ∑ (yэ(i)/tэ(i)), Y-координат их вершин),
тогда последовательная отрисовка граней от дальней к ближней
даст примерно верную картину перекрытия их друг другом. Во-вторых, закрашивание граней может быть сделано ориентируясь
на абсолютную величину косинуса угла между вектором нормали к грани и вектором 0'Y' - направлением взгляда наблюдателя. Чем меньше
этот самый модуль косинуса, тем "темнее" должна быть нарисована грань. Вектор же нормали
легко определить, посчитав векторное произведение любых двух смежных рёбер. В результате мы получаем возможность строить нечто более красивое:
Здесь, конечно, видно, что наша реализация несвободна от артефактов (координатные оси должны, по-хорошему, частично перекрываться тором),
но в данном случае это связано с несовершенством критерия сортировки "по центрам" граней и отрезков.
С этим можно побороться, разбивая объекты сцены на элементы примерно соизмеримых размеров.
Понадобился. И оказалось, что SVG не умеет переносить текст! К счастью, в моём случае SVG создаётся при помощи python, а там не очень сложно написать функцию, разбивающую текст на строки длиной не больше заданного количества символов (главное, чтобы резала по пробелам):
def TextToLines(txt, maxLen):
result = []
lines0 = txt.split("\n") for line in lines0: chunks = line.split(" ") newline, l = "", 0 for chunk in chunks: if newline != "": l = 1 else: l = 0 if len(newline.decode("UTF-8")) + l + len(chunk.decode("UTF-8")) > maxLen: result.append(newline) newline = "" if newline != "": newline += " " newline += chunk if newline != "": result.append(newline)
return result
Соответственно, эта процедура применяется так:
textLines = TextToLines(мойТекст, 66) innerText, y = "", 15 for line in textLines: innerText += """<text x="10" y="%(y)d" font-size="10pt" fill="black">%(text)s</text>""" % {"text":line, "y":y} y += 15
Ну и дальше результат из innerText вставляем в нужное место формирующегося документа.
Не очень изящно, скажем прямо. Может, со временем найдется решение получше.
Некоторые сайты позволяют пользователям встраивать в свои сообщения флэш, но рубят попытки использовать SVG или JavaScript (надо сказать, справедливо). Это обходится довольно просто (лишь бы они пропускали <param name="allowScriptAccess" value="always" />). Итак:
1. Создаем флэш-ролик (для flex файл main.mxml может выглядеть примерно так):
3. Всё. Дальше в myscript.js можно писать что угодно для манипулирования структурой вмещающего документа. Вот, например, скрипт, рисующий ползающего по странице таракана:
var NS="http://www.w3.org/2000/svg"; var svg=document.createElementNS(NS,"svg"); svg.setAttribute("width",40); svg.setAttribute("height",40); svg.style.setProperty("position", "absolute", null); document.body.appendChild(svg);
var svgG = document.createElementNS(NS,"g"); svgG.setAttribute("transform","translate(20,20) rotate(120)"); svg.appendChild(svgG);
var el = document.createElementNS(NS,"ellipse"); el.setAttribute("style","fill:black"); el.setAttribute("cx",0); el.setAttribute("cy",0); el.setAttribute("rx",10); el.setAttribute("ry",4); svgG.appendChild(el);
var l111 = document.createElementNS(NS,"line");l111.setAttribute("style","stroke:black"); l111.setAttribute("x1",4);l111.setAttribute("y1",0);l111.setAttribute("x2",6);l111.setAttribute("y2",-6); svgG.appendChild(l111); var l112 = document.createElementNS(NS,"line");l112.setAttribute("style","stroke:black"); l112.setAttribute("x1",6);l112.setAttribute("y1",-6);l112.setAttribute("x2",2);l112.setAttribute("y2",-8); svgG.appendChild(l112);
var l211 = document.createElementNS(NS,"line");l211.setAttribute("style","stroke:black"); l211.setAttribute("x1",0);l211.setAttribute("y1",0);l211.setAttribute("x2",2);l211.setAttribute("y2",-6); svgG.appendChild(l211); var l212 = document.createElementNS(NS,"line");l212.setAttribute("style","stroke:black"); l212.setAttribute("x1",2);l212.setAttribute("y1",-6);l212.setAttribute("x2",-2);l212.setAttribute("y2",-8); svgG.appendChild(l212);
var l311 = document.createElementNS(NS,"line");l311.setAttribute("style","stroke:black"); l311.setAttribute("x1",-4);l311.setAttribute("y1",0);l311.setAttribute("x2",-2);l311.setAttribute("y2",-6); svgG.appendChild(l311); var l312 = document.createElementNS(NS,"line");l312.setAttribute("style","stroke:black"); l312.setAttribute("x1",-2);l312.setAttribute("y1",-6);l312.setAttribute("x2",-6);l312.setAttribute("y2",-8); svgG.appendChild(l312);
var l121 = document.createElementNS(NS,"line");l121.setAttribute("style","stroke:black"); l121.setAttribute("x1",4);l121.setAttribute("y1",0);l121.setAttribute("x2",6);l121.setAttribute("y2",6); svgG.appendChild(l121); var l122 = document.createElementNS(NS,"line");l122.setAttribute("style","stroke:black"); l122.setAttribute("x1",6);l122.setAttribute("y1",6);l122.setAttribute("x2",2);l122.setAttribute("y2",8); svgG.appendChild(l122);
var l221 = document.createElementNS(NS,"line");l221.setAttribute("style","stroke:black"); l221.setAttribute("x1",0);l221.setAttribute("y1",0);l221.setAttribute("x2",2);l221.setAttribute("y2",6); svgG.appendChild(l221); var l222 = document.createElementNS(NS,"line");l222.setAttribute("style","stroke:black"); l222.setAttribute("x1",2);l222.setAttribute("y1",6);l222.setAttribute("x2",-2);l222.setAttribute("y2",8); svgG.appendChild(l222);
var l321 = document.createElementNS(NS,"line");l321.setAttribute("style","stroke:black"); l321.setAttribute("x1",-4);l321.setAttribute("y1",0);l321.setAttribute("x2",-2);l321.setAttribute("y2",6); svgG.appendChild(l321); var l322 = document.createElementNS(NS,"line");l322.setAttribute("style","stroke:black"); l322.setAttribute("x1",-2);l322.setAttribute("y1",6);l322.setAttribute("x2",-6);l322.setAttribute("y2",8); svgG.appendChild(l322);
el = document.createElementNS(NS,"line");el.setAttribute("style","stroke:black"); el.setAttribute("x1",8);el.setAttribute("y1",0);el.setAttribute("x2",20);el.setAttribute("y2",-4); svgG.appendChild(el); el = document.createElementNS(NS,"line");el.setAttribute("style","stroke:black"); el.setAttribute("x1",8);el.setAttribute("y1",0);el.setAttribute("x2",20);el.setAttribute("y2",4); svgG.appendChild(el);
var legPosition = 0;
function placeLegs() { if (legPosition == 0) { l111.setAttribute("x2",6); l112.setAttribute("x1",6); l211.setAttribute("x2",2); l212.setAttribute("x1",2); l311.setAttribute("x2",-2); l312.setAttribute("x1",-2);