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 — 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’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>— 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.