-
Notifications
You must be signed in to change notification settings - Fork 0
/
2d.html
418 lines (417 loc) · 16.1 KB
/
2d.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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
<!doctype html>
<html lang="en">
<head>
<title>Quasicrystals</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#000000" />
<link rel="stylesheet" type="text/css" href="icons/webfont.css" />
<link rel="stylesheet" type="text/css" href="umbra.css" />
<link rel="icon" sizes="400x400" href="thumb-2d.png" />
<style type="text/css">
html, body {
width: 100%; height: 100%; margin: 0px; padding: 0px;
overflow: hidden;
}
body { background: #000; }
canvas {
position: absolute;
top: 0px; left: 0px; right: 0px; bottom: 0px;
}
.box {
background: rgba(15, 15, 15, 0.9);
box-shadow: 0em 0em 2em 0em rgba(255, 255, 255, 0.5);
}
#config .box { text-align: center; }
h2 { text-align: center; }
#help h2, #config h2 { text-align: left; }
.formline { margin: 0.5em -1em 0em; }
.formline .button { margin: 0em 0.25em; }
.license { opacity: 0.75; }
</style>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script type="text/javascript" src="hammer.min.js"></script>
<script type="text/javascript" src="umbra.min.js"></script>
<script type="text/javascript" src="anima.min.js"></script>
<script type="text/javascript" src="linear.js"></script>
<script type="text/javascript" src="latticehelper.js"></script>
<script type="text/javascript">
var fillCircle = function (context, x, y, r) {
context.beginPath();
context.arc(x, y, r, 0, Math.PI*2, true);
context.fill();
};
var DEGREE_LIMIT = 23;
var draw = function () {
window.movable.update(this.dt);
if (!window.lattice_helper) { return; }
window.movable.moveAdjust();
// Set up context
var canvas = $('#main')[0];
var w = canvas.width = $(window).width();
var h = canvas.height = $(window).height();
window.movable.setScreenCenter(canvas);
var context = canvas.getContext('2d');
var data = window.current_data;
context.save();
var gradient = context.createRadialGradient(0, 0, 0, 0, 0, 1);
gradient.addColorStop(0, '#fff');
gradient.addColorStop(0.5, '#ff0');
gradient.addColorStop(1, '#000');
context.fillStyle = gradient;
context.globalCompositeOperation = 'lighter';
context.translate(canvas.width/2, canvas.height/2);
var scale = window.screen_radius / window.options.r;
context.scale(scale, -scale);
var cull_x = 0.5 * w / scale;
var cull_y = 0.5 * h / scale;
var store = [0.0, 0.0, 0.0];
var moving = window.movable.isMoving();
var realtime = moving || this.next_id || window.movable.is_hammer_busy;
if (realtime) { this.selectLOD('realtime'); }
else { this.selectLOD('hd'); }
var limit = Math.min(data.length, this.lod.num_verts);
for (var i = 0; i < limit; i++) {
window.lattice_helper.renderPoint(data[i], store);
var xt = store[0];
var yt = store[1];
store[0] = window.movable.cos * xt + window.movable.sin * yt;
store[1] = -window.movable.sin * xt + window.movable.cos * yt;
var d2 = window.movable.dotsize * Math.exp(-0.25 * store[2] / window.variance);
if (Math.abs(store[0]) - d2 > cull_x) { continue; }
if (Math.abs(store[1]) - d2 > cull_y) { continue; }
context.save();
context.translate(store[0], store[1]);
context.scale(d2, d2);
context.fillRect(-1, -1, 2, 2);
context.restore();
}
context.restore();
this.adjustLOD();
return window.movable.isMoving();
};
var resetZoom = function () {
$('meta[name="viewport"]').attr('content', 'width=device-width, initial-scale=1');
};
window.current_n = 5;
window.current_data = [];
window.screen_radius = 1000;
window.options = {
n: 5,
r: 5,
};
$(document).ready(function () {
window.message_box = Umbra.MessageBox('messages');
window.movable = new Anima.Movable2D();
// We flip the y axis because mathematicians like the y axis up
// Then we flip both axes again to use position subtractively
// The result is to invert the x axis.
movable.key_map[Anima.KEYS.LEFT].amount *= -1;
movable.key_map[Anima.KEYS.RIGHT].amount *= -1;
movable.key_map[Anima.KEYS.UP].amount *= -1;
movable.key_map[Anima.KEYS.DOWN].amount *= -1;
movable.touch_map.pan_x.amount = 1;
movable.touch_map.pan_y.amount = 1;
movable.bindKeyboard(window);
movable.bindTouch($('#main'));
var superMoveReset = movable.moveReset;
movable.moveReset = function () {
superMoveReset.call(this);
if (window.lattice_helper) { window.lattice_helper.reset(); }
};
movable.moveZoom = function (amount) {
window.lattice_helper.zoom(amount);
this.scale = window.lattice_helper.scale[0];
};
var superMovePan = movable.movePan;
movable.movePan = function (x, y) {
superMovePan.call(this, x, y);
var lh = window.lattice_helper;
lh.offset[0] = this.position[0];
lh.offset[1] = -this.position[1];
};
movable.moveAdjust = function () {
var lh = window.lattice_helper;
if (window.current_n == 2) { lh.offset[1] = 0; }
lh.zoomAdjust();
lh.recenter();
this.position[0] = lh.offset[0];
this.position[1] = -lh.offset[1];
this.scale = lh.scale[0];
};
window.renderWorker = new Worker('projector.js');
window.renderWorker.onmessage = function (e) {
var data = e.data;
if (data.type == 'update') {
window.current_data = data.data;
window.lattice_helper.translators = data.translators;
window.variance = data.variance;
window.movable.dotsize = data.dotsize;
window.frame_manager.requestFrame();
window.requestMorePoints((window.screen_radius || 512) / Math.sqrt(data.next_weight), data.next_radius);
window.message_box.post(data.source + ': ' + data.data.length + ' ' + data.next_weight + ' ' + data.next_radius, 'debug');
} else if (data.type == 'init') {
window.lattice_helper = new LatticeHelper(
data.dim_hidden + 2, 2, [], data.scale_factors);
if (data.scale_defect > 0) {
window.message_box.post('Zoom check failed; zoom might not work correctly.', 'warning');
}
} else if (data.type == 'message') {
window.message_box.post(data.message, data.message_class);
} else {
window.message_box.post('Window ignored message of type ' + data.type, 'debug warning');
}
};
window.replaceLattice = function (n, radius) {
window.current_n = n;
// Init lattice
window.requestMorePoints = (function () {
var LIMIT_SIZE = 200;
var LIMIT_TIME = 10000;
var LIMIT_DIST = window.options.r;
var finalized = false;
var start_time = 0;
var distance_reached = 0;
var progress_bar;
var SCALE_FACTOR = 0;
return function (neighbor_size, neighbor_distance) {
if (finalized) {
$('#config_addpoints').prop('disabled', false);
if (progress_bar) { progress_bar.set(1); }
return;
}
if (!SCALE_FACTOR) {
SCALE_FACTOR = 1;
if (window.lattice_helper.scale_factors) {
SCALE_FACTOR = window.lattice_helper.scale_distortion;
}
LIMIT_DIST *= SCALE_FACTOR;
}
if (!progress_bar) {
progress_bar = window.message_box.makeProgress('Finding points...');
progress_bar.set(0.1);
}
if (!start_time) { start_time = (new Date()).valueOf(); }
if (neighbor_distance > distance_reached) {
distance_reached = neighbor_distance;
}
var progress = Math.max(0,
((new Date()).valueOf() - start_time)/LIMIT_TIME,
(Math.min(LIMIT_SIZE/neighbor_size, 1) +
Math.min(distance_reached/LIMIT_DIST, 1.9)) / 2);
window.message_box.post(LIMIT_SIZE/neighbor_size + ', ' + distance_reached/LIMIT_DIST, 'debug');
progress_bar.set(0.1 + progress * 0.8);
if (progress >= 1) {
finalized = true;
}
window.renderWorker.postMessage({type: 'addVerts'});
};
})();
window.renderWorker.postMessage({
type: 'setSymmetry',
"n": n,
});
window.renderWorker.postMessage({type: 'addVerts'});
window.movable.moveReset();
resetZoom();
resizeCallback();
};
window.frame_manager = new Anima.FrameManager(draw);
window.frame_manager.upLOD = function () {
if (this.lod.num_verts < window.current_data.length) {
this.lod.num_verts = Math.min(window.current_data.length,
1 + Math.floor(this.lod.num_verts * 6 / 5));
this.requestFrame();
}
};
window.frame_manager.downLOD = function () {
this.lod.num_verts = Math.min(window.current_data.length,
Math.ceil(this.lod.num_verts * 6 / 7));
};
window.frame_manager.addLOD('realtime', {
frame_min: 8, frame_max: 16, num_verts: 50,
});
window.frame_manager.addLOD('hd', {
frame_min: 250, frame_max: 500, num_verts: 50000,
});
var resizeCallback = function () {
var r = window.screen_radius =
Math.sqrt(window.innerWidth * window.innerWidth +
window.innerHeight * window.innerHeight)/2;
var s = window.movable.units_per_px = window.options.r / r;
window.movable.touch_map.pan_x.amount = s;
window.movable.touch_map.pan_y.amount = s;
window.frame_manager.requestFrame();
};
movable.motionCallback = function () {
window.frame_manager.requestFrame();
};
$(window).on('resize', resizeCallback);
// Activate the form
var replacement_form = new Umbra.Form($('#form_config'));
replacement_form.field_processors.n = function (field) {
var ans = Umbra.Form.numericProcessor(field);
if (ans[0]) { return ans; }
if (ans[1] < 2) { return [true, 'Enter a number at least 2.']; }
if (ans[1] > DEGREE_LIMIT) { return [true,
'Degrees higher than ' + DEGREE_LIMIT +' not supported.']; }
return ans;
};
replacement_form.actions.push(function (e, data) {
e.preventDefault();
replaceLattice(data.n);
window.location = '#';
});
// Set initial lattice
replaceLattice(5);
});
</script>
</head>
<body>
<canvas id="main">
<img src="thumb-2d.png" alt="Preview" />
This demo requires HTML5 Canvas support.
</canvas>
<div id="messages" class="messagebox"></div>
<div id="config" class="overlay"><div class="box">
<h2>Choose symmetry</h2>
<form method="post" action="#" id="form_config"><div class="formline">
<a class="button icon button_close" title="Cancel" href="#">×</a>
<label for="config_n" class="icon">✣</label>
<input type="text" class="numeric valid" id="config_n" name="n" value="5" />
<button type="submit" class="button icon"
id="config_submit" title="Redraw">»</button>
</div></form>
</div></div>
<div id="help" class="overlay">
<div class="box">
<a class="button icon button_close"
title="Close" href="#">×</a>
<h2>Help</h2>
<p><a href="#help_about">What am I seeing here?</a></p>
<ul>
<li><b>Move</b>: arrow keys and
<span class="keyboard-key"><<sub>,</sub></span>,
<span class="keyboard-key">><sub>.</sub></span>
or mouse/finger dragging.</li>
<li><b>Zoom</b>: <span class="keyboard-key">+</span> and
<span class="keyboard-key">-</span>, mouse scrolling,
or pinch zoom.</li>
</ul>
<p class="license">Code written by
<a href="http://pteromys.melonisland.net/">Pteromys</a>
and released under the <a href="http://unlicense.org/">Unlicense</a>.</p>
</div>
</div>
<div id="help_about" class="overlay">
<div class="box">
<a class="button icon button_back"
title="Back" href="#help">«</a>
<a class="button icon button_close"
title="Close" href="#">×</a>
<h2>About</h2>
<p>This page draws
<a href="http://en.wikipedia.org/wiki/Quasicrystal"
>quasicrystalline</a> patterns by slicing
and projecting higher-dimensional
<a href="http://en.wikipedia.org/wiki/Lattice_(group)"
>lattices</a> into the plane.
Points are sized by their proximity to the slice.</p>
<p>Thanks to the
<a href="http://en.wikipedia.org/wiki/Crystallographic_restriction_theorem"
>crystallographic restriction theorem</a>, rotational
symmetry of order other than 1, 2, 3, 4, or 6 requires a lattice of
dimension higher than 2. In fact, to have a 2-D plane that rotates
with order \(n\), the required dimension of the lattice is
<a href="http://en.wikipedia.org/wiki/Euler%27s_totient_function"
>\(\phi(n)\)</a>. (<a href="#help_dimension">Why?</a>)
</p>
<p>The patterns have some self-similarity—they look the same
on high and low magnification. This takes its simplest form
when \(n = 5\); then it has a direct analogue in the
<a href="3d.html#help_selfsimilarity">3-D version</a>
and is related to the game
<a href="http://www.math.brown.edu/~res/Java/App12/test1.html"
>Lucy and Lily</a>.
But when \(n\) is larger, <a href="#help_zooming">it
gets more complicated</a>.</p>
</div>
</div>
<div id="help_dimension" class="overlay">
<div class="box">
<a class="button icon button_back"
title="Back" href="#help_about">«</a>
<a class="button icon button_close"
title="Close" href="#">×</a>
<h2>Dimension</h2>
<p>A rotation preserving a lattice can be written as an
integer matrix. If it rotates a 2-D plane with order \(n\),
one of its eigenvalues there is a primitive \(n\)th
<a href="http://en.wikipedia.org/wiki/Root_of_unity"
>root of unity</a> \(\zeta_n\).
Then all \(\phi(n)\) of the other primitive \(n\)th roots of
unity—i.e. all of \(\zeta_n\)'s
<a href="http://en.wikipedia.org/wiki/Conjugate_element_(field_theory)"
>Galois conjugates</a>—are eigenvalues too,
so the matrix's size is at least \(\phi(n)\).</p>
</div>
</div>
<div id="help_zooming" class="overlay">
<div class="box">
<a class="button icon button_back"
title="Back" href="#help_about">«</a>
<a class="button icon button_close"
title="Close" href="#">×</a>
<h2>Zooming</h2>
<p>To keep the number of dots drawn roughly constant,
as we expand the plane we also uniformly shrink the
unseen dimensions. This way as the dots spread out,
more dots come in from outside to fill in the gaps.</p>
<p>Self-similarity occurs because there are transformations
(realizable as
<a href="http://en.wikipedia.org/wiki/Circular_shift"
>circular shifts</a> in \(n\) dimensions) taking dots
to other dots while scaling the visible two dimensions
in place. So the view before and after look the same, yet
they differ by some scaling in the visible dimensions!</p>
<p>If \(n = 5, 8, 10, \) or \(12\), or in the
<a href="3d.html">3-D version</a>,
then this is the full story—exactly as
many dimensions are hidden from view as are visible;
and such a transformation scales them by reciprocal
amounts, exactly undoing some level of zoom.</p>
<p>Taking dots to dots requires that
this zoom level is a real unit in the
<a href="http://en.wikipedia.org/wiki/Ring_of_integers"
>ring of integers</a> of
<a href="http://en.wikipedia.org/wiki/Cyclotomic_field"
>\(\mathbf{Q}(e^{2 \pi i /n})\)</a>,
and that the zoom levels in the hidden dimensions are its
<a href="http://en.wikipedia.org/wiki/Conjugate_element_(field_theory)"
>Galois conjugates</a>, with the same
multiplicity. When \(n\) is not one of the values
named above, the visible and hidden dimensions
don't match in number; so this kind of transformation
can't scale all the hidden dimensions uniformly!</p>
<p>Happily,
<a href="http://en.wikipedia.org/wiki/Dirichlet%27s_unit_theorem"
>Dirichlet's unit theorem</a> ensures there
are enough units (thus enough transformations) to
approximate any level of uniform zooming
with bounded error. What results is a coarser sort of
self-similarity, where small scales look like large
scales if some distortion in the unseen dimensions
is acceptable.</p>
<p>Of course, finding a complete set of
units is tricky; this program as written only looks for
<a href="http://en.wikipedia.org/wiki/Cyclotomic_unit"
>cyclotomic units</a>, which is good enough
for \(n < 34\).</p>
</div>
</div>
<a class="button icon button_root" data-key="QUESTION"
href="#help" id="button_help" title="Help">?</a>
<a class="button icon button_root" data-key="SPACE"
href="#config" id="button_config" title="Settings">⚙</a>
</body>
</html>