PageSourceSearch

https://joshdata.me/iceberger.html

html joshdata.me collected 2026-10-02 03:18:02 UTC 34,154 bytes, 771 lines download raw bytes

1<!doctype html>
2<html lang="en">
3  <head>
4    <!-- Required meta tags -->
5    <meta charset="utf-8">
6    <meta name="viewport" content="width=device-width, initial-scale=1">
7
8    <!-- Bootstrap CSS -->
9    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
10
11    <title>Iceberger</title>
12
13    <link rel="preconnect" href="https://fonts.gstatic.com">
14    <link href="https://fonts.googleapis.com/css2?family=Oswald&family=Oxygen:wght@400&family=Source+Code+Pro:wght@300&display=swap" rel="stylesheet">
15
16    <style>
17      body {
18        padding: 50px 0;
19        font-family: 'Oxygen', sans-serif;
20        font-size: 12pt;
21      }
22
23      h1 {
24        font-weight: bold;
25        font-family: 'Oswald', sans-serif;
26      }
27
28      p.subhead {
29        font-size: 140%;
30        margin-bottom: 0;
31      }
32      p.subhead2 {
33        margin-bottom: 50px;
34      }
35
36      strong {
37        font-family: 'Oswald', sans-serif;
38        font-size: 15pt;
39        letter-spacing: .5px;
40      }
41
42      a, a:visited, a:focus {
43        color: #2c7bb6;
44      }
45
46      #canvas {
47        border: 2px solid black;
48        width: 100%;
49        height: 20em;
50        background-color: black;
51        touch-action: none;
52      }
53    </style>
54  </head>
55  <body>
56    <div class="container-md" style="max-width: 50em">
57
58    <h1>Iceberger</h1>
59    <p class="subhead">Draw an iceberg and see how it will float.</p>
60    <p class="subhead2">
61        Inspired by a <a href="https://twitter.com/GlacialMeg/status/1362557149147058178?ref_src=twsrc%5Etfw">tweet by @GlacialMeg</a>
62        &mdash;
63        Game by <a href="/">@[email protected]</a>
64    </p>
65
66    <canvas id="canvas">
67    </canvas>
68
69    <p style="margin: 25px 0; color: #554;">Icebergs are less dense than water, so they always float with about 10% of their mass above the water. But which way up? An iceberg wouldn&rsquo;t float <i>exactly</i> like on this page in reality. Its three-dimensional distribution of mass and its relative density compared to the water are both significant factors that are only approximated here.</p>
70
71    <hr style="margin: 45px 0 50px 0">
72
73    <p>Inspired by a tweet by @GlacialMeg:</p>
74    <blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="en" dir="ltr">Today I channeled my energy into this very unofficial but passionate petition for scientists to start drawing icebergs in their stable orientations. I went to the trouble of painting a stable iceberg with my watercolors, so plz hear me out.<br><br>(1/4) <a href="https://t.co/rtkCYub38b">pic.twitter.com/rtkCYub38b</a></p>&mdash; Megan Thompson-Munson (@GlacialMeg) <a href="https://twitter.com/GlacialMeg/status/1362557149147058178?ref_src=twsrc%5Etfw">February 19, 2021</a></blockquote>
75    
75<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
75
76
77    
77<script async src="ext/imagetracer_v1.2.6.js"></script>
77
78    <canvas id="paste_buffer" style="display: none"></canvas>
79
80    </div>
81
82    
82<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
82
83    
83<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
83
84    
84<script src="https://cdn.jsdelivr.net/npm/@turf/turf@5/turf.min.js"></script>
84
85
86    
86<script>
87        $(function() {
88            // Make the canvas nicely sized.
89            var h = $("#canvas").width() * 9/16;
90            if (h > $(window).height() - 50) h = $(window).height() - 50;
91            $("#canvas").height(h);
92
93            // Set the canvas coordinate system to match page units.
94            $("#canvas").attr("width", $("#canvas").width());
95            $("#canvas").attr("height", $("#canvas").height());
96
97            // Get the water level.
98            var yZero = $("#canvas").height() / 3;
99            var ctx = document.getElementById('canvas').getContext("2d");
100
101            // Icebergs in play.
102            var icebergs = [];
103
104            // Draw backgound.
105            function draw_background() {
106                // Sky
107                ctx.beginPath();
108                ctx.fillStyle = "#bbddee";
109                ctx.moveTo(0, 0);
110                ctx.lineTo(0, ctx.canvas.height);
111                ctx.lineTo(ctx.canvas.width, ctx.canvas.height);
112                ctx.lineTo(ctx.canvas.width, 0);
113                ctx.closePath();
114                ctx.fill();
115
116                // Horizon
117                ctx.beginPath();
118                ctx.lineWidth = 1;
119                ctx.strokeStyle = "yellow";
120                ctx.moveTo(0, yZero);
121                ctx.lineTo(ctx.canvas.width, yZero);
122                ctx.closePath();
123                ctx.stroke();
124
125                // Sun
126                ctx.beginPath();
127                ctx.fillStyle = "#fefec4";
128                ctx.arc(ctx.canvas.width - 50, 35, 20, 0, 2 * Math.PI);
129                ctx.fill();
130
131                // Water
132                draw_water();
133            }
134            function draw_water() {
135                // Water
136                ctx.beginPath();
137                ctx.fillStyle = "#2c7bb6";
138                ctx.moveTo(0, yZero);
139                ctx.lineTo(0, ctx.canvas.height);
140                ctx.lineTo(ctx.canvas.width, ctx.canvas.height);
141                ctx.lineTo(ctx.canvas.width, yZero);
142                ctx.closePath();
143                ctx.fill();
144            }
145            draw_background();
146            draw_water();
147
148            // Hook up draw events.
149            var drawingInProgress = null;
150            function add_point(e) {
151                if (e.touches)
152                    e = e.touches[0];
153
154                var rect = $("#canvas")[0].getBoundingClientRect();
155                var x = e.clientX - rect.left;
156                var y = e.clientY - rect.top;
157                drawingInProgress.push([x, y]);
158
159                // Draw lasso.
160                if (drawingInProgress.length >= 2) {
161                    ctx.beginPath();
162                    ctx.strokeStyle = "black";
163                    ctx.lineWidth = 3;
164                    ctx.lineJoin = "round";
165                    let polygon = drawingInProgress;
166                    ctx.moveTo(polygon[polygon.length-2][0], polygon[polygon.length-2][1]);
167                    ctx.lineTo(polygon[polygon.length-1][0], polygon[polygon.length-1][1]);
168                    ctx.closePath();
169                    ctx.stroke();
170                }
171            };
172            $('#canvas').on('mousedown touchstart', function (e) {
173                // Reset canvas.
174                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
175                draw_background();
176                draw_water();
177
178                drawingInProgress = [];
179                add_point(e);
180            });
181            $('#canvas').on('mousemove touchmove', function (e) {
182                if (drawingInProgress !== null) {
183                    add_point(e);
184                    e.preventDefault(); // prevent scrolling on iOS 12
185                }
186            });
187            $('#canvas').on('mouseup touchend', function (e) {
188                // Finish drawing - reset the polygon and inertia.
189                if (drawingInProgress.length < 3) {
190                    drawingInProgress = null;
191                    return;
192                }
193
194                addIceberg(drawingInProgress);
195                drawingInProgress = null;
196            });
197
198            function addIceberg(polygon) {
199                polygon = polygon.concat([polygon[0]]); // make first and last point the same
200
201                let iceberg = {
202                    polygon: polygon
203                };
204
205                // Reset the velocity vector.
206                iceberg.velocity = [0, 0, 0];
207
208                // People like to draw shapes with kinks, which results
209                // in part of the polygon being treated as negative area.
210                // turf.unkinkPolygon can be used to find the kinks and
211                // separate the polygon into multiple polygons at the
212                // kinks, but it's a little fragile and hard to combine
213                // into a single unkinked polygon. We also want to avoid
214                // showing errors when there are just small degenerate
215                // kinds from messy drawing.
216                // (If turf.kinks(turf.polygon([polygon])).features.length
217                // is non-zero, there are kinks. We could show an error,
218                // but that's not friendly.)
219                try {
220                    // Remove duplicate vertices which breaks unkinkPolygon.
221                    var found_vertices = { };
222                    var polygon2 = [];
223                    iceberg.polygon.forEach(function(pt) {
224                        var k = pt[0] + "|" + pt[1];
225                        if (k in found_vertices) return;
226                        found_vertices[k] = true;
227                        polygon2.push(pt);
228                    })
229                    iceberg.polygon = polygon2;
230                    iceberg.polygon.push(iceberg.polygon[0]);
231
232                    // Split polygon into parts separated at kinks.
233                    var k = turf.polygon([iceberg.polygon]);
234                    k = turf.unkinkPolygon(k);
235                    if (k.features.length > 1) {
236                        // Take the part with the largest area.
237                        k = k.features.map(function(kk) { return kk.geometry.coordinates[0]; })
238                        var areas = k.map(polygonArea);
239                        var maxArea = 0, maxAreaIndex = 0;
240                        areas.forEach(function(area, i) {
241                            if (area > maxArea) {
242                                maxArea = area;
243                                maxAreaIndex = i;
244                            }
245                        });
246                        iceberg.polygon = k[maxAreaIndex];
247                    }
248                } catch (e) {
249                }
250
251                // Make the polygon defined in a consistent clockwise/counter-clockwise
252                // order to simplify the computation of the fun drag effect.
253                iceberg.area = signedPolygonArea(iceberg.polygon);
254                if (iceberg.area < 0) {
255                    iceberg.polygon = iceberg.polygon.reverse();
256                    iceberg.area = Math.abs(iceberg.area);
257                }
258
259                // Calculate the polygon's angular mass, normalized to
260                // the polygon area because it plays nicer when the
261                // math is size-independent.
262                iceberg.rotationalInertia = polygonMomentOfInertia(iceberg.polygon) / iceberg.area**.5 / 1000000;
263
264                // The distribution of mass across the 2D polygon is
265                // non-uniform if we're thinking about it as a projection
266                // of a realistic 3D shape. The mass density is actually
267                // zero at the edge of the polygon and increases as you
268                // go away from the edge toward the center. The ultimate
269                // way to model this would be to impute a 3D volume from
270                // the polygon and then perform the geometric operations
271                // (compute the centroid, clip to the water line) on the
272                // volume. But without a straightforward way to impute a
273                // 3D volume, we can instead a) devise an approximate mass
274                // density function over the plane and b) lump the polygon
275                // mass at some number of points within the polygon.
276                //
277                // The lump points will be uniformly distributed over the
278                // polygon.
279
280                // Triangulate the polygon, yielding awkwardly shaped triangles.
281                // Compute the area of each triangle, divide by the total area,
282                // and store this as triangle & area objects.
283                var triangles = turf.tesselate(turf.polygon([iceberg.polygon]))
284                  .features.map(function(f) { return f.geometry.coordinates[0]; });
285                triangles = triangles.map(function(t) { return { triangle: t, area: polygonArea(t) / iceberg.area } });
286
287                // Create some uniformly distributed points within the polygon.
288                iceberg.lumpedMassPoints = [];
289                for (let i = 0; i < 250; i++) {
290                    // Choose a triangle at random, weighted by each triangle's area,
291                    // and a coordinate within the triangle.
292                    let tR = Math.random(), tX = Math.random(), tY = Math.random();
293                    for (var ti = 0; ti < triangles.length; ti++) {
294                        tR -= triangles[ti].area;
295                        if (tR <= 0 || ti == triangles.length - 1) break;
296                    }
297
298                    // Get a point randomly within the bounds of the triangle.
299                    // (https://stackoverflow.com/q/4778147)
300                    let t = triangles[ti].triangle;
301                    function triangle_interpolate(r1, r2, a, b, c) {
302                        return (1 - Math.sqrt(r1)) * a + (Math.sqrt(r1) * (1 - r2)) * b + (Math.sqrt(r1) * r2) * c;
303                    }
304                    iceberg.lumpedMassPoints.push({
305                        point: [triangle_interpolate(tX, tY, t[0][0], t[1][0], t[2][0]),
306                                triangle_interpolate(tX, tY, t[0][1], t[1][1], t[2][1]) ]
307                    })
308                }
309
310                // Distribute mass to each point by weighting each point
311                // by its distance to the nearest edge of the polygon.
312                var pMassTotal = 0;
313                iceberg.lumpedMassPoints.forEach(function(p) {
314                    for (let i = 0; i < iceberg.polygon.length - 1; i++) {
315                        let d = pointSegmentDistance(p.point, iceberg.polygon[i], iceberg.polygon[i+1]);
316                        if (!p.mass || d < p.mass)
317                            p.mass = d;
318                    }
319                    pMassTotal += p.mass;
320                })
321                iceberg.lumpedMassPoints.forEach(function(p) {
322                    p.mass /= pMassTotal;
323                });
324
325                // Compute the height of the iceberg.
326                var yMin = null, yMax = null;
327                iceberg.polygon.forEach(function(pt, i) {
328                    if (i == 0 || pt[1] < yMin) yMin = pt[1];
329                    if (i == 0 || pt[1] > yMax) yMax = pt[1];
330                });
331                iceberg.height = yMax - yMin;
332
333                icebergs = [ iceberg ];
334
335                /*
336                // For testing, create a duplicate iceberg to the right.
337                var iceberg2 = JSON.parse(JSON.stringify(iceberg));
338                iceberg2.polygon.forEach(p => { p[0] += ctx.canvas.width/3 });
339                iceberg2.lumpedMassPoints.forEach(p => { p.point[0] += ctx.canvas.width/3 });
340                icebergs.push(iceberg2);
341                iceberg.lumpedMassPoints = [];
342                */
343            }
344
345            // Paste an image of an iceberg anywhere in the document window.
346            // Use the Browser's Edit -> Paste command.
347            document.onpaste = function (event) {
348              // https://stackoverflow.com/a/15369753 and https://stackoverflow.com/a/18387322
349              var items = (event.clipboardData  || event.originalEvent.clipboardData).items;
350              var blob = null;
351              for (var i = 0; i < items.length; i++)
352                if (items[i].type.indexOf("image") === 0)
353                  blob = items[i].getAsFile();
354              if (blob === null) return;
355              var reader = new FileReader();
356              reader.onload = function(event) {
357                var image = new Image();
358                image.onload = function () {
359                  var canvas = $('#paste_buffer')[0];
360                  canvas.width = image.width;
361                  canvas.height = image.height;
362                  let ctx = canvas.getContext("2d");
363                  ctx.drawImage(image, 0, 0);
364                  addIcebergFromImageData(ctx.getImageData(0, 0, image.width, image.height));
365                };
366                image.src = event.target.result; // data URL
367              };
368              reader.readAsDataURL(blob);
369            }
370
371            function addIcebergFromImageData(image) {
372                // Use ImageTracer to find the most complex polygon
373                // in the image, and add that polygon to the canvas.
374                let polygon = null;
375                ImageTracer.imagedataToTracedata(image)
376                    .layers.forEach((layer) => {
377                        layer.forEach((object) => {
378                            if (polygon === null || object.segments.length > polygon.length) {
379                                polygon = object.segments.map((seg) => {
380                                    return [seg.x1, seg.y1]
381                                });
382                            }
383                        });
384                    });
385                if (polygon !== null)
386                    addIceberg(polygon);
387                return;
388            }
389
390            // frames of the animation
391
392            var stop_error = false;
393            setInterval(tick_with_error_handler, 50);
394
395            function tick_with_error_handler() {
396                if (stop_error) return;
397                try {
398                    tick();
399                } catch (e) {
400                    console.log("ending tick timer", e);
401                    clearInterval(tick_with_error_handler);
402                    stop_error = true;
403                }
404            }
405
406            function tick() {
407                if (drawingInProgress !== null || icebergs.length == 0)
408                    return;
409
410                // Reset canvas.
411                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
412                draw_background();
413
414                // Update and draw each iceberg.
415                icebergs.forEach(function(iceberg) {
416                    // Update cooordinates.
417                    iceberger(iceberg);
418
419                    // Draw polygon as a filled iceberg.
420                    // TODO: Draw ice texture?
421                    ctx.strokeStyle = "black";
422                    ctx.fillStyle = "white";
423                    ctx.lineWidth = 2;
424                    ctx.lineJoin = "round";
425                    ctx.beginPath();
426                    ctx.moveTo(iceberg.polygon[0][0], iceberg.polygon[0][1]);
427                    iceberg.polygon.forEach(function(pt) {
428                        ctx.lineTo(pt[0], pt[1]);
429                    });
430                    ctx.closePath();
431                    ctx.fill();
432                    ctx.stroke();
433
434                    /*
435                    iceberg.lumpedMassPoints.forEach(function(pt) {
436                        ctx.beginPath();
437                        ctx.arc(pt.point[0], pt.point[1],
438                                pt.mass*500,
439                                0, 2 * Math.PI);
440                        ctx.stroke();
441                    });
442                    */
443                })
444
445                // water overlay over the submerged part
446                ctx.globalAlpha = .5;
447                draw_water();
448                ctx.globalAlpha = 1;
449            }
450
451            function iceberger(iceberg) {
452                // Compute the force on centroid of the iceberg polygon
453                // at this time instant. The polygon has three degrees
454                // of freedom: x, y, and rotation.
455                var force = [0, 0, 0];
456
457                // Compute the center of mass of the iceberg and the
458                // center of mass of the submerged portion.
459                var pc = centroid(iceberg.polygon);
460                var polygon_submerged = turf.bboxClip(turf.polygon([iceberg.polygon]),
461                    [-Infinity, yZero, Infinity, Infinity]).geometry.coordinates[0];
462                var pc_submerged = (polygon_submerged && polygon_submerged.length >= 4) ? centroid(polygon_submerged) : [0, 0];
463
464                // The vector between the two centroids determines a
465                // force that is applied. Gravity pulls down (+y because
466                // the canvas coordinates are upside-down) at the
467                // center of mass of the full iceberg and bouyancy
468                // pushes up (-y) at the center of mass of the submerged
469                // portion.
470                var submerged_ratio = polygonArea(polygon_submerged) / iceberg.area;
471                var specific_gravity = .92;
472                var time_scale = 1.1;
473
474                if (iceberg.lumpedMassPoints.length == 0) {
475                    // In 2D, because more mass will fall at the edges,
476                    // a slightly lower specific gravity produces a
477                    // more realistic looking 90%-below-water result.
478                    // But then slow down the simulation so that it
479                    // doesn't get pushed out of the water so quickly.
480                    specific_gravity -= .07;
481                    time_scale = .9;
482
483                    // Apply gravity. Everything will be normalized to the
484                    // area of the iceberg, so gravity will have a force of
485                    // 1 in the vertical direction.
486                    force[1] += 1;
487
488                    // The simple computation for boyancy is a force at
489                    // the center of mass of the submerged portion.
490                    var fb = submerged_ratio / specific_gravity;
491                    force[1] -= fb;
492
493                    // Apply a moment at the centroid of the iceberg due to
494                    // the eccentricity between the iceberg centroid and the
495                    // centroid of the submerged portion where the bouyant
496                    // force acts.
497                    force[2] += fb * (pc_submerged[0] - pc[0]);
498
499                } else {
500                    // The more complex computation of bouyancy uses the
501                    // lumped mass points that are submerged rather than
502                    // the center of mass of the submerged portion.
503                    iceberg.lumpedMassPoints.forEach(function(pt) {
504                        var f = 0;
505
506                        // Apply gravity for this point.
507                        f += pt.mass;
508
509                        // Apply bouyant force if submerged. To avoid
510                        // discrete computations at our arbitrary
511                        // lumped points, treat 'submerged' as non-binary
512                        // near the water level.
513                        let s = (pt.point[1] - yZero) / (iceberg.height/20);
514                        s = Math.min(Math.max(s, 0), 1);
515                        f -= pt.mass / specific_gravity * s;
516
517                        force[1] += f;
518
519                        // Apply a moment at the centroid of the iceberg due to
520                        // the eccentricity between the iceberg centroid and the
521                        // centroid of the submerged portion where the bouyant
522                        // force acts.
523                        force[2] -= f * (pt.point[0] - pc[0]);
524                    });
525                }
526
527                // Apply some drag. This may not be totally scientifically valid.
528                // For each line segment on the perimeter of the polygon that is
529                // facing the direction of motion (i.e. its perpendicular points
530                // in the direction of motion), apply a force equal to the dot
531                // product of the velocity and the perpendicular (i.e. 0 when
532                // the face and the velocity are parallel, maximal when they are
533                // perpendicular). Apply this force at the midpoint of the segment
534                // perpendicular to the segment. Transform the force to the centroid
535                // of the polygon.
536                var perimeter = 0;
537                iceberg.polygon.forEach(function(p1, i) {
538                    if (i == 0) return;
539                    var p0 = iceberg.polygon[i-1];
540                    var v = [p1[0]-p0[0], p1[1]-p0[1]];
541                    var d = (v[0]**2 + v[1]**2)**.5;
542                    perimeter += d;
543                });
544                iceberg.polygon.forEach(function(p1, i) {
545                    if (i == 0) return;
546                    var p0 = iceberg.polygon[i-1];
547
548                    // Get the perpendicular facing *outward* 
549                    // (away from the polygon).
550                    var v = [p1[0]-p0[0], p1[1]-p0[1]];
551                    v = [v[1], -v[0]]; // perpendicular (easy in 2D)
552
553                    // Normalize the vector so that the total force
554                    // from all line segments is 1 but each line
555                    // segment contributes proportionally to its
556                    // length.
557                    v[0] /= perimeter;
558                    v[1] /= perimeter;
559
560                    // Compute the force as the dot product of the perpendicular
561                    // with the velocity.
562                    // Skip if the force is negative which means the
563                    // line segment is not on the front side of the polygon
564                    // w.r.t. the direction of motion.
565                    var f = v[0]*iceberg.velocity[0] + v[1]*iceberg.velocity[1];
566                    if (f < 0) return;
567
568                    // Scale the force. There's more drag under water.
569                    f *= (1 + 2 * submerged_ratio);
570
571                    // Apply this force in the opposite direction of the
572                    // perpendicular, i.e. into the iceberg.
573                    f = [-v[0] * f, -v[1] * f];
574
575                    // Apply to the force vector. Skip Fy because we
576                    // don't want actual drag --- we handle that by
577                    // damping.
578                    force[0] += f[0];
579                    //force[1] += f[1];
580
581                    // Transform the force from the center of the segment
582                    // to the centroid. Limit the applied moment.
583                    var p = [(p0[0] + p1[0])/2, (p0[1] + p1[1])/2];
584                    m = f[0] * (p[1] - pc[1]) - f[1] * (p[0] - pc[0]);
585                    if (Math.abs(m) > 1) m = Math.sign(m) * 1;
586                    force[2] += m;
587                });
588
589                // If the polygon is out of the horizontal bounds, push it in.
590                var minx = Infinity, maxx = -Infinity;
591                iceberg.polygon.forEach(function(pt) {
592                    if (pt[0] < minx) minx = pt[0];
593                    if (pt[0] > maxx) maxx = pt[0];
594                });
595                if (minx < 0) force[0] += -minx/ctx.canvas.width * 10;
596                if (maxx > ctx.canvas.width) force[0] += (ctx.canvas.width-maxx)/ctx.canvas.width * 10;
597
598                // The translational force is normalized to the polygon area so we can
599                // consider it an acceleration. Increment the velocity with the acceleration.
600                iceberg.velocity[0] += force[0] * time_scale;
601                iceberg.velocity[1] += force[1] * time_scale;
602
603                // But the rotational force must be divided by the polygon's angular mass.
604                iceberg.velocity[2] += force[2] / iceberg.rotationalInertia * time_scale;
605
606                // Apply damping. There is more damping in water than in air.
607                // Lowering the damping in air boosts the free-fall a bit for
608                // a nice effect when it hits the water.
609                var damping_air = .99;
610                var damping_water = .96;
611                var damping = damping_air*(1-submerged_ratio) + damping_water*submerged_ratio;
612                iceberg.velocity[0] *= damping;
613                iceberg.velocity[1] *= damping;
614                iceberg.velocity[2] *= (damping-.1);
615
616                // Apply velocity to the coordinates.
617                function update_polygon_point(pt) {
618                    pt = rotate(pc[0], pc[1], pt[0], pt[1], iceberg.velocity[2]);
619                    pt[0] += iceberg.velocity[0];
620                    pt[1] += iceberg.velocity[1];
621                    return pt;
622                }                
623                iceberg.polygon = iceberg.polygon.map(update_polygon_point);
624                iceberg.lumpedMassPoints.forEach(function(pt) {
625                    pt.point = update_polygon_point(pt.point);
626                })
627
628                // Melt the iceberg slowly. Melt faster above water.
629                iceberg.polygon.forEach(function(pt) {
630                    if (pt[1] < yZero)
631                        pt[1] = (pt[1] - yZero)*.9999 + yZero;
632                    else
633                        pt[1] = (pt[1] - yZero)*.99999 + yZero;
634                });
635            }           
636        })
637
638        // https://stackoverflow.com/a/1501725
639        function pointSegmentDistance(p, v, w) {
640          function sqr(x) { return x * x }
641          function dist2(v, w) { return sqr(v[0] - w[0]) + sqr(v[1] - w[1]) }
642          var l2 = dist2(v, w);
643          if (l2 == 0) return Math.sqrt(dist2(p, v));
644          var t = ((p[0] - v[0]) * (w[0] - v[0]) + (p[1] - v[1]) * (w[1] - v[1])) / l2;
645          t = Math.max(0, Math.min(1, t));
646          return Math.sqrt(dist2(p, [v[0] + t * (w[0] - v[0]),
647                                     v[1] + t * (w[1] - v[1]) ]));
648        }
649
650        function centroid(pts) {
651            // https://stackoverflow.com/a/33852627
652            var nPts = pts.length;
653            var off = pts[0];
654            var twicearea = 0;
655            var x = 0;
656            var y = 0;
657            var p1,p2;
658            var f;
659            for (var i = 0, j = nPts - 1; i < nPts; j = i++) {
660                p1 = pts[i];
661                p2 = pts[j];
662                f = (p1[0] - off[0]) * (p2[1] - off[1]) - (p2[0] - off[0]) * (p1[1] - off[1]);
663                twicearea += f;
664                x += (p1[0] + p2[0] - 2 * off[0]) * f;
665                y += (p1[1] + p2[1] - 2 * off[1]) * f;
666            }
667            f = twicearea * 3;
668            return [ x / f + off[0], y / f + off[1] ];
669        }
670
671        function signedPolygonArea(vertices) {
672            if (!vertices || vertices.length < 3) return 0;
673            // https://stackoverflow.com/a/33670691
674            var total = 0;
675            for (var i = 0, l = vertices.length; i < l; i++) {
676              var addX = vertices[i][0];
677              var addY = vertices[i == vertices.length - 1 ? 0 : i + 1][1];
678              var subX = vertices[i == vertices.length - 1 ? 0 : i + 1][0];
679              var subY = vertices[i][1];
680
681              total += (addX * addY * 0.5);
682              total -= (subX * subY * 0.5);
683            }
684            return total;
685        }
686        function polygonArea(vertices) {
687            return Math.abs(signedPolygonArea(vertices));
688        }
689
690        function rotate(cx, cy, x, y, angle) {
691            // https://stackoverflow.com/a/17411276
692            var radians = (Math.PI / 180) * angle,
693                cos = Math.cos(radians),
694                sin = Math.sin(radians),
695                nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
696                ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
697            return [nx, ny];
698        }
699
700        function polygonMomentOfInertia(points) {
701          // Thanks to https://fotino.me/moment-of-inertia-algorithm/.
702          function sub(p1, p2) {
703            return [p1[0] - p2[0], p1[1] - p2[1]];
704          }
705          function add(p1, p2) {
706            return [p1[0] + p2[0], p1[1] + p2[1]];
707          }
708          function mul(p, f) {
709            return [p[0] * f, p[1] * f];
710          }
711          function dot(p1, p2) {
712            return p1[0] * p2[0] + p1[1] * p2[1];
713          }
714          function cross(p1, p2) {
715            return p1[0] * p2[1] - p2[0] * p1[1];
716          }
717          function dist(p1, p2) {
718            p2 = p2 || [0, 0];
719            const x = p1[0] - p2[0], y = p1[1] - p2[1];
720            return Math.sqrt(x * x + y * y);
721          }
722          let momentOfInertia = 0;
723          for (let i = 1; i < points.length - 1; i++) {
724            const p1 = points[0], p2 = points[i], p3 = points[i + 1];
725            
726            const w = dist(p1, p2);
727            const w1 = Math.abs(dot(sub(p1, p2), sub(p3, p2)) / w);
728            const w2 = Math.abs(w - w1);
729            
730            const signedTriArea = cross(sub(p3, p1), sub(p2, p1)) / 2;
731            const h = 2 * Math.abs(signedTriArea) / w;
732            
733            const p4 = add(p2, mul(sub(p1, p2), w1 / w));
734            
735            const cm1 = [
736              (p2[0] + p3[0] + p4[0]) / 3,
737              (p2[1] + p3[1] + p4[1]) / 3
738            ];
739            const cm2 = [
740              (p1[0] + p3[0] + p4[0]) / 3,
741              (p1[1] + p3[1] + p4[1]) / 3
742            ];
743            
744            const I1 = w1 * h * ((h * h / 4) + (w1 * w1 / 12));
745            const I2 = w2 * h * ((h * h / 4) + (w2 * w2 / 12));
746            const m1 = 0.5 * w1 * h;
747            const m2 = 0.5 * w2 * h;
748
749            const I1cm = I1 - (m1 * Math.pow(dist(cm1, p3), 2));
750            const I2cm = I2 - (m2 * Math.pow(dist(cm2, p3), 2));
751            
752            const momentOfInertiaPart1 = I1cm + (m1 * Math.pow(dist(cm1), 2));
753            const momentOfInertiaPart2 = I2cm + (m2 * Math.pow(dist(cm2), 2));
754            if (cross(sub(p1, p3), sub(p4, p3)) > 0) {
755              momentOfInertia += momentOfInertiaPart1;
756            } else {
757              momentOfInertia -= momentOfInertiaPart1;
758            }
759            if (cross(sub(p4, p3), sub(p2, p3)) > 0) {
760              momentOfInertia += momentOfInertiaPart2;
761            } else {
762              momentOfInertia -= momentOfInertiaPart2;
763            }
764          }
765          return Math.abs(momentOfInertia);
766        }                
767
768    </script>
768
769
770  </body>
771</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.