Модернизация JavaScript-проекта для моделирования физических экспериментов и роль ИИ в этом
- среда, 11 ноября 2026 г. в 00:00:16
Несколько лет назад с целью изучения языка JavaScript мной была выбрана тема, связанная с симуляцией физических явлений. Поскольку основной задачей было как можно более полное понимание самого языка программирования, было решено отказаться от использования библиотек и фреймворков. Сам проект включал в себя симуляции физических приборов, механических процессов и экспериментов с жидкостями. Подобные явления рассматривались многими на лабораторных работах в средней школе.
Старый программный код можно найти по ссылке. Предыдущая статья доступна здесь. Также доступен работающий сайт со старыми демонстрациями по этому адресу .

Позже я вернулся к развитию своего проекта. Моей целью было добавление новых возможностей, новых симуляций, изменение структуры программы с учетом новых возможностей JavaScript, адаптация под смартфоны, а также улучшение интерфейса. Для модернизации проекта и добавления новых функций требовалось не только сохранить поведение моделей, но и улучшить программную архитектуру.
Подобные изменения при работе в одиночку занимают массу времени. В подобной ситуации пригодились инструменты искусственного интеллекта, которые смогли помочь отрефакторить код и модернизировать проект за гораздо более короткий промежуток времени.
В данной статье рассказано, как менялся проект, как использовался ИИ ChatGPT и Cursor и с какими проблемами пришлось столкнуться.
С обновленным проектом можно ознакомиться здесь. Веб-сайт с демонстрациями доступен по этой ссылке.
Проект представляет собой классическое веб-приложение, в котором используются HTML– страницы, Javascript и CSS файлы без подключения каких-либо сторонних библиотек.
На панели слева можно выбрать интересующую демонстрацию. Посередине находится пространство для сцен, где отображается физический эксперимент. Здесь пользователь имеет возможность наблюдать за поведением объектов или приборов. Можно также влиять на реакцию моделей, например, перетаскивать и подвешивать физические тела. Сами сцены в программе отображаются на холсте Canvas при помощи JavaScript. На панели справа пользователь может менять физические величины и параметры, применяемые при расчетах в формулах.
Каждая сцена для демонстрации физических явлений относится к конкретному разделу физики. В программе представлены эксперименты из разделов: механика, механические колебания, гидростатика.

Сила, воздействующая на тело, является векторной величиной и измеряется в ньютонах. Когда на тело действуют две силы под углом, их результирующая сила вычисляется по формуле:
Пользователь демонстрации в проекте может изменять массы грузов и угол между динамометрами для измерения силы. После перетаскивания и подвешивания тела к динамометрам рассчитывается равнодействующая сила и демонстрируется анимация стрелок динамометров.


Принцип Архимеда: на тело, погружённое в жидкость или газ, действует выталкивающая сила, равная весу объёма жидкости или газа, вытесненного этим телом. Сила Архимеда высчитывается по формуле:
Пользователь в проекте может изменять:(ро) — плотность жидкости или газа (
),
— объём погружённого тела (
) и вес тела. После перетаскивания и подвеса тела к пружинному динамометру подсчитываются объём тела, сила Архимеда, сила тяжести, а также демонстрируется анимация динамометра.


Математический маятник — это материальная точка, подвешенная на невесомой нерастяжимой нити (идеализированная система). Период колебаний — время, за которое маятник совершает одно полное колебание. Вычисляется по формуле:
Пользователь в проекте может изменять — длину нити и начальный угол колебания, а также видеть результаты подсчёта периода и частоты колебаний.


Затухающими колебаниями пружин называют свободные колебания, энергия которых под воздействием сил сопротивления уменьшается с течением времени. Для подсчета частоты затухающих колебаний применяется формула:
Частота измеряется в герцах () и используется только для отображения информации пользователю. Для реальной реализации затухающих колебательных движений в коде применяется дифференциальное уравнение движения.
Пользователь сайта может изменять: — жёсткость пружины(
),
— массу груза (
),
— коэффициент затухания(
). После перетаскивания и подвешивания тела к пружине подсчитывается частота колебаний, а также демонстрируется анимация затухающих колебаний пружины.

Центральной страницей проекта является index.html
В предыдущей версии index.html подключал объекты JS - кода и скрипты напрямую через <script src> без использования модулей.

В старой версии проекта для организации архитектуры использовался классический модульный паттерн, который разбивал js-код на объекты const и группировал функции, другие элементы по назначению.
Логика экспериментов по необходимой физической теме осуществлялась в следующих объектах: appliancesDemo, archimedesPrincipleDemo, forcesbyAngleDemo, pendulumDemo (например, const archimedesPrincipleDemo = { ... }, const pendulumDemo = { ... } и. т. д.). Внутри этих const-объектов происходила обработка параметров, поступающих от пользователя, последующая компоновка приборов, группировка функций и отрисовка сцены на холсте canvas.
За группировку логики по перетаскиванию по экрану и группировку функций общего назначения отвечали объекты: dragRendering, contextLayout.
При таком подходе требуется подключение используемых js-файлов в HTML-коде через script src. Например, приходилось подключать все используемые js-файлы в центральном месте index.html, что требовало соблюдения порядка последовательности файлов:
<script src="./js/appMethods.js"></script> <script src="./js/dragging.js"></script> <script src="./js/topicsRender/forcesbyAngleRender.js"></script>
Предыдущий подход разбиения архитектуры на блоки const, с сгруппированным функционалом является устаревшим. Это создавало неудобства при поддержке, при внесении изменений или расширении проекта. Кроме того, при применении объектов const неудобно создавать приватные методы, поля для скрытия данных от внешнего использования.
Для визуализации и создания физических приборов (динамометров, линеек, пружин и.т.д.), которые могут применяться многократно, но с разными параметрами использовался метод функций – конструкторов. Например, абстрактное устройство для визуализации циферблата со стрелкой без чисел:
function Appliance(options = {}) { // Значения по умолчанию Object.assign(this, { … }, options); … }
Прибор для измерения силы — динамометр — строился на основании этого абстрактного устройства с добавлением новых свойств. Для этого применялось прототипное наследование. Пример создания стрелочного динамометра и наследование от Appliance:
function Dynamometer(options) { Appliance.call(this, options); // вызов базового конструктора ... } Dynamometer.prototype = Object.create(Appliance.prototype); // организация цепочки прототипов Dynamometer.prototype.constructor = Dynamometer; Dynamometer.prototype = { draw: function () { ... ); }
В современном JavaScript чаще используется подход использования модулей (import/export). Это позволяет экспортировать классы, функции для использования в других JS – файлах. В классах можно организовать код более структурировано и понятно, организовать поддержку приватных полей с помощью встроенного синтаксиса. Для использования экспортированных объектов достаточно прописать , что происходит их импорт на уровне JavaScript кода.
Иерархия наследования при использовании классов решается более понятным способом, чем с использованием функций-конструкторов. Для этого применяется ключевое слово extends. А чтобы получить доступ к конструктору родителя и его свойствам в классах используется простое использование конструкции super. К примеру, создание объекта Appliance и его наследника Dynamometer, которые были описаны выше, реализуется следующим образом:
export class Appliance { #fieldsAppl; // приватные свойства constructor(options = {}) { // Свойства значения по умолчанию Object.assign( this, { centerX: 0, centerY: 0, radius: 50, }, options, ); } ... } // Dynamometer наследует Appliance export class Dynamometer extends Appliance { constructor(options = {}) { // наследование конструктора от Appliance super(options); // Свойства значения по умолчанию для Dynamometer Object.assign( this, { maxValue: 10, }, options, ); } ... }
На этом этапе для решения задачи по переходу на import/export использовался ChatGPT вместе с Cursor. Cursor способен исследовать и переделать весь проект целиком. Однако такой объем изменений сложно будет отследить и позже переработать самостоятельно. Поэтому сначала объекты переводились в классы постепенно через ChatGPT.
Первая проблема, которая возникает, — не получается сделать модульными и настроить import/export объектов в проекте поочередно. Например, для начала, переводится в класс, прописывается export объекта Spring (функционал для отображения пружины из файла "Tools/spring.js" ). Если использовать Spring внутри другого объекта, который не является модульным, например внутри файла springDynamometer.js , то проявляется ошибка “ Cannot use import statement outside a module”.
То есть нельзя сделать модульным spring.js, если springDynamometer.js подключается как обычный <script src>. Придется переводить все объекты проекта в классы для экспорта одновременно. ChatGPT навел на мысль, что для начала объект можно перевести в класс и при этом сохранить глобальные объекты через window. Иными словами сначала создается class Spring, после его описания прописывается window.Spring = Spring.При этом нет необходимости использовать export и ломать структуру проекта.
Такой подход позволил переписать постепенно все объекты из каталога js/Tools на классы и привязать к window. После этого все объекты, отвечающие за реализацию логики обработки демонстраций из папкиjs/TopicsRender.
Затем, используя Cursor, удалось завершить работу и добиться работающего функционала.
Изначально сайт содержал строго закодированные в HTML текстовые значения надписей, заголовков меню, блочных элементов на английском языке. Это усложняло расширение и внесение изменений в структуру HTML, например при смене языка веб-сайта.
Для разрешения возникшей проблемы также использовался ChatGPT одновременно с Cursor.
Чтобы собрать надписи в одном месте, потребовалось создать файлы ресурсов для разных языков. В качестве файла ресурсов можно выбрать JSON-формат. Однако ChatGPT подсказал, что более удобно использовать просто JavaScript-объекты без необходимости асинхронной загрузки JSON – данных.
Пример части файла strings.ru.js:
export default { Title : "Физика", SubTitle : "Моделирование", ... };
В HTML-коде элементам, которые должны содержать текст из файла ресурсов, должен присваиваться пользовательский атрибут data-resource (например, <p data-resource="SubTitle"></p>). Впоследствии JS-код находит через атрибут значение в JS-объекте. Например, ресурс Panel.Title:
import ru from "../resources/strings.ru.js"; const attribute = "data-resource"; document.querySelectorAll(`[${attribute}]`).forEach((elem) => { elem.innerHTML = ru.Panel.Title; });
Итоговая функция, чтобы найти любое значение из файла ресурсов:
static get(key) { const parts = key.split("."); let result = Resources.currentLang; for (const part of parts) { result = result[part]; if (result === undefined) { return key; } } return result; }
Дальше через запрос к Cursor были присвоены атрибуты ресурсов во всем проекте и создана локализация.
Демонстрации подразумевают наблюдение вживую за физическими явлениями и поведением приборов, что требует создание различных анимаций. Например, возможна анимация движения стрелки динамометра или сжатие/разжатие пружины.
В старой версии приложения уже было предусмотрено использование анимаций с помощью общего механизма для всего проекта setTimeout. Данная функция, как известно, выполняет какую-либо другую функцию только один раз через заданный интервал времени. В предыдущем проекте использовался рекурсивный setTimeout с асинхронным вызовом через async/await.
Пример анимации стрелки динамометра через setTimeout:
const application = { timeout: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), } ... setValue: async function (valuePointer = 0) { ... async function tick() { face(); // отрисовка циферблата ... // draws value pointer according to condition async function setTimer(condition) { if (condition) { value(currentValue); await application.timeout(1); // ожидание 1 мс return await tick(); } else { redraw(valuePointer); value(valuePointer); return; } } if (initValue <= valuePointer) { currentValue = application.roundTwoDigits(currentValue + rotateSpeed); return await setTimer(currentValue <= valuePointer); } ... } // tick return tick(); } // setValue
Если требовалась отмена анимации, могла применяться функция, которая создавала Promiseна основе setTimeoutи возвращала объект:
timeoutPromise: (ms) => { let timer = 0; const promise = new Promise((resolve) => { timer = setTimeout(resolve, ms); }); return { promise, timer }; } ... let timeout = application.timeoutPromise(1); if (this.cancelTimer == true) { clearTimeout(timeout.timer); }
Такой предыдущий подход через setTimeoutимеет ряд недостатков. Анимация получается не привязана к частоте обновления экрана. Это означает, что может возникнуть рассохранизация между тем, как код обновляется на основании таймера, и как отрисовываются кадры на экране монитора. Также скорость анимации может зависеть от браузера: к примеру, setTimeout(1) после нескольких вызовов вложенных таймеров может задерживаться до нескольких миллисекунд. Следовательно, такой механизм может использоваться для создания только простых задач по анимации.
Для реализации более плавного анимирования, без задержек и “дерганий”, в данном проекте было решено перейти на современные возможности JavaScript посредством использования requestAnimationFrame.
Данный метод позволяет более эффективно реализовать анимацию и в качестве параметра принимает функцию (callback), которая вызовется перед следующим вызовом фрейма перерисовки. В отличие от setTimeout() и setInterval(), для requestAnimationFrame не требуется указывать время, через которое запустится функция. Данный метод возвращает requestID, который служит для отмены анимации через window.cancelAnimationFrame().
Самой простой идеей по переводу функции setValue, приведенной выше, к использованию описанного механизма является применение Promise, внутри которого применяется requestAnimationFrame. При достижении необходимых условий Promise возвращает результат и анимация завершается. Однако requestAnimationFrame строго привязывается к частоте экрана, которая отличается на разных устройствах (на смартфонах, на компьютерах).
Например:
const tick = () => { currentValue += 1; // всегда прибавляем 1 за кадр requestAnimationFrame(tick); };
Это означает, что на мониторе с частотой 60 Гц стрелка будет двигаться со скоростью 60 пикселей в секунду, а при частоте 100 Гц – 100 пикселей в секунду.
Во избежание этого ChatGPT предложил ввести некий коэффициент для гарантии того, что анимация будет длиться всегда одно и то же время, независимо от частоты экрана.
Итоговая функция по анимированию стрелки динамометра с учетом частоты монитора:
setValue(valuePointer = 0) { const rotateSpeed = this.propsDynam.getRotateStep(); let currentValue = this.value; let previousTime = null; ... return new Promise((resolve) => { const tick = (currentTime) => { if (previousTime === null) previousTime = currentTime; // время между кадрами const deltaTime = currentTime - previousTime; previousTime = currentTime; // Нормирование шага анимации для различной частоты экрана const step = rotateSpeed * deltaTime / Application.frameTime; // frameTime = 1000 / 60 // движение к цели, currentValue += step; ... // достигли цели if (currentValue === valuePointer) { this.#animationFrame = null; resolve(); return; } // следующий кадр this.#animationFrame = requestAnimationFrame(tick); }; this.#animationFrame = requestAnimationFrame(tick); }); } // setValue
Через запросы Cursor аналогичный приницип был применен для изменения принципа создания анимации во всем проекте.
В первоначальной версии веб-сайта адаптивный дизайн отсутствовал, и демонстрации отображались корректно только на компьютерных мониторах и ноутбуках. Сайт невозможно было использовать на экранах с маленьким разрешением, например на смартфонах. Чтобы корректно отображать физические демонстрации на различных устройствах, потребовалось не только изменить CSS-дизайн, но и переработать JavaScript-код для создания масштабирования сцен с демонстрациями.
Компоновка элементов на странице реализована в виде таблицы через CSS Grid Layout. Это позволяет работать с раскладкой в виде колонок и рядов. Сам Grid отображается в файле index.html , а его раскладка прописана в файле main.css.
Изначальная верстка предыдущего проекта Grid выглядела следующим образом:
.grid-container { height: 100vh; display: grid; grid-template-areas: ". . headerArea headerArea headerArea . ." ". . . . . . ." ". menuArea . contentArea . sidebarArea ." ". . . . . . ." ". footerArea footerArea footerArea footerArea footerArea ."; grid-template-columns: 10px 14% 15px 1fr 15px 25% 10px; grid-template-rows: 55px 5px 1fr 5px 30px; }
В .grid-container строки такие: строка под заголовок, пустая строка-отступ, пространство под сцену и панели, строка-отступ, нижний блок пространства. Колонки, где происходит взаимодействие с демонстрациями: отступ слева, колонка с меню, пустая колонка, колонка, содержащая Canvas для отрисовки, пустая колонка, колонка, содержащая iframe с параметрами для каждой сцены, отступ справа. Заданные настройки дизайна слабо подходят под адаптивность.
ЧтобыGridс содержимым подстраивался под разные разрешения, необходимо, чтобы колонки и строки имели не фиксированные размеры, а динамические - в процентах. Также можно задавать настройку дизайна контейнера для разных разрешений экрана и для разных устройств с помощью директивы @media. Поведение шрифтов можно настроить через относительные единицы (vw, vh) и clamp, которая задаёт диапазон значений между нижней и верхней границами. Одной из важных особенностей адаптивной верстки является использование единиц измерения vh и dvh. С помощью dvh возможно учитывать высоту в зависимости от того, скрыта или видима адресная строка на смартфонах.
Зная эти особенности и возможности CSS и используя Cursor удалось добиться корректного отображения веб-сайта на различных устройствах . Чтобы контролировать процесс и понять, какие изменения были внесены, пришлось делать запросы к Cursor поэтапно.
Вначале через запрос к ИИ было изменено визуальное поведения Grid-контейнера. Чтобы код не разрастался и не создавались множественные CSS-файлы пришлось запросить конкретно: доработать существующий интерфейс минимальными изменениями, не переписывать всю вёрстку и не менять существующий дизайн, по возможности использовать @media для адаптивности.
Следующим этапом была адаптация самих шрифтов. На этом этапе в запросе было указано: по возможности необходимо использовать clamp, нельзя менять существующие шрифты и оформление, а также избегать появления горизонтальной прокрутки.
Последним этапом была проверка и корректировка, так как некоторые элементы (например, заголовки, кнопки или текстбоксы) могли съезжать или иметь неправильный размер.
По итогу, после изменений макет Grid остался прежним (menuArea, headerArea, contentArea…) , но теперь прописывались не фиксированные размеры, а размеры через CSS-переменные:
grid-template-columns: var(--gutter-outer) var(--menu-width) … grid-template-rows: var(--header-height) var(--grid-gap) var(--stage-height) …;
Переменные прописались в @media блоке:
/* Планшеты */ @media (max-width: 1024px) { :root { --menu-width: max(9.5rem, 14%); --controls-width: max(12rem, 22%); ... } } /* смартфоны, повернутые горизонтально */ @media (orientation: landscape) and (max-height: 500px) { :root { --footer-height: 12px; --grid-gap: 4px; }
Пример CSS-переменных для дизайна шрифтов, кнопок:
--font-title: clamp(22px, 3.2vw, 40px); --button-width: clamp(5.5rem, 36vw, 120px);
В итоге, сайт корректно показывается на различных устройствах с разным разрешением.
Другой проблемой сайта было то, что JavaScript-cцены с демонстрациями экспериментов всегда отображались без учета изменения размеров разрешения экрана и, соответственно, размеров холста с отображенными сценами. Размеры физических приборов, координаты сброса грузов, координаты анимаций — все это имело фиксированные величины и координаты.
Для подсчета коэффициента масштабирования, который применяется для того, чтобы объекты и демонстрации подстраивались под разные размеры экрана, можно использовать простую функцию:
static applyScale(scene) { const scaleX = scene.canvas.width / scene.baseWidth; const scaleY = scene.canvas.height / scene.baseHeight; scene.stageScale = Math.min(scaleX, scaleY); return scene.stageScale; }
Впоследствии, применяя Cursor, удалось настроить все демонстрации на использование этого метода.
Первоначальный размер холста со сценой до применения масштабирования, определяется следующим образом:
static setBaseSize(scene, baseWidth, baseHeight) { scene.baseWidth = baseWidth; scene.baseHeight = baseHeight; scene.stageScale = 1; }
При начальной загрузке (например, init) каждой сцены применяется setBaseSize. А при любом изменении размеров сцены уже высчитывается коэффициент масштабирования через applyScale и применяются размеры или настройки:
init () { CanvasUtils.setBaseSize( this, 900, 700, ); … resizeAppliances(){ CanvasUtils.applyScale(this); let cst = this.constants; this.resizeSpringDynamometer(cst); this.resizeRuler(cst); } ... resizeRuler(cst) { this.ruler.startX = cst.ruler.startX * this.stageScale; this.ruler.length = cst.ruler.length * this.stageScale; … } ...

На экранах с маленьким разрешением, например, на экранах смартфонов, область с демонстрациями занимает мало места и неудобна для применения . Было решено расширить это пространство за счет сворачивания/разворачивания боковых панелей (меню, параметры).
Отправной точкой является идея в том, чтобы сделать нажимаемыми узкие области между боковыми панелями и областью с холстом. В эти маленькие области необходимо добавить текстовые надписи “>” и “<” (свернуть/развернуть). Курсор при наведении должен меняться, размер текста - тоже. После нажатия на эту область необходимая панель должна становиться узкой, а элементы на ней - скрываться.
Генерацией через Cursor данной задачи является на первый взгляд рабочее решение. Панели сворачиваются, в свернутом состоянии изменяется дизайн на уменьшенный размер панели с надписями “Меню” и “Параметры”.
Однако при тестировании в режиме смартфона выяснилось, что сами панели куда-то съезжают. Кроме того, генерация кода выдала сложные манипуляции с CSS-грид и много дублирующегося CSS-кода. Скрывающиеся панели начали превращаться в подобие всплывающих окон, у которых меняется z-index . В JavaScript-коде прописалась сложная логика: необходимые контейнеры находились через getElementById, к ним добавлялись CSS-атрибуты и классы. Сворачивание прописалось через requestAnimationFrame.

Предложенное через ИИ решение оказалось громоздким и сложным в применении. В то же время в интернете можно найти много подобных реализаций. Одной из идей является возможность просто менять ширину необходимых блоков и добавлять или убирать требуемые CSS–классы. В запросе Cursor была заложена эта идея, а также необходимость избегать дублирования CSS-правил, не использовать всплывающие блоки через z-index и сократить манипуляции через JavaScript код CSS-стилями, разместить CSS-код для сворачивания боковых панелей в отдельном файле.
В результате реализация сворачивания и разворачивания боковых панелей упростилась. Ширина свернутых панелей задается через CSS-переменные:
.grid-container.menu-collapsed { --menu-width: var(--sidebar-collapsed-width); } .grid-container.controls-collapsed { --controls-width: var(--sidebar-collapsed-width); }
Сами панели скрываются через стили:
.grid-container.menu-collapsed #divMenuInner, .grid-container.controls-collapsed #divControlsInner { visibility: hidden; position: absolute; width: 0; height: 0; … }
При нажатии кнопки сворачивания добавляется CSS-класс блоку-панели:
this.bindToggle(this.menuButton, "menu-collapsed"); ... bindToggle(button, collapsedClass) { button.addEventListener("click", () => { this.shell.classList.toggle(collapsedClass); this.onLayoutChange(); }); }

Суть физической сцены - в наблюдении как параметры влияют на затухание пружины в вертикальном положении. Пользователь может переместить груз к пружине, который затем начинает колебания от положения равновесия, а сопротивление постепенно уменьшает амплитуду движений.
Скорость затухания колебаний зависит от силы сопротивления: чем больше сопротивление, тем быстрее уменьшается амплитуда колебаний.
Сила сопротивления рассчитывается по формуле:
,
где:
— коэффициент вязкого сопротивления,
;
— скорость груза, м/с.
На затухание колебаний также влияет сила упругости пружины (закон Гука):
,
где:
— жёсткость пружины,
;
— смещение груза от положения равновесия,
.
Закон движения пружинного маятника – это второй закон Ньютона в виде дифференциального уравнения:
,
где:
— ускорение тела
.
Объединив закон Гука и силу сопротивления пружины, можно получить дифференциальное уравнение движения при колебаниях:
,
где:
— смещение груза относительно положения равновесия;
— масса тела,
;
— коэффициент затухания,
;
— жёсткость пружины,
;
— скорость тела,
;
— ускорение груза,
.
Используя это выражение, подсчитывается необходимая координата груза в требуемый момент времени:
Зная необходимую теорию, можно создавать новую физическую демонстрацию через ИИ. Вначале через Cursor создается новая HTML-страница (Oscillations.html) с параметрами, которые должны изменяться пользователем сайта. Пользователь симуляции может изменять массу, жёсткость и затухание. Другим параметром, изменяемым пользователем является начальное смещение - отклонение груза от точки равновесия (начальная амплитуда).

При этом начальное смещение, задаваемое пользователем, считается относительно точки равновесия ненагруженной пружины. После подвеса груза точка равновесия, вокруг которой будут совершаться колебания, перемещается.
Величина, на которую смещается новая точка равновесия рассчитывается по формуле статического удлинения пружины:
Следующим запросом к Cursor является, создание новой сцены-класса OscillationsDemo в файле oscillationsRender.js и размещение на ней пружины, линейки и других необходимых грузов.
Далее уже можно запрашивать ИИ на создание анимации колебаний. Для этого дается задание для Cursor на написание функции, которая принимает параметры от пользователя: масса тела, жесткость пружины, затухание, начальное смещение и y-координату, которая движется во время анимации затуханий. Функция должна реализовать движение через requestAnimationFrame и возвращать Promise, по окончанию промиса возвращается then. При этом должна подсчитываться новая точка равновесия, после подвеса тела.
Результатом генерации Cursor является требуемая функция:
animateDampedOscillation({startY,mass,stiffness,damping,initialDisplacement,scale = 100,onUpdate = null}) { this.stopOscillation(); … return new Promise((resolve, reject) => { const sag = Application.kgToNewton(mass) / stiffness; // растяжение пружины под действием груза const equilibriumY = startY + (sag - initialDisplacement) * scale; // координата равновесия с грузом. Колебания идут вокруг этой точки. // x — смещение от равновесия с грузом let x = initialDisplacement - sag; let v = 0; const startTime = performance.now(); let lastTime = startTime; // стрелочная функция сохраняет внешний this const step = (time) => { // условие остановки колебаний ... const dt = Math.min((time - lastTime) / 1000, 0.05); lastTime = time; // ускорение вокруг равновесия с грузом const a = (-stiffness * x - damping * v) / mass; // обновление скорости и позиции v += a * dt; x += v * dt; // координата груза на холсте const y = equilibriumY + x * scale; if (typeof onUpdate === "function") { onUpdate(y); } // условие остановки колебаний ... this.animationFrame = requestAnimationFrame(step); }; this.animationFrame = requestAnimationFrame(step); }); }
В данной функции используется дифференциальное уравнение движения для нахождения скорости и координаты:
В коде уравнение решается численным методом:
,
где:
(velocity) — скорость;
(acceleration) — ускорение;
(delta time) — время между текущим и предыдущим кадрами;
(displacement) — смещение относительно положения равновесия.
Использование современных возможностей языка JavaScript позволило существенно преобразить и модернизировать проект, созданный довольно давно. Роль ИИ в этом оказалась полезной и существенной. Такие заявленные цели, как рефакторинг структуры программы, создание адаптивности под смартфоны, улучшение интерфейса и создание новой демонстрации, были достигнуты.
Помимо этого, в изменённом решении удалось реализовать дополнительные изменения, не описанные в данной статье. К ним можно отнести:
реализацию перетаскивания для touchscreen-устройств (смартфонов)
улучшение визуального отображения элементов на сцене-холсте (динамометров, тел)
доработку анимации поведения элементов на сцене (стрелок, пружин).
Измененная структура проекта приведена ниже:

Сами же ИИ–инструменты не смогли полностью заменить роль человека-программиста при модернизации проекта. LLM – модели делают буквально только то, что попросишь. Прямые запросы, без нюансов и деталей могут привести к разрастанию кода, дублированию функций и к использованию сложных и длинных способов решения поставленной задачи.
К примеру, для моего проекта, если запросить Cursor применить масштабирование одной отрисованной JavaScript-сцены, а затем другой, то по-умолчанию может сгенерироваться похожий код или код даже с дублированными функциями для разных сцен-классов. Поэтому, лучше самому заранее знать, как организовать структуру, куда вынести функции или переменные общего использования и в каком месте их применять.
В другой задаче, связанной с созданием адаптивного интерфейса Cursor генерировал плохо структурированный спагетти-код. Адаптивность многих HTML-элементов и блоков работала некорректно, и в большом объеме кода было трудно найти причину такого поведения. Чтобы этого избежать, потребовалось указать, какой конкретно алгоритм применить и как лучше его использовать.
С другой стороны, ИИ минимизируют рутинную работу, связанную с выявлением багов, поиском в интернете того, как реализовать решение и комментированием, объяснением программного кода. Причем наиболее эффективной получается связка нескольких ИИ-инструментов, например ChatGPT и Cursor. ChatGPT можно использовать в качестве универсального помощника, который может подсказать общие идеи и рассказать теорию (например, по физическим процессам или по программированию). Также после генерации этой информации ChatGPT может вырабатывать эффективные и подробно описанные запросы для Cursor, который видит всю структуру проекта целиком.
В результате, по этой ссылке можно ознакомиться с программным кодом.
Здесь можно тестировать физические демонстрации.