Да, поэтому дописал слово “например”. Но это не существенно все.
На однозадачных можно вместо задержки вызывать свой main_tick(), штоп управление отдать всем по чуть-чуть. Ну или хотя бы диодиком моргать, чтобы было понятно, где мы сидим
Имхо, Раневская бы поставила это действие рядом с хоккеем на траве.
Для того, чтобы быть совсем уж уверенным, на Вашу идеализированную математику нужно навесить реальные люфты и упругость элементов конструкции, реальный разброс параметров, реальную нелинейность зависимостей, а также реальные силы и моменты, причем следует сравнить последние, полученные из расчета, с реальными возможностями сервоприводов и с условиями прочности конструкции.
Нашел логическую ошибку - метод parse неправильно определял факт отсутствия запрашиваемого параметра. Исправлено. Кроме того заменил все “служебные” символы (теперь “{ } & ~”) так как предыдущие слишком часто хочется использовать в передаваемом тексте.
class cWirelessLink {
public:
// открытие порта, проверка работоспособности модуля
String start(HardwareSerial *_serial, uint8_t _setPin) {
serial = _serial;
setPin = _setPin;
String log = "Methood start (pin " + String(setPin) + " might been connected to HC-12 SET pad)";
pinMode(setPin, OUTPUT); // конфигурация пина активации режима AT
serial -> begin(115200); // поднятие порта на 115200
delay(500);
digitalWrite(setPin, LOW); // активация режима AT команд
// далее идет отправка AT команд в модуль и получение результата
// по datasheet так и не стало понятно, перед отправкой нужны задержки 40мс или после, или между
// поэтому используются задержки по 40мс между каждыми взаимодействиями с модулем
// отправка запроса на проверку подключения
delay(40);
serial -> print("AT\r");
delay(40);
if (serial -> readString().indexOf("OK") != -1) {
// подключение удалось сразу на 11520
// считаем что модуль уже сконфигурирован
// однако это лишь допущение
// так как не проводится проверка других параметров
log += "\r\nSucessfully connected on 115200";
serial -> print("AT+RX\r"); // запрос конфигурации
delay(40);
log += "\r\n" + serial -> readString();
} else {
log += "\r\n Failed to connect on 115200, trying configurate on 9600"; // походу подключен новый модуль, попытка настроить на 9600
serial -> end(); // закрытие порта
serial -> begin(9600); // поднятие порта на стандартнх 9600
delay(40);
serial -> print("AT+B115200\r"); // отправка конфигурации - скорость порта
delay(40);
serial -> print("AT+FU1\r"); // отправка конфигурации - режим
delay(40);
serial -> print("AT+P3\r"); // отправка конфигурации - мощность
delay(40);
digitalWrite(setPin, HIGH); // выход из режима AT команд
delay(80); // задержка для применения настроек модулем по datasheet 80мс
serial -> flush(); // очистка буфера порта (там результат применения конфигов, возможно прочий мусор)
serial -> end(); // закрытие порта
serial -> begin(9600); // поднятие порта на вожделенных 115200
digitalWrite(setPin, LOW); // запуск режима AT команд
delay(40);
serial -> print("AT\r"); // отправка запроса на проверку подключения
delay(40);
if (serial -> readString().indexOf("OK") != -1) {
log += "\r\nSucessfully connected on 115200 after configurate"; // подключение после настройки удалось
serial -> print("AT+RX\r"); // запрос конфигурации
delay(40);
log += "\r\n" + serial -> readString(); // вывод конфигурации
} else log += "\r\nFailed to configure HC-12 wia AT commands, check physical connection"; // попытка настройки не удалась - проверяем провода,
// подаем питание закоротив SET на GND (отключив от Arduino!!!)
}
// serial -> print("AT+DEFAULT\r"); delay(40); // сброс модуля на заводские (еси нужно потестить работу метода)
digitalWrite(setPin, HIGH); // выход из режима AT команд
delay(80);
serial -> flush(); // очистка буфера порта
return log;
}
// все сообщения начинаются с { и заканчиваются }
// сразу после { следует строковое представление числа - хеш суммы сообщения
// все элементы в сообщении разделяются &
// имя элемента от его значения отделяется ~
// использование { } & ~ в именах и значениях элементов не допускается,
// проверки на эти вхождения в методах add нет! (можно добавить, но целесообразно ли?)
// добавление элемента к предназначенному к отправке сообщению, вернет true только если было добавлено
bool add (String name, String value) {
if (outcoming.length() + value.length() + 1 < byteTreshold) {
// элемент будет добавлен, только если это не приведет к превышению допустимого размера сообщения,
// а также если элемента с таким же именем еще нет в сообщении
if (outcoming.indexOf("&" + name + "~") != -1) return false;
outcoming += name + "~" + value + "&";
return true;
} else return false;
}
// поиск элемента по его имени в принятом сообщении, возврат значения, либо пустой строки если элемент не нашелся
String parse (String name) {
int16_t pos = recieved.indexOf("&" + name + "~");
if (pos > -1) {
pos += name.length() + 2;
return (pos > -1) ? recieved.substring(pos, recieved.indexOf("&", pos)) : "";
} else return "";
}
// отправка сообщения, вернет true если не превышен максимальный размер сообщения
// строка, содержащая подготовленное сообщение очищается в любом случаае
bool send () {
if (outcoming.length() <= byteTreshold) {
// первая "&" считается частью сообщения (упрощает извлечение первого элемента в сообщении методом parse)
outcoming = "&" + outcoming;
// обрамляется в {}, сразу после стартового символа помещается хеш
serial -> print("{" + hash(outcoming) + outcoming + "}");
// запоминается момент последней отправки сообщения
lastTransmittingTime = millis();
outcoming = "";
return true;
} else {
outcoming = "";
return false;
}
}
// возвращает время в милисекундах, прошедшее с момента последней состоявшейся отправки исходящего сообщения
uint16_t silence () { return millis() - lastTransmittingTime; }
// вернет true, когда получит корректное сообщение целиком
// что будет поводом вызвать parse() для получения значений ожидаемых элементов
bool get() {
// принимаемый символ
char character = '_';
// счетчик принятых символов
uint16_t count = 0;
// время последнего успешного приема символа
uint32_t last = millis();
// ожидание символа начала сообщения
while (character != '{') {
if (serial -> available()) {
character = (char)serial -> read();
last = millis();
count++;
if (count > startByteTreshold) return false; // может быть прервано по превышению количества принятых бит
} else if (millis() - last > timeTreshold) return false; // может быть прервано по времени "молчания" порта
}
// в character будет "<" можно было присвоить константой, или вовсе не записывать,
recieved = "";
recieved += character; // но так нагляднее отлаживать
// ожидание символа конца сообщения, аккумуляция принимаемых данных
while (character != '}') {
if (serial -> available()) {
character = (char)serial -> read();
recieved += character;
last = millis();
count++;
if (count > byteTreshold) {
// может быть прервано по превышению количества принятых бит
recieved = "";
return false;
}
} else if (millis() - last > timeTreshold) {
// может быть прервано по времени "молчания" порта
recieved = "";
return false;
}
}
// это последнее место в коде, где сообщение можно посмотреть/вывести целиком
// сообщение получено, необходимо выделить хеш
String declared = recieved.substring(1, recieved.indexOf("&"));
// из сообщения удаляеются хеш, символы начала и конца
recieved.remove(0, recieved.indexOf("&"));
recieved.remove(recieved.indexOf("}"));
// если вычисленый методом класса хеш равен полученному в сообщении, то элементы полученного сообщения остаются в памяти
if (declared == hash(recieved)) return true;
else {
// если хеш не сошелся то сообщение очищается
recieved = "";
return false;
}
}
private:
// ссылка на используемый HC-12 serial порт
HardwareSerial *serial;
// номер вывода, к которому подключен вход SET платы HC-12
uint8_t setPin;
// максимально допустимая длина сообщения
const uint16_t byteTreshold = 512;
// максимальное количество перебираемых бит данных при попытке принять сообщение
// (длина + максимальное кол-во символов хеш + открывающий и закрывающий тег) * 2
const uint16_t startByteTreshold = (byteTreshold + 5 + 2) * 2;
// количество миллисекунд неполучения ответа от serial, по достижении которых происходит сброс при приеме данных
const uint16_t timeTreshold = 10;
// время последней состоявшейся отправки сообщения (не путать с успешным приемом на стороне получателя)
uint16_t lastTransmittingTime = 0;
// строка для хранения принятого сообщения (в случае неудачной попытки будет пустой)
String recieved = "";
// строка для хранения подготавливаемых к отправке данных (опустошается после любой попытки отправки)
String outcoming = "";
// функция вычисления хеш суммы
String hash (String& data) {
// в качестве хеша используем XOR по четным и нечетным символам (байтам) строки
uint8_t odd = 0, even = 0;
for (uint8_t i = 0; i < data.length(); i++)
if (i % 2) odd ^= data[i]; // нечетные
else even ^= data[i]; // четные
// четная сумма занимает старший байт в итоговом числе
return String((uint16_t) even * 256 + odd);
}
};
способ использования не поменялся.
Если кому интересно, на данный момент реализовал пару менюшек и скролл по ним энкодером. Так же “особый” экран для калибровки сервопривода: позволяет энкодером управлять сервой напрямую, задавать текущее положение в качестве 1 из 2 крайних точек. Это у меня пока что сериал монитор компа вместо экрана пульта, и подключен только энкодер и один аналоговый джойстик с кнопкой.
Пока не знаю, как упихать менюшки в класс, но возможно и не буду, т.к. выбор чуть ли не каждого пункта требует своей, особой обработки действия.
Насколько я себе представляю, эти “варианты” отличаются исключительно тем, где именно будут проводиться вычисления. В одном случае считает сам МК в процессе движения, а во втором - ПК - заранее, после чего складывает результаты расчета в массивы, размещаемые в PROGMEM, откуда МК их извлекает при движении.
А вот в этом случае расчет на ПК выглядит предпочтительнее: есть возможность сгладить эти рывки. Хотя бы итерациями.
Это как?
12 серв при 6 ногах: по 2 сервы на ногу.
Т.е. у ноги только две степени свободы. Этого недостаточно для вменяемого передвижения.
Естественно.
Только причина этому не
а ошибка при проектировании: по 2 сервы на ногу явно недостаточно.
Почти да. Просто с компа дискретные состояния будут покадрово, а в математике на МК все же параметрические кривые, которые более точно определят текущие координаты в зависимости от дельты времени.
одно другому не мешает. С плохими сервами хоть 3dof делай - результат все равно будет обескураживающий

одно другому не мешает. С плохими сервами хоть 3dof делай - результат все равно будет обескураживающий
Ну да, с неработающими сервами ошибки проектирования становятся совсем незаметными.

а также реальные силы и моменты, причем следует сравнить последние, полученные из расчета, с реальными возможностями сервоприводов и с условиями прочности конструкции
по этому поводу, могу только сказать, что когда были sg90, у меня длины плеча 2 и плеча 3 были 95 и 135 мм соответственно. И оно ходило на хлипкой раме из листового плексигласа. Прям видно было как ему тяжело, но ходило. И реальные движения вполне соответствовали вычисленным. Сейчас у меня плечи 44 и 70 мм, т.е. грубо вдвое короче, что в том числе уменьшает погрешность установки законцовки. Есть поддержка осей вращения (втулки с противоположной стороны выхода вала сервопривода на каждом сочленении), цифровые сервоприводы с хорошей точностью и как минимум втрое большим моментом. Подвоха не вижу

а ошибка при проектировании: по 2 сервы на ногу явно недостаточно.
Нет. Люди по всему миру манстрячат ходунов на 2,3,4,8,12,18 сервах. Это уже стандарты. По ногам - двуноги, четвероноги, шестиноги.
… конечно sg90 лучше ограничивать числом в 8 т.е. квадропод ещё нормально.
Не видел паука на 8 ногах, 7, 5 - не ходовые числа.

Не видел паука на 8 ногах
Дело в том, что пауков великое множество, более 40 тысяч разновидностей, и они могут сильно отличаться друг от друга, но 8 ног — обязательный признак паука
Это к тому, что гекса иногда зовут паук. Но для паука надо 8 ног, а не 6.
… во, какая была ходовая у моего варианта.

что б народ знал какие сервы надо
пока не могу пруффнуть (просто времени жалко видео записать и выложить). В общем, новые сервы, которые скорее всего оригинал, и правда огонь. Точность и линейность угла поворота относительно входящего ШИМ на глаз достойная, для хоббийного робота хватит однозначно. И порадовал момент: при питании 6 вольт я за 1 лапу поднял робота, на этой лапе 2я серво была включена и выставлена на 90 градусов. В общем, она держит вес всего остального робота без установленных плат и аккумов (корпус, 5 лап и 16 сервоприводов, что тоже весит прилично), и при этом даже не нагревалась в течение минуты.
Из грустного, при маловероятно достижимых нагрузках, а именно вращение против шерсти, с большей силой чем может выдать серва, удалось выжать потребление в почти 1 ампер. Придется использовать бек на 10А, а так хотелось более компактный на 5А.
Гексапод пошёл уже, или не побежал ещё?
привет, еще нет. Я продвигаюсь, но медленно, другие дела навалились

Не, мне это трудно понять. Опишите качественно, если не сложно, последовательность движений групп лап при перемещении боком.
Привет! Я помню ваш вопрос. У меня как раз дошло дело до кинематики при прямолинейном движении, и чтобы не тронуться умом окончательно, прошивая раз за разом нерабочие варианты в контроллер, я накидал анимированную трехмерную html демку, в которой и обкатал алгоритм. Можете заценить. Поскольку .html тут прикладывать нельзя, могу только ее исходник привести:
<HTML>
<HEAD>
<TITLE>Bhla Bhla</TITLE>
<meta charset="utf-8">
<style>
div#circle {
position: relative;
display: block;
border-radius: 50%;
background: linear-gradient(#fff, transparent 1px), linear-gradient(90deg, #fff, #bdb 1px);
background-size: 10mm 10mm;
background-position: 0 0;
transform: perspective(140mm) rotateX(60deg) translateZ(-8mm);
transform-style: preserve-3d;
perspective-origin: center bottom;
}
div#circle>div {
position: absolute;
display: block;
width: 4mm;
height: 4mm;
font-size: 3mm;
line-height: 4mm;
font-weight: bold;
border: 1mm solid #777;
color: #777;
border-radius: 3mm;
margin: -3mm 0 0 -3mm;
text-align: center;
background: #fff;
}
div#circle>div.floating {
border-color: #37f;
color: #37F;
}
div#circle>div.landed {
border-color: #f33;
color: #f33;
}
div#circle>div#S {
border-color: #fa3;
color: #fa3;
}
div#joystick {
display: inline-block;
width: 70mm;
height: 70mm;
background: radial-gradient(circle at center, #da0 0, #da0 2.5mm, transparent 2.5mm),
repeating-linear-gradient(90deg, transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%),
repeating-linear-gradient(transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%), #333;
}
</style>
</HEAD>
<BODY>
<div id="circle">
<div id="A" class="landed">A</div>
<div id="B" class="landed">B</div>
</div>
<div id="joystick"></div>
<script type="text/javascript">
var right_x = 0;
var right_y = 0;
var bg_x = 0;
var bg_y = 0;
document.getElementById('joystick').onmousemove = function(e) {
var rect = e.target.getBoundingClientRect();
right_x = Math.round((e.clientX - rect.left - rect.width / 2) / rect.width * 2000);
right_y = Math.round((rect.height / 2 - e.clientY + rect.top) / rect.height * 2000);
e.target.style.background = "radial-gradient(circle at " + String(e.clientX - rect.left)+"px "+String(e.clientY - rect.top)+"px, #da0 0, #da0 2.5mm, transparent 2.5mm),"
+ "repeating-linear-gradient(90deg, transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%),"
+ "repeating-linear-gradient(transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%), #333";
}
document.getElementById('joystick').onmouseout = function(e) {
right_x = 0;
right_y = 0;
e.target.style.background = "radial-gradient(circle at center, #da0 0, #da0 2.5mm, transparent 2.5mm),"
+ "repeating-linear-gradient(90deg, transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%),"
+ "repeating-linear-gradient(transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%), #333";
}
function circle(r, dx, dy) {
var circ = document.getElementById("circle");
circ.style.height = r * 2 + "mm";
circ.style.width = r * 2 + "mm";
bg_x = (bg_x + dx) % 10;
bg_y = (bg_y + dy) % 10;
circ.style.backgroundPosition = bg_x + "mm " + bg_y + "mm";
}
function pos (el, x, y, z = 0) {
el.style.left = x + "mm";
el.style.top = y + "mm";
el.style.transform = "translateZ(" + z + "mm)";
}
function hypot (dx, dy) { return Math.hypot(dx, dy); }
function max (x, y) { return Math.max(x, y); }
function abs (x) { return Math.abs(x); }
function min (x, y) { return Math.min(x, y); }
function sqrt (x) { return Math.sqrt(x); }
// МАГИЯ ТУТ
function div (val, divider) { return divider > 0 ? val / divider : 0; }
var max_radius = 35;
var max_height = 30;
// максимальные скорость в мм/с и ускорение в мм/с², отмасштабированные до периода итераций 20мс
var maximum_velocity_value = 100 / 50; // за 1 / 50 секунды
var maximum_acceleration_value = 75 / 2500; // за 1 / 2500 секунды за секунду
// текущий вектор скорости
var current_velocity_dx = 0;
var current_velocity_dy = 0;
// координаты находящейся на поверхности конечности
var landed_limb_x = 0;
var landed_limb_y = 0;
// координаты поднятой конечности
var floating_limb_x = 0;
var floating_limb_y = 0;
// фаза
var phase = true;
function calc() {
// ПОЛУЧЕНИЕ ДАННЫХ С ДЖОЙСТИКА -1000..1000
var joystick_dx = right_x;
var joystick_dy = right_y;
var joystick_dl = hypot(joystick_dx, joystick_dy);
// получение единичного вектора желаемого направления
var setpoint_velocity_cos = div(joystick_dx, joystick_dl);
var setpoint_velocity_sin = div(joystick_dy, joystick_dl);
// получение нормализованного значения отклонения джойстика
var joystick_normalized_value = div(joystick_dl, hypot(min(abs(joystick_dx), abs(joystick_dy)), 1000));
// получение значения желаемой скорости
var setpoint_velocity_value = joystick_normalized_value * maximum_velocity_value;
// преобразование данных джойстика в вектор целевой скорости "setpoint_velocity"
var setpoint_velocity_dx = setpoint_velocity_cos * setpoint_velocity_value;
var setpoint_velocity_dy = setpoint_velocity_sin * setpoint_velocity_value;
// ВЫЧИСЛЕНИЕ ВЕКТОРА СКОРОСТИ ПРИЗЕМЛЕННОЙ КОНЕЧНОСТИ
// текущий вектор скорости "current_velocity" переходит в "setpoint_velocity" с заданным ускорением
// определение вектора из конца "current_velocity" в конец "setpoint_velocity"
var to_setpoint_dx = setpoint_velocity_dx - current_velocity_dx;
var to_setpoint_dy = setpoint_velocity_dy - current_velocity_dy;
var to_setpoint_dl = hypot(to_setpoint_dx, to_setpoint_dy);
if (to_setpoint_dl > maximum_acceleration_value) {
// если длина найденного вектора превышает значение максимального ускорения за итерацию
// вектор ускорения применяется к текущей скорости
current_velocity_dx += div(to_setpoint_dx, to_setpoint_dl) * maximum_acceleration_value;
current_velocity_dy += div(to_setpoint_dy, to_setpoint_dl) * maximum_acceleration_value;
} else {
// длина вектора меньше максимального ускорения за итерацию,
// применение вектора длиной maximum_acceleration_value вызовет перемещение дальше требуемого значения
// поэтому "current_velocity" просто приравнивается к "setpoint_velocity"
current_velocity_dx = setpoint_velocity_dx;
current_velocity_dy = setpoint_velocity_dy;
}
// ВЫЧИСЛЕНИЕ ВЕКТОРА СКОРОСТИ ПОДНЯТОЙ КОНЕЧНОСТИ
var floating_velocity_dx = 0;
var floating_velocity_dy = 0;
// текущая скорость
var current_velocity_value = hypot(current_velocity_dx, current_velocity_dy);
if (current_velocity_value != 0) {
// определение целевой позиции поднятой конечности - на краю окружности в направлении current_velocity
var floating_target_x = div(current_velocity_dx, current_velocity_value) * max_radius;
var floating_target_y = div(current_velocity_dy, current_velocity_value) * max_radius;
// текущее расстояние от центра до позиции приземленной конечности
var center_to_landed_limb_distance = hypot(landed_limb_x, landed_limb_y);
// конечность движется в направлении, обратном движению - вычисляем косинус угла между векторами landed_limb и -current_velocity
var cos = div(landed_limb_x * -current_velocity_dx + landed_limb_y * -current_velocity_dy, center_to_landed_limb_distance * current_velocity_value);
// расстояние - сторона треугольника напротив угла с найденным косинусом, к которому прилегают стороны center_to_landed_limb_distance и max_radius
var landed_to_out_distance = sqrt(center_to_landed_limb_distance * center_to_landed_limb_distance + max_radius * max_radius - 2 * cos * center_to_landed_limb_distance * max_radius);
// определение векора из текущего положения поднятой конечности в ее целевую позицию
var to_floating_target_dx = floating_target_x - floating_limb_x;
var to_floating_target_dy = floating_target_y - floating_limb_y;
var to_floating_target_dl = hypot(to_floating_target_dx, to_floating_target_dy);
// определение значения скорости поднятой конечности, необходимой чтобы успеть переместиться в целевую позицию
var floating_velocity_value = to_floating_target_dl * current_velocity_value / landed_to_out_distance;
// определение вектора скорости поднятой конечности
floating_velocity_dx = div(to_floating_target_dx, to_floating_target_dl) * floating_velocity_value;
floating_velocity_dy = div(to_floating_target_dy, to_floating_target_dl) * floating_velocity_value;
}
// СМЕНА ФАЗ ПРИ ПРИБЛИЖЕНИИ ПРИЗЕМЛЕННОЙ КОНЕЧНОСТИ К КРАЮ ОКРУЖНОСТИ
// не перешагнет ли край окружности при приращении координат на значения вектора скорости?
if (hypot(landed_limb_x - current_velocity_dx, landed_limb_y - current_velocity_dy) >= max_radius) {
// смена роли конечностей и фазы
var temp = landed_limb_x;
landed_limb_x = floating_limb_x;
floating_limb_x = temp;
temp = landed_limb_y;
landed_limb_y = floating_limb_y;
floating_limb_y = temp;
phase = !phase;
}
// ПЕРЕМЕЩЕНИЕ ПО ПОЛУЧЕННЫМ ВЕКТОРАМ СКОРОСТИ
landed_limb_x -= current_velocity_dx;
landed_limb_y -= current_velocity_dy;
floating_limb_x += floating_velocity_dx;
floating_limb_y += floating_velocity_dy;
// ВЫЧИСЛЕНИЕ ВЫСОТЫ ОТ ПОВЕРХНОСТИ ДЛЯ ПОДНЯТОЙ КОНЕЧНОСТИ
// множитель высоты поднятой конечности f(x) = 1 - (x - 1)² где x = 0..1 удаленность поднятой конечности от радиуса
var height_scale = (1 - (floating_limb_x * floating_limb_x + floating_limb_y * floating_limb_y) / (max_radius * max_radius));
// второй множитель - отношение текущей скорости к максимально возможной
height_scale *= current_velocity_value / maximum_velocity_value;
// итоговая высота
var floating_limb_height = height_scale * max_height;
// ОТОБРАЖЕНИЕ
/* актуальное значение скорости
console.log(Math.floor(Date.now() / 1000 % 100) + " velocity: " + Math.round(current_velocity_value*50*100)/100 + "mm/s");/**/
circle(max_radius, -current_velocity_dx, current_velocity_dy);
if (phase) {
pos(A, max_radius + landed_limb_x, max_radius - landed_limb_y);
pos(B, max_radius + floating_limb_x, max_radius - floating_limb_y, floating_limb_height);
A.className = "landed";
B.className = floating_limb_height > 0 ? "floating" : "landed";
} else {
pos(A, max_radius + floating_limb_x, max_radius - floating_limb_y, floating_limb_height);
pos(B, max_radius + landed_limb_x, max_radius - landed_limb_y);
A.className = floating_limb_height > 0 ? "floating" : "landed";
B.className = "landed";
}
}
var timerId = null;
timerId = setInterval(calc, 20);
</script>
</BODY>
</HTML>
скопировать в текстовик, изменить расширение на .html и запустить в браузере.
На самом деле, разумеется, точки будет не две, а шесть, и каждая в своем круге своей конечности. Но у всех четных конечностей (0, 2, 4) будет координата точки А в своем круге, а у нечетных (1, 3, 5) - координата точки В.
Почти самое главное, что кинематика здесь не просто в “попугаях” - максимальная скорость ограничена мм/с, ускорение мм/с² - все эти величины можно найти в коде. Гекса с таким управлением будет плавно разгоняться, тормозить и менять курс в любой ситуации.

Можете заценить.
Здорово, когда хорошо программируешь и математика идёт на ура. Я остановился на переходах-кадрах.
…для иллюстрации превосходства первого над вторым делайте два демонстрационных варианта движения гекса ))…в проморолике.

Здорово, когда хорошо программируешь и математика идёт на ура.
наура это громко сказано. Я так баловался последний раз в универе, когда физику автомобиля с пробуксовками и заносами пытался напрограммировать… а сейчас приходится гуглить, чем синус от косинуса отличается, и что такое скалярное произведение векторов… забылось всё напрочь

Я остановился на переходах-кадрах.
но все равно этим способом многого не получится добиться, результат-то будет, эффект не тот. Первые мои попытки тоже кадровые были. Даже в вопросе “хочу что-то поменять” - будь любезен все кадры перерисовать. Т.ч. если втянет - все равно в геометрию уйдете. Векторные модели не так уж и сложны