Код паровоза на мосхабе

По такому случаю создал аккаунт на мосхабе, вот программа для рисования паровоза: 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-карточки.
И на этом пока всё.Далее на очереди: параллакс (несколько "планов"), масштабирование средствами некомм. дуал. компл. чисел, и на вкусное - интерполяция движений.
|
</> |