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

топ 100 блогов nabbla1 — 21.08.2024 Мне подумалось, что код программы для рисования паровоза вполне может послужить "учебным пособием" по ознакомлению с некоммутирующими дуальными комплексными числами. Написан он на старом добром 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-карточки.

И на этом пока всё.Далее на очереди: параллакс (несколько "планов"), масштабирование средствами некомм. дуал. компл. чисел, и на вкусное - интерполяция движений.

Оставить комментарий

Архив записей в блогах:
Джон Маккаллум, посол Канады в Китае (на фото), лишился своей должности, после того как посмел намекнуть, что экстрадиция в США финдиректора Huawei Мэн Ваньчжоу отразится на судьбе арестованных в КНР канадцев. Причем, поначалу премьер-министр Джастин Трюдо не собирался его увольнять, а ...
Адвокат объяснил противоречия в деле об убийстве на Чистых прудахДело Ахмедпаши Айдаева, обвиняемого в убийстве Юрия Волкова, выделено в отдельное производство и передано в СКП. Об этом агентству "Интерфакс" заявил адвокат Абусульян ...
Побывал вчера в очень интересном ресторанчике Los Caracoles. (Это я все о своем, о ...
Музыка - это явление почти уникальное, богатое разными течениями, группами, исполнителями, композиторами. Каждый из нас слушает то, что ему нравится, и получает удовольствие. Мы находим музыку повсюду: на различных интернет-площадках, где можно не только ознакомиться с последними ...
Не, таки я просто должна этим поделиться. luide_anna , огромное спасибо за рецепт. Я не дождалась воскресенья и сделала сегодня :) И таки получилось вкусно. Получилось офигительно вкусно, я бы сказала. Итак, у нас есть печеньки :)Овсяные. На вид не сильно отличается от магазинн ...