1 <!DOCTYPE HTML PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns=
"http://www.w3.org/1999/xhtml">
4 <title>Metaballs
</title>
5 <style type=
"text/css">
7 background-color: #232220;
13 <canvas id=
"can"></canvas>
16 <script type=
"text/javascript">
18 /** rnd() random value from 0 to 1
19 * rnd(n) random from 0 to n
20 * rnd(n, m) random between n and m
21 * rnd(array) random element
24 switch (arguments
.length
) {
28 if (a
instanceof Array
)
29 return a
[Math
.floor(Math
.random() * a
.length
)];
31 return a
* Math
.random();
32 default: // case 2 actually
33 return a
+ (b
- a
) * Math
.random();
38 return Math
.floor(rnd
.apply(null, arguments
));
44 <script type=
"text/javascript">
46 function Point(x
, y
) {
51 Point
.prototype.toString = function () { return this.x
+ ',' + this.y
}
53 Point
.prototype.equal = function (p
) {
54 return this.x
== p
.x
&& this.y
== p
.y
;
57 Point
.prototype.times = function (n
) {
58 return new Point(this.x
* n
, this.y
* n
);
61 Point
.prototype.add = function (dx
, dy
) {
62 return new Point(this.x
+ dx
, this.y
+ dy
);
65 Point
.prototype.plus = function (p
) {
66 return new Point(this.x
+ p
.x
, this.y
+ p
.y
);
72 <script type=
"text/javascript">
74 function GridPoint(i
, j
) {
79 var sq32
= Math
.sqrt(3) / 2;
81 function world(i
, j
) {
82 if (arguments
.length
== 1) {
87 return new Point(j
* sq32
, i
- j
/ 2);
91 if (arguments
.length
== 1) {
98 return new GridPoint(y
+ j
/ 2, j
);
101 function MakeData(extremes
, initFn
) {
103 var minmax
= extremes
.reduce(function(current
, point
) {
107 return current
== null
110 Math
.min(current
[0], i
),
111 Math
.min(current
[1], j
),
112 Math
.max(current
[2], i
),
113 Math
.max(current
[3], j
)
117 var mini
= minmax
[0];
118 var minj
= minmax
[1];
119 var maxi
= minmax
[2];
120 var maxj
= minmax
[3];
122 // x, y (getStore(origin) -> 0 0)
123 var origin
= [-mini
, -minj
];
126 var n
= maxi
- mini
+ 1;
127 var m
= maxj
- minj
+ 1;
136 row
.push(initFn(new GridPoint(i
- origin
[1], j
- origin
[0])));
143 return [origin
, data
];
146 function MakeHexStore(extremes
, initFn
, cloneFn
) {
147 var originData
= MakeData(extremes
, initFn
);
149 return new HexStore(originData
[0], originData
[1], initFn
, cloneFn
);
152 function HexStore(origin
, data
, initFn
, cloneFn
) {
153 this.origin
= origin
;
155 this.initFn
= initFn
;
156 this.cloneFn
= cloneFn
;
159 HexStore
.prototype.set = function (pos
, value
) {
160 var n
= this.data
.length
;
161 var m
= this.data
[0].length
;
163 var xo
= this.origin
[0];
164 var yo
= this.origin
[1];
169 if (i
>= 0 && j
>= 0 && i
< n
&& j
< m
) this.data
[i
][j
] = value
;
172 HexStore
.prototype.forEach = function (processFn
) {
174 var n
= this.data
.length
;
175 var m
= this.data
[0].length
;
177 var xo
= this.origin
[0];
178 var yo
= this.origin
[1];
184 if (this.data
[i
][j
] != null)
185 processFn(this.data
[i
][j
], new GridPoint(i
- yo
, j
- xo
));
198 [/*down-right*/ 1, 0],
202 [/*down-left*/ -1, -1],
206 function Adjacent(dir
, cell
) {
211 HexStore
.prototype.get = function (pos
, dir
) {
212 var i
= pos
.i
+ this.origin
[1];
213 var j
= pos
.j
+ this.origin
[0];
215 var data
= this.data
;
216 var n
= this.data
.length
;
217 var m
= this.data
[0].length
;
219 var id
= i
+ dirsdeltas
[dir
][1];
220 var jd
= j
+ dirsdeltas
[dir
][0];
222 return (id
>= 0 && jd
>= 0 && id
< n
&& jd
< m
) ? this.data
[id
][jd
] : null
225 HexStore
.prototype.adjacent = function (pos
) {
227 var i
= pos
.i
+ this.origin
[1];
228 var j
= pos
.j
+ this.origin
[0];
230 var data
= this.data
;
231 var n
= this.data
.length
;
232 var m
= this.data
[0].length
;
234 return dirsdeltas
.map(function (d
, dir
) {
241 return new Adjacent(dir
, (id
>= 0 && jd
>= 0 && id
< n
&& jd
< m
) ? data
[id
][jd
] : null);
246 HexStore
.prototype.copy = function () {
248 var n
= this.data
.length
;
249 var m
= this.data
[0].length
;
253 for (var i
= 0; i
< n
; ++i
) {
255 for (var j
= 0; j
< m
; ++j
) {
256 var val
= this.data
[i
][j
];
257 row
.push(val
== null ? null : this.cloneFn(val
));
262 return new HexStore(this.origin
, copy
, this.initFn
, this.cloneFn
);
265 HexStore
.prototype.nextGen = function (cellCellStoreStoreFunc
) {
267 var newStore
= this.copy();
269 var n
= this.data
.length
;
270 var m
= this.data
[0].length
;
272 var xo
= this.origin
[0];
273 var yo
= this.origin
[1];
275 for (var i
= 0; i
< n
; ++i
) {
276 for (var j
= 0; j
< m
; ++j
) {
277 var pos
= new GridPoint(i
- yo
, j
- xo
);
278 cellCellStoreStoreFunc(this.data
[i
][j
], newStore
.data
[i
][j
], pos
, this, newStore
);
286 <!-- Fullscreen canvas and drawhex -->
287 <script type=
"text/javascript">
288 function fullscreenCanvas(id
) {
289 var c
= window
.document
.getElementById(id
);
290 var ctx
= c
.getContext('2d');
292 ctx
.canvas
.width
= window
.innerWidth
;
293 ctx
.canvas
.height
= window
.innerHeight
;
294 ctx
.canvas
.style
.position
= 'absolute';
295 ctx
.canvas
.style
.top
= 0;
296 ctx
.canvas
.style
.left
= 0;
301 var ctx
= fullscreenCanvas('can');
302 var w
= ctx
.canvas
.width
;
303 var h
= ctx
.canvas
.height
;
305 var q
= 1 / Math
.sqrt(3);
307 ctx
.pathHex = function (h
) {
312 this.moveTo(2 * dx
, 0);
313 this.lineTo(dx
, -dy
);
314 this.lineTo(-dx
, -dy
);
315 this.lineTo(-2 * dx
, 0);
316 this.lineTo(-dx
, dy
);
321 ctx
.fillHex = function (x
, y
, h
) {
323 this.translate(x
, y
);
329 ctx
.strokeHex = function (x
, y
, h
) {
331 this.translate(x
, y
);
337 ctx
.translate(ctx
.canvas
.width
/ 2, ctx
.canvas
.height
/ 2);
342 <script type=
"text/javascript">
343 /* Hexagonal area with same tiling properties as underlying cells.
345 function SuperCell(x
, y
, rank
) {
351 SuperCell
.prototype.getCorners = function () {
354 return [new Point(2 * r
, r
- 1),
357 new Point(r
- 1, 2 * r
),
358 new Point(-r
+ 1, r
+ 1),
360 new Point(-2 * r
, -r
),
361 new Point(-2 * r
, -r
- 1),
362 new Point(-r
- 1, -2 * r
),
363 new Point(-r
, -2 * r
),
365 new Point(r
+ 1, -r
+ 1)
369 SuperCell
.prototype.contains = function (point
) {
370 var x
= point
.j
- this.x
;
371 var y
= point
.i
- this.y
;
373 var a
= this.rank
* 3 + 1;
374 var b
= this.rank
* 3;
376 var leftup
= 2 * x
+ b
;
377 var top
= (x
+ a
) / 2;
379 var rightdown
= 2 * x
- a
;
380 var bottom
= (x
- b
) / 2;
381 var leftdown
= -x
- a
;
394 <script type=
"text/javascript">
397 function hcy2rgb(h
, c
, y
, a
) {
406 var k
= (1 - Math
.abs((h
% 2) - 1));
408 var K
= h
< 1 ? r
+ k
* g
417 if (y
<= 0 || y
>= 1) cmax
= 0;
418 else cmax
*= K
< y
? (y
- 1) / (K
- 1) : K
> y
? y
/ K
: 1;
420 c
= Math
.min(c
, cmax
);
423 var rgb
= h
< 1 ? [c
, x
, 0]
430 var m
= y
- (r
* rgb
[0] + g
* rgb
[1] + b
* rgb
[2]);
432 var rgbdata
= [rgb
[0] + m
, rgb
[1] + m
, rgb
[2] + m
];
433 return 'rgba(' + (rgbdata
[0] * 255).toFixed(0) + ',' + (rgbdata
[1] * 255).toFixed(0) + ',' + (rgbdata
[2] * 255).toFixed(0) + ', ' + (a
|| 1) + ')';
440 <script type=
"text/javascript">
441 function Ball(pos
, r
, v
) {
448 <script type=
"text/javascript">
451 var supercell
= new SuperCell(0, 0, rank
);
452 var extremes
= supercell
.getCorners();
454 function dist(p
, q
) {
455 var dx
= (p
.x
- q
.x
);
456 var dy
= (p
.y
- q
.y
);
457 return Math
.sqrt(dx
* dx
+ dy
* dy
);
460 var zero
= new Point(0, 0);
462 var zoom
= Math
.min(w
, h
) / supercell
.getCorners().reduce(function (max
, c
) { return Math
.max(max
, dist(zero
, c
)) }, 0)*sq32
;
464 function CellValue(v
) {
468 function CloneCellValue(arg
) {
469 return new CellValue(arg
.v
);
472 var tau
= 2 * Math
.PI
;
476 var maxd
= supercell
.getCorners().reduce(function (max
, c
) { return Math
.max(max
, dist(zero
, c
)) }, 0) / 2;
485 // generate metaballs
486 for (var i
= 0; i
< rnd(minballs
, maxballs
) ; ++i
) {
488 var d
= rnd(mind
, maxd
);
489 var pos
= new Point(d
* cos(a
), d
* sin(a
));
491 var v
= new Point(mind
* cos(a
) / 100, mind
* sin(a
) / 100);
492 var r
= rnd(minr
, maxr
);
494 balls
.push(new Ball(pos
, r
, v
));
497 function MakeCellValue(arg
) {
498 if (supercell
.contains(arg
)) {
499 return new CellValue(0);
505 var store
= new MakeHexStore(supercell
.getCorners(), MakeCellValue
, CloneCellValue
);
511 new Point(-sq32
, .5),
512 new Point(-sq32
, -.5)
515 function getk(a
, b
) {
519 var enableLepr
= true;
522 function draw(store
, clear
) {
524 function z(a
, b
, c
) {
525 return (a
>=1 && b
< 1 && c
< 1) || (a
< 1 && b
>= 1 && c
>= 1);
528 function calcpoint(a
, b
, k
) {
532 return a
.plus(b
.plus(a
.times(-1)).times(k
))
535 function line(dc
, dk
, a1
, a2
, b1
, b2
, ka
, kb
) {
536 if (!enableLepr
) ka
= kb
= .5;
538 var a
= calcpoint(dk
+ a1
, dk
+ a2
, ka
);
539 var b
= calcpoint(dk
+ b1
, dk
+ b2
, kb
);
542 ctx
.moveTo(a
.x
, a
.y
);
543 ctx
.lineTo(b
.x
, b
.y
);
547 function getk(v0
, v1
){
548 return (1 - v0
) / (v1
- v0
);
551 function triangle(a
, b
, c
, t
, x
) {
552 var va
= (a
== null ? 0 : a
.v
) / t
;
553 var vb
= (b
== null ? 0 : b
.v
) / t
;
554 var vc
= (c
== null ? 0 : c
.v
) / t
;
560 if ((a
== 0 && b
== 0 && c
== 0) || (a
!= 0 && b
!= 0 && c
!= 0)) return;
562 ctx
.strokeStyle
= hcy2rgb(360*t
, 1, .5, 1);
564 if (z(a
, b
, c
)) line(0, x
, 0, 2, 0, 3, getk(va
, vb
), getk(va
, vc
));
565 else if (z(b
, c
, a
)) line(1, x
, 0, 2, 2, 3, getk(va
, vb
), getk(vb
, vc
));
566 else if (z(c
, a
, b
)) line(2, x
, 2, 3, 0, 3, getk(vb
, vc
), getk(va
, vc
));
571 function drawCell(cell
, pos
) {
572 var screen
= world(pos
);
579 var a
= store
.get(pos
, dirup
);
580 var b
= store
.get(pos
, dirul
);
581 var c
= store
.get(pos
, dirdl
);
584 triangle(cell
, a
, b
, .5, 0);
585 triangle(cell
, b
, c
, .5, 1);
589 for (var i
= 0; i
< n
; ++i
) {
590 triangle(cell
, a
, b
, i
/ (n
- 1), 0);
591 triangle(cell
, b
, c
, i
/ (n
- 1), 1);
599 ctx
.clearRect(-ctx
.canvas
.width
/ 2, -ctx
.canvas
.height
/ 2, ctx
.canvas
.width
, ctx
.canvas
.height
);
602 ctx
.scale(zoom
, zoom
);
603 var lw
= ctx
.lineWidth
= 1 / zoom
;
605 ctx
.strokeStyle
= '#fff';
606 store
.forEach(drawCell
);
612 var a
= 4 * Math
.PI
/ 3;
613 var da
= [a
, a
+ x
, a
+ 2 * x
, a
+ 3 * x
, a
+ 4 * x
, a
+ 5 * x
];
614 var ddd
= rnd(Math
.PI
*2);
616 var csr
= new Point(0, 0);
619 var frames
= new Array(frameNum
);
621 function fl(n
) { return Math
.floor(n
); }
623 function fade(t
) { return t
* t
* t
* (t
* (t
* 6 - 15) + 10); }
625 store
= store
.nextGen(function (oldCell
, newCell
, pos
, oldStore
, newStore
) {
627 if (oldCell
!= null) {
629 var sum
= balls
.reduce(function (s
, b
) {
630 var d
= dist(p
, b
.pos
) / b
.r
/ 2;
634 return s
+ fade(1 - d
);
642 balls
.forEach(function (b
) {
644 var nextpos
= b
.pos
.plus(b
.v
);
646 while(dist(nextpos
, zero
) > maxd
) {
648 b
.v
= new Point(mind
* cos(a
) / 50, mind
* sin(a
) / 50);
649 nextpos
= b
.pos
.plus(b
.v
);
659 window
.requestAnimationFrame(tick
);
664 window
.addEventListener('click', function () {
666 if (oneOnly
== enableLepr
)
668 else enableLepr
= !enableLepr
;