Как сделать анимацию саблевидного кулачкового механизма?


Поздравляем победителя конкурса Stanislav Volodarskiy!


Поздравляем победителя повторного конкурса @DiD


Конкурсное задание:

У меня есть изображение саблевидного кулачкового механизма.

введите сюда описание изображения

Есть *.gif анимации механизма

введите сюда описание изображения

Я создал svg код кулачкового механизма:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400" viewBox="0 0 500 500">
                                                                          
     <!-- Шарнир -->
<path fill="grey" stroke="black" stroke-width="3" d="M344.3 197.6a49.6 49.6 0 0 1 34.4 13A57 57 0 0 1 396 251a54 54 0 0 1-17.4 36.8c-9 8.2-22 13.1-34.2 13a54.4 54.4 0 0 1-36.8-15.4A51.4 51.4 0 0 1 294 251a55.4 55.4 0 0 1 13.7-37.6 53.7 53.7 0 0 1 36.6-16z"/>
            <!-- Вертикальный стержень -->
<path fill="grey" stroke="black" stroke-width="3" d="M387.3 13.1h30v440.5h-30z"/>
                <!-- Ось кулачка -->
<path  fill="#626262" stroke="black" stroke-width="2" d="M344.3 223.5c8.9 0 18 5.5 23.7 12.3 3.4 4.1 4.9 10 4.5 15.3-.4 6.7-3.2 13.9-8.2 18.4a27 27 0 0 1-20 6.5c-7.1-.8-13.8-5.4-18.6-10.6a24.4 24.4 0 0 1-1-31.8c4.7-5.7 12.2-10.1 19.6-10z"/>

                 <!-- Кулачок -->
<path fill="grey" stroke="black" stroke-width="3" d="M41.3 153c32.7 0 78.5 3.3 117.6 7 35 3.2 69.9 6.8 104.3 13.2 16.8 3.1 33.3 7.7 49.8 12.2 13 3.5 43.8 11.3 38.7 11.6-20 1.4-39.1 7.2-48.7 22.3-20.3 31.8-.9 54.9-7 41.8-5.4-11.8-18.1-18-29-24.3-13.4-7.9-28.7-11.9-43.5-16.5-19-5.9-38.9-9.2-58.2-14.3-21.5-5.5-42.7-12-64.1-17.4-14-3.5-26.3-5.5-41.9-9.6a25 25 0 0 1-13.2-10.6c-3-4.4-10.1-15.3-4.8-15.3z"/>
             <!-- Цилиндр -->
<path fill="black" d="M377.5 54.3c3.8-.7 50.6-.4 50.6-.4l9.3 10.5v88.8h-66.6V64.4Z"/>
            <!-- Линия фаски -->
<path  stroke="white" d="m370.8 64 66.6.4"/>
              <!-- Верхний рычаг -->
<path fill="grey" stroke="black" stroke-width="3" d="M21.2 153c-3.1 0 1.1-6.2 2.6-9 1.4-2.4 3-5.7 5.9-6.3a17587 17587 0 0 1 341-65.7v81H21.3z"/>

</svg>

Как создать анимацию работы кулачкового механизма в соответствии с приведенным файлом .gif?

Update

Дополнительные данные,
так как возникают вопросы при решении задачи.
Надеюсь это поможет.

Исходник Inkscape, в котором создавался статичный файл SVG механизма.
Размер холста SVG 500x500 px
Размеры деталей можно взять из векторного редактора, запустив в нем исходник inkscape Код ниже

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="500"
   height="500"
   viewBox="0 0 500 500"
   version="1.1"
   id="svg4"
   sodipodi:docname="Заготовка-ink2.svg"
   inkscape:version="0.92.3 (2405546, 2018-03-11)">
  <metadata
     id="metadata10">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <defs
     id="defs8" />
  <sodipodi:namedview
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1"
     objecttolerance="10"
     gridtolerance="10"
     guidetolerance="10"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:window-width="1440"
     inkscape:window-height="837"
     id="namedview6"
     showgrid="false"
     inkscape:zoom="0.944"
     inkscape:cx="295.12041"
     inkscape:cy="242.72973"
     inkscape:window-x="-8"
     inkscape:window-y="-8"
     inkscape:window-maximized="1"
     inkscape:current-layer="svg4"
     showguides="true"
     inkscape:guide-bbox="true">
    <sodipodi:guide
       position="436.69836,486.88497"
       orientation="0,1"
       id="guide4520"
       inkscape:locked="false" />
    <sodipodi:guide
       position="387.26081,395.5004"
       orientation="1,0"
       id="guide4522"
       inkscape:locked="false" />
    <sodipodi:guide
       position="417.22296,388.75892"
       orientation="1,0"
       id="guide4524"
       inkscape:locked="false" />
    <sodipodi:guide
       position="448.68322,46.441335"
       orientation="0,1"
       id="guide4526"
       inkscape:locked="false" />
    <sodipodi:guide
       position="370.78163,381.26838"
       orientation="1,0"
       id="guide4532"
       inkscape:locked="false" />
    <sodipodi:guide
       position="437.44742,370.03257"
       orientation="1,0"
       id="guide4534"
       inkscape:locked="false" />
    <sodipodi:guide
       position="489.88118,471.15484"
       orientation="0,1"
       id="guide4536"
       inkscape:locked="false" />
    <sodipodi:guide
       position="489.13213,416.47391"
       orientation="0,1"
       id="guide4538"
       inkscape:locked="false" />
    <sodipodi:guide
       position="411.97959,435.94931"
       orientation="0,1"
       id="guide4546"
       inkscape:locked="false" />
    <sodipodi:guide
       position="288.66525,346.92797"
       orientation="0,1"
       id="guide4550"
       inkscape:locked="false" />
    <sodipodi:guide
       position="344.27966,512.1822"
       orientation="1,0"
       id="guide4554"
       inkscape:locked="false" />
    <sodipodi:guide
       position="370.78163,248.94068"
       orientation="0,1"
       id="guide4556"
       inkscape:locked="false" />
  </sodipodi:namedview>
  <image
     
  <path
     style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
     d="m 387.26081,13.11503 h 29.96215 v 440.44364 h -17.97729 -11.98486 z"
     id="path4528"
     inkscape:connector-curvature="0" />
  <path
     style="fill:none;stroke:#0000f7;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
     d="m 387.26081,13.11503 h 29.96215 v 440.44364 h -29.96215 z"
     id="path4530"
     inkscape:connector-curvature="0" />
  <path
     style="fill:none;stroke:#e50000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
     d="m 377.52311,54.312992 c 3.74527,-0.749054 50.56113,-0.374527 50.56113,-0.374527 l 9.36318,10.486753 V 153.18809 H 370.78163 V 64.425218 Z"
     id="path4544"
     inkscape:connector-curvature="0" />
  <path
     style="fill:none;stroke:#570400;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
     d="m 370.78163,64.05069 66.66579,0.374528"
     id="path4548"
     inkscape:connector-curvature="0" />
  <path
     style="fill:none;stroke:#008800;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     d="m 21.186441,153.07203 c -3.12854,0 1.110117,-6.27995 2.648305,-9.00423 1.413084,-2.50271 3.015439,-5.75628 5.826271,-6.35594 C 135.51425,115.12916 370.78163,72.033898 370.78163,72.033898 v 81.038132 c 0,0 -212.74111,0 -349.595189,0 z"
     id="path4552"
     inkscape:connector-curvature="0"
     sodipodi:nodetypes="sasccs" />
  <path
     style="fill:none;stroke:#0000e7;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     d="m 344.27966,223.51695 c 8.90907,0.0198 18.05228,5.44187 23.72125,12.31462 3.36808,4.08327 4.85373,9.94633 4.50212,15.22775 -0.44642,6.70562 -3.23009,13.91363 -8.22867,18.40572 -5.21172,4.68364 -13.02457,7.20658 -19.9947,6.48835 -7.07962,-0.72951 -13.76582,-5.31323 -18.53813,-10.59322 -3.49791,-3.87002 -6.03275,-9.09431 -6.35594,-14.30085 -0.37718,-6.07621 1.39653,-12.80421 5.29661,-17.47881 4.70441,-5.63868 12.25403,-10.07987 19.59746,-10.06356 z"
     id="path4558"
     inkscape:connector-curvature="0"
     sodipodi:nodetypes="aaaaaaaaa" />
  <path
     style="fill:none;stroke:#0000ed;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     d="m 344.27966,197.56356 c 12.26035,-0.30456 25.5788,4.48545 34.42797,12.97669 10.5885,10.16022 17.51589,25.8475 17.21398,40.51907 -0.2794,13.57787 -7.48027,27.60038 -17.4599,36.81145 -8.95578,8.26607 -21.9962,13.17457 -34.18205,12.97669 -13.2941,-0.21588 -27.5112,-5.85832 -36.81144,-15.36017 -8.6229,-8.80982 -13.36567,-22.10127 -13.50636,-34.42797 -0.15236,-13.3485 4.61476,-27.89174 13.77119,-37.60593 9.11148,-9.66649 23.26688,-15.55995 36.54661,-15.88983 z"
     id="path4560"
     inkscape:connector-curvature="0"
     sodipodi:nodetypes="aaaaaaaaa" />
  <path
     style="fill:none;stroke:#00aa40;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
     d="m 41.313559,153.07203 c 32.676369,0 78.490031,3.2645 117.584751,6.8856 34.91059,3.23355 69.86925,6.85792 104.34322,13.24152 16.80002,3.11088 33.30494,7.68524 49.78813,12.18221 12.98636,3.54296 43.73855,11.29999 38.66526,11.65254 -19.95701,1.38685 -39.13749,7.18503 -48.72882,22.24576 -20.23867,31.77966 -0.83862,54.89455 -6.88559,41.84322 -5.46997,-11.80596 -18.20255,-17.97596 -29.13136,-24.3644 -13.36198,-7.81076 -28.65095,-11.82981 -43.4322,-16.4195 -19.0979,-5.93004 -38.9043,-9.28613 -58.26271,-14.30084 -21.4357,-5.55283 -42.60775,-12.10487 -64.08899,-17.47882 -13.877535,-3.47173 -26.30999,-5.43823 -41.843216,-9.5339 -5.465675,-1.44114 -10.056956,-5.92321 -13.241525,-10.59322 -3.020329,-4.42916 -10.127906,-15.36017 -4.76695,-15.36017 z"
     id="path4562"
     inkscape:connector-curvature="0"
     sodipodi:nodetypes="saaasssaaaasas" />
</svg>

Скриншот из векторного редактора

введите сюда описание изображения

Note:

При проектировании анимации вращения кулачка обратите внимание на неравномерность вращения: большие углы поворота чередуются с малыми углами!

Связанные топики:

  1. Анимация Кривошипно-шатунного механизма
  2. Как сделать анимацию кулачкового механизма?

Конкурс будет закрыт в последние часы по истечению срока.
Времени ещё много, дерзайте, ищите решения!


Ответы (5 шт):

Автор решения: Leonid

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

При заданной общей длине рычагов в 120мм и хода поршня на 40мм получаем радиус кромки кулака в 180мм и полезный угол вращения (серым в программе обозначаются в данном случае зависимые размеры, выводимые из прочих параметров).

графическая геометрия задачи

Исходя из полученных параметров отрисовываем на холсте дугу кулака и горизонталь рычага поршня. Устанавливаем зависимость от изменения угла кулака, ограничивая 39 градусами, за пределами которых положение рычага не меняется.

Зависимость от изменения угла вращения рычага следующая:

Высота перемещения рычага = (угол вращения/общий полезный угол)² * общий ход поршня

const canvas = document.getElementById('kulak');
const ctx = canvas.getContext('2d');
const w = canvas.width = 300;
const h = canvas.height = 220;

const rx = 280;
const ry = 90;

const kulak = new Path2D('M 0 0 a 360 360 39 0 0 -226 -80');
const hor = new Path2D('M 0 0 h -240');

const total_angle = 39;
const total_move = 80;

const angle_inp = document.getElementById('angle');
const angle_p = document.getElementById('anglep');
angle_inp.addEventListener('change', rotate);
angle_inp.dispatchEvent(new Event('change'));

function rotate(e) {
  const angle = e.target.value;
  angle_p.innerText = angle;

  let h_move;

  if (angle > 39) {
    h_move = 0;
  } else {
    h_move = Math.pow((total_angle - angle) / total_angle, 2) * total_move;
  }

  ctx.clearRect(0, 0, w, h);

  ctx.translate(rx, ry);
  ctx.rotate(-angle * (Math.PI / 180));

  ctx.stroke(kulak);
  ctx.resetTransform();

  ctx.translate(rx, ry - h_move);
  ctx.stroke(hor);
  ctx.resetTransform();
}
body {display: flex}
<canvas id="kulak"></canvas>
<input type="range" id="angle" min="0" max="50" value="0">
<p id="anglep"></p>

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

const canvas = document.getElementById('kulak');
const ctx = canvas.getContext('2d');
const w = canvas.width = 400;
const h = canvas.height = 300;

const kulak = new Path2D(`M -66.24 -35.74 c11.78,-0.29 36.06,0.55 44.88,3.15 32.39,9.51 45.53,12.3 46.3,33.43 0.97,26.94 -28.59,31.18 -94.18,31.18 -65.58,0 -68.27,-10.15 -68.88,-27.35 -0.29,-8.21 2.24,-13.89 13.48,-21.89 14.75,-10.5 26.65,-17.75 58.4,-18.52zm66.18 21.58c8.28,0 15,6.72 15,15 0,8.29 -6.72,15 -15,15 -8.29,0 -15,-6.71 -15,-15 0,-8.28 6.71,-15 15,-15z`);



ctx.fillStyle = 'grey';

let angle = 0;

function rotate() {
  angle += 0.4;

  ctx.clearRect(0, 0, w, h);

  ctx.translate(w / 2, h / 2);
  ctx.rotate(angle * (Math.PI / 180));
  ctx.fill(kulak, 'evenodd');
  ctx.resetTransform();
  y = -h / 2;
  x = -w / 2;

  ctx.rotate(angle * (Math.PI / 180));

  draw: for (; y <= 0; y++) {
    for (x = -w / 2; x <= w / 2; x++) {
      if (ctx.isPointInPath(kulak, x, y)) {
        y--;
        break draw;
      }
    }

  }


  ctx.resetTransform();

  ctx.fillRect(0, h / 2 + y, w, -30);

  requestAnimationFrame(rotate);
}

requestAnimationFrame(rotate);
<canvas id="kulak"></canvas>

→ Ссылка
Автор решения: Stanislav Volodarskiy

"Шарнир", "ось кулачка" и "кулачок" из оригинального svg собраны в группу driver. Группа анимирована вращением вокруг центра оси. Центр подбирался на глаз.

"Вертикальный стержень", "цилиндр", "линия фаски" и "верхний рычаг" объединены в группу drivee, которая перемещается скриптом по вертикали. "Верхний рычаг" отмечен идентификатором lever.

Группа driver анимируется средствами svg. Функция drive перемещает группу drivee по вертикали так, чтобы нижняя точка lever совпадала с верхней точкой drivee. Это хорошо выглядит потому что нижняя кромка lever горизонтальна в экранных координатах. Если бы рычаг был наклонён, понадобились бы дополнительная настройка. Если бы рабочая кромка рыгача была криволинейной, понадобилось бы менять алгоритм полностью.

Браузеры предоставляют стандартные средства для вычисления описанных прямоугольников. Но они не работают достаточно аккуратно. В спецификации нигде не указано что описанный прямоугольник должен быть минимальным. И Firefox и Chrome вычисляют их с некоторым запасом и разными способами.

Вызов getExtreme([0, -1], driver) вычисляет верхнюю точку группы driver в экранных координатах. getExtreme([0, 1], lever) вычисляет нижнюю точку lever в экранных координатах.

getExtreme перебирает все узлы типа path. Атрибут d разбирается на составляющие команды (функция parsePathD). Чтобы решить задачу нужно уметь обрабатывать команды для дуг эллипса, кубических кривых Безье и отрезков. Все команды переводятся в форму допускающую преобразование с помощью матриц. Например, эллипс представляется своим центром и концами полуосей. Кривая Безье сама по себе задана четырьмя точками. Фрагменты кривых отображаются в экранную систему координат (вызовы node.getScreenCTM() и part.map(matrix)).

Для каждого фрагмента вычисляется экстремальное значение в направлении dir. Каждый тип фрагмента решает эту задачу по-своему:

  • для отрезка выбирается максимум значений на концах;
  • для дуги эллипса берётся максимум из значений на концах дуги и две точки эллипса ("верхняя" и "нижняя" в направлении dir), если они попадают внутрь дуги;
  • для кривой Безье берётся максимум из значений на концах кривой и значения во всех точках, где производная (квадратичная форма) равна нулю.

const solveQuadraticEquation = (a, b, c) => {
    if (a === 0) {
        if (b === 0) {
            return [];
        }
        return [-c / b];
    }
    b /= 2 * a;
    c /= 2 * a;
    const d = b * b - 2 * c;
    if (d < 0) {
        return [];
    }
    if (d === 0) {
        return [-b];
    }
    return [-b - Math.sqrt(d), -b + Math.sqrt(d)]
};

const makePoint = (x, y) => DOMPointReadOnly.fromPoint({x, y});

const dot = (dir, p) => dir[0] * p.x + dir[1] * p.y;

const makeLine = (p1, p2) => {
    const map = matrix => makeLine(
        p1.matrixTransform(matrix),
        p2.matrixTransform(matrix)
    );
    const getExtreme = dir => Math.max(dot(dir, p1), dot(dir, p2));
    return {map, getExtreme};
};

const makeCubicBezier = (p1, p2, p3, p4) => {
    const map = matrix => makeCubicBezier(
        p1.matrixTransform(matrix),
        p2.matrixTransform(matrix),
        p3.matrixTransform(matrix),
        p4.matrixTransform(matrix)
    );
    const getExtreme = dir => {
        const v1 = dot(dir, p1);
        const v2 = dot(dir, p2);
        const v3 = dot(dir, p3);
        const v4 = dot(dir, p4);
        const roots = solveQuadraticEquation(
            -v1 + 3 * (v2 - v3) + v4,
            2 * (v1 - 2 * v2 + v3),
            -v1 + v2
        ).filter(x => 0 < x && x < 1);

        const values = [v1, v4].concat(roots.map(t => {
            const t2 = t * t;
            const t3 = t * t2;
            const s = 1 - t;
            const s2 = s * s;
            const s3 = s * s2
            return s3 * v1 + 3 * (s2 * t * v2 + s * t2 * v3) + t3 * v4;
        }));
        return Math.max(...values);
    };
    return {map, getExtreme};
};

const makeArc = (theta1, theta2, c, u, v) => {
    const map = matrix => makeArc(
        theta1, theta2,
        c.matrixTransform(matrix),
        u.matrixTransform(matrix),
        v.matrixTransform(matrix)
    );
    const getExtreme = dir => {
        const value = th => dot(dir, makePoint(
            c.x + Math.cos(th) * (u.x - c.x) + Math.sin(th) * (v.x - c.x),
            c.y + Math.cos(th) * (u.y - c.y) + Math.sin(th) * (v.y - c.y)
        ));
        const inRange = th => (theta1 < theta2) ?
            (theta1 < th && th < theta2) :
            (th < theta2 || theta1 < th)
        ;
        const thetas = [theta1, theta2];
        const th = Math.atan2(
            dot(dir, makePoint(v.x - c.x, v.y - c.y)),
            dot(dir, makePoint(u.x - c.x, u.y - c.y))
        );
        if (inRange(th)) {
            thetas.push(th);
        }
        if (inRange(th + Math.PI)) {
            thetas.push(th + Math.PI);
        }
        return Math.max(...thetas.map(value));
    };
    return {map, getExtreme};
};

const convertArc = (
    rx, ry, x1, y1, x2, y2, xAxisRotation, largeArcFlag, sweepFlag
) => {
    const [px, py] = [(x1 + x2) / 2, (y1 + y2) / 2];
    if (!sweepFlag) {
        [x1, y1, x2, y2] = [x2, y2, x1, y1];
    }
    const a = Math.PI / 180 * xAxisRotation;
    const ca = Math.cos(a);
    const sa = Math.sin(a);

    const p_pp = (x, y) => {
        x -= px;
        y -= py;
        return [(ca * x + sa * y) / rx, (-sa * x + ca * y) / ry];
    };
    const pp_p = (xx, yy) => {
        xx *= rx;
        yy *= ry;
        return [px + ca * xx - sa * yy, py + sa * xx + ca * yy];
    };

    const [xx1, yy1] = p_pp(x1, y1);

    const u = Math.hypot(xx1, yy1);
    const v = (u < 1) ? Math.sqrt(1 - u * u) : 0;
    if (v === 0) {
        rx *= u; 
        ry *= u; 
    }

    const f = (largeArcFlag) ? v / u : -v / u;
    const cxx = f * -yy1;
    const cyy = f *  xx1;

    const theta1 = Math.atan2( yy1 - cyy,  xx1 - cxx);
    const theta2 = Math.atan2(-yy1 - cyy, -xx1 - cxx);

    return makeArc(
        theta1, theta2,
        makePoint(...pp_p(cxx    , cyy    )),
        makePoint(...pp_p(cxx + 1, cyy    )),
        makePoint(...pp_p(cxx    , cyy + 1))
    );
};

const parseFloats = text => {
    const m = text.match(
        /[-+]?([0-9]*\.[0-9]+|[0-9]+\.[0-9]*|[0-9]+)(e[-+]?[0-9]+)?/gi
    );
    if (m === null) {
        return [];
    }
    return m.map(parseFloat);
};

const parsePathD = (() => {
    const cmdAbsMove = (context, x, y) => {
        context.initialX = x;
        context.initialY = y;
        context.x = x;
        context.y = y;
        return undefined;
    };

    const cmdAbsHLine = (context, x) => {
        const part = makeLine(
            makePoint(context.x, context.y), makePoint(x, context.y)
        );
        context.x = x;
        return part;
    };

    const cmdRelVLine = (context, dy) => cmdAbsVLine(context, context.y + dy);

    const cmdAbsVLine = (context, y) => {
        const part = makeLine(
            makePoint(context.x, context.y), makePoint(context.x, y)
        );
        context.y = y;
        return part;
    };

    const cmdClose = context => {
        context.x = context.initialX;
        context.y = context.initialY;
        return undefined;
    };

    const cmdRelCubicBezierCurve = (context, x1, y1, x2, y2, x, y) => {
        const part = makeCubicBezier(
            makePoint(context.x     , context.y     ),
            makePoint(context.x + x1, context.y + y1),
            makePoint(context.x + x2, context.y + y2),
            makePoint(context.x + x , context.y + y )
        );
        context.x += x;
        context.y += y;
        return part;
    };

    const cmdAbsArc = (context,
        rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y
    ) => {
        let part;
        if (rx === 0 || ry === 0) {
            part = makeLine(makePoint(context.x, context.y), makePoint(x, y));
        } else {
            part = convertArc(
                Math.abs(rx), Math.abs(ry),
                context.x, context.y,
                x        , y        ,
                xAxisRotation, largeArcFlag, sweepFlag
            );
        }
        context.x = x;
        context.y = y;
        return part;
    };

    const cmdRelArc = (context,
        rx, ry, xAxisRotation, largeArcFlag, sweepFlag, dx, dy
    ) => cmdAbsArc(
        context,
        rx, ry,
        xAxisRotation, largeArcFlag, sweepFlag,
        context.x + dx, context.y + dy
    );

    const factory = {
     // 'm': cmdRelMove,
        'M': cmdAbsMove,
     // 'l': cmdRelLine,
     // 'L': cmdAbsLine,
     // 'h': cmdRelHLine,
        'H': cmdAbsHLine,
        'v': cmdRelVLine,
        'V': cmdAbsVLine,
        'z': cmdClose,
        'Z': cmdClose,
     // 'C': cmdAbsCubicBezierCurve,
        'c': cmdRelCubicBezierCurve,
     // 'S': 
     // 's': 
     // 'Q': 
     // 'q': 
     // 'T': 
     // 't': 
        'A': cmdAbsArc,
        'a': cmdRelArc
    };

    const parsePathD = d => {
        const context = {
            initialX: 0,
            initialY: 0,
            x: 0,
            y: 0
        };

        const parts = [];

        for (const c of d.match(/([mlhvzcsqta])([^mlhvzcsqta]*)/gi)) {
            const cb = factory[c[0]];
            const step = cb.length - 1;
            if (step === 0) {
                const part = cb(context);
                if (part !== undefined) {
                    parts.push(part);
                }
            } else {
                const args = parseFloats(c.substring(1));
                for (let i = 0; i < args.length; i += step) {
                    const part = cb(context, ...args.slice(i, i + step));
                    if (part !== undefined) {
                        parts.push(part);
                    }
                }
            }
        }
        return parts;
    };
    
    return parsePathD;
})();

const getExtremeNode = (dir, node) => {
    const d = node.getAttribute('d');
    const sw = parseFloat(node.getAttribute('stroke-width')) || 0;
    const parts = parsePathD(d);
    const matrix = node.getScreenCTM();
    return sw / 2 + Math.max(
        ...parts.map(part => part.map(matrix).getExtreme(dir))
    );
};

const getExtreme = (dir, root) => {
    const nodes = [...root.getElementsByTagName('path')];
    if (root.tagName === 'path') {
        nodes.push(root);
    }
    return Math.max(...nodes.map(node => getExtremeNode(dir, node)));
}

const driver = document.getElementById('driver');
const drivee = document.getElementById('drivee');
const lever = document.getElementById('lever');

const drive = () => {
    const h1 = getExtreme([0, -1], driver);
    const h2 = getExtreme([0, 1], lever );
    const y = parseFloat(drivee.getAttribute('y'));
    drivee.setAttribute('y', y - h1 - h2 + 2);
    requestAnimationFrame(drive);
};
requestAnimationFrame(drive);
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400" viewBox="0 0 500 500">
  <svg>
    <g id="driver">
      <!-- Шарнир -->
      <path fill="grey" stroke="black" stroke-width="3" d="M344.3 197.6a49.6 49.6 0 0 1 34.4 13A57 57 0 0 1 396 251a54 54 0 0 1-17.4 36.8c-9 8.2-22 13.1-34.2 13a54.4 54.4 0 0 1-36.8-15.4A51.4 51.4 0 0 1 294 251a55.4 55.4 0 0 1 13.7-37.6 53.7 53.7 0 0 1 36.6-16z"/>
      <!-- Ось кулачка -->
      <path fill="#626262" stroke="black" stroke-width="2" d="M344.3 223.5c8.9 0 18 5.5 23.7 12.3 3.4 4.1 4.9 10 4.5 15.3-.4 6.7-3.2 13.9-8.2 18.4a27 27 0 0 1-20 6.5c-7.1-.8-13.8-5.4-18.6-10.6a24.4 24.4 0 0 1-1-31.8c4.7-5.7 12.2-10.1 19.6-10z"/>
      <!-- Кулачок -->
      <path fill="grey" stroke="black" stroke-width="3" d="M41.3 153c32.7 0 78.5 3.3 117.6 7 35 3.2 69.9 6.8 104.3 13.2 16.8 3.1 33.3 7.7 49.8 12.2 13 3.5 43.8 11.3 38.7 11.6-20 1.4-39.1 7.2-48.7 22.3-20.3 31.8-.9 54.9-7 41.8-5.4-11.8-18.1-18-29-24.3-13.4-7.9-28.7-11.9-43.5-16.5-19-5.9-38.9-9.2-58.2-14.3-21.5-5.5-42.7-12-64.1-17.4-14-3.5-26.3-5.5-41.9-9.6a25 25 0 0 1-13.2-10.6c-3-4.4-10.1-15.3-4.8-15.3z">
    
      </path>
      <animateTransform
        attributeName="transform"
        attributeType="XML"
        type="rotate"
        values="0 347 250;-30 347 250;0 347 250;-10 347 250;0 347 250"
        dur="5s"
        repeatCount="indefinite" />
    </g>
  </svg>
  <svg id="drivee" y="0">
    <!-- Вертикальный стержень -->
    <path fill="grey" stroke="black" stroke-width="3" d="M387.3 13.1h30v440.5h-30z"/>
    <!-- Цилиндр -->
    <path fill="black" d="M377.5 54.3c3.8-.7 50.6-.4 50.6-.4l9.3 10.5v88.8h-66.6V64.4Z"/>
    <!-- Линия фаски -->
    <path  stroke="white" d="m370.8 64 66.6.4"/>
    <!-- Верхний рычаг -->
    <path id="lever" fill="grey" stroke="black" stroke-width="3" d="M21.2 153c-3.1 0 1.1-6.2 2.6-9 1.4-2.4 3-5.7 5.9-6.3a17587 17587 0 0 1 341-65.7v81H21.3z"/>
  </svg>
</svg>

→ Ссылка
Автор решения: DiD

Как и обещал.

Решение SVG+CSS

<svg xmlns="http://www.w3.org/2000/svg" style="max-height: 100vh; max-width: 100vw;" viewBox="0 0 500 500" fill="none">
   <style>
      <![CDATA[
      & {
         --cx: 350px;       /* Координаты оси вращения */
         --cy: 250px;
         --cp0y: 200px;     /* высота нижней стороны верхней части системы */
         --dy0: 0px;        /* Начальная позиция (полезно для отсчета относительного смещения)  */
         --scene-time: 4s;  /* Время сцены  */
      }
      
/* Обычные переменные не могут участвовать в анимациях,  так как области 
их видимости ограничены местом определения значения + дочерние элементы.
А у анимаций нет никаких дочерних элемеентов. Поэтому значения, 
определенные в анимациях никогда не смогут быть получены снаружи.
       
Другое дело - property. Они имеют одно общее значение на всей странице и 
не подчиняются каскаду. */

      @property --rotation {   /* Угол вращения */
         syntax: '<angle>';    /* Определяем строгий тип - Угол */
         initial-value: 0deg;  /* Начальное значение */
         inherits: false;      /* Получать значение от родителей */
      }

      /* Значения угла вращения на каждый интервал таймлайна */

      @keyframes rotate {        
         0%  {  --rotation: 0deg;     }       
         16% {  --rotation: 13.5deg;  }        
         18% {  --rotation: 15deg;    }        
         20% {  --rotation: 13.5deg;  }        
         36% {  --rotation: 0deg;     }        
         48% {  --rotation: -13.5deg; }        
         50% {  --rotation: -15deg;   }        
         52% {  --rotation: -13.5deg; }       
         64% {  --rotation: 0deg;     }        
         80% {  --rotation: 13.5deg;  }        
         82% {  --rotation: 15deg;    }        
         84% {  --rotation: 13.5deg;  }        
         100% { --rotation: 0deg;    }
      }
      ]]>
   </style>
   <!-- <image href="image.gif" width="500" height="500" x="1" y="1" preserveAspectRatio="xMidYMid meet" crossorigin="anonymous" /> -->
   <g style="transform: scale(1.1) translate(-28px, -56px);">
      <path style="stroke: #f00; transform-origin: 350px 250px; 
                transform: rotate(var(--rotation)); 
                animation: rotate var(--scene-time) linear infinite;"
         d="m 350 200 c -125 -3 -240 15 -305 33 c 2.5 8.5 9.5 13.5 20 15 
            c 10 1 200 9 220 13 c 9 2 17 8 23 14 a 50 50 0 0 1 42 -75z
            a 50 50 0 1 1 -42 +75 m18 -25 a 25 25 0 1 1 50 0 25 25 0 1 1 -50 0" />
      <g stroke="#1D1D5D"
         style="transform: translateY(min(
               calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 86.8deg) * 305.5px), 
               calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 84.1deg) * 264.4px), 
               calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 82.5deg) * 242.1px), 
               calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 77.8deg) * 189.3px), 
               calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 69.2deg) * 132.6px), 
               var(--dy0)
             )); 
             animation: rotate var(--scene-time) linear infinite;">
         <path d="m 410 200 v 310 h 30 v -310 m 0 -81 v -30 h -30 v 30" />
         <path d="
   m 450 125 v 75 h -405 c 0 0 -1 -12 15 -14 
   c 113 -18 225 -38 340 -58 v -3 h 50 l -6 -6 h -38 l -6 6 v 75" />
      </g>
   </g>
</svg>

К сожалению, у меня не получилось использовать несколько переменных в анимации (я пробовал в одной и в разных). Почему-то в анимациях работает либо одна переменная, либо не работает ничего вообще. Переменные перестают получать плавные значения на каждый FPS, вместо этого рандомно получают либо начальное либо конечное значение.

Было бы здорово использовать переменные, чтобы уменьшить размеры расчетов. К сожалению, всю математику приходится вписывать в агрумент style="...", иначе не получается читать значения, которые определяются внутри анимации.

И так мы получаем расчет трансформации сдвига.

transform: translateY(min(
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 86.8deg) * 305.5px), 
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 84.1deg) * 264.4px), 
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 82.5deg) * 242.1px), 
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 77.8deg) * 189.3px), 
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 69.2deg) * 132.6px), 
           var(--dy0)

где двумя столбиками (86.8deg, 305.5px) идут константы, расчитанные заранее. Были взяты 5 точек с полукруглой поверхности. 86.8deg - это угол, образующийся из вектора от оси вращения к выбранной точке относительно оси X (то есть, нижней границе поднимаемой вверх части поршня). А 305.5px - это длина этого вектора, то есть расстояние от точки вращения до выбранной точки.

Такие значения можно было бы рассчитать на JS. Но можно обойтись без JS. Возьмем например точки с координатами: 45,223, 110,218.5, 165,210, 226,203 и 350,200.

Пробы координат можно брать из inkscape.

Выбранные точки

Перечислим значения координат в переменных...

И дальше сделаем как-то так:

<svg xmlns="http://www.w3.org/2000/svg" style="max-height: 100vh; max-width: 100vw;" viewBox="0 0 500 500" fill="none">
   <style>
      <![CDATA[
      :root {
         /* Центр вращения */
         --cx: 350px;
         --cy: 250px;
         /* Уровень нижней границы верхней части */         
         --cp0y: 200px;
         /* Первая точка */
         --cp1x: 45px;
         --cp1y: 233px; 
         /* Вторая точка */
         --cp2x: 110px;
         --cp2y: 218.5px;
         /* Третья точка */
         --cp3x: 165px;
         --cp3y: 210px;
         /* Четвертая точка */
         --cp4x: 226px;
         --cp4y: 203px;
         /* Пятая точка */
         --cp5x: 350px;
         --cp5y: 200px;
         /* Нулевой уровень */
         --dy0: 0px;
         /* Интервал сцены */
         --scene-time: 4s;
      }

      @property --rotation {
         syntax: '<angle>';
         initial-value: 0deg;
         inherits: false;
      }

      @keyframes rotate {        
         0%  {  --rotation: 0deg;     }       
         16% {  --rotation: 13.5deg;  }        
         18% {  --rotation: 15deg;    }        
         20% {  --rotation: 13.5deg;  }        
         36% {  --rotation: 0deg;     }        
         48% {  --rotation: -13.5deg; }        
         50% {  --rotation: -15deg;   }        
         52% {  --rotation: -13.5deg; }       
         64% {  --rotation: 0deg;     }        
         80% {  --rotation: 13.5deg;  }        
         82% {  --rotation: 15deg;    }        
         84% {  --rotation: 13.5deg;  }        
         100% { --rotation: 0deg;    }
      }
      ]]>
   </style>
   <path style="stroke: #f00; transform-origin: 350px 250px; 
               transform: rotate(var(--rotation)); 
               animation: rotate var(--scene-time) linear infinite;"
      d="m 350 200 c -125 -3 -240 15 -305 33 c 2.5 8.5 9.5 13.5 20 15 
         c 10 1 200 9 220 13 c 9 2 17 8 23 14 a 50 50 0 0 1 42 -75z
         a 50 50 0 1 1 -42 +75 m18 -25 a 25 25 0 1 1 50 0 25 25 0 1 1 -50 0" />
   <g stroke="#1D1D5D"
      style="transform: translateY(min(
            calc(var(--cy) - var(--cp0y) - cos(var(--rotation) + atan2(calc(var(--cp1x) - var(--cx)), calc(var(--cy) - var(--cp1y)))) * hypot(calc(var(--cp1x) - var(--cx)), calc(var(--cp1y) - var(--cy)))), 
            calc(var(--cy) - var(--cp0y) - cos(var(--rotation) + atan2(calc(var(--cp2x) - var(--cx)), calc(var(--cy) - var(--cp2y)))) * hypot(calc(var(--cp2x) - var(--cx)), calc(var(--cp2y) - var(--cy)))), 
            calc(var(--cy) - var(--cp0y) - cos(var(--rotation) + atan2(calc(var(--cp3x) - var(--cx)), calc(var(--cy) - var(--cp3y)))) * hypot(calc(var(--cp3x) - var(--cx)), calc(var(--cp3y) - var(--cy)))), 
            calc(var(--cy) - var(--cp0y) - cos(var(--rotation) + atan2(calc(var(--cp4x) - var(--cx)), calc(var(--cy) - var(--cp4y)))) * hypot(calc(var(--cp4x) - var(--cx)), calc(var(--cp4y) - var(--cy)))), 
            calc(var(--cy) - var(--cp0y) - cos(var(--rotation) + atan2(calc(var(--cp5x) - var(--cx)), calc(var(--cy) - var(--cp5y)))) * hypot(calc(var(--cp5x) - var(--cx)), calc(var(--cp5y) - var(--cy)))), 
            var(--dy0)
            )); 
            animation: rotate var(--scene-time) linear infinite;">
      <path d="m 410 200 v 310 h 30 v -310 m 0 -81 v -30 h -30 v 30" />
      <path d="m 450 125 v 75 h -405 c 0 0 -1 -12 15 -14 
               c 113 -18 225 -38 340 -58 v -3 h 50 l -6 -6 h -38 l -6 6 v 75" />
   </g>

</svg>

Формулы на CSS выглядят жестковато. В решении задачи использовались функции CSS: cos(), atan2(), hypot(). Использовались анимации с переменной @property.

Честно скажу. Когда я первый раз узнал о наличии геометрических функций в CSS, думал за свою жизнь никогда не найду повод применить их в действии. Но это может быть неплохой заменой JS.

И чуть не забыл.

Есть сравнение анимации SVG с GIF.

<svg xmlns="http://www.w3.org/2000/svg" style="max-height: 100vh; max-width: 100vw;" viewBox="0 0 500 500" fill="none">
   <style>
  <![CDATA[
  & {
     --cy: 250px;
     --cp0y: 200px;
     --dy0: 0px;
     --scene-time: 4s;
  }

  @property --rotation {
     syntax: '<angle>';
     initial-value: 0deg;
     inherits: false;
  }

      @keyframes rotate {        
         0%  {  --rotation: 0deg;     }       
         16% {  --rotation: 13.5deg;  }        
         18% {  --rotation: 15deg;    }        
         20% {  --rotation: 13.5deg;  }        
         36% {  --rotation: 0deg;     }        
         48% {  --rotation: -13.5deg; }        
         50% {  --rotation: -15deg;   }        
         52% {  --rotation: -13.5deg; }       
         64% {  --rotation: 0deg;     }        
         80% {  --rotation: 13.5deg;  }        
         82% {  --rotation: 15deg;    }        
         84% {  --rotation: 13.5deg;  }        
         100% { --rotation: 0deg;    }
      }
  ]]>
   </style>
   <image href="https://i.sstatic.net/JqMRvW2C.gif" width="500" height="500" x="1" y="1" preserveAspectRatio="xMidYMid meet" crossorigin="anonymous" />
   <g style="transform: scale(1.1) translate(-28px, -56px);">
  <path style="stroke: #f00; transform-origin: 350px 250px; 
            transform: rotate(var(--rotation)); 
            animation: rotate var(--scene-time) linear infinite;"
     d="m 350 200 c -125 -3 -240 15 -305 33 c 2.5 8.5 9.5 13.5 20 15 
        c 10 1 200 9 220 13 c 9 2 17 8 23 14 a 50 50 0 0 1 42 -75z
        a 50 50 0 1 1 -42 +75 m18 -25 a 25 25 0 1 1 50 0 25 25 0 1 1 -50 0" />
  <g stroke="#1D1D5D"
     style="transform: translateY(min(
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 86.8deg) * 305.5px), 
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 84.1deg) * 264.4px), 
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 82.5deg) * 242.1px), 
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 77.8deg) * 189.3px), 
           calc(var(--cy) - var(--cp0y) - cos(var(--rotation) - 69.2deg) * 132.6px), 
           var(--dy0)
         )); 
         animation: rotate var(--scene-time) linear infinite;">
     <path d="m 410 200 v 310 h 30 v -310 m 0 -81 v -30 h -30 v 30" />
     <path d="
   m 450 125 v 75 h -405 c 0 0 -1 -12 15 -14 
   c 113 -18 225 -38 340 -58 v -3 h 50 l -6 -6 h -38 l -6 6 v 75" />
  </g>
   </g>
</svg>

Пояснения и теоретическую выкладку сделал в отдельном ответе

→ Ссылка
Автор решения: Leonid

Вариант SVG + JS. Верхняя точка кулака находится с помощью двухэтапного перебора нескольких точек методом SVGGeometryElement.getPointAtLength(). Точки распределяются по длине пути, получаемом методом SVGGeometryElement.getTotalLength(). Сначала грубо находим 1 точку с наименьшим y (наивысшую), затем еще раз делим участок между двумя смежными точками и находим самую высокую точку с бОльшей точностью.

К сожалению упомянутые методы ничего не знают о вращении пути. Поэтому точки приходится вращать при переборе. Для этого функция rotatePoint().

const kulak = document.getElementById('kulak');
const piston = document.getElementById('piston');

const init_h = findHighestPoint(kulak, 0).y;

const start_time = performance.now();
const anim_queue = [{
  start: 0,
  angle: 0
}, {
  start: 1600,
  angle: -40
}, {
  start: 3200,
  angle: 0
}, {
  start: 4200,
  angle: -20
}, {
  start: 5200,
  angle: 0
}];
const anim_total = anim_queue[anim_queue.length - 1].start;

function animate() {
  const time_passed = (performance.now() - start_time) % anim_total;
  const next_i = anim_queue.findIndex(el => el.start > time_passed);
  const angle = ((time_passed - anim_queue[next_i - 1].start) / (anim_queue[next_i].start - anim_queue[next_i - 1].start)) * (anim_queue[next_i].angle - anim_queue[next_i - 1].angle) + anim_queue[next_i - 1].angle;
  rotate(angle);
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

function rotate(angle) {
  kulak.setAttribute('transform', `rotate(${angle})`);
  piston.setAttribute('transform', `translate(0, ${findHighestPoint(kulak, angle).y - init_h})`);
}


function findHighestPoint(element, angle) {
  const len = element.getTotalLength();
  const precision = 1 / 20;

  let h_point = rotatePoint(element.getPointAtLength(0), angle);
  let step = len * precision;
  let len_i = 0;
  let cur_point;

  for (let i = step; i <= len; i += step) {
    cur_point = rotatePoint(element.getPointAtLength(i), angle);
    if (cur_point.y < h_point.y) {
      h_point = cur_point;
      len_i = i;
    }
  }

  for (let j = len_i - step; j <= len_i + step; j += step * precision) {
    cur_point = rotatePoint(element.getPointAtLength(j), angle);
    if (cur_point.y < h_point.y) {
      h_point = cur_point;
    }
  }

  return h_point;
}

function rotatePoint(point, rotate_angle) {
  const angle = Math.atan2(point.y, point.x) + rotate_angle * (Math.PI / 180);
  const r = Math.hypot(point.x, point.y);

  point.x = r * Math.cos(angle);
  point.y = r * Math.sin(angle);

  return point;
}
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="500px" height="500px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality;"
viewBox="-350 -250 500 500"
 xmlns:xlink="http://www.w3.org/1999/xlink">
        <path id="kulak" transform="rotate(0)" fill="dimgrey" stroke="darkslategrey" d="M22.5 -44.05c-0.01,-0.03 -0.04,-0.05 -0.08,-0.08 -69.79,-38.5 -282.86,-50.37 -314.5,-45.84 -2.4,0.43 0.91,12.73 6.93,17.26 27.48,20.66 219.84,38.32 236.96,88.22 6.27,20.61 25.43,35.61 48.09,35.61 27.76,0 50.27,-22.5 50.27,-50.26 0,-19.63 -11.26,-36.63 -27.67,-44.91z"/>
        <path id="center" fill="grey" stroke="darkslategrey" d="M-0.06 -24.58c14.04,0 25.42,11.38 25.42,25.42 0,14.04 -11.38,25.42 -25.42,25.42 -14.04,0 -25.42,-11.38 -25.42,-25.42 0,-14.04 11.38,-25.42 25.42,-25.42zm0 -24.84c27.76,0 50.26,22.5 50.26,50.26 0,27.76 -22.5,50.27 -50.26,50.27 -27.76,0 -50.27,-22.51 -50.27,-50.27 0,-27.76 22.51,-50.26 50.27,-50.26z"/>
        <rect id="bar" fill="grey" stroke="darkslategrey" x="40.87" y="-224.64" width="29.85" height="419.83"/>
        <path id="piston" transform="translate(0,0)" stroke="darkslategrey" fill="grey" d="M23.3 -90.96l64.99 0 0 -84.25 -64.99 0 0 84.25zm0 -84.25l64.99 0 -8.72 -10.11 -47.55 0 -8.72 10.11zm0 84.25c-111.58,0 -223.15,0 -334.73,0 -0.9,-0.22 -0.18,-11.93 9.21,-14.74l325.52 -62.29 0 77.03z"/>
    </svg>

UPD. Чтобы не вникать в JS и мелочи реализации добавляю вариант с вэб-компонентом. В атрибут data-anim расписываются углы поворота кулака в формате: время начала вращения, длительность вращения, начальный угол, конечный угол. Отдельные анимации разделены символом |, они должны смыкаться по времени и углам поворота (некоторая избыточность).

В разметку можно просто добавить <kulak-svg data-anim="0,1600,0,-40|1600,1600,-40,0|3200,1000,0,-20|4200,1000,-20,0"></kulak-svg> для получения искомой анимации. Никакого JS, код из сниппета записывается, например, в kulak-svg.js и можно вставлять теги <kulak-svg></kulak-svg> с нужными данными анимации на страницу.

Для примера вставлены 4 элемента с разными анимациями, они будут видны если развернуть на всю страницу. Первые две анимации вполне легальны с точки зрения физики. Последние две вращают кулак на полный оборот, поэтому положение рычага поршня не оправдано в положениях от -90 до 0 градусов (в пересчете на правую горизонталь как 0). Обусловлен "ляп" тем, что мы просто находим верхнюю точку кулака, и по ней выставляем нижнюю кромку рычага поршня, вне зависимости от реального "соприкосновения объектов". Таким образом лучше демонстрируются плюсы и минусы подхода.

Также добавил возможность менять viewBox, width и height для каждого отдельного элемента <kulak-svg>, который переписывает соответствующие атрибуты <svg>, установленные в <template> по умолчанию.

class KulakSVG extends HTMLElement {

  constructor() {
    super();

    const template = document.createElement('template');
    template.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="250px" height="250px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality;"
viewBox="-400 -425 750 750"
xmlns:xlink="http://www.w3.org/1999/xlink">       
        <path id="kulak" transform="rotate(0)" fill="dimgrey" stroke="darkslategrey" d="M22.5 -44.05c-0.01,-0.03 -0.04,-0.05 -0.08,-0.08 -69.79,-38.5 -282.86,-50.37 -314.5,-45.84 -2.4,0.43 0.91,12.73 6.93,17.26 27.48,20.66 219.84,38.32 236.96,88.22 6.27,20.61 25.43,35.61 48.09,35.61 27.76,0 50.27,-22.5 50.27,-50.26 0,-19.63 -11.26,-36.63 -27.67,-44.91z"/>
        <path id="center" fill="grey" stroke="darkslategrey" d="M-0.06 -24.58c14.04,0 25.42,11.38 25.42,25.42 0,14.04 -11.38,25.42 -25.42,25.42 -14.04,0 -25.42,-11.38 -25.42,-25.42 0,-14.04 11.38,-25.42 25.42,-25.42zm0 -24.84c27.76,0 50.26,22.5 50.26,50.26 0,27.76 -22.5,50.27 -50.26,50.27 -27.76,0 -50.27,-22.51 -50.27,-50.27 0,-27.76 22.51,-50.26 50.27,-50.26z"/>
        <rect id="bar" fill="grey" stroke="darkslategrey" x="40.87" y="-425" width="29.85" height="750"/>
        <path id="piston" transform="translate(0,0)" stroke="darkslategrey" fill="grey" d="M23.3 -90.96l64.99 0 0 -84.25 -64.99 0 0 84.25zm0 -84.25l64.99 0 -8.72 -10.11 -47.55 0 -8.72 10.11zm0 84.25c-111.58,0 -223.15,0 -334.73,0 -0.9,-0.22 -0.18,-11.93 9.21,-14.74l325.52 -62.29 0 77.03z"/>
    </svg>
`;

    this.root = this.attachShadow({
      mode: 'closed'
    });
    this.root.append(template.content.cloneNode(true));
  }

  connectedCallback() {
    const kulak = this.root.querySelector('#kulak');
    const piston = this.root.querySelector('#piston');
    const svg = this.root.querySelector('svg');

    const attribute_vb = this.getAttribute('viewBox');
    if(attribute_vb){svg.setAttribute('viewBox', attribute_vb);}

    const attribute_width = this.getAttribute('width');
    if(attribute_width){svg.setAttribute('width', attribute_width);}

    const attribute_height = this.getAttribute('height');
    if(attribute_height){svg.setAttribute('height', attribute_height);}

    const init_h = findHighestPoint(kulak, 0).y;

    const start_time = performance.now();
    const anim_queue = this.getAttribute('data-anim').split('|').map(str => {
      const data = str.split(',');
      return {
        start: +data[0],
        dur: +data[1],
        from: +data[2],
        to: +data[3]
      };
    });


    const anim_total = anim_queue.reduce((sum, cur) => sum + cur.dur, 0);

    function animate() {
      const time_passed = (performance.now() - start_time) % anim_total;
      const cur_anim = anim_queue.findLast(el => el.start < time_passed);
      const angle = ((time_passed - cur_anim.start) / cur_anim.dur) * (cur_anim.to - cur_anim.from) + cur_anim.from;
      rotate(angle);
      requestAnimationFrame(animate);
    }

    requestAnimationFrame(animate);


    function rotate(angle) {
      kulak.setAttribute('transform', `rotate(${angle})`);
      piston.setAttribute('transform', `translate(0, ${findHighestPoint(kulak, angle).y - init_h})`);
    }


    function findHighestPoint(element, angle) {
      const len = element.getTotalLength();
      const precision = 1 / 20;

      let h_point = rotatePoint(element.getPointAtLength(0), angle);
      let step = len * precision;
      let len_i = 0;
      let cur_point;

      for (let i = step; i <= len; i += step) {
        cur_point = rotatePoint(element.getPointAtLength(i), angle);
        if (cur_point.y < h_point.y) {
          h_point = cur_point;
          len_i = i;
        }
      }

      for (let j = len_i - step; j <= len_i + step; j += step * precision) {
        cur_point = rotatePoint(element.getPointAtLength(j), angle);
        if (cur_point.y < h_point.y) {
          h_point = cur_point;
        }
      }

      return h_point;
    }

    function rotatePoint(point, rotate_angle) {
      const angle = Math.atan2(point.y, point.x) + rotate_angle * (Math.PI / 180);
      const r = Math.hypot(point.x, point.y);

      point.x = r * Math.cos(angle);
      point.y = r * Math.sin(angle);

      return point;
    }
  }

  disconnectedCallback() {

  }

}

customElements.define('kulak-svg', KulakSVG);
<!-- data-anim format: start,dur,angle_from,angle_to| start,dur,angle_from,angle_to... -->

<kulak-svg viewBox="-350 -250 500 500" data-anim="0,1600,0,-40|1600,1600,-40,0|3200,1000,0,-20|4200,1000,-20,0" ></kulak-svg>
<kulak-svg data-anim="0,1000,0,15|1000,2000,15,-15|3000,1000,-15,0"></kulak-svg>
<kulak-svg data-anim="0,15000,0,-360" height="300px" width="300px"></kulak-svg>
<kulak-svg data-anim="0,15000,0,360"></kulak-svg>

→ Ссылка
Автор решения: DiD

Этот ответ - теоретические пояснения к ответу

Пролог

Мы имеем viewBox с точкой отсчета с координатами 0,0 в верхнем левом углу картинки. Ось X отсчитывается слева направо. Ось Y отсчитывается сверху вниз.

В большинстве языков программирования, как и в JS (объект Math) и с некоторого времени назад уже в CSS, все функции для расчетов имеют названия и логику работы в точном соответствии с языком FORTRAN. Для работы с углами используются единицы измерения радианы. В SVG для удобства восприятия в местах, не связанных с логикой работы скриптов, углы устанавливаются в градусах, в CSS углы указываются в любых единицах измерения при условии, что после цифрового значения указаны названия единиц измерения без пробела между собой. Например, 45deg, 1.5turn.

Язык SVG имеет богатую на споры историю (как и HTML). Создатели браузеров оспаривают любые решения и предложения новшеств, кроме фундаментальных фичей, которые уже устаканились за долго до начала их обсуждений. Поэтому, в 2024г мы имеем язык SVG, который по возможностям не сильно богаче формата TTF 1992г, имеет модную на начало 2000-х годов структурную декларативную основу XML.

Расчеты координат точки P1, повернутой на заданный угол angle вокруг точки Po

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

Пусть у нас есть:

  • точка P1 с координатами P1 [p1x,p1y],
  • точка Po с координатами Po [pox,poy],
  • начало отсчета координат О [x0,y0],
  • угол поворота angle.

Необходимо получить координаты точки P2 [p2x,p2y], которые соответствуют координатам точки P1, повёрнутой на угол angle вокруг точки Po.

Для начала надо определить текущий угол вектора от точки, вокруг которой происходит вращение, Po до точки, которую вращаем, P1.

В языке FORTRAN принято отмерять углы против часовой стрелки с нулевым началом на восток (3 часа). Соответственно, в классических алгоритмах все углы отмеряются к оси X.

         90
         |
180 ---     --- 0
         |
        270

Вероятно, математики закидают камнями, и будут правы. Но для моего опыта работы с графикой, это не очень удобное представление для вычислений приблизительных расчетов в уме. Для меня удобнее, когда углы имеют нулевое начало на север (12 часов) и отсчитываются в положительную сторону по часовой стрелке.

         0
         |
270 ---     --- 90
         |
        180

Первопричин этому назвать не могу. Может быть, из-за того, что ось Y в SVG направлена сверху вниз, а не как на школьной доске снизу вверх. А может быть, из-за того что впервые я столкнулся с тригонометрией в программировании именно с визуализацией часов.

Самый простой способ найти угол вектора имея координаты, это Math.atan2(), так как угол эквивалентен арктангенсу. В FORTRAN, как и в JS также есть функция Math.atan(), но она возвращает NaN для углов, которые имею косинус равный нулю (хотя по идее значение Infinity было бы уместнее). В случае с Math.atan2() таких неудобств не возникает. Math.atan2() всегда возвращает значения от -Math.PI до +Math.PI.

В классических расчетах вызов имеет вид

Math.ata2(y,x)

где x, y - координаты вектора. Но в SVG ось Y направлена сверху вниз, поэтому функция имела бы вид

Math.atan2(-y,x)

Я же использую ее в виде

Math.atan2(x,-y)

Возвращаемый угол будет в радианах, что для привычного представления в градусах требует умножить на 180 и поделить на число Math.PI.

Подставив наши значения, расчет угла будет иметь вид

let sangle = Math.atan2(p1x - pox, -(p1y - poy)) * 180 / Math.PI;

Далее для полного представления вектора надо получить расстояние между двумя точками. На помощь приходит теорема Пифагора. И в частном случае - функция Math.hypot(), которая своим названием как бы намекает на расчет длины гипотенузы по длинам двух катетов. Здесь наши катеты эквивалентны координатам точки вектора на плоскости.

let length = Math.hypot(p1x - pox, p1y - poy)

При использовании Math.hypot() знаки значений аргументов уже по барабану, потому что в расчетах аргументы возводятся в квадрат, и поэтому функция выдаст те же значение, например так:

Math.hypot(pox - p1x, poy - p1y)

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

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

let rangle = sangle + angle;

let p2x = pox + Math.sin(rangle) * length;
let p2y = poy - Math.cos(rangle) * length;

Эти формулы надо запомнить. При работе с графикой векторы используются повсеместно. Да и не только с графикой. Также полезны, например, при отслеживании направления движения курсора мыши. А при обработке сырых значений touch-событий их использования избежать невозможно. Если вы делаете, например, управление каким-нибудь персонажем, вам нужно определить в какой стороне от персонажа курсор, и обратно - в какую точку придёт персонаж, если пойдёт в этом направлении.

Я даже сделал небольшое демо для демонстрации этих формул. Можно перетягивать точки по принципам drag-n-drop, или использовать слайдеры для отслеживания изменения конкретных параметров.

let p1xValue = 300;
let p1yValue = 20;
let poxValue = 10;
let poyValue = 100;
let angleValue = 0;
let lengthValue = 0;

function setValue(event) {
  let { target } = event;
  eval(`${target.id}Value = ${target.value}`);
  if (target.id.startsWith('p')) {
angleValue = Math.atan2(p1xValue - poxValue, poyValue - p1yValue) * 180 / Math.PI | 0;
lengthValue = Math.hypot(p1xValue - poxValue, p1yValue - poyValue) | 0;
  } else {
p1xValue = poxValue + Math.sin(angleValue / 180 * Math.PI) * lengthValue | 0;
p1yValue = poyValue - Math.cos(angleValue / 180 * Math.PI) * lengthValue | 0;
  }
  lengtho.value = document.querySelector('#length').value = lengthValue;
  angleo.value = angle.value = angleValue;
  p1xo.value = p1x.value = p1xValue;
  p1yo.value = p1y.value = p1yValue;
  poxo.value = pox.value = poxValue;
  poyo.value = poy.value = poyValue;
  ppo.setAttribute('d', `M${poxValue},${poyValue}m-5,0a5,5,0,1,1,10,0,5,5,0,1,1-10,0`)
  pp1.setAttribute('d', `M${p1xValue},${p1yValue}m-5,0a5,5,0,1,1,10,0,5,5,0,1,1-10,0`)
  ppp.setAttribute('d', `M${poxValue},${poyValue}L${p1xValue},${p1yValue}`);
}


function lock(event) {
  event.target.setPointerCapture(1);
}
function move(event) {
  if (pp1.hasPointerCapture(1)) {
p1xValue += event.movementX;
p1yValue += event.movementY;
angleValue = Math.atan2(p1xValue - poxValue, poyValue - p1yValue) * 180 / Math.PI | 0;
lengthValue = Math.hypot(p1xValue - poxValue, p1yValue - poyValue) | 0;
  }
  if (ppo.hasPointerCapture(1)) {
poxValue += event.movementX;
poyValue += event.movementY;
angleValue = Math.atan2(p1xValue - poxValue, poyValue - p1yValue) * 180 / Math.PI | 0;
lengthValue = Math.hypot(p1xValue - poxValue, p1yValue - poyValue) | 0;
  }
  lengtho.value = document.querySelector('#length').value = lengthValue;
  angleo.value = angle.value = angleValue;
  p1xo.value = p1x.value = p1xValue;
  p1yo.value = p1y.value = p1yValue;
  poxo.value = pox.value = poxValue;
  poyo.value = poy.value = poyValue;
  ppo.setAttribute('d', `M${poxValue},${poyValue}m-5,0a5,5,0,1,1,10,0,5,5,0,1,1-10,0`)
  pp1.setAttribute('d', `M${p1xValue},${p1yValue}m-5,0a5,5,0,1,1,10,0,5,5,0,1,1-10,0`)
  ppp.setAttribute('d', `M${poxValue},${poyValue}L${p1xValue},${p1yValue}`);
}

function unlock(event) {
  event.target.releasePointerCapture(1);
}
body {
  margin: 0;
}

#cockpit {
  position: fixed;
  right: 0;
  top: 0;
  border: 1px solid #000;
  padding: .3rem;
  background: #aaa;
}

span,
output {
  display: inline-block;
  width: 3rem;
}

label {
  display: block;
}

svg {
  aspect-ratio: 1;
  width: 100vw;
  height: 100vh;
}

#ppo,
#pp1 {
  cursor: pointer;
}
<form id="cockpit">
<label><span>p1x=</span><input id="p1x" type="range" min="0" max="500" step="1" value="70" oninput="setValue(event)" /><output id="p1xo"></output></label>
<label><span>p1y=</span><input id="p1y" type="range" min="0" max="500" step="1" value="20" oninput="setValue(event)" /><output id="p1yo"></output></label>
<label><span>pox=</span><input id="pox" type="range" min="0" max="500" step="1" value="10" oninput="setValue(event)" /><output id="poxo"></output></label>
<label><span>poy=</span><input id="poy" type="range" min="0" max="500" step="1" value="40" oninput="setValue(event)" /><output id="poyo"></output></label>
<label><span>angle=</span><input id="angle" type="range" min="-180" max="180" step="1" value="0" oninput="setValue(event)" /><output
    id="angleo"></output></label>
<label><span>length=</span><input id="length" type="range" min="0" max="500" step="1" value="0" oninput="setValue(event)" /><output
    id="lengtho"></output></label>
  </form>
  <svg id="svg">
<path stroke-width="3" stroke="#00f8" id="ppp" d="M10,100L300,20"></path>
<path id="pp1" d="M300,20m-5,0a5,5,0,1,1,10,0,5,5,0,1,1-10,0" onpointerdown="lock(event)" onpointerup="unlock(event)" onpointermove="move(event)"></path>
<path id="ppo" d="M10,100m-5,0a5,5,0,1,1,10,0,5,5,0,1,1-10,0" onpointerdown="lock(event)" onpointerup="unlock(event)" onpointermove="move(event)"></path>
  </svg>

Как видно, тут нет ничего сложнее геометрии 7 класса. Нет никаких логарифмов, производных и интегралов. Только синус, косинус, арктангенс и теорема Пифагора.

Path data

Path представляют из себя последовательность сегментов трех видов: линейных (L, l, H, h, V, v, Z, z), квадратичных (Q, q, T, t) и кубических безье (C, c, S, s). Элементы Path в SVG используются повсеместно. Несмотря на поддержку в SVG других элементов группы shape, все shape свободно и даже без расчетов конвертируются в path. Сам path по возможностям, как упоминалось выше, соответствует принципам path в формате TTF с той разницей, что в шрифтах внутренние контуры path, замкнутые в том же направлении, что и внешний контур, закрашивается, а замкнутые с обходом в противоположном от внешнего контура направлении - вырезается. В SVG path либо полностью закрашиваются, либо чередуются - внешний контур всегда закрашивается, четные само-пересечения контура - вырезаются, нечетные - закрашиваются независимо от направления обхода.

Еще есть четвертый тип сегментов - эллиптические дуги, но все рендеры SVG конвертируют их в безье. Один сегмент дуги может быть конвертирован в 1-4 кубические безье. Для приблизительного представления любой дуги нужны максимум 4 сегмента кубических безье. Можно опробовать разные параметры дуг в примере ниже.

const circle = (x, y, r, c) => {
  let s = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  s.setAttribute('cx', x);
  s.setAttribute('cy', y);
  s.setAttribute('r', r);
  s.setAttribute('fill', c);
  document.querySelector('svg').appendChild(s);
};

const line = (x1, y1, x2, y2, c) => {
  let s = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  s.setAttribute('x1', x1);
  s.setAttribute('y1', y1);
  s.setAttribute('x2', x2);
  s.setAttribute('y2', y2);
  s.setAttribute('stroke', c);
  s.setAttribute('stroke-width', 1);
  document.querySelector('svg').appendChild(s);
};

// Функция преобразования SVG дуги в кубические кривые Безье
function arc2cubic([sx, sy], [ex, ey], [rx, ry], ad = 0, lf = 0, sf = 0) {
  
  const { acos, cos, sin, tan, hypot, sign, min, max, abs, ceil, PI } = Math;
  // Нужна только положительная часть от радиусов
  rx = abs(rx); ry = abs(ry);
  if ((sx == ex && sy == ey) || !(rx = abs(rx)) || !(ry = abs(ry))) return;

  // Угол поворота радиусов в радианах
  let ar = ((ad % 360) / 180) * PI;

  // Вычисляем синус и косинус угла
  let ca = cos(ar);
  let sa = sin(ar);

  // рассчитываем вектор до средней точки между стартовой и конечной
  let dx2 = (sx - ex) / 2.0;
  let dy2 = (sy - ey) / 2.0;

  // поворачиваем вектор
  let x1 = ca * dx2 + sa * dy2;
  let y1 = -sa * dx2 + ca * dy2;

  // Квадраты сторон вектора
  let x1_sq = x1 ** 2;
  let y1_sq = y1 ** 2;

  // Квадраты радиусов
  let rx_sq = rx ** 2;
  let ry_sq = ry ** 2;

  let radiisum = x1_sq / rx_sq + y1_sq / ry_sq;
  // если радиусов не хватает
  if (radiisum > 0.99999) {
// Масштаб
let scale = radiisum ** 0.5 * 1.00001;
rx *= scale;
ry *= scale;
// квадраты радиусов пересчитаем тоже
rx_sq = rx ** 2;
ry_sq = ry ** 2;
  }

  // Коэффициент трансформации
  let k =
(lf == sf ? -1 : 1) * 
max(0, (rx_sq * ry_sq - rx_sq * y1_sq - ry_sq * x1_sq) / (rx_sq * y1_sq + ry_sq * x1_sq)) ** 0.5;

  let cx1 = k * ((rx * y1) / ry);
  let cy1 = k * -((ry * x1) / rx);

  // Центр эллиптической дуги
  let cx = (sx + ex) / 2.0 + (ca * cx1 - sa * cy1);
  let cy = (sy + ey) / 2.0 + (sa * cx1 + ca * cy1);

  let ux = (x1 - cx1) / rx;
  let uy = (y1 - cy1) / ry;
  let vx = (-x1 - cx1) / rx;
  let vy = (-y1 - cy1) / ry;

  // Начальный угол
  let start = sign(uy) * acos(ux / hypot(ux, uy));
  let rs = sign(ux * vy - uy * vx);

  // косинус угла поворота
  let rc = (ux * vx + uy * vy) / ((ux ** 2 + uy ** 2) * (vx ** 2 + vy ** 2)) ** 0.5;
  let extent = rc < -1 ? rs * PI : rc > 1 ? 0 : rs * acos(rc);

  // Если поворота нет
  if (extent == 0) return -1;

  if (!sf && extent > 0) {
extent -= 2 * PI;
  } else if (sf && extent < 0) {
extent += 2 * PI;
  }

  extent %= 2 * PI;
  start %= 2 * PI;

  let num = ceil(abs(extent) / (PI / 2));
  let inc = extent / num;
  let len = ((4.0 / 3.0) * sin(inc / 2.0)) / (1.0 + cos(inc / 2.0));
  let segs = [];
  for (let cur = 0; cur < num; cur++) {
// первая контрольная точка
let ang = start + cur * inc;
let [dx, dy] = [cos(ang), sin(ang)];
let cp1 = [dx - len * dy, dy + len * dx];
// вторая контрольная точка
ang += inc;
[dx, dy] = [cos(ang), sin(ang)];
let cp2 = [dx + len * dy, dy - len * dx];
/* конечная точка сегмента */
let ep = [dx, dy];
segs.push([cp1, cp2, ep]);
  }

  // Растягиваем безье
  return segs.map(pts => pts.map(([x, y]) => [
  cx + x * rx * ca - y * ry * sa, cy + y * ry * ca + x * rx * sa,       ])
  );
}

let $ = sel => document.querySelector(sel);
let p = n => parseFloat(n);
let f = value => p(value).toFixed(0).padStart(4);
let d = sel => f($(sel).value);

function updateAll() {

  let arc_path =
'M' + d('#start_x') + ',' + d('#start_y') +
' A' + d('#rx') + ',' + d('#ry') + ' ' + d('#angle') +
' ' + ($('#large').checked ? '1' : '0') + ($('#sweep').checked ? '1' : '0') + ' ' + d('#end_x') + ',' + d('#end_y');

  $('#arc').setAttribute('d', arc_path);

  $('#path_arc').innerText = `<path d="${arc_path}" />`.replace(/\s+/g, ' ').replace(/, +/g, ',');

  let segments = arc2cubic(
[p($('#start_x').value), p($('#start_y').value)],
[p($('#end_x').value), p($('#end_y').value)],
[p($('#rx').value), p($('#ry').value)],
p($('#angle').value),
$('#large').checked ? 1 : 0,
$('#sweep').checked ? 1 : 0
  );

  let output = ['M ' + f($('#start_x').value) + ',' + f($('#start_y').value),];

  output.push(segments.map(pts => ['\n C', ...pts.map(([x, y]) => [f(x), f(y)])].join(' ')).join(' '));

  $('#curve').setAttribute('d', output.join(' '));

  $('#path_curve').innerText = '<path d="' + output.join(' ') + '\n" />'.replace(/, /g, ',');

  let first_dot = `M${parseFloat($('#start_x').value)},${parseFloat(
$('#start_y').value
  )}m-5,0a5,5 0 1 1 10,0 5,5 0 1 1-10 0z`;

  document
.querySelector('#curve_dots')
.setAttribute(
  'd',
  first_dot + segments.map(([, , [epx, epy]]) => `M${epx},${epy}m-5,0a5,5 0 1 1 10,0 5,5 0 1 1-10 0z`).join('')
);

  $('#control_dots').setAttribute(
'd',
segments
  .map(([cp1, cp2]) => [cp1, cp2].map(([x, y]) => `M${x},${y}m-5,0a5,5 0 1 1 10,0a5,5 0 1 1-10,0z`))
  .flat()
  .join('')
  );


  let lines = [];
  let bp = [parseFloat($('#start_x').value), parseFloat($('#start_y').value)];
  for (let i = 0; i < segments.length; i++) {
let [cp1, cp2, ep] = segments[i];
lines.push([bp, cp1].flat());
lines.push([cp2, ep].flat());
bp = ep;
  }

  document
.querySelector('#lines')
.setAttribute('d', lines.map(([x1, y1, x2, y2]) => `M${x1},${y1}L${x2},${y2}`).join(''));

  let h = (...e) => e.map(s => $(s).classList.add('hidden'));
  let _h = (...e) => e.map(s => $(s).classList.remove('hidden'));

  ($('#hide_arc').checked ? h : _h)('#path_arc', '#arc');
  ($('#hide_curve').checked ? h : _h)('#path_curve', '#curve');
  ($('#hide_control_dots').checked ? h : _h)('#control_dots', '#lines');
  ($('#hide_curve_dots').checked ? h : _h)('#curve_dots');
}

$('#cockpit').addEventListener('input', e => updateAll());

updateAll();
#cockpit {
  position: fixed;
  right: 0;
  top: 0;
  border: 1px solid #000;
  padding: .3rem;
  background: #ccc;

  span {
display: inline-block;
width: 3rem;
  }

  #path_arc {
color: #080;
  }

  #path_curve {
color: #f00;
  }

  #path_arc,
  #path_curve {
font-weight: bold;

&.hidden {
  color: #888;
  text-shadow: -1px -1px 1px #fff;
}
  }
}

header {
  top: 1rem;
  left: 50%;
  transform: translate(-50%, 0);
  position: fixed;
  font-size: 1.8rem;
  padding: 1rem;
  background: #ccc;
  border: 1px solid #000;
  font-weight: bold;
}

#arc {
  stroke: #080;
}

#curve {
  stroke: #f008;
}

#arc,
#curve {
  stroke-width: 3px;

  &.hidden {
display: none;
  }
}

#curve_dots {
  fill: #000;
}


#control_dots {
  fill: #00f;
}

#lines {
  stroke: #00f;
  stroke-width: 1px;
}

#curve_dots,
#control_dots,
#lines {
  &.hidden {
display: none;
  }
}

body {
  overflow: hidden;
}

svg {
  aspect-ratio: 1;
  width: 100vw;
  height: 100vh;
}

label {
  user-select: none;
}

pre {
  margin: 0;
}

h3 {
  margin: 0 auto;
  text-align: center;
}

summary>pre {
  display: inline;
}
<svg viewBox="150 200 500 200">
  <path id="arc" fill="none" stroke="#000" d="m200 200 a 300 100 80 1 1 50 10" />
  <path id="curve" fill="none" stroke="#000" d="
  M  -200.00,  200.00 
  C  -255.96,  154.00  -307.95,    8.95  -321.14, -137.96 
  C  -334.33, -284.86  -304.56, -387.39  -251.79, -376.84 
  C  -199.01, -366.28  -139.84, -245.97  -113.91,  -96.51 
  C   -87.99,   52.94  -103.48,  184.50  -150.00,  210.00" />
  <path id="lines" d="" />
  <path id="curve_dots" d="" />
  <path id="control_dots" d="" />
</svg>
<form id="cockpit">
  <div>
<label> <span>start_x=</span> <input id="start_x" type="number" value="230" /></label>
<label> <span>start_y=</span> <input id="start_y" type="number" value="410" /></label>
  </div>
  <div>
<label> <span>rx=</span> <input id="rx" type="number" value="150" /></label>
<label> <span>ry=</span> <input id="ry" type="number" value="75" /></label>
  </div>
  <div>
<label> <span>angle=</span> <input id="angle" type="number" value="80" /></label>
Flags:
<label> <input id="large" type="checkbox" checked /> <span>large</span></label>
<label> <input id="sweep" type="checkbox" checked /> <span>sweep</span> </label>
  </div>
  <div>
<label> <span>end_x=</span> <input id="end_x" type="number" value="260" /></label>
<label> <span>end_y=</span> <input id="end_y" type="number" value="420" /></label>
  </div>

  <details>
<summary>
  <pre id="path_arc"></pre>
</summary>
<pre id="path_curve">
</pre>
  </details>
  <label> <input id="hide_arc" type="checkbox" /> hide arc </label>
  <label> <input id="hide_curve" type="checkbox" /> hide curve </label>
  <label> <input id="hide_control_dots" type="checkbox" /> hide control dots </label>
  <label> <input id="hide_curve_dots" type="checkbox" /> hide curve dots </label>
</form>

Так как дуга довольно просто конвертируется в кубические безье, все редакторы с этими безье и работают. Алгоритмов преобразования безье в дугу обратно нет. Это можно сравнить с извлечением пароля из хэша sha-512. Поэтому, если вы видите в SVG-файле использование эллиптических дуг - это будет служить маркером того, что файл собирался вручную.

Для трансформации Path мы должны понимать, что path - это последовательность точек. Выбрав точку центра (вращения), мы можем производить трансформации со всеми точками path - можно пропорционально удалять, приближать, вращать, искривлять элементы. Для трансформации элемента необходимо всего лишь трансформировать каждую его точку. И, если для самих трансформаций точек, указанных в path data достаточно, для получения нужной формы, то, например, определение точных размеров и поиск пересечений path, которые содержат нелинейные сегменты, может быть не таким очевидным процессом.

Тут пригодятся функции, написанные ниже. Первым аргументом принимают координаты точки начала сегмента безье. Далее идут координаты, указанные в самом сегменте: в квадратичных безье - 2 точки, в кубических - 3 точки. Последний аргумент - необходимое количество сегментов (не обязательный аргумент). В результате функция возвращает массив координат точек. Размер массива точек будет на один элемент больше, чем заданное количество под-сегментов. Если количество под-сегментов не задано, то функция возвращает 11 координат точек.

const quadraticPoints = ([p0x, p0y], [p1x, p1y], [p2x, p2y], T = 10) => {
  const x = t => (1 - t) ** 2 * p0x + 2 * (1 - t) * t * p1x + t ** 2 * p2x;
  const y = t => (1 - t) ** 2 * p0y + 2 * (1 - t) * t * p1y + t ** 2 * p2y;
  return Array.from({ length: T + 1 }).map((_, t) => [x(t / T), y(t / T)]);
};

const cubicPoints = ([p0x, p0y], [p1x, p1y], [p2x, p2y], [p3x, p3y], T = 10) => {
  const y = t => (1 - t) ** 3 * p0y + 3 * (1 - t) ** 2 * t * p1y + 3 * (1 - t) * t ** 2 * p2y + t ** 3 * p3y;
  const x = t => (1 - t) ** 3 * p0x + 3 * (1 - t) ** 2 * t * p1x + 3 * (1 - t) * t ** 2 * p2x + t ** 3 * p3x;
  return Array.from({ length: T + 1 }).map((_, t) => [x(t / T), y(t / T)]);
};

Далее с каждым под-сегментом можно работать, как с обычной линией.

Чтобы найти пересечение двух линий, можно воспользоваться функцией ниже. Функция в аргументах принимает координаты четырех точек и возвращает либо координаты точки пересечения двух линий либо null.

const intersectPoint = ([ax, ay], [bx, by], [cx, cy], [dx, dy]) => {
  const d = (ax - bx) * (dy - cy) - (ay - by) * (dx - cx);
  const [ta, tb] = [
    ((ax - cx) * (dy - cy) - (ay - cy) * (dx - cx)) / d,
    ((ax - bx) * (ay - cy) - (ay - by) * (ax - cx)) / d,
  ];
  return ta >= 0 && ta <= 1 && tb >= 0 && tb <= 1 ? [ax + ta * (bx - ax), ay + ta * (by - ay)] : null;
}

Получив точки на кривых в достаточном количестве, мы можем применять функцию пересечения двух линий для каждого отрезка безье и линии, или, например, для каждых отрезков двух разных безье. Найденные точки безье можно использовать, например, для определения минимальных расстояний к другим элементам используя Math.hypot().

Если нужно найти факт пересечения, то можно изначально производить с небольшим количеством под-сегментов, и если по условиям пересечение найдено, можно увеличить количество точек и найти точку пересечения точнее. Для получения более-менее приемлемых результатов возможно подойдет даже 10 отрезков. Чем меньше отрезков - тем меньше вычислений. Больше отрезков - точнее результат.

Работа функций продемонстрирована ниже.

const circle = ([x, y], r, c) => {
  let s = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  s.setAttribute('cx', x);
  s.setAttribute('cy', y);
  s.setAttribute('r', r);
  s.setAttribute('fill', c);
  document.querySelector('svg').appendChild(s);
};

const cubic = ([p0x, p0y], [p1x, p1y], [p2x, p2y], [p3x, p3y], T = 60) => {
  const y = t => (1 - t) ** 3 * p0y + 3 * (1 - t) ** 2 * t * p1y + 3 * (1 - t) * t ** 2 * p2y + t ** 3 * p3y;
  const x = t => (1 - t) ** 3 * p0x + 3 * (1 - t) ** 2 * t * p1x + 3 * (1 - t) * t ** 2 * p2x + t ** 3 * p3x;
  return Array.from({ length: T + 1 }).map((_, t) => [x(t / T), y(t / T)]);
};


const quadratic = ([p0x, p0y], [p1x, p1y], [p2x, p2y], T = 60) => {
  const x = t => (1 - t) ** 2 * p0x + 2 * (1 - t) * t * p1x + t ** 2 * p2x;
  const y = t => (1 - t) ** 2 * p0y + 2 * (1 - t) * t * p1y + t ** 2 * p2y;
  return Array.from({ length: T + 1 }).map((_, t) => [x(t / T), y(t / T)]);
};

const intersect = ([ax, ay], [bx, by], [cx, cy], [dx, dy]) => {
  const d = (ax - bx) * (dy - cy) - (ay - by) * (dx - cx);
  const [ta, tb] = [
((ax - cx) * (dy - cy) - (ay - cy) * (dx - cx)) / d,
((ax - bx) * (ay - cy) - (ay - by) * (ax - cx)) / d,
  ];
  return ta >= 0 && ta <= 1 && tb >= 0 && tb <= 1 ? [ax + ta * (bx - ax), ay + ta * (by - ay)] : null;
}

let cubic_points = cubic([-300, -300], [400, -400], [-400, 400], [300, 300], 10);
cubic_points.forEach(p => circle(p, 5, '#00f'));

let quadratic_points = quadratic([-200, -200], [400, 0], [-200, 200], 10);
quadratic_points.forEach(p => circle(p, 5, '#00f'));

let line_points = [[-400, 400], [400, -300]];

for (let i = 0; i < 10; i++) {
  for (let j = 0; j < 10; j++) {
let cubic_quadratic_intersect = intersect(cubic_points[i], cubic_points[i + 1], quadratic_points[j], quadratic_points[j + 1]);
if (cubic_quadratic_intersect) {
  circle(cubic_quadratic_intersect, 10, '#f00');
}
  }
  let line_cubic_intersect = intersect(line_points[0], line_points[1], cubic_points[i], cubic_points[i + 1]);
  if (line_cubic_intersect) {
circle(line_cubic_intersect, 10, '#f00');
  }
}

for (let j = 0; j < 10; j++) {
  let line_quadratic_intersect = intersect(line_points[0], line_points[1], quadratic_points[j], quadratic_points[j + 1]);
  if (line_quadratic_intersect) {
circle(line_quadratic_intersect, 10, '#f00');
  }
}
path {
  fill: none;
  stroke-width: 5px;
}

#cubic_curve {
  stroke: #000;
}

#quadratic_curve {
  stroke: #000;
}

#line {
  stroke: #000;
}

svg {
  max-width: 100vw;
  max-height: 100vh;
}
<svg viewBox="-500 -500 1000 1000">
  <path id="cubic_curve" d="M -300,-300 C 400,-400 -400,400 300,300" />
  <!-- <path id="control_lines" d="M-300,-300 L 400,-400 M 300,300 L -400,400"/> -->
  <path id="quadratic_curve" d="M -200,-200 Q 400,0 -200,200" />
  <path id="line" d="M-400,400 L 400,-300" />
</svg>

Последний пример без интерактивных элементов. Можно подставить свои значения вручную. Синими точками отмечены рассчитанные точки, разбивающие безье на под-сегменты. Красными точками отмечены найденные пересечения.

Обратите внимание, под-сегменты не равной длины. В местах сильных изгибов концентрация синих точек плотнее.

Этих знаний должно быть достаточно для решения любых подобных этой задач.

→ Ссылка