-
Notifications
You must be signed in to change notification settings - Fork 1
/
program-4.html
338 lines (296 loc) · 7.85 KB
/
program-4.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
<!DOCTYPE html>
<html>
<head>
<title>Program 4</title>
<style>
.col-group > div {
padding: 0;
margin: 0;
margin-bottom: 1em;
}
@media screen and (min-width: 44em) {
.col-group {
overflow: hidden;
}
.col-group > div {
float: left;
width: 50%;
}
}
table {
width: 100%;
}
td {
width: 50%;
text-align: center;
}
th {
text-align: center;
font-style: italic;
font-weight: normal;
}
svg {
width: calc(100% - 0.2em);
border: 0.1em solid #aaa;
}
circle {
fill: #aaa;
stroke: black;
stroke-width: 1px;
}
rect {
fill: #000;
}
button {
width: 100%;
height: 4em;
}
</style>
</head>
<body>
</body>
<script type="text/javascript">
/*
Program 4 - Push protocols in models
So far all our models have notified views that something has changed
and left it to the views to figure out what needs to be done. For
some cases this will not work, in particular if the model
is changing steadily with time and views need to always display
a consistent view of it. In this case we have the model's calls to
notify push the necessary state for the views to update themselves
so we can ensure that all views receive the same data.
Our example will be a simulation of a ball bouncing back and forth on
a one dimensional track, with elastic reflections at the walls.
We will have two views, one showing the balls bouncing back and forth,
and one showing its current coordinates and velocity. We will also
provide controls to pause and resume the simulation.
Our Observable changes slightly, since notify now needs to pass
information to update.
*/
var Observable = {
initializeObservable: function() {
this.observers = new Set();
},
addObserver: function(observer) {
this.observers.add(observer);
},
deleteObserver: function(observer) {
this.observers.delete(observer);
},
notify: function(message) {
this.observers.forEach(function(observer) {
observer.update(message);
});
}
};
/*
Our model's state will consist of the coordinates and
velocities of the ball, its radius, and the time at which those
values were valid. We provide methods to step the simulation,
and to pause and resume it.
*/
var Ball = Object.create(Observable);
Ball.initializeObservable();
Ball.radius = 50;
Ball.trackLength = 1000;
Ball.x = Math.random()*(Ball.trackLength-2*Ball.radius) + Ball.radius;
Ball.v = 0.25;
Ball.t = null;
Ball.timer = null;
Ball.step = function(dt) {
// Calculate the amount of time to advance our
// simulation, and update the time on the model.
var r = this.radius;
var x = this.x;
var v = this.v;
var L = Ball.trackLength;
if (x + v*dt < r) {
// Bounce off left wall
dtAfterBounce = dt - (r - x)/v;
this.v = -v;
this.x = r + dtAfterBounce*this.v;
} else if (x + v*dt >= L - r) {
// Bounce off right wall
dtAfterBounce = dt - (L - r - x)/v;
this.v = -v;
this.x = L-r + dtAfterBounce*this.v;
} else {
// No bounce
this.x = x + v*dt;
}
this.t = new Date(this.t.valueOf() + dt);
this.notify({x: this.x, v: this.v, dt: dt, t: this.t});
};
Ball.play = function() {
this.t = new Date();
var f = function() {
var tprime = new Date();
var dt = tprime - this.t;
Ball.step(dt);
Ball.timer = window.setTimeout(f, 10);
}.bind(this);
this.notify({x: this.x, v: this.v, dt: 0, t: this.t});
f();
};
Ball.pause = function() {
if (Ball.timer) {
window.clearTimeout(Ball.timer);
Ball.timer = null;
}
this.notify({x: this.x, v: this.v, dt: 0, t: this.t});
};
Ball.isPlaying = function() {
return Ball.timer !== null;
}
/*
We use the same two column layout that we used for the calculator as
our root element.
*/
var TwoColumns = {
leftSubviews: [],
rightSubviews: [],
show: function() {
var fragment = document.createDocumentFragment();
var div = document.createElement('div');
div.setAttribute('class', 'col-group');
fragment.appendChild(div);
var leftDiv = document.createElement('div');
this.leftSubviews.forEach(function(subview) {
var el = document.createElement('div');
leftDiv.appendChild(el);
subview.writeOver(el);
}, this);
div.appendChild(leftDiv);
var rightDiv = document.createElement('div');
this.rightSubviews.forEach(function(subview) {
var el = document.createElement('div');
rightDiv.appendChild(el);
subview.writeOver(el);
}, this);
div.appendChild(rightDiv);
document.body.appendChild(fragment);
},
addLeft: function(view) {
this.leftSubviews.push(view);
},
addRight: function(view) {
this.rightSubviews.push(view);
}
};
/*
Next we write the display of the bouncing ball in the left
column, along with play and pause buttons.
*/
var svgns = "http://www.w3.org/2000/svg";
var BallCanvas = {
writeOver: function(el) {
var svg = document.createElementNS(svgns, 'svg');
this.svg = svg;
var ball = document.createElementNS(svgns, 'circle');
this.ball = ball;
svg.appendChild(ball);
el.parentNode.replaceChild(svg, el);
},
update: function(msg) {
var rects = this.svg.getClientRects();
if (rects.length === 0) {
return;
}
// TODO: Use a g element with a transform to scale
// this instead of doing it by hand. Switch the cy, r,
// and height updates to not happen on each model update.
var width = rects[0].width;
var scale = width/Ball.trackLength;
var svgX = scale*msg.x;
this.ball.setAttributeNS(null, 'cx', scale*msg.x);
this.ball.setAttributeNS(null, 'cy',
scale*1.2*Ball.radius);
this.ball.setAttributeNS(null, 'r',
scale*Ball.radius);
this.svg.setAttributeNS(null, 'height',
scale*2.4*Ball.radius);
}
};
Ball.addObserver(BallCanvas);
TwoColumns.addLeft(BallCanvas);
var PlayPauseButton = {
button: null,
label: function() {
if (Ball.isPlaying()) {
return 'Pause';
} else {
return 'Play';
}
},
writeOver: function(el) {
var button = document.createElement('button');
button.textContent = this.label();
button.addEventListener('click', function() {
if (Ball.isPlaying()) {
Ball.pause();
} else {
Ball.play();
}
button.textContent = this.label();
});
this.button = button;
el.parentNode.replaceChild(button, el);
},
update: function() {
if (this.button) {
var label = this.label();
if (this.button.textContent !== label) {
this.button.textContent = label;
}
}
}
}
TwoColumns.addLeft(PlayPauseButton);
Ball.addObserver(PlayPauseButton);
/*
In the right column, we render a table showing the coordinates
of each ball, plus controls to delete a particular ball and
a button to add a new ball at a random location and velocity.
*/
var BallCoordinates = {
writeOver: function(el) {
var table = document.createElement('table');
var tr = document.createElement('tr');
table.appendChild(tr);
var td = document.createElement('th');
td.textContent = 'Position';
tr.appendChild(td);
var td = document.createElement('th');
td.textContent = 'Velocity';
tr.appendChild(td);
tr = document.createElement('tr');
table.appendChild(tr);
td = document.createElement('td');
tr.appendChild(td);
this.xView = td;
td = document.createElement('td');
tr.appendChild(td);
this.vView = td;
el.parentNode.replaceChild(table, el);
},
update: function(msg) {
if (this.xView) {
var txt = msg.x.toLocaleString(
{maximumSignificantDigits: 3});
this.xView.textContent = txt;
}
if (this.vView) {
var txt = msg.v.toLocaleString(
{maximumSignificantDigits: 3});
this.vView.textContent = txt;
}
}
}
TwoColumns.addRight(BallCoordinates);
Ball.addObserver(BallCoordinates);
document.addEventListener('DOMContentLoaded', function() {
TwoColumns.show();
Ball.play();
});
</script>
</html>