КП: Многочастичный симулятор
Курсовой проект по Теоретической механике
Исполнитель: Старобинский Егор
Группа: 09
Семестр: весна 2015
Содержание
Аннотация проекта[править]
Рассматриваемой задачей этого проекта является определение поведения задаваемой механической двухмерной системы[1] из частиц, сил и ограничений. Для этого был создан интернет-сайт с программой, позволяющей найти решение уравнения движения системы (физический движок). Также на базе этой программы был реализован пользовательский интерфейс с возможностями создания и редактирования исходной системы и визуализацией её поведения.
В процессе решения широко применялись знания из классической механики и программирования, благодаря чему была реализована возможность моделирования тел из большого числа частиц и ограничений (интерес с точки зрения механики) и консоль управления с упрощёнными интерпретируемыми командами (интерес с точки зрения программирования). Был внедрён ряд математических методов как для вычисления решения уравнения движения, так и для анализа разрешённой системы. Были проведены проверки получившегося движка на предварительно решённых задачах.
Научная новизна
Созданный движок позволяет человеку без специальных знаний в области программирования проводить моделирование собственных систем. Не требуется установки никакого дополнительного софта, программа запускается при помощи браузера как на компьютерах, так и на телефонах, планшетах, телевизорах класса Smart.
Визуализация не применяет технологию WebGL, благодаря чему многократно увеличивается диапазон устройств, способных запустить сайт с полноценной функциональностью.
Применён метод нахождения периода движения частицы по участку траектории путём последовательного разбиения поля на сетки с возрастающей плотностью ячеек (идея позаимствована из теории слов).
Движок представляет все тела как набор частиц со связями и применяет базовый двухшаговый метод численного интегрирования Верле и другие математические методы для разрешения уравнения движения в короткое время.
По возможности упрощено интегрирование в систему новых алгоритмов анализа (требует знаний в программировании на javascript как для реализации алгоритма, так и для его интегрирования), при этом все пользователи оперируют только актуальной версией программы.
Объединение физического движка и пользовательского интерфейса получило название "Многочастичный симулятор", имеет открытый исходный код и выполнено без использования готовых решений по теме проекта.
Формулировка задачи[править]
Цель работы[править]
Создание интернет-сайта, позволяющего пользователю моделировать многоточечную систему онлайн.
Решаемые задачи[править]
- решение уравнения движения;
- визуализация.
Общие сведения по теме[править]
Уравнение движения[править]
Пусть мы наблюдаем тело в момент времени
.Хотим знать, где окажется тело через малое изменение времени -
.Рассмотрим базовый метод интегрирования Верле:
, где
- позиция точки,
- равнодействующая всех сил, действующих на тело,
- масса тела,
- текущий момент времени,
- малое изменение времени.
Метод Верле позволяет вычислять траекторию по упрощённой схеме: зная предыдущее и текущее положения (
и соответственно) и мгновенное значение равнодействующей приложенных сил в текущем положении .Достоинства метода: самокоррекция и бóльшая точность по сравнению с численным методом Эйлера.
Язык реализации: JavaScript.
Визуализация[править]
Язык рализации: pure SCSS.
Обработка событий: JavaScript.
Манипуляции с DOM: jQuery (безболезненно заменяется на Zepto).
Отказ от WebGL продиктован выбором методов оптимизации для возможности работы с тысячами частиц.
Решение[править]
Результат[править]
Вы можете попробовать возможности симулятора прямо здесь, либо перейдя на полноразмерную страницу решения.
В пункте 4.7 представлен пример готовой команды для создания трёхатомной молекулы.
Элементы системы[править]
- Частицы;
- Стержни и пружины[2];
- Стенки;
- Поле сил;
- Рабочее окно;
- Сетки разметки;
- Консоль;
- Плеер.
Возможности консоли[править]
- Конфигурация начальной системы тел;
- Изменение системы в процессе работы ("на лету");
- Запуск алгоритмов анализа системы;
- Распознавание и вывод ошибок в пользовательских запросах и в исходном коде;
- Распознавание и вывод предупреждений в пользовательских запросах и в исходном коде;
- Подключение/отключение сеток разметки, в том числе с пользовательскими размерами ячейки;
- Тетрис.
Команды консоли[править]
Координаты пишутся в декартовой системе (х,у), единица измерения - пиксели, орт х направлен от левого края к правому, орт у от верхнего края к нижнему. Пример: (0,100) - координаты точки, лежащей на левом краю экрана на 100 пикселей ниже верхней границы.
Консоль выводит каждое сообщение как одну строку. Если доступной длины строки не хватает, сообщение обрезается. Для отображения полной версии сообщения необходимо кликнуть по нему мышью.
Примеры основных запросов
Очистить поле консоли
- clrscr
Отобразить статистику элементов системы и число тиков:
- getInfo
Создать частицу(100,100) - текущие координаты
(0,10) - вектор скорости относительно начала координат
80 - радиус частицы в пикселях
5 - масса частицы в у. е.
Задать вектор скорости (относительно начала координат)
- setVelocity #0 (10,10), где
0 - id частицы[5]
(10,10) - новый вектор скорости относительно начала координат
Переместить частицу (относительно начала координат)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 - команды консоли, могут также состоять из объединённых команд
Возможности плеера[править]
- Воспроизведение/пауза симуляции с заданным ;
- Скачок вперёд на кратное время;
- "Замедление времени"[10].
Кнопки интерфейса[править]
- Кнопка Get point's id
После нажатия на кнопку, а затем на частицу выводит id последней в консоль.
- Кнопка Move point[6]
Эквивалентна команде консоли movePoint без флага saveV.
После нажатия на кнопку, а затем на частицу закрепляет управление положением последней за курсором мыши.
Дальнейшие нажатия на свободные участки поля переносят эту частицу в точку нажатия, при этом её скорость считается нулевой.
Для прекращения управления следует вновь нажать исходную кнопку.
- Клавиша клавиатуры ~ (также `, ё, Ё)
Эквивалентна командам консоли play/stop и кнопкам ► / | | плеера.
- Кнопки плеера
► / | | - воспроизведение/остановка симуляции (эквивалентна командам консоли play/stop).
► - переход на тик вперёд (эквивалентна команде консоли step(1)).
►► - переход на 50 тиков вперёд (эквивалентна команде консоли step(50)).
►| - переход на 100 тиков вперёд (эквивалентна команде консоли step(100)).
Пример системы[править]
Смоделируем молекулу из трёх атомов (подобную молекуле углекислого газа, рассматриваемой в курсовом проекте А. Смирнова).
Для создания такой системы необходимо последовательно выполнить следующие команды:
Создаём атомы кислорода, углерода и кислорода соответственно.
- 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>
Суммарно код текущей версии (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
Другой метод перемещения частицы в указанные координаты. Вектор скорости зануляется, в консоль записывается информация о перемещении.
Сопроводительная информация[править]
Ссылки по теме[править]
T. Jakobsen. "Advanced Character Physics", 2003. (перевод статьи )
Л. Ландау, Е. Лифшиц. "Теоретическая физика", том первый, "Механика", 1988.
А. Смирнов. "Курсовой проект: молекула углекислого газа", 2015. (страница проекта)
См. также[править]
Примечания[править]
- ↑ Строго говоря, мы рассматриваем проекцию трёхмерной системы на двухмерное пространство. Так, на изображении в заголовке страницы демонстрируется следующий эксперимент: есть тело в форме параллелепипеда (набор частиц со связями: пружинами), на тело сверху падает стержень. Проекцией этого тела является его слой: треугольная решётка, закреплённая на концах. Проекция стержня - частица (на изображении отсутствует).
- ↑ Стержни рассчитываются на расстяжение/сжатие методом коррекции координат.
Действие пружин учитывается как действие сил упругости. - ↑ 3,0 3,1 3,2 Без выделения жирным написаны необязательные параметры. При желании указать необязательный параметр все значения слева от него следует считать обязательными (во избежание путаницы при парсинге безразмерных величин в команде).
- ↑ Значения по умолчанию: вектор скорости нулевой, радиус равен 50 пикселям, масса равна 5 у. е.
- ↑ 5,0 5,1 5,2 5,3 5,4 5,5 5,6 5,7 Идентификационный номер элемента в системе. Генерируется последовательно, начиная с нуля, для стенок, стержней, пружин и частиц при их добавлении. Для частиц значение id можно найти нажатием сначала на кнопку "Get point's id", а затем на частицу: тогда Id отобразится в консоли.
- ↑ 6,0 6,1 6,2 6,3 Важно! Визуально действие этой команды применится только при перерисовке кадра.
- ↑ Жёсткость пружины по умолчанию равна 5 у. е.
- ↑ Важно! Отображается максимум одна таблица за раз.
- ↑ Запросы выводятся в всплывающем окне одной цельной командой, для воспроизведения цепочки запросов достаточно ввести эту команду в консоль.
Важно! Учитываются только запросы, успешно введённые в консоль. Воздействия на систему при помощи кнопки "Move point" будут потеряны. - ↑ При малой производительности клиента уменьшаем число отрисовок в единицу времени для сохранения гладкости анимации. Управляется через консоль.