Мне подумалось, что код программы для
рисования паровоза вполне может послужить "учебным пособием" по ознакомлению с
некоммутирующими дуальными комплексными числами. Написан он на старом добром Delphi, с рисованием на Canvas средствами GDI (одна-единственная процедура PolyBezier, ну и FillRect впридачу) а потому читается весьма неплохо. Я здесь не выпендривался, потому как сама по себе эта математика - уже какой-никакой вызов. Ну не реализовывать же её впридачу на Haskell'е, или на каком-нибудь самом свежем C++ с темплейтами, умными указателями, std::move (и рассуждениями на несколько страниц, где здесь lvalue, а где rvalue), лямбдами и прочими модными фишками! (или наоборот, непременно на ассемблере QuatCore и "аппаратным ускорением" на верилоге)
По такому случаю создал аккаунт на мосхабе, вот программа для рисования паровоза:
https://hub.mos.ru/nabbla/noncommdualcomplexdemoтам не только исходники, но и экзешник есть, чтобы скачать и поиграться слегка. Нажимая на энтер, можно заставить его двигаться непрерывно. Там же, на мосхабе, не сходя с места, можно исходники посмотреть с подсветкой синтаксиса.
Но всё-таки ключевые места и здесь приведу. Подсвечивать буду, пока ЖЖ не выругается на слишком большую запись.
Начнём с "верхнего уровня", и потом уже непосредственно к реализации математики.
Мы завели глобальные переменные (с тем же успехом могли бы посадить их на "форму"):
var
engine, bgnd, wheel, connector, rod, piston: TSVGPath;
enginePos, Wheel1Pos, Wheel2Pos, Wheel3Pos, ConnectorPos, RodPos, PistonPos: TNonCommDualComplexNum;
png: TPngImage;
Первая строчка - это "картинки". Класс TSVGPath - мой собственный, определённый в файле Svgparser.pas, его опишу чуть ниже.
Вторая строчка - те самые некоммутирующие дуальные комплексные числа, определённые в файле NonCommDualComplexNumber.pas, причём эти числа определены не как класс, а как record, благо, в Delphi XE2 (а может и чуть раньше) появилась возможность для них вводить class operator, т.е определить операции сложения, вычитания, умножения и деления для этих "записей", причём не только между собой, но и вперемешку с действительными числами (Real). Это делает записи весьма удобными. Также можно определять методы, например, Assign, чтобы одним махом присвоить все компоненты числа.
И наконец, TPngImage сидит в vcl.imaging.pngimage, это также с какой-то версии стало стандартной библиотекой. На этом самом png можно рисовать (у него есть свойство Canvas), и можно его отображать на экране, ну и сохранить в файл, само собой. Чтобы получить гифку, я и сохраняю отдельные кадры в PNG, а потом всю папку гружу на сайт, который мне эту анимированную гифку и делает. Когда-нибудь научусь делать гифку непосредственно в программе... Но хоть PNG вместо BMP - это уже подспорье, иначе я бы тут уже сотни мегабайт забил кадрами этого паровоза! А так каждый кадр по 13 КБ занимает примерно.
Не стал соединять "картинку" каждой детали паровоза с её положением на плоскости, сейчас это "две разные вещи". Вообще, даже тут видно, что оно и не должно представляться попарно: вон, картинка ведущего колеса у нас всего одна, а положений - аж три, по числу таких колёс! Позже, может быть, загоню всё это в древовидную структуру, в которой паровоз принадлежит "миру", колёса принадлежат паровозу, дышла - второму ведущему колесу, и так далее, с отдельным "хранилищем картинок" и указателями на них, с автоматическим проходом по дереву и отрисовкой, а пока всё это делаем "ручками".
При запуске программы надо подгрузить все картинки, вот содержание Tform1.FormCreate:
procedure TForm1.FormCreate(Sender: TObject);
begin
engine := TSVGpath.Create;
engine.LoadFromSVGFile('engine.svg');
engine.SetPlane(1);
enginePos.Assign(1,0,-252,256);
wheel := TSVGpath.Create;
wheel.LoadFromSVGFile('wheel3.svg');
wheel.SetPlane(1);
Wheel1Pos.Assign(1,0,-189/2,-289.412/2);
Wheel2Pos.Assign(1,0,-189/2,-127.865/2);
Wheel3Pos.Assign(1,0,-189/2,33.659/2);
connector := TSVGpath.Create;
connector.LoadFromSVGfile('connector.svg');
connector.SetPlane(1);
connectorPos.Assign(1,0,-27.15/2,0);
rod := TSVGpath.Create;
rod.LoadFromSVGFile('rod.svg');
rod.SetPlane(1);
rodPos.Assign(1,0,-27.15/2,0);
piston := TSVGpath.Create;
piston.LoadFromSVGfile('piston.svg');
piston.SetPlane(1);
pistonPos.Assign(1,0,-157/2,123/2);
png := TPngImage.CreateBlank(COLOR_GRAYSCALE, 1, 1024, 768);
end;
Святая простота: создаём объекты, загружаем из файла. Когда-то я заморочался и делал в своих классах конструктор LoadFromFile (благо, в Delphi допустимы два варианта создания объекта, просто obj.Create, когда под obj уже выделена память, но мы всё же выполняем то, что написали в конструкторе, и obj := TMyClass.Create, где конструктор выделяет новую память и её адрес записывает в obj), это позволило бы здесь укоротить код на 5 строк. Не помню точно, почему потом отказался от этого.
Метод SetPlane задаёт компоненту при i для конкретной картинки, "плоскость", в которой эта картинка сидит. Пока это повсюду единица. Можно было бы заставить эту штуку автоматом ставить единицу, а SetPlane вызывать только когда хотим поставить какое-то другое значение, а пока вот так.
Ну и методом Assign мы одним махом инициализируем наши некомм. дуал. компл. числа, т.е задаём начальное положение всех деталей паровоза на плоскости, включая и поворот, и смещение.
Наконец, создаём черно-белую картинку 1024х768, на которой всё это будем рисовать.
Далее, мы ввели метод TForm1.Redraw:
procedure TForm1.Redraw;
begin
png.Canvas.FillRect(Rect(0,0,1024,768));
engine.Draw(png.Canvas, enginePos);
wheel.Draw(png.Canvas, enginePos*wheel1pos);
wheel.Draw(png.Canvas, enginePos*wheel2pos);
wheel.Draw(png.Canvas, enginePos*wheel3pos);
connector.Draw(png.Canvas, enginePos * wheel2pos * connectorPos);
rod.Draw(png.Canvas, enginePos * wheel2pos * rodPos);
piston.Draw(png.Canvas, enginePos *pistonpos);
image1.Picture.Assign(png);
end;
Сначала "всё стираем" с помощью FillRect, и затем рисуем детальку за деталькой. Метод Draw у TSVGPath запрашивает два аргумента: "полотно", где рисовать, и некомм. дуал. компл. число, с помощью которого надо преобразовать все координаты точек, прежде чем рисовать их. Под конец отображаем картинку на экране.
Именно в этих умножениях чисел, передаваемых в метод Draw, "выстроена иерархия", какая деталька на какой детальке сидит.
И наконец, введён метод, который пересчитывает положения объектов для следующего кадра:
procedure TForm1.btnMakeStepClick(Sender: TObject);
var rotation: TNonCommDualComplexNum;
angle: Real;
rodAngle: Real;
endOfRodPos: TNonCommDualComplexNum;
begin
angle := StrToFloat(txtRotationAngle.Text) * pi /180;
// rotation.Assign(cos(angle/2), sin(angle/2), 0, 0);
rotation.Assign(1, angle/2, 0, 0);
// rotation.Assign(1 - angle*angle/12, angle/2, 0, 0);
wheel1pos := wheel1pos * rotation;
wheel2pos := wheel2pos * rotation;
wheel3pos := wheel3pos * rotation;
(*
wheel1pos := rotation * wheel1pos;
wheel2pos := rotation * wheel2pos;
wheel3pos := rotation * wheel3pos;
*)
wheel1pos.FastNorm;
wheel2pos.FastNorm;
wheel3pos.FastNorm;
rotation.Conjugate;
connectorPos := connectorPos * rotation;
connectorPos.FastNorm;
// rodAngle := angle; //попробуем, если с коррекцией по вертикали, но без хорошего угла
rodAngle := angle * (1 - 2 * wheel2pos.c * wheel2pos.s * 21.2/(258.54+21.2));
//уже весьма недурственно!
rotation.Assign(1, -rodAngle/2, 0, 0);
rodPos := rodPos * rotation;
rodPos.FastNorm;
//и теперь игра в constraint
//говорим, что конец этого дышла должен сохранять одну и ту же координату.
endOfRodPos.Assign(0,1,251,-62); //относительно центра дышла, которым считаем его крепление к колесу.
rotation := wheel2pos * rodPos; //сложение двух движений - вращение колеса и положение дышла на нём
endOfRodPos := NonCommDualComplexRotate(endOfRodPos, rotation);
//должны получить y-координату 154.
//т.е если оно отклонилось, то применим доп. поворот который довернёт его примерно куда надо!
angle := (endOfRodPos.y - 154) / 258.54;
rotation.Assign(1, -angle/2, 0, 0);
rodPos := rodPos * rotation;
rodPos.FastNorm;
pistonPos.Assign(1, 0, -157/2, endOfRodPos.x/2);
redraw;
end;
Несколько закомментированных строк - это разные вариацию на тему. То мы поворот посчитали честно через синус и косинус, но потом решили - фи такими быть! То применили метод второго порядка, но потом отказались от него, т.к при повороте на 3 градуса за кадр нас и метод первого порядка полностью устраивает.
Ещё три строки с wheel закомментированы - это когда мы не с той стороны умножили, из-за чего колёса начали крутиться вокруг центра паровоза.
И наконец, строка rodAngle := angle; - вариант посчитать угловое положение сцепного дышла (шатуна), без попытки сколько-нибудь точно изобразить его угловую скорость (как он колеблется вверх-вниз), уповая исключительно на последующую коррекцию положения тупо по координате y. В общем-то, хоть так, хоть эдак - работает, особой разницы не чувствуется.
Эти строки, хоть и в "псевдокоде", я уже привёл в прошлый раз и, надеюсь, описал, как они работают.
С "высоким уровнем" мы уже и закончили! Разве что осталось ещё пара методов, чтобы на автомате мне 120 кадров сгенерить и сохранить в отдельную папочку, но это уже совсем не интересно.
Теперь заглянем под капот некомм. дуал. компл. чисел, файл NonCommDualComplexNumber.pas. Сначала секция interface:
type TNonCommDualComplexNum = record
c, s, x, y: Real; //косинус, синус и смещение по двум осям
procedure Assign(vc,vs,vx,vy: Real);
procedure Conjugate; //заменить на сопряжённое, на месте
procedure Invert; //заменить на обратную величину, на месте
procedure FastNorm; //нормализация через метод Ньютона
class operator Add(Left, Right: TNonCommDualComplexNum): TNonCommDualComplexNum;
class operator Subtract(Left, Right: TNonCommDualComplexNum): TNonCommDualComplexNum;
class operator Multiply(Left, Right: TNonCommDualComplexNum): TNonCommDualComplexNum;
class operator Multiply(Left: TNonCommDualComplexNum; Right: Real): TNonCommDualComplexNum;
class operator Divide(Left, Right: TNonCommDualComplexNum): TNonCommDualComplexNum;
class operator Divide(Left: TNonCommDualComplexNum; Right: Real): TNonCommDualComplexNum;
end;
function NonCommDualComplexLn(value: TNonCommDualComplexNum): TNonCommDualComplexNum;
function NonCommDualComplexExp(value: TNonCommDualComplexNum): TNonCommDualComplexNum;
function NonCommDualComplexRotate(vec, transf: TNonCommDualComplexNum): TNonCommDualComplexNum;
function sinc(x: Real): Real;
function invsinc(x: Real): Real;
const NonCommDualCmplxUnity: TNonCommDualComplexNum = (c: 1; s: 0; x: 0; y: 0);
На всякий случай мы тут все математические операции определили, хотя на деле многими из них можем и не воспользоваться ни разу. Как ни странно, редко бывает нужно вот брать и СКЛАДЫВАТЬ несколько некомм. дуал. компл. чисел! Да и вычитать тоже - физический смысл тут не очень ясный, хотя ясно, что без сложения и вычитания у нас умножение бы не получилось. Деление нам понадобится для интерполяции, для него же мы ввели отдельно лежащие функции натурального логарифма и экспоненты, а также sinc, который sin(x)/x, и обратная ему величина. Рассмотрим их, когда поговорим об интерполяции, чуть позже.
NonCommDualCmplxUnity - это обычная "единичка", выражающая нулевое движение (никакого поворота, никакого параллельного переноса). Эта константа может нам пригодиться, если захочется отрисовать картинку вообще без преобразования координат.
Глянем на реализацию арифметических операций и нескольких вспомогательных методов:
class operator TNonCommDualComplexNum.Add(Left, Right: TNonCommDualComplexNum): TNonCommDualComplexNum;
begin
Result.c := Left.c + Right.c;
Result.s := Left.s + Right.s;
Result.x := Left.x + Right.x;
Result.y := Left.y + Right.y;
end;
class operator TNonCommDualComplexNum.Subtract(Left, Right: TNonCommDualComplexNum): TNonCommDualComplexNum;
begin
Result.c := Left.c - Right.c;
Result.s := Left.s - Right.s;
Result.x := Left.x - Right.x;
Result.y := Left.y - Right.y;
end;
class operator TNonCommDualComplexNum.Multiply(Left, Right: TNonCommDualComplexNum): TNonCommDualComplexNum;
begin
Result.c := Left.c * Right.c - Left.s * Right.s;
Result.s := Left.c * Right.s + Left.s * Right.c;
Result.x := Left.c * Right.x - Left.s * Right.y + Right.c * Left.x + Right.s * Left.y;
Result.y := Left.c * Right.y + Left.s * Right.x + Right.c * Left.y - Right.s * Left.x;
end;
//здесь совершенно убийственная была выкладка для коммутирующих дуал комплекс.
//зато простейшая для некоммут!
procedure TNonCommDualComplexNum.Conjugate; //превратить в сопряжённое число
begin
s := -s;
x := -x;
y := -y;
end;
//здесь вся сложность убиралась внутрь Conjugate, оставшееся довольно очевидно.
procedure TNonCommDualComplexNum.Invert;
var invabssqr: Real;
begin
invabssqr := 1/(Sqr(c) + Sqr(s));
Conjugate;
c := c * invabssqr;
s := s * invabssqr;
x := x * invabssqr;
y := y * invabssqr;
end;
class operator TNonCommDualComplexNum.Multiply(Left: TNonCommDualComplexNum;
Right: Real): TNonCommDualComplexNum;
begin
Result.c := Left.c * Right;
Result.s := Left.s * Right;
Result.x := Left.x * Right;
Result.y := Left.y * Right;
end;
class operator TNonCommDualComplexNum.Divide(Left, Right: TNonCommDualComplexNum): TNonCommDualComplexNum;
begin
Right.Invert;
Result := Left * Right;
end;
class operator TNonCommDualComplexNum.Divide(Left: TNonCommDualComplexNum; Right: Real): TNonCommDualComplexNum;
begin
Right := 1 / Right;
Result.c := Left.c * Right;
Result.s := Left.s * Right;
Result.x := Left.x * Right;
Result.y := Left.y * Right;
end;
procedure TNonCommDualComplexNum.FastNorm;
var norm: Real;
begin
norm := Sqr(c) + Sqr(s);
norm := 1.5-0.5*norm;
c := c * norm;
s := s * norm;
x := x * norm;
y := y * norm;
end;
Напомню, в Delphi Sqr(x) - это возведение в квадрат.
Как видно, ничего экстраординарного. Ну и ещё одна полезная для нашего рисования функция, NonCommDualComplexRotate(). Может, стоило бы назвать Transform, но так уж исторически сложилось. Вот она:
function NonCommDualComplexRotate(vec, transf: TNonCommDualComplexNum): TNonCommDualComplexNum;
begin
Result := transf * vec;
transf.Conjugate;
Result := Result * transf;
end;
В общем, мы "вектор" умножаем на оператор слева, и на него же, но сопряжённый - справа. В кватернионах подобная операция давала вращение в пространстве, а здесь - движение в плоскости.
И совсем коротко взглянем на SVGparser.pas. В нём код самый некрасивый и костыльный, при том весьма объёмный. Мы тупо идём по SVG-файлу и ищем все теги
. Найдя такой тег, мы ищем в нём параметр d (data), там содержится некий набор команд (см.
Про формат SVG и Inkscape ), который мы сводим исключительно к Безье 3-го порядка. До сих пор мы вообще игнорировали команды a, A (arc - дуга, т.е кусочек эллипса), q,Q (quadratic, квадратичная парабола) и t,T (квадратичная парабола, упр. точка которой расположена зеркально по отн. к предыдущей упр точке), но ВНЕЗАПНО во всех файлах, что пока мы грузили - их и не было! Колёса у паровоза, как это ни странно, сразу были выполнены кривой Безье. Кстати, если посмотреть гифки на 100% масштабе, можно заметить, как обод колеса немного "шевелится". Так-то приближение там очень хорошее, я в Inkscape поверх этого колеса нарисовал "настоящую" окружность, и какой бы масштаб не задавал - разницы увидеть не смог. Но когда я все эти управляющие точки и точки кривой округляю до целых пикселей, и передаю их в PolyBezier, тут-то и появляется незначительное дрожание, буквально отдельные точки, не более 1 пикселя.
Вообще-то, стоило бы ещё смотреть на параметр transform, который может быть у самого
, а может быть и у , т.е у ГРУППЫ объектов. А значит, ещё начать строить деревья вложенных групп, чтобы затем применить все необходимые преобразования. А этот самый transform может выражаться и через аффинную матрицу (2x3), и через translate (только параллельный перенос), и через rotate, всё это надо отдельно парсить. А слои (layers) в Inkscape в итоге тоже в SVG представлены как ГРУППЫ. Я пару раз налетел на эти самые преобразования, из-за чего у меня всё сместилось непонятно куда (т.е я их не учёл), но пока не стал дорабатывать парсер. Вместо этого в inkscape выделял все элементы слоя, Ctrl-X, затем сам слой удалял, а элементы вставлял через Ctrl-Alt-V (т.е без изменения их позиции) "наверх", без слоя вообще. После такого финта ушами, Inkscape уже сам применяет все преобразования, и сохраняет финальные точки, которые можно использовать "как есть". А ещё могут лишние Path у меня прочитаться, но пока мне было лениво все (да хоть какие-то) премудрости SVG реализовывать, вижу - что-то лишнее нарисовалось, лезу в SVG и руками оттуда удаляю.
Покажу лишь непосредственно отрисовку, метод Draw:
type
TPointArray = array [0..MaxInt div SizeOf(TPoint) - 1] of TPoint;
PPointArray = ^TPointArray;
procedure TSvgPath.Draw(canvas: TCanvas; transform: TNonCommDualComplexNum);
var seq, i, cur: Integer;
conj, moved: TNonCommDualComplexNum;
begin
conj := transform;
conj.Conjugate;
for i := 0 to fCount-1 do
fTransformedPoints[i] := transform * fPoints[i] * conj;
seq := 0; //номер точки в текущей последовательности
for i := 0 to fBezierCount-1 do begin
cur := fBezierIndices[i];
if cur <> -1 then begin //нормальное значение
moved := fTransformedPoints[cur];
fBezierPoints[seq].X := Round(moved.x);
fBezierPoints[seq].Y := Round(moved.y);
inc(seq);
end
else begin
canvas.PolyBezier(Slice(PPointArray(@fBezierPoints[0])^, seq));
seq := 0;
end;
end;
canvas.PolyBezier(Slice(PPointArray(@fBezierPoints[0])^, seq));
end;
И тут, увы, один костыль закрался. Дело в том, что метод PolyBezier (обёртка над функцией GDI) оформлен "по-дельфийски", с Open array parameter. Т.е мы просто передаём массив, и не передаём его размера. Обычно там всё красиво получается. Можно статический массив передать - никаких проблем. Можно динамический - тоже заработает. Можно "бесплатно" (без выделения памяти где бы то ни было) передать лишь кусочек статического массива с помощью "волшебной" (т.е реализуемой при компиляции программы) процедуры Slice(). А вот от динамического отрезать кусок, без копирования данных, увы, "штатными средствами" не выходит...
Приходится компилятору подсунуть динамический массив под видом статического, и от него уже сделать Slice. По большому счёту, весь этот Open array parameter "под капотом" - это передача указателя на начало массива и количества элементов, просто это укрывается от программиста, чтобы выглядело покрасивше. Вот я и передаю ему указатель на начало массива и количество, сформированное с помощью "волшебного" Slice. Это всё, что он и делает - заполняет невидимый аргумент Count.
А всё опять из-за моей жадности. Я на этапе парсинга SVG запоминаю максимальное количество точек для кривой Безье. Нарисовать всё единым вызовом PolyBezier никак нельзя, т.к этот метод рисует НЕРАЗРЫВНУЮ кривую. Чтобы лишней памяти не жрать, и не выделять-освобождать её на ровном месте, у меня выделено памяти под fBezierPoints с некоторым запасом, выделено ещё на этапе загрузки рисунка из файла. Но всё же, жадность - это иногда полезно. Работает оно весьма шустро, как-никак, Delphi один из последних КОМПИЛИРУЕМЫХ языков, ну окромя C/C++. А все новомодные (C#, Java, Python, Haskell) - интерпретируемые заразы.
А что касается математической стороны дела - как видно, вообще ничего сложного. Все точки преобразую, и потом рисую.
Если кому-то хочется почувствовать мою БОЛЬ, вот код парсера path:
type TParsingState = (psIdle, psIntegerPart, psDecimalPart);
procedure TSvgPath.AppendFromString(str: AnsiString);
var curCmd: AnsiChar; //если команда та же самая, то она может повторно и не указываться!
isRelative: Boolean;
argNum, argsTotal: Integer; //сколько уже аргументов прочитано по текущей команде
i: Integer;
state: TParsingState;
curNumber: Real; //число, которое по циферке собираем
curSign: Real;
curCoordX, curCoordY: Real; //чтобы преобразовать отн координаты в абс
bearing: Real;
co, si: Real;
divider: Real;
FirstPointOfThisBatch: Boolean;
zIndex: Integer; //в какой индекс вернуться по команде Z
procedure ProcessCmd;
begin
//по умолчанию считаем, что команда не изменится (искл: M/m),
//аргументы снова начинаются с нуля,
//и аргументы абсолютные
argNum := 0;
isRelative := false;
case curCmd of
//у меня чувство, что придётся каждую команду всё-таки отдельно рассмотреть, всё у них через задницу...
'M': //MoveTo, с абс. координатами
begin
//ожидаем 2 числа, X,Y
AddBezierIndex(-1); //разрываем текущую фигуру.
NewPoint; //резервируем место под точку, а запишем когда время придёт
AddBezierIndex(fCount-1); //начинаем новый PolyBezier, ему нужна начальная точка, это она и будет
curCmd := 'L'; //вот такой парадокс! Первую точку впишем, не подавимся, а вторая уже как линия должна пойти...
argsTotal := 2;
zIndex := fCount-1;
end;
'm': //MoveTo, с отн. координатами
begin
AddBezierIndex(-1);
NewPoint;
AddBezierIndex(fCount-1);
curCmd := 'l';
argsTotal := 2;
zIndex := fCount-1;
isRelative := true;
end;
'L': //LineTo, произвольная прямая линия с абс координатами
begin
AddBezierIndex(fCount-1); //первая контрольная точка совпадает с начальной (которая уже добавлена ранее)
NewPoint; //резервируем место под конечную точку, туда всё и запишется как придёт время
AddBezierIndex(fCount-1); //вторая контрольная точка совпадает с конечной (знаем её индекс, вот-вот запишем)
AddBezierIndex(fCount-1); //конечная точка
argsTotal := 2;
end;
'l': //LineTo, произв прямая линия с отн координатами
begin
AddBezierIndex(fCount-1); //первая контрольная точка совпадает с начальной (которая уже добавлена ранее)
NewPoint; //резервируем место под конечную точку, туда всё и запишется как придёт время
AddBezierIndex(fCount-1); //вторая контрольная точка совпадает с конечной (знаем её индекс, вот-вот запишем)
AddBezierIndex(fCount-1); //конечная точка
argsTotal := 2;
isRelative := true;
end;
'H': //Horizontal, с абс координатами
begin
AddBezierIndex(fCount-1); //первая контрольная точка совпадает с начальной
NewPoint; //резервируем место под конечную точку
fPoints[fCount-1].y := fPoints[fCount-2].y;
AddBezierIndex(fCount-1); //вторая контр совпадает с конечной
AddBezierIndex(fCount-1); //конечная точка
argsTotal := 1; //т.е мы записали икс - И УСПОКОИЛИСЬ
end;
'h': //Horizontal, с отн координатами
begin
AddBezierIndex(fCount-1); //первая контрольная точка совпадает с начальной
NewPoint; //резервируем место под конечную точку
fPoints[fCount-1].y := fPoints[fCount-2].y;
AddBezierIndex(fCount-1); //вторая контр совпадает с конечной
AddBezierIndex(fCount-1); //конечная точка
argsTotal := 1; //т.е мы записали икс - И УСПОКОИЛИСЬ
isRelative := true;
end;
'V': //Vertical, с абс координатами
begin
AddBezierIndex(fCount-1); //первая контрольная точка совпадает с начальной
NewPoint; //резервируем место под конечную точку
fPoints[fCount-1].x := fPoints[fCount-2].x;
AddBezierIndex(fCount-1); //вторая контр совпадает с конечной
AddBezierIndex(fCount-1); //конечная точка
argsTotal := 2;
argNum := 1; //т.е мы сделали вид, что икс уже записали и сейчас запишем игрек!
end;
'v': //vertical, с отн координатами
begin
AddBezierIndex(fCount-1); //первая контрольная точка совпадает с начальной
NewPoint; //резервируем место под конечную точку
fPoints[fCount-1].x := fPoints[fCount-2].x;
AddBezierIndex(fCount-1); //вторая контр совпадает с конечной
AddBezierIndex(fCount-1); //конечная точка
argsTotal := 2; //т.е мы записали икс - И УСПОКОИЛИСЬ
argNum := 1;
isRelative := true;
end;
'C': //Cube Bezier, кубический Безье с абс координатами
begin
NewPoint; //1я упр
AddBezierIndex(fCount-1);
NewPoint; //2я упр
AddBezierIndex(fCount-1);
NewPoint; //конечная
AddBezierIndex(fCount-1);
argsTotal := 6;
end;
'c': //CubeBezier, кубический Безье с отн координатами
begin
NewPoint; //1я упр
AddBezierIndex(fCount-1);
NewPoint; //2я упр
AddBezierIndex(fCount-1);
NewPoint; //конечная
AddBezierIndex(fCount-1);
argsTotal := 6;
isRelative := true;
end;
end;
end;
//обрабатывали число, циферку за циферкой, но затем увидели что-то ещё и поняли, что число уже завершилось
//пожалуй, у нас будет трюк с V/H, где мы как будто бы прочитаем лишнее число.
procedure FinishNumber;
var offset: Integer;
begin
if state <> psIdle then begin
curNumber := curNumber * curSign;
if argNum >= argsTotal then begin //вот где нужно озаботиться исполнением очередной команды!
if FirstPointOfThisBatch then
FirstPointOfThisBatch := false
//пришлось эту переменную вводить на случай, добавляем новый path,
//который начинается с команды "m", и тогда нужно двинуться относительно (0;0),
//а если бы мы на fCount опирались, то сдвинулись бы отн. прошлого path, что неправильно
else begin
curCoordX := fPoints[fCount-1].x; //последняя точка, выделенная уже исполненной командой
curCoordY := fPoints[fCount-1].y; //при вызове ProcessCmd зарезервируются новые места!
end;
ProcessCmd;
end;
if isRelative then
if argNum AND 1 = 0 then
curNumber := curNumber + curCoordX * co - curCoordY * si
//задел под SVG 2.0, с их командой b (bearing) и "черепашьей графикой". Но сама b пока не реализована.
else
curNumber := curNumber + curCoordY * co + curCoordX * si;
offset := (argsTotal - argNum + 1) shr 1;
if argNum AND 1 = 0 then //первый, третий, пятый аргумент, т.е по оси X
fPoints[fCount-offset].x := curNumber
else
fPoints[fCount-offset].y := curNumber;
inc(argNum);
state := psIdle;
curSign := 1;
end;
end;
begin
state := psIdle;
bearing := 0;
co := 1;
si := 0;
curSign := 1;
curCoordX := 0;
curCoordY := 0;
argNum := 0;
argsTotal := 0;
FirstPointOfThisBatch := true;
zIndex := 0;
for i := 1 to Length(str) do begin
case str[i] of
'A'..'Y', 'a'..'y':
begin
finishNumber;
curCmd := str[i]; //Z - особый случай!
end;
'z','Z':
begin
finishNumber;
AddBezierIndex(fCount-1); //1я управляющая
AddBezierIndex(zIndex); //2я управляющая
AddBezierIndex(zIndex); //конец отрезка
end;
'0'..'9':
begin
//когда прочитываем циферку - никогда не останавливаемся, считаем что она всё продолжается и продолжается!
if state = psIdle then begin
curNumber := Byte(str[i]) - Byte('0');
state := psIntegerPart;
end
else if state = psIntegerPart then
curNumber := curNumber * 10 + Byte(str[i]) - Byte('0')
else begin
curNumber := curNumber + (Byte(str[i]) - Byte('0')) / divider;
divider := divider * 10;
end;
end;
'.':
begin
if state = psIdle then
curNumber := 0 //число из серии .5, т.е целой части вообще нет
else if state = psDecimalPart then begin
finishNumber; //уже вторую точку встретили, это значит, новое число началось!
curNumber := 0;
end;
divider := 10;
state := psDecimalPart;
end;
'-':
begin
finishNumber; //ежели мы его записывали, то сейчас уже за следующее взялись...
curSign := -1;
end;
' ', #9,',',#13,#10: finishNumber;
end;
end;
//и под самый конец у нас могут остаться незаконченные дела!
finishNumber;
if Length(fBezierPoints) < fBezierPointsSoFar then
SetLength(fBezierPoints, fBezierPointsSoFar);
end;
Профдеформация как она есть - никакого лексического анализа, читаем ровно по одному символу и меняем своё состояние, описанное минимально-возможным способом. В общем, в ПЛИСине, подозреваю, мог бы сделать аппаратный парсинг, и он бы отожрал не шибко много. А ведь была ветвь в вычислительной технике, с векторными мониторами. Вживую их не встречал, а вообще было бы любопытно нечто такое забабахать, скажем, на осциллографе отобразить SVG-файл с SD-карточки.
И на этом пока всё.Далее на очереди: параллакс (несколько "планов"), масштабирование средствами некомм. дуал. компл. чисел, и на вкусное - интерполяция движений.