КП: Многочастичный симулятор

Материал из Department of Theoretical and Applied Mechanics
Перейти к: навигация, поиск
А.М. Кривцов > Теоретическая механика > Курсовые проекты ТМ 2015 > Многочастичный симулятор
IPhone image 2015-4-13-1431495674163 1.jpg

Курсовой проект по Теоретической механике

Исполнитель: Старобинский Егор

Группа: 09 (23604)

Семестр: весна 2015

Аннотация проекта

Рассматриваемой задачей этого проекта является определение поведения задаваемой механической двухмерной системы[1] из частиц, сил и ограничений. Для этого был создан интернет-сайт с программой, позволяющей найти решение уравнения движения системы (физический движок). Также на базе этой программы был реализован пользовательский интерфейс с возможностями создания и редактирования исходной системы и визуализацией её поведения.

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


Научная новизна

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

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

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


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

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


Объединение физического движка и пользовательского интерфейса получило название "Многочастичный симулятор", имеет открытый исходный код и выполнено без использования готовых решений по теме проекта.

Формулировка задачи

Цель работы

Создание интернет-сайта, позволяющего пользователю моделировать многоточечную систему онлайн.

Решаемые задачи
  1. решение уравнения движения;
  2. визуализация.

Общие сведения по теме

Уравнение движения

Пусть мы наблюдаем тело в момент времени [math]t[/math].

Хотим знать, где окажется тело через малое изменение времени - [math]\Delta t[/math].

Рассмотрим базовый метод интегрирования Верле:

[math]\vec{x}(t + \Delta t) = 2\vec{x}(t) - \vec{x}(t - \Delta t) +\frac{ \vec{R}(t) \Delta t^2 }m[/math], где

[math]\vec{x}[/math] - позиция точки,

[math]\vec{R}[/math] - равнодействующая всех сил, действующих на тело,

[math]m[/math] - масса тела,

[math]t[/math] - текущий момент времени,

[math]\Delta t[/math] - малое изменение времени.

Метод Верле позволяет вычислять траекторию по упрощённой схеме: зная предыдущее и текущее положения ([math]\vec{x}(t - \Delta t)[/math] и [math]\vec{x}(t)[/math] соответственно)  и мгновенное значение равнодействующей приложенных сил в текущем положении [math]\vec{R}(t)[/math].

Достоинства метода: самокоррекция и бóльшая точность по сравнению с численным методом Эйлера.

Язык реализации: JavaScript.


Визуализация

Язык рализации: pure SCSS.

Обработка событий: JavaScript.

Манипуляции с DOM: jQuery (безболезненно заменяется на Zepto).

Отказ от WebGL продиктован выбором методов оптимизации для возможности работы с тысячами частиц.

Решение

Результат

Страница решения

Вы можете попробовать возможности симулятора прямо здесь, либо перейдя на полноразмерную страницу решения.

В пункте 4.7 представлен пример готовой команды для создания трёхатомной молекулы.

Нахождение периода в простом движении


Элементы системы
  • Частицы;
  • Стержни и пружины[2];
  • Стенки;
  • Поле сил;
  • Рабочее окно;
  • Сетки разметки;
  • Консоль;
  • Плеер.


Пример вывода консоли


Возможности консоли
  • Конфигурация начальной системы тел;
  • Изменение системы в процессе работы ("на лету");
  • Запуск алгоритмов анализа системы;
  • Распознавание и вывод ошибок в пользовательских запросах и в исходном коде;
  • Распознавание и вывод предупреждений в пользовательских запросах и в исходном коде;
  • Подключение/отключение сеток разметки, в том числе с пользовательскими размерами ячейки;
  • Тетрис.


Команды консоли

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

Консоль выводит каждое сообщение как одну строку. Если доступной длины строки не хватает, сообщение обрезается. Для отображения полной версии сообщения необходимо кликнуть по нему мышью.


 Примеры основных запросов

Очистить поле консоли

  • clrscr


Отобразить статистику элементов системы и число тиков:

  • getInfo


Создать частицу

  • addPoint (100,100) (0,10) 80 5[3][4], где

(100,100) - текущие координаты

(0,10) - вектор скорости относительно начала координат

80 - радиус частицы в пикселях

5 - масса частицы в у. е.


Задать вектор скорости (относительно начала координат)

  • setVelocity #0 (10,10), где

0 - id частицы[5]

(10,10) - новый вектор скорости относительно начала координат


Переместить частицу (относительно начала координат)

  • movePoint #3 (100,100) saveV[3][6], где

3 - id частицы[5]

(100,100) - новые координаты

saveV - флаг сохранения скорости. Если указан, частица после перемещения сохранит вектор своей скорости.


Задать массу

  • setProp #0 mass 10, где

0 - id частицы[5]

10 - новая масса в у. е.

  • setProp #0 invmass 0.1, где

0 - id частицы[5]

0.1 - обратное значение новой массы


Задать радиус

  • setProp #0 radius 100, где

0 - id частицы[5]

100 - радиус частицы в пикселях


Создать пружину между частицами

0, 1 - id частиц[5]

50 - жёсткость пружины в у. е.


Изменить жёсткость пружины

  • changeSpring #5 10, где

5 - id пружины[5]

10 - новая жёсткость в у. е.


Изменить жёсткость одинаковых пружин

  • changeSprings 10->15, где

10 - текущая жёсткость в у. е.

15 - новая жёсткость в у. е.


Изменить жёсткость всех пружин

  • changeSprings all->20, где

20 - новая жёсткость в у. е.


Создать стержень между частицами

  • addStick #2 #1[6], где

2, 1 - id частиц[5]


Отключение гравитации

  • gravity disable


Задать вектор ускорения свободного падения (относительно начала координат)

  • gravity (0,10)


Отобразить сетку[8]

  • showGrid type 0

Сетка 100х50 пикселей.

  • showGrid type 1

Сетка 50х25 пикселей.

  • showGrid type 2

Сетка 20х10 пикселей.

  • showGrid 100x75, где

100 - ширина ячеек в пикселях

50 - высота ячеек в пикселях


Спрятать сетку

  • hideGrid


Запустить симуляцию

  • play


Остановить симуляцию

  • stop


"Промотать" симуляцию

  • step(100), где

100 - число пропускаемых отрисовкой тиков


Исполнить внутренний метод

  • execute nameMethod(params), где

nameMethod - название метода

params - сообщаемые параметры


Посмотреть историю запросов

  • getHistory [9]


Объединить команды в одном запросе

  • commandOne---commandTwo, где

commandOne, commandTwo - команды консоли, могут также состоять из объединённых команд

Возможности плеера
  • Воспроизведение/пауза симуляции с заданным [math]\Delta t[/math];
  • Скачок вперёд на кратное [math]\Delta t[/math] время;
  • "Замедление времени"[10].
Кнопки интерфейса
  • Кнопка Get point's id

После нажатия на кнопку, а затем на частицу выводит id последней в консоль.


  • Кнопка Move point[6]

Эквивалентна команде консоли movePoint без флага saveV.

После нажатия на кнопку, а затем на частицу закрепляет управление положением последней за курсором мыши.

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

Для прекращения управления следует вновь нажать исходную кнопку.


  • Клавиша клавиатуры ~ (также `, ё, Ё)

Эквивалентна командам консоли play/stop и кнопкам / | | плеера.


  • Кнопки плеера

/ | | - воспроизведение/остановка симуляции (эквивалентна командам консоли play/stop).

- переход на тик вперёд (эквивалентна команде консоли step(1)).

►► - переход на 50 тиков вперёд (эквивалентна команде консоли step(50)).

►| - переход на 100 тиков вперёд (эквивалентна команде консоли step(100)).

Пример системы

Смоделируем молекулу из трёх атомов (подобную молекуле углекислого газа, рассматриваемой в курсовом проекте А. Смирнова).

PhenCO2Example.png

Для создания такой системы необходимо последовательно выполнить следующие команды:

Создаём атомы кислорода, углерода и кислорода соответственно.

  • addPoint (100,100) (0,0) 40 5
  • addPoint (250,100) (0,0) 50 10
  • addPoint (400,100) (0,0) 40 5

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

  • addSpring #0 #2 10
  • addSpring #0 #1 20
  • addSpring #1 #2 20

Дополнительно можем задать скорости у атомов кислорода для создания колебаний.

  • setVelocity #0 (1,0)
  • setVelocity #2 (-1,0)

Итоговый код (одна строка):

addPoint (100,100) (0,0) 40 5---addPoint (250,100) (0,0) 50 10---addPoint (400,100) (0,0) 40 5---addSpring #0 #2 10---addSpring #0 #1 20---addSpring #1 #2 20---setVelocity #0 (1,0)---setVelocity #2 (-1,0)

Выдержки кода решения

Исходный код некоторых файлов [HTML, JS, CSS]

Файл "point.js"

 1 function Point(mass, radius, coorXY, oldCoorXY, isExpToPot)
 2 {
 3     this.mass = mass;
 4     this.invmass = 1 / mass;
 5     this.radius = radius;
 6     this.coor = coorXY;
 7     this.oldCoor = oldCoorXY || coorXY;
 8     this.id = getId();
 9     this.type = 'point';
10     this.isExpToPot = isExpToPot || false;
11     
12 // log
13     console.log('Create: '+this.toString());
14 }
15 
16 
17 Point.prototype.checkCoor = function(){
18     if (this.coor.isNaN())
19     {
20         console.error('Coor is NaN: ' + this);
21         player.stop();
22     }
23     else
24     {
25         if (this.oldCoor.isNaN())
26         {
27             console.warning('OldCoor is NaN: ' + this);
28             this.oldCoor = new Coor(this.coor);
29         }
30         else
31         {
32             return ;
33         }
34     }
35 }
36 
37 Point.prototype.toString = function(type){
38     if (type === 'full')
39     {
40         return JSON.stringify(this, [   
41                                         'type',
42                                         'id',
43                                         'coor',
44                                         'mass_',
45                                         'radius_',
46                                         'x',
47                                         'y',
48                                         'begin',
49                                         'end'
50                                 ], 4)
51     }
52     else
53     {
54         return JSON.stringify(this, [   
55                                         'type',
56                                         'id',
57                                         'coor',
58                                         'x',
59                                         'y',
60                                         'begin',
61                                         'end'
62                                 ], 4)
63     }
64 }
65 
66 Point.prototype.move = function(coorXY){
67 	var delta = minusCoor(this.coor, this.oldCoor);
68     this.coor = coorXY;
69     this.oldCoor = minusCoor(coorXY, delta);
70 // log
71     console.log('Move: '+this.toString());
72 }
73 
74 Point.prototype.moveEase = function(coorXY){
75     this.coor = coorXY;
76     this.oldCoor = coorXY;
77 // log
78     console.log('Move: '+this.toString());
79 }

Файл "player.js"

  1 var step;
  2 
  3 function Player(dt, phys, render, console)
  4 {
  5     this.tick = 0;
  6     this.dt = dt;
  7     this.tickCount = 0;
  8     this.physics = new Physics(phys, this);
  9     this.renderer = new Renderer(render, this);
 10     this.parser = new Parser(this);
 11     this.console = new Console(console, this);
 12     var checker = checkerPl.bind(this);
 13     
 14     this.stop = function(){
 15         clearTimeout(this.tick);
 16         this.tick = 0;
 17         
 18 // log
 19         this.console.logAns('Pause...');
 20     }
 21     
 22     this.play = function(){
 23         if (!this.tick)
 24         {
 25             this.tick = setTimeout(function stepTm(pl){
 26                 step(pl, checker);
 27                 pl.tick = setTimeout(stepTm, pl.dt, pl);
 28             }, this.dt, this);
 29 // log
 30             this.console.logAns('Play');
 31         }
 32     }
 33     
 34     this.shifter = function(){
 35         if (this.tick)
 36         {
 37             this.stop();
 38             
 39             return 0;
 40         }
 41         else
 42         {
 43             this.play();
 44           	
 45             return 1;
 46         }
 47     }
 48     
 49     this.nextStep = function(i){
 50         if (!i)
 51         {
 52             step(this, checker);
 53 // log
 54             this.console.logAns('step');
 55         }
 56         else
 57         {
 58             this.console.logAns('step x ' + i);
 59             
 60             while (i--)
 61             {
 62                 step(this, checker);
 63             }
 64         }
 65         
 66     }
 67     
 68     this.getInfo = function(){
 69         this.console.logAns('Tick count: '+this.tickCount);
 70         this.console.logAns('Points count: '+this.physics.Points.length);
 71         this.console.logAns('Walls count: '+this.physics.Walls.length);
 72         this.console.logAns('Springs count: '+this.physics.Springs.length);
 73         this.console.logAns('Sticks count: '+this.physics.Sticks.length);
 74     }
 75     
 76     this.find = function(id){
 77         if (id in this.renderer.arr)
 78         {
 79             return this.renderer.arr[id].ptr;
 80         }
 81         
 82         return 0;
 83     }
 84     
 85     this.addPoint = function(mass, radius, coorXY, oldCoorXY, isExpToPot){
 86         var tmp = this.physics.addPoint(mass, radius, coorXY, oldCoorXY, isExpToPot);
 87         this.renderer.addPoint(tmp);
 88 // log
 89         this.console.logAns('Create point.');
 90     }
 91     
 92     this.addWall = function(vectM, vectO){
 93         var tmp = this.physics.addWall(vectM, vectO);
 94         this.renderer.addWall(tmp);
 95 // log
 96         this.console.logAns('Create wall.');
 97     }
 98     
 99     this.addStick = function(elem1, elem2){
100         var tmp = this.physics.addStick(elem1, elem2);
101         this.renderer.addStick(tmp);
102 // log
103         this.console.logAns('Create stick.');
104     }
105     
106     this.addSpring = function(elem1, elem2, k){
107         var tmp = this.physics.addSpring(elem1, elem2, k);
108         this.renderer.addSpring(tmp);
109 // log
110         this.console.logAns('Create spring.');
111     }
112     
113     this.parse = function(str){
114         return this.parser.parse(str);
115     }
116     
117     this.log = function(str){
118         this.console.log(str);
119     }
120     
121     this.warn = function(str){
122         this.console.warn(str);
123     }
124     
125     this.error = function(str){
126         this.console.error(str);
127     }
128 }      
129 
130 step = function(pl, checker){
131     if (!checker())
132     {
133         pl.physics.step();
134         pl.renderer.draw();
135         pl.tickCount++;
136     }
137     else
138     {
139         pl.stop();
140         checker('delete');
141     }  
142 }   
143 
144 function checkerPl(mode, vr, val)
145 {
146     var st = checkerPl.state;
147     
148     if (arguments.length == 0) // check
149     {
150         for (var i in st)
151         {
152             if (player[st[i].vr] < st[i].val)
153             {
154                 return 1;
155             }
156         }
157         
158         return 0;
159     }
160     
161     if (mode == 'delete') // delete
162     {
163         for (var i in st)
164         {
165             if (player[st[i].vr] < st[i].val)
166             {
167                 delete st[i];
168             }
169         }
170         
171         return 0;
172     }
173     
174     if (mode == 'add') // add
175     {
176         st.push({'vr': vr, 'val': val});
177         
178         return 0;
179     }
180 }

Файл "renderer.js"

  1 function Renderer(param, pl)
  2 {
  3     this.scr = $('#'+param.idEl);
  4     this.borderSize = param.borderSize;
  5     this.arr = new Array();
  6     this.player = pl;
  7 }
  8 
  9 Renderer.prototype.addPoint = function(point){   
 10     var el = $('<div data-id="'+point.id+'" class="point"></div>');
 11     el.css('width', 2 * point.radius - 2 * this.borderSize);
 12     el.css('height', 2 * point.radius - 2 * this.borderSize);
 13     el.css('border-radius', 2 * point.radius - 2 * this.borderSize);
 14   	el.css('font-size', 10 * (point.radius - this.borderSize) + '%');
 15     this.scr.append(el);
 16     
 17     var tmp = {
 18         'el': el,
 19         'ptr': point,
 20         'draw': function(){
 21                 var c = this.patch(this.ptr.coor);
 22                 this.el.css('top', c.y);
 23                 this.el.css('left', c.x);
 24         },
 25         'setCoor': function(c){
 26                 c = c || this.patch(this.ptr.coor);
 27                 this.el.css('top', c.y);
 28                 this.el.css('left', c.x);
 29         },
 30         'patch': function(c){
 31             return new Coor(c.x - this.ptr.radius, c.y - this.ptr.radius);
 32         },
 33         'patchC': function(c){
 34             return new Coor(c.x + this.ptr.radius, c.y + this.ptr.radius);
 35         }
 36     };
 37     this.arr[point.id] = tmp;
 38     
 39     tmp.setCoor();
 40 }
 41 
 42 
 43 Renderer.prototype.log = function(str){
 44     this.player.log(str);
 45 }
 46 
 47 
 48 Renderer.prototype.warn = function(str){
 49     this.player.warn(str);
 50 }
 51 
 52 
 53 Renderer.prototype.error = function(str){
 54     this.player.error(str);
 55 }
 56 
 57 
 58 Renderer.prototype.addWall = function(wall){
 59     var el = $('<div data-id="'+wall.id+'" class="wall"></div>');
 60     el.css('left', wall.main_.midX());
 61     el.css('top', wall.main_.midY())
 62     this.scr.append(el);
 63     
 64     var tmp = {
 65         'el': el,
 66         'ptr': wall
 67     }
 68     
 69     this.arr[wall.id] = tmp;
 70 }
 71 
 72 Renderer.prototype.addStick = function(stick){
 73     var el = $('<div data-id="'+stick.id+'" class="stick"></div>');
 74     this.scr.append(el);
 75     
 76     var tmp = {
 77         'el': el,
 78         'ptr': stick,
 79         'setCoor': function(b, e, l){
 80             setCoorLink.call(this, b, e, l);
 81         },
 82         'draw': function(){
 83             this.setCoor(this.ptr.coorB, this.ptr.coorE, this.ptr.length_);
 84         }
 85     };
 86 
 87     this.arr[stick.id] = tmp;
 88 }
 89 
 90 Renderer.prototype.addSpring = function(spring){
 91     var el = $('<div data-id="'+spring.id+'" class="spring"></div>');
 92     this.scr.append(el);
 93     
 94     var tmp = {
 95         'el': el,
 96         'ptr': spring,
 97         'setCoor': function(b, e, l){
 98             setCoorLink.call(this, b, e, l);
 99         },
100         'draw': function(){
101             if (this.ptr.k == 0)
102             {
103                 this.el.hide();
104                 delete this.draw;
105             }
106             else
107             {
108                 this.setCoor(this.ptr.coorB, this.ptr.coorE, length(this.ptr.coorB, this.ptr.coorE));
109             }
110         }
111     };
112 
113     this.arr[spring.id] = tmp;
114 }
115 
116 Renderer.prototype.draw = function(){
117     for (var key in this.arr)
118     {
119         var l = this.arr[key];
120         
121         if (l.draw)
122         {
123             l.draw();            
124         }
125 
126     }
127 }

Файл "func.js"

  1 function Coor(e, y)
  2 {
  3     if (e.pageX)
  4     {
  5         this.x = e.pageX;
  6         this.y = e.pageY;
  7         
  8         return ;
  9     }
 10     if (typeof e == 'number')
 11     {
 12         this.x = e;
 13         this.y = y;
 14         
 15         return ;
 16     }
 17     else
 18     {
 19         this.x = e.x;
 20         this.y = e.y;
 21         
 22         return ;
 23     }
 24 }
 25 
 26 function createHTMLTable(id, numberOfCellsPerRow, numberOfRows, cellWidth, cellHeight)
 27 {
 28 	var generatedHTMLTable = '<style>    \
 29 		#table' + id + '.show{}        \
 30 		#table' + id + '.hide{display:none;}     \
 31 		#table' + id + '{display:table;position:fixed;width: 2000px;height:1000px;top:27px;border-collapse:collapse;left:0px;z-index:1;outline:2px solid gray;}   \
 32 		#table' + id + ' > div{display:table-row;}                                                \
 33 		#table' + id + ' > div > div{display:table-cell;width:' + cellWidth + 'px;height:' + cellHeight + 'px;outline:1px solid white;}    \
 34 		</style>';
 35 	for(var ii = 0; ii < numberOfRows; ++ii)
 36 	{
 37 		generatedHTMLTable += "<div>";
 38 		
 39 		for(var i = 0; i < numberOfCellsPerRow; ++i)
 40 		{
 41 			generatedHTMLTable += "<div></div>";
 42 		}
 43 		
 44 		generatedHTMLTable += "</div>";
 45 	}
 46 	                                              
 47 	var tableDiv = $('<div id="table'+id+'" class="show" data-type="grid">'+generatedHTMLTable+'</div>')
 48 	
 49 	$('body').append(tableDiv);
 50 }
 51 
 52 function colorHTMLTableCell(id, numberOfRows, numberOfCell)
 53 {
 54   	var fix = 1;
 55   	var rowBefore = Math.floor((fix + numberOfCell) / numberOfRows);
 56 	var styleToColorDiv = $('<style>#table' + id + ' > div:nth-child(' + (rowBefore + 1 + fix) + ') > div:nth-child(' + (0 + Math.floor(fix + numberOfCell - rowBefore * numberOfRows)) +'){outline:3px dashed dodgerblue;}</style>');
 57 	$('#table' + id).append(styleToColorDiv);
 58 }
 59 
 60 
 61 Coor.prototype.toString = function(type){
 62     return JSON.stringify(this);
 63 }
 64 
 65 Coor.prototype.plus = function(B){
 66     this.x += B.x;
 67     this.y += B.y;
 68     
 69     return this;
 70 }
 71 
 72 Coor.prototype.isNaN = function(){
 73     if (isNaN(this.x) || isNaN(this.y))
 74         return 1;
 75     return 0;
 76 }
 77 
 78 function dot(A, B)
 79 {
 80     return (A.x * B.x + A.y * B.y);
 81 }
 82 
 83 function length(A, B)
 84 {
 85     return Math.sqrt((A.x - B.x)*(A.x - B.x) + (A.y - B.y)*(A.y - B.y));
 86 }
 87 
 88 function sign(x)
 89 {
 90     return (x > 0 ? 1 : x < 0 ? -1 : 0);
 91 }
 92  
 93 function Vector(A, B)
 94 {
 95     this.begin = A;
 96     this.end = B;
 97     this.length = length(A, B);
 98 }
 99 
100 Vector.prototype.midX = function(){
101     return (this.begin.x + this.end.x)/2;
102 }
103 
104 
105 Vector.prototype.midY = function(){
106     return (this.begin.y + this.end.y)/2;
107 }
108 
109 Vector.prototype.toString = function(type){
110     if (type === 'full')
111     {
112         return JSON.stringify(this, null, 4)
113     }
114     else
115     {
116         return JSON.stringify(this, [   
117                                         'begin',
118                                         'end',
119                                         'length',
120                                         'x',
121                                         'y',
122                                 ], 4)
123     }
124 }
125 
126 Vector.prototype.resize = function(){
127     this.length = length(A, B);
128 }
129 
130 Vector.prototype.plus = function(B){
131     this.begin.plus(B.begin);
132     this.end.plus(B.end);
133     this.resize();
134     
135     return this;
136 }
137 
138 function plusVector(A, B)
139 {
140     return new Vector(plusCoor(A.begin, B.begin), plusCoor(A.end, B.end));
141 }
142 
143 function plusCoor(A, B)
144 {
145     return new Coor(A.x + B.x, A.y + B.y);
146 }
147           
148 function minusCoor(A, B)
149 {
150     return new Coor(A.x - B.x, A.y - B.y);
151 }
152 
153 function timesCoor(e, k)
154 {
155     return new Coor(k * e.x, k * e.y);
156 }    
157 function deltaCoor(e)
158 {
159     return Math.sqrt(dot(e,e));
160 } 
161 
162 function setCoorLink(a, b, length)
163 {
164   	
165     this.el.css('top', a.y);
166     this.el.css('left', a.x);
167 	this.el.css('height', length);
168 	var angle = 0;
169   	var deltaX = b.x - a.x;
170   	var deltaY = b.y - a.y;
171   
172   	if (deltaX == 0)
173 		angle = ((deltaY > 0) - (deltaY < 0)) * Math.PI / 2;
174 	else
175         if(deltaX < 0) 
176         	angle = ((deltaY >= 0) - (deltaY < 0)) * Math.PI + Math.atan(deltaY / deltaX);
177         else angle = Math.atan(deltaY / deltaX);
178 
179    angle -= Math.PI/2;
180    this.el.css('transform', 'rotate(' + angle + 'rad)');
181 }

Файл "console.js"

  1 function Console(par, pl)
  2 {
  3     this.player = pl;
  4     this.btn = $(par.btn);
  5     this.input = $(par.inp);
  6     this.txt = $(par.log);
  7     this.history = new Array();
  8     this.oHistory = new Array();
  9     var exec = this.execute.bind(this);
 10     var keydown = keydownCons.bind(this);
 11     
 12     this.btn.click(function(){
 13         exec();
 14     });
 15     
 16     this.input.keydown(keydown);
 17 }
 18 
 19 function keydownCons(e)
 20 {
 21     var e = e || window.event;
 22     var key = e.which;
 23         
 24     if (key == 38)
 25     {
 26         this.input.val(this.getHist());
 27     }
 28     
 29     if (key == 40)
 30     {
 31         this.input.val(this.getHistInv());
 32     }
 33     
 34     if (e.ctrlKey && key == 13)
 35     {
 36         this.input.val(this.input.val() + '---');
 37         
 38         return ;
 39     }
 40         
 41     if (key == 13)
 42     {
 43         this.btn.trigger('click');
 44     }
 45 }
 46 
 47 Console.prototype.log = function(str){
 48     var el = $('<div></div>').addClass('log').html(str.toString().trim()).click(showAllLog);
 49     
 50     this.txt.prepend(el);
 51 };
 52 
 53 function showAllLog()
 54 {
 55     $(this).addClass('all');
 56 }
 57 
 58 Console.prototype.warn = function(str){
 59     var el = $('<div></div>').addClass('warn').html(str.toString().trim()).click(showAllLog);
 60     
 61     this.txt.prepend(el);
 62 };
 63 
 64 Console.prototype.error = function(str){
 65     var el = $('<div></div>').addClass('err').html(str.toString().trim()).click(showAllLog);
 66     
 67     this.txt.prepend(el);
 68 };
 69 
 70 Console.prototype.logAns = function(str){
 71     var el = $('<div></div>').addClass('log').html('> ' + str.toString().trim()).click(showAllLog);
 72     
 73     this.txt.prepend(el);
 74 };
 75 
 76 Console.prototype.warnAns = function(str){
 77     var el = $('<div></div>').addClass('warn').html('> ' + str.toString().trim()).click(showAllLog);
 78     
 79     this.txt.prepend(el);
 80 };
 81 
 82 Console.prototype.errorAns = function(str){
 83     var el = $('<div></div>').addClass('err').html('> ' + str.toString().trim()).click(showAllLog);
 84     
 85     this.txt.prepend(el);
 86 };
 87 
 88 Console.prototype.saveHist = function(txt){
 89     while(this.oHistory.length)
 90     {
 91         this.history.push(this.oHistory.shift());
 92     }
 93     
 94     this.history.push(txt);
 95 };
 96 
 97 Console.prototype.getHist = function(){
 98     var tmp = this.history.pop();
 99     if (!tmp || tmp.length < 3)
100     {
101         return '';
102     }
103     this.oHistory.unshift(tmp);
104     
105     return tmp;  
106 };
107 
108 Console.prototype.getHistInv = function(){
109     var tmp = this.oHistory.shift();
110     if (!tmp || tmp.length < 3)
111     {
112         return '';
113     }
114     this.history.push(tmp);
115     
116     return tmp;  
117 };
118 
119 Console.prototype.execute = function(){
120     var txt = this.input.val().trim();
121     
122     if (txt === '')
123     {
124         return ;
125     }
126     
127     
128     if (txt === 'getHistory')
129     {
130 		this.input.val('');
131 		
132 	    var string = '';
133 	    
134 	    if(this.history.length > 0){
135 		    var historyLength = this.history.length;
136 		    
137 		    for(var i = 0; i < historyLength; ++i){
138 			    console.log(this.history[i] + '---');
139 			    if(this.history[i] != undefined)
140 		    		string += this.history[i] + '---';
141 		    }
142 			string = string.slice(0,-3);
143 	    }
144 	    	
145 	    if(this.history.length == 0)
146 	    	string = 'History is empty';
147 	    	
148 	    alert(string);
149 
150 	    return 0;
151     }
152     
153     this.log(txt);
154     
155     if(!this.player.parse(txt))
156     {
157         this.input.val('');
158         
159         this.saveHist(txt);
160     }
161     else
162     {
163         this.warnAns('Can not disassemble');
164     }
165 };

Файл "phen.css"

  1 *{margin:0;padding:0;}
  2 body{background: gainsboro;}
  3 .point{
  4     position: absolute;
  5     z-index: 20;
  6     counter-increment: num;
  7     text-align: center;
  8     border: 4px solid white;
  9     background: dodgerblue;
 10 }
 11 .point:before{
 12     content: counter(num);
 13     color: white;
 14     -webkit-user-select: 	none;
 15     -khtml-user-select: 	none;
 16     -moz-user-select: 		none;
 17     -ms-user-select: 		none;
 18     -o-user-select: 		none;
 19     user-select: 		none;
 20 }
 21 #screen{
 22     position: fixed;
 23     z-index: 2;
 24     top:27px;
 25     counter-reset: num;
 26     display: block;
 27     overflow-x: hidden;
 28     width: 100%;
 29     height: auto;
 30     bottom:0;
 31     background: transparent;
 32 }
 33 #btns input{
 34     font-size: 18px;
 35     position: fixed;
 36     z-index: 2;
 37     overflow: hidden;
 38     width: 25%;
 39     white-space: nowrap;
 40     text-overflow: ellipsis;
 41     color: white;
 42     border: 3px solid white;
 43     border-right: none;
 44     border-radius: none!important;
 45     outline: none!important;
 46     background: dodgerblue;
 47 }
 48 #btns input.hide{display:none;}
 49 #btns input:nth-child(2){left:25%;}
 50 #btns input:nth-child(3){left:50%;}
 51 #btns input:nth-child(4){left:50%;}
 52 #btns input:last-child{
 53     left: 75%;
 54     border: 3px solid white;
 55 }
 56 #btns input.notActive{}
 57 #btns input:active,
 58 #btns input.active{
 59     background: crimson!important;
 60 }
 61 
 62 #btns input[disabled],
 63 #btns input[disabled]:active{
 64     background: gray!important;
 65 }
 66 .wall{
 67     left: 252.5px;
 68     top: 227.5px;
 69     height:0px;
 70     width:500px;
 71     z-index:1000;
 72     margin-top:-2px;
 73     position: absolute;
 74     -webkit-transform: 	rotate(180deg);
 75     -khtml-transform: 	rotate(180deg);
 76        -moz-transform: 	rotate(180deg);
 77        -ms-transform: 	rotate(180deg);
 78         -o-transform: 	rotate(180deg);
 79            transform: 	rotate(180deg);
 80 }
 81 .stick,
 82 .spring{
 83 	background: white;
 84 	width: 2px;
 85 	position:absolute;
 86 	-webkit-transform-origin:       50% 0;
 87 	-khtml-transform-origin: 	50% 0;
 88 	   -moz-transform-origin: 	50% 0;
 89 	    -ms-transform-origin: 	50% 0;
 90 	     -o-transform-origin: 	50% 0;
 91 	        transform-origin: 	50% 0;
 92 }
 93 .spring{
 94 	background: url('./spring.png') no-repeat;
 95 	background-size: 10px 100%;
 96   	width:10px;margin-left:-5px;
 97 }
 98 input[disabled],
 99 input[disabled]:active{
100     background: gray!important;
101 }
102 #stack{
103     width: 130px;
104     height: 260px;
105     border: solid 1px black;
106     border-top: 0px;
107 }
108 
109 #stack .brick{
110     width: 11px;
111     height: 11px;
112     border: solid 1px white;
113     background: white;
114     float: left;
115 }
116 #stack .brick.on{
117     background: black;
118 }
119 #stack .brick.now{
120     background: green;
121 }
122 
123 #controls{
124     position: fixed;
125     z-index: 22;
126     bottom: 5px;
127     left: 5px;
128     width: 165px;
129     height: 30px;
130     padding: 15px;
131     -webkit-transition: all 400ms;
132     -khtml-transition:  all 400ms;
133        -moz-transition: all 400ms;
134          -o-transition: all 400ms;
135             transition: all 400ms;
136     opacity: .5;
137     border: 3px solid #eee;
138     border-radius: 20px;
139     background-color: dodgerblue;
140 }
141 #controls.show{opacity: 1;}
142 #controls:before{
143     position: absolute;
144     top: -2px;
145     left: -2px;
146     display: block;
147     width: 334px;
148     height: 65px;
149     content: '';
150 }
151 #playerBtns{
152     position: relative;
153     left: 30px;
154 }
155 #playerBtns span{
156     position: absolute;
157     top: 4px;
158 }
159 #controls[type='0'] #playerBtns span:nth-of-type(1){
160     top: 0;
161     width: 0;
162     height: 0;
163     border-width: 15px 0 15px 30px;
164     border-style: solid;
165     border-color: transparent transparent transparent white;
166 }
167 #controls[type='1'] #playerBtns span:nth-of-type(1){
168     top: 0;
169     right: 169px!important;
170     width: 11px;
171     height: 30px;
172     border-right: 8px solid crimson;
173     border-left: 8px solid crimson;
174 }
175 #playerBtns span:nth-of-type(2){
176     width: 0;
177     height: 0;
178     border-width: 10px 0 10px 20px;
179     border-style: solid;
180     border-color: transparent transparent transparent white;
181 }
182 #playerBtns span:nth-of-type(3){
183     width: 0;
184     height: 0;
185     border-width: 10px 0 10px 20px;
186     border-style: solid;
187     border-color: transparent transparent transparent white;
188 }
189 #playerBtns span:nth-of-type(3):after{
190     position: absolute;
191     top: -10px;
192     width: 0;
193     height: 0;
194     content: '';
195     border-width: 10px 0 10px 20px;
196     border-style: solid;
197     border-color: transparent transparent transparent white;
198 }
199 #playerBtns span:nth-of-type(4){
200     width: 0;
201     height: 0;
202     border-width: 10px 0 10px 20px;
203     border-style: solid;
204     border-color: transparent transparent transparent white;
205 }
206 #playerBtns span:nth-of-type(4):after{
207     position: absolute;
208     top: -10px;
209     width: 0;
210     height: 0;
211     content: '';
212     border-width: 10px 2px;
213     border-style: solid;
214     border-color: white;
215 }
216 #playerBtns span:nth-of-type(1){right: 165px;}
217 #playerBtns span:nth-of-type(2){right: 130px;}
218 #playerBtns span:nth-of-type(3){right: 90px;}
219 #playerBtns span:nth-of-type(4){right: 35px;}
220 #consoleWindow{
221 	position: fixed;
222 	top: 30px;
223 	z-index: 2;
224 	right: 0;
225 	width: 25%;
226 	font-weight: bold;
227 	font-size: 14px;
228 	overflow: hidden;
229 	max-height: 339px;
230 	background: white;
231 }
232 #consoleWindow.hide{height:0;}
233 #consoleWindow.show{height: auto;}
234 #consoleInput{border-bottom: 3px solid gainsboro;}
235 #consoleInput:before{
236 	content: '>';
237 	font-size: 14px;
238 	position: absolute;
239 	top: 4px;
240 	left: 4px;
241 }
242 #consoleInput [type="text"]{
243 	border: none!important;
244 	outline: none!important;
245 	height: 20px;
246 	font-size: 14px;
247 	width: 100%;
248 	box-sizing:border-box;
249 	padding-left: 15px;
250 }
251 #consoleInput [type="button"]{
252 	font-size: 18px;
253     overflow: hidden;
254     width: 100%;
255     right: 0;
256     white-space: nowrap;
257     text-overflow: ellipsis;
258     color: white;
259     border: 3px solid white;
260     border-radius: none!important;
261     outline: none!important;
262     background: dodgerblue;
263 }
264 #consoleInput [type="button"]:active{background: crimson!important;}
265 #consoleLog{
266 	width: 100%;
267 	max-height: 290px;
268 	box-sizing: border-box;
269 	height: auto;
270 	word-wrap: break-word;
271 	overflow-y: auto;
272 	overflow-x: hidden;
273 	background: white;
274 	padding-left: 3px;
275 	padding-bottom: 10px;
276 }
277 
278 #consoleLog div:not(.all){
279 	text-overflow: ellipsis;
280 	overflow: hidden;
281 	max-height: 37px;
282 	width: 100%;
283 	white-space: nowrap;
284 }
285 
286 #consoleLog .warn{color: #DAA520;}
287 #consoleLog .err{color: crimson;}
288 #consoleLog .log{color: grey;}

Файл "index.php"

 1 <!DOCTYPE html>
 2 <html lang="ru">
 3 <head>
 4    	<title>Phen v2.0 a</title>
 5    	<link rel="stylesheet" href="./phen.css">
 6    	<script type="text/javascript" src="./jquery-2.1.3.min.js"></script>
 7         <script type="text/javascript" src="./jquery-css-transform.js"></script>
 8         <script type="text/javascript" src="./point.js"></script>
 9         <script type="text/javascript" src="./spring.js"></script>
10         <script type="text/javascript" src="./stick.js"></script>
11         <script type="text/javascript" src="./func.js"></script>
12 	<script type="text/javascript" src="./renderer.js"></script>
13   	<script type="text/javascript" src="./wall.js"></script>
14   	<script type="text/javascript" src="./phen.js"></script>
15         <script type="text/javascript" src="./parser.js"></script>
16 	<script type="text/javascript" src="./console.js"></script>
17   	<script type="text/javascript" src="./player.js"></script>
18   	<script type="text/javascript" src="./cycle.js"></script>
19         <script type="text/javascript" src="./clean.js"></script>
20 </head>
21 <body>
22     <div id="screen"></div>
23     <div id="btns">
24         <input type="button" value="Open/Save" disabled="disabled">
25         <input id="getPoint" type="button" value="Get point's id">
26       	<input id="movePoint" type="button" value="Move point">
27       	<input id="demovePoint" type="button" class="active hide" value="Move point">
28       	<input id="consoleOpen" class="active" type="button" value="Console">
29     </div>
30     <div id="consoleWindow" class="show">
31         <div id="consoleInput">
32             <input id="console" type="text" autofocus="autofocus">
33             <input id="consoleBtn" type="button" value="Enter">
34         </div>
35         <div id="consoleLog">
36         </div>
37     </div>
38     <div id="controls" class="show" type="0">
39         <div id="playerBtns">
40             <span id="stop"></span>
41             <span id="stepMin"></span>
42             <span id="stepMid"></span>
43             <span id="stepMax"></span>
44         </div>
45     </div>
46 </body>
47 </html>

Исходный код [php js css].zip

Суммарно код текущей версии (v2.0 b от 2 июня 2015 года) движка занимает 2 500 строк без учёта библиотеки.

Отличие от семейства версий 1.* в полностью переписанной логике проекта для оптимизации вычислений и соблюдения принципов OOP JS.

Разбор кода файла point.js

Файл "point.js"

 1 function Point(mass, radius, coorXY, oldCoorXY, isExpToPot)
 2 {
 3     this.mass = mass;
 4     this.invmass = 1 / mass;
 5     this.radius = radius;
 6     this.coor = coorXY;
 7     this.oldCoor = oldCoorXY || coorXY;
 8     this.id = getId();
 9     this.type = 'point';
10     this.isExpToPot = isExpToPot || false;
11     
12 // log
13     console.log('Create: '+this.toString());
14 }
15 
16 
17 Point.prototype.checkCoor = function(){
18     if (this.coor.isNaN())
19     {
20         console.error('Coor is NaN: ' + this);
21         player.stop();
22     }
23     else
24     {
25         if (this.oldCoor.isNaN())
26         {
27             console.warning('OldCoor is NaN: ' + this);
28             this.oldCoor = new Coor(this.coor);
29         }
30         else
31         {
32             return ;
33         }
34     }
35 }
36 
37 Point.prototype.toString = function(type){
38     if (type === 'full')
39     {
40         return JSON.stringify(this, [   
41                                         'type',
42                                         'id',
43                                         'coor',
44                                         'mass_',
45                                         'radius_',
46                                         'x',
47                                         'y',
48                                         'begin',
49                                         'end'
50                                 ], 4)
51     }
52     else
53     {
54         return JSON.stringify(this, [   
55                                         'type',
56                                         'id',
57                                         'coor',
58                                         'x',
59                                         'y',
60                                         'begin',
61                                         'end'
62                                 ], 4)
63     }
64 }
65 
66 Point.prototype.move = function(coorXY){
67 	var delta = minusCoor(this.coor, this.oldCoor);
68     this.coor = coorXY;
69     this.oldCoor = minusCoor(coorXY, delta);
70 // log
71     console.log('Move: '+this.toString());
72 }
73 
74 Point.prototype.moveEase = function(coorXY){
75     this.coor = coorXY;
76     this.oldCoor = coorXY;
77 // log
78     console.log('Move: '+this.toString());
79 }


3 this.mass = mass;

Сообщаем нашей частице массу, равную mass.


4 this.invmass = 1 / mass;

Вводим обратную массу, равную 1/mass. В дальнейшем работаем именно с обратным значением массы, так как это позволит вводить “бесконечно тяжёлые” частицы (значение invmass можно задать отдельно впоследствие).


5 this.radius = radius;

Задаём радиус, равный radius. Измеряется в пикселях.


6 this.coor = coorXY;

Задаём текущие координаты значениями coorXY.x и coorXY.y.


7 this.oldCoor = oldCoorXY || coorXY;

Если были заданы предыдущие координаты (oldCoor.x и oldCoor.y), то записываем их. Иначе записываем текущие координаты, тогда частица будет обладать нулевым вектором скорости


8 this.id = getId();

Генерируем уникальный номер для частицы.


9 this.type = 'point';

Указываем тип элемента как “point”, чтобы обработчик отличал частицу от других объектов.


10 this.isExpToPot = isExpToPot || false;

Сообщаем, должна ли участвовать в потенциальном парном взаимодействии. Да - если isExpToPot равняется true, нет - во всех прочих случаях.


13 console.log('Create: +this.toString());

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


17 Point.prototype.checkCoor

Метод проверки значений координат положений частицы. Если текущее положение частицы не может быть определено (не верный запрос, статически неопределимая система и пр.), останавливаем симуляцию и выводим в консоль ошибку. Если не удаётся определить значения координат предыдущего положения (симуляция запущена пользователем после получения ошибки в текущих координатах частицы и пр.), выдаём предупреждение, используем координаты текущего положения также и как координаты предыдущего.


37 Point.prototype.toString

Метод, собирающий информацию о частице и выдающий её в удобном виде.


66 Point.prototype.move

Метод перемещения частицы в указанные координаты. Вектор скорости сохраняется, в консоль записывается информация о перемещении.


74 Point.prototype.moveEase

Другой метод перемещения частицы в указанные координаты. Вектор скорости зануляется, в консоль записывается информация о перемещении.

Сопроводительная информация

Общая презентация
симулятора
Подробная презентация
физического движка
в составе симулятора
Отчёт
MultiparticleSimulator.pdf
Ошибка создания миниатюры: convert convert: Not a JPEG file: starts with 0x20 0x20 (/tmp/gmZCr0Av).
Ошибка создания миниатюры: convert convert: Not a JPEG file: starts with 0x20 0x20 (/tmp/gmSRTBtL).
12 слайдов 55 слайдов 21 страница

Ссылки по теме

T. Jakobsen. "Advanced Character Physics", 2003. (перевод статьи )

Л. Ландау, Е. Лифшиц. "Теоретическая физика", том первый, "Механика", 1988.

А. Смирнов. "Курсовой проект: молекула углекислого газа", 2015. (страница проекта)

См. также

Примечания

  1. Строго говоря, мы рассматриваем проекцию трёхмерной системы на двухмерное пространство. Так, на изображении в заголовке страницы демонстрируется следующий эксперимент: есть тело в форме параллелепипеда (набор частиц со связями: пружинами), на тело сверху падает стержень. Проекцией этого тела является его слой: треугольная решётка, закреплённая на концах. Проекция стержня - частица (на изображении отсутствует).
  2. Стержни рассчитываются на расстяжение/сжатие методом коррекции координат.
    Действие пружин учитывается как действие сил упругости.
  3. 3,0 3,1 3,2 Без выделения жирным написаны необязательные параметры. При желании указать необязательный параметр все значения слева от него следует считать обязательными (во избежание путаницы при парсинге безразмерных величин в команде).
  4. Значения по умолчанию: вектор скорости нулевой, радиус равен 50 пикселям, масса равна 5 у. е.
  5. 5,0 5,1 5,2 5,3 5,4 5,5 5,6 5,7 Идентификационный номер элемента в системе. Генерируется последовательно, начиная с нуля, для стенок, стержней, пружин и частиц при их добавлении. Для частиц значение id можно найти нажатием сначала на кнопку "Get point's id", а затем на частицу: тогда Id отобразится в консоли.
  6. 6,0 6,1 6,2 6,3 Важно! Визуально действие этой команды применится только при перерисовке кадра.
  7. Жёсткость пружины по умолчанию равна 5 у. е.
  8. Важно! Отображается максимум одна таблица за раз.
  9. Запросы выводятся в всплывающем окне одной цельной командой, для воспроизведения цепочки запросов достаточно ввести эту команду в консоль.
    Важно! Учитываются только запросы, успешно введённые в консоль. Воздействия на систему при помощи кнопки "Move point" будут потеряны.
  10. При малой производительности клиента уменьшаем число отрисовок в единицу времени для сохранения гладкости анимации. Управляется через консоль.