├── .gitattributes ├── .gitignore ├── .vscode ├── settings.json └── spellright.dict ├── README ├── docs ├── circular-bold.woff ├── circular-book.woff ├── circular-medium.woff ├── env │ ├── pillars_ibl.ktx.bmp │ └── pillars_skybox.ktx.bmp ├── favicon.png ├── filament.wasm ├── images │ ├── icon.png │ ├── img0_05.png │ ├── img0_40.png │ ├── img0_65.png │ ├── img1_00.png │ ├── img1_24.png │ ├── img1_45.png │ ├── img1_82.png │ ├── img2_00.png │ ├── img2_20.png │ ├── img2_35.png │ ├── img2_57.png │ ├── img2_62.png │ ├── img2_75.png │ ├── img3_16.png │ ├── img3_41.png │ ├── img3_89.png │ ├── img4_13.png │ ├── img4_29.png │ ├── img4_44.png │ └── thumb.png ├── index.html ├── main.js ├── main.js.map ├── materials │ ├── step1.filamat.bmp │ ├── step1_cylinder_back.filamat.bmp │ ├── step1_cylinder_front.filamat.bmp │ ├── step2.filamat.bmp │ ├── step3.filamat.bmp │ ├── step4.filamat.bmp │ ├── step5.filamat.bmp │ └── step5_poly.filamat.bmp ├── showcase │ ├── gm.sh │ ├── montage.png │ ├── pair.png │ ├── screenshot_3d_1_0.24.png │ ├── screenshot_3d_2_0.13.png │ ├── screenshot_3d_2_0.74.png │ └── screenshot_3d_4_0.43.png └── style.css ├── materials ├── matc ├── step1.mat ├── step1_cylinder_back.mat ├── step1_cylinder_front.mat ├── step2.mat ├── step3.mat ├── step4.mat ├── step5.mat └── step5_poly.mat ├── package.json ├── src ├── app.ts ├── display.ts ├── markdownToHtml.ts ├── polyhedron.ts ├── scene.ts ├── scrollytell.ts ├── timeline.ts ├── urls.ts └── verbiage.md ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/* linguist-vendored=true 2 | docs/main.js binary 3 | docs/main.js.map binary 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [ 3 | 120 4 | ], 5 | "typescript.autoClosingTags": false, 6 | "html.autoClosingTags": false, 7 | "spellright.language": [ 8 | "en" 9 | ], 10 | "spellright.documentTypes": [ 11 | "markdown", 12 | "latex", 13 | "plaintext", 14 | "html" 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/spellright.dict: -------------------------------------------------------------------------------- 1 | prideout 2 | euler 3 | πrs 4 | Richeson 5 | scrollytelling 6 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | To build and run the web app: 2 | 3 | yarn install 4 | yarn build 5 | yarn run http-server -p 8080 docs 6 | 7 | To publish: 8 | 9 | yarn build:html 10 | yarn build:release 11 | git commit -a -m 'Publish' && git push 12 | 13 | See also: 14 | 15 | "Euler's Gem" by David Richeson 16 | -------------------------------------------------------------------------------- /docs/circular-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/circular-bold.woff -------------------------------------------------------------------------------- /docs/circular-book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/circular-book.woff -------------------------------------------------------------------------------- /docs/circular-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/circular-medium.woff -------------------------------------------------------------------------------- /docs/env/pillars_ibl.ktx.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/env/pillars_ibl.ktx.bmp -------------------------------------------------------------------------------- /docs/env/pillars_skybox.ktx.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/env/pillars_skybox.ktx.bmp -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/favicon.png -------------------------------------------------------------------------------- /docs/filament.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/filament.wasm -------------------------------------------------------------------------------- /docs/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/icon.png -------------------------------------------------------------------------------- /docs/images/img0_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img0_05.png -------------------------------------------------------------------------------- /docs/images/img0_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img0_40.png -------------------------------------------------------------------------------- /docs/images/img0_65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img0_65.png -------------------------------------------------------------------------------- /docs/images/img1_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img1_00.png -------------------------------------------------------------------------------- /docs/images/img1_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img1_24.png -------------------------------------------------------------------------------- /docs/images/img1_45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img1_45.png -------------------------------------------------------------------------------- /docs/images/img1_82.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img1_82.png -------------------------------------------------------------------------------- /docs/images/img2_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img2_00.png -------------------------------------------------------------------------------- /docs/images/img2_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img2_20.png -------------------------------------------------------------------------------- /docs/images/img2_35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img2_35.png -------------------------------------------------------------------------------- /docs/images/img2_57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img2_57.png -------------------------------------------------------------------------------- /docs/images/img2_62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img2_62.png -------------------------------------------------------------------------------- /docs/images/img2_75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img2_75.png -------------------------------------------------------------------------------- /docs/images/img3_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img3_16.png -------------------------------------------------------------------------------- /docs/images/img3_41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img3_41.png -------------------------------------------------------------------------------- /docs/images/img3_89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img3_89.png -------------------------------------------------------------------------------- /docs/images/img4_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img4_13.png -------------------------------------------------------------------------------- /docs/images/img4_29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img4_29.png -------------------------------------------------------------------------------- /docs/images/img4_44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/img4_44.png -------------------------------------------------------------------------------- /docs/images/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/images/thumb.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Euler's Polyhedron Formula 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Euler's Polyhedron Formula

16 |

Chrome / Android is recommended for viewing this page because it uses WebGL 2.

17 |

This page uses continuous scrollytelling to present a variation of Legendre's proof for the following formula.

18 |

V - E + F = 2

19 |

For any convex polyhedron (or planar graph), the number of vertices minus the number of edges plus the number of faces is 2. Leonhard Euler discovered this in 1752 although Descartes discovered a variation over 100 years earlier.

20 |

This equation makes it easy to prove that there only 5 Platonic solids, but perhaps its real beauty lies in how it connects disparate fields of mathematics. These connections are suggested by Legendre's proof, which is purely geometrical and leverages some interesting properties of geodesic triangles.

21 |

Scroll down at your own pace, but try not to go too fast, otherwise you'll skip over the animation. The proof is divided into 5 steps:

22 |
    23 |
  1. Surface Area of Sphere
  2. 24 |
  3. Area of Double Lune
  4. 25 |
  5. Girard's Theorem
  6. 26 |
  7. Spherical Polygons
  8. 27 |
  9. The Conclusion
  10. 28 |
29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 |
37 |

Part 1: Surface Area of Sphere

38 |

The surface area of a sphere can be derived with freshman calculus, but it was discovered by Archimedes long before the invention of calculus.

39 |

Archimedes realized that the surface area of a sphere is equal to the area of its smallest enclosing cylinder, which is 4πr2. This is somewhat intuitive if you think about lat-long rectangles.

40 |

At the equator, lat-long rectangles are fat and short. Closer to the poles they are tall and thin. As they move closer to the poles, they become tall at the same rate at which they become thin, so their area remains constant.

41 |

Going forward, we'll keep things simple by focusing on a sphere whose radius is 1. The surface area of this sphere is .

42 |
43 | 44 |
45 |

Part 2: Area of a Double Lune

46 |

Great circles are lines on the surface of a sphere that divide the sphere in half. If you place your car anywhere on a sphere's surface, then continuously drive without turning the steering wheel, you'll always inscribe a great circle. Any portion of a great circle is called a geodesic path.

47 |

The region on a sphere between two great circles is called a "lune". Let's figure out the surface area of a lune bounded by θ radians.

48 |

If θ is π radians, the lune encompasses one hemisphere. Since the area of a hemisphere is , the area of the lune is .

49 |

This is one reason why radians are more elegant than degrees!

50 |

The surface area of a lune plus its antipode is . Let's call this a double lune.

51 |
52 | 53 |
54 |

Part 3: Girard's Theorem

55 |

Spherical triangles are inscribed by geodesic lines (i.e. portions of great circles). Unlike a planar triangle, the area of a spherical triangle can be determined solely from its angles. With planar triangles, the sum of the three angles is always π radians. Not so with spherical triangles!

56 |

For example, consider the triangle that encompasses one-eighth of the sphere surface, which has three 90° angles, or π/2 radians each. Clearly these do not add up to π.

57 |

Let's figure out the area of any geodesic triangle with angles a, b, and c.

58 |

Each corner in the triangle corresponds to a double lune on the surface. We can visualize each double lune with one of the additive primary colors.

59 |

The sum of the lune areas can be visualized by adding up their respective colors.

Notice that the total area of the lunes is equivalent to the surface of the entire sphere, except that the triangle and its antipode are each counted an additional 2x times.

60 |

Recall that:

61 |
    62 |
  • The surface area of the unit sphere is .
  • 63 |
  • The surface area of each double lune is .
  • 64 |
65 |

Moreover, we now know that:

66 |
    67 |
  • The surface area of the sphere is equal to the area of all the triangle's double lunes, minus 4x the area of the 68 | triangle.
  • 69 |
70 |

Therefore, if the area of geodesic triangle abc is A, then:

71 |
    72 |
  • 4π = 4a + 4b + 4c - 4A
  • 73 |
74 |

Or, more simply stated:

75 |
    76 |
  • A = a + b + c - π
  • 77 |
78 |

This formula was independently discovered by Albert Girard (1595-1632) and Thomas Harriot (1560-1621).

79 |
80 | 81 |
82 |

Part 4: Spherical Polygons

83 |

Now that we know how to compute the area of a geodesic triangle, can we figure out how to compute the area of a geodesic polygon?

84 |

Yes, we can! Every n-gon can be decomposed into (n-2) triangles.

85 |

The sum of all the angles in a polygon is equal to the sum of all the angles in its constituent triangles. And, we now know that each of those (n-2) constituent triangles has an area of:

86 |

<angle sum> - π

87 |

Therefore, the area of the polygon must be:

88 |

<angle sum> - (n-2) π

89 |

Stated another way:

90 |

A = (a + b + c + d + ...) - nπ + 2π.

91 |

To portray this formula visually, we've inscribed the components of the sum onto the sphere.

92 |

Note that each vertex and edge correspond to a component of the sum, as well as the face itself. This will be useful later in the proof.

93 |
94 | 95 |
96 |

Part 5: The Conclusion

97 |

Now that we have a few tools under our belt, let's consider a convex polyhedron.

98 |

What happens when we inflate the polyhedron to meet its enclosing sphere? The sum of the areas of all the resulting geodesic polygons should be equivalent to the surface area of the sphere!

99 |

Next, apply the visual method for computing the area sum across all polygons. Each vertex contributes a total of radians, each edge contributes -2π (one for each side), and each face contributes .

100 |

Putting it all together:

101 |
    102 |
  • Surface area of unit sphere =
    2πV - 2πE + 2πF
  • 103 |
104 |

Or, simply stated:

105 |
    106 |
  • 4π = 2πV - 2πE + 2πF
  • 107 |
108 |

Therefore:

109 |
    110 |
  • V - E + F = 2
  • 111 |
112 |

Et Voilà!

113 |

The polyhedron formula is also known as Euler's Characteristic Formula because the right-hand side of the equation is actually a "characteristic" of the sphere's topology. If we were to inscribe the graph on a torus instead of a sphere, the Euler characteristic would be 0 rather than 2.

114 |

To learn more about this, I recommend David Richeson's excellent book Euler's Gem, which was the inspiration for this page.

115 |

Also the inimitable 3blue1brown has a great video on the polyhedron formula, as well as a video about the surface area of a sphere.

116 |

Thanks for reading the proof! Take a look at the code if you're interested in how this was made.

GitHub Projects:

117 | 122 | 123 | The Little Grasshopper 124 |
125 | 126 |
127 |
128 | 129 | 130 | -------------------------------------------------------------------------------- /docs/materials/step1.filamat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/materials/step1.filamat.bmp -------------------------------------------------------------------------------- /docs/materials/step1_cylinder_back.filamat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/materials/step1_cylinder_back.filamat.bmp -------------------------------------------------------------------------------- /docs/materials/step1_cylinder_front.filamat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/materials/step1_cylinder_front.filamat.bmp -------------------------------------------------------------------------------- /docs/materials/step2.filamat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/materials/step2.filamat.bmp -------------------------------------------------------------------------------- /docs/materials/step3.filamat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/materials/step3.filamat.bmp -------------------------------------------------------------------------------- /docs/materials/step4.filamat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/materials/step4.filamat.bmp -------------------------------------------------------------------------------- /docs/materials/step5.filamat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/materials/step5.filamat.bmp -------------------------------------------------------------------------------- /docs/materials/step5_poly.filamat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/materials/step5_poly.filamat.bmp -------------------------------------------------------------------------------- /docs/showcase/gm.sh: -------------------------------------------------------------------------------- 1 | # gm montage *.png -geometry 1038x1114+0+0 montage.png 2 | gm montage screenshot_3d_1_0.24.png screenshot_3d_2_0.13.png -geometry 1038x1114+0+0 pair.png 3 | -------------------------------------------------------------------------------- /docs/showcase/montage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/showcase/montage.png -------------------------------------------------------------------------------- /docs/showcase/pair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/showcase/pair.png -------------------------------------------------------------------------------- /docs/showcase/screenshot_3d_1_0.24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/showcase/screenshot_3d_1_0.24.png -------------------------------------------------------------------------------- /docs/showcase/screenshot_3d_2_0.13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/showcase/screenshot_3d_2_0.13.png -------------------------------------------------------------------------------- /docs/showcase/screenshot_3d_2_0.74.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/showcase/screenshot_3d_2_0.74.png -------------------------------------------------------------------------------- /docs/showcase/screenshot_3d_4_0.43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/docs/showcase/screenshot_3d_4_0.43.png -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "circular"; 3 | src: url("circular-book.woff") format('woff'); 4 | } 5 | 6 | @font-face { 7 | font-family: "circular-bold"; 8 | src: url("circular-bold.woff") format('woff'); 9 | } 10 | 11 | html, body { padding: 0; margin: 0; height:100% } 12 | html { overflow: hidden } 13 | body { 14 | height: 100%; 15 | } 16 | h1, h2 { 17 | font-family: "Alegreya"; 18 | text-align: center; 19 | } 20 | .container { 21 | height:100%; 22 | overflow-y: scroll; 23 | position: relative; 24 | font-family: "circular", sans-serif; 25 | font-size: 20px; 26 | -webkit-overflow-scrolling: touch; 27 | } 28 | .chart { 29 | position: sticky; 30 | width: 100%; 31 | height: 500px; /* <== Manipulated by Javascript */ 32 | top: calc(50% - 100px); 33 | text-align: center; 34 | z-index: -1; 35 | 36 | /* Safari workarounds: */ 37 | pointer-events: none; 38 | display: block; 39 | position: -webkit-sticky; 40 | } 41 | .chart > div { 42 | width: 100%; 43 | height: 100%; 44 | position: relative; 45 | } 46 | .chart > div > * { 47 | width: 100%; 48 | height: 100%; 49 | position: absolute; 50 | left: 0; 51 | top: 0; 52 | } 53 | .container.constrain { 54 | border: 1px solid #ddd; 55 | border-radius: 3px; 56 | box-sizing: border-box; 57 | } 58 | .container.constrain > div { 59 | max-width: 600px; 60 | margin: 0 auto 0 auto; 61 | } 62 | .container > * > h1 { 63 | margin-bottom: 12px; 64 | } 65 | .container > * > p:first-of-type { 66 | font-size: 12px; 67 | margin: 0; 68 | } 69 | .center { 70 | text-align: center; 71 | } 72 | segment.small { 73 | font-size: 18px; 74 | } 75 | .panel > h2 { 76 | height: inherit; 77 | background: rgba(255, 200, 0, 0.7); 78 | } 79 | segment { 80 | display: block; 81 | height: 500px; /* <== Manipulated by Javascript */ 82 | width: 100%; 83 | } 84 | segment > * { 85 | background: rgba(255, 255, 255, 0.7); 86 | margin: 0 auto 0 auto; 87 | padding: 10px; 88 | } 89 | .intro { 90 | padding: 10px; 91 | font-size: 18px; 92 | } 93 | a, a:visited { 94 | text-decoration: none; 95 | color: #06c; 96 | } 97 | a:hover { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /materials/matc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/euler/bdfc373d6f71b91de22a7ce17f8dd58bba02c08c/materials/matc -------------------------------------------------------------------------------- /materials/step1.mat: -------------------------------------------------------------------------------- 1 | material { 2 | name : step1, 3 | shadingModel : lit, 4 | culling: back, 5 | parameters : [ 6 | { type : float3, name : baseColor }, 7 | { type : float, name : roughness }, 8 | { type : float, name : progress }, 9 | { type : float, name : clearCoat }, 10 | { type : float, name : clearCoatRoughness }, 11 | { type : float, name : gridlines } 12 | ], 13 | } 14 | 15 | fragment { 16 | 17 | void material(inout MaterialInputs material) { 18 | prepareMaterial(material); 19 | material.baseColor.rgb = materialParams.baseColor; 20 | material.baseColor.b *= 0.5; 21 | 22 | // Gridlines 23 | vec4 wpos = vec4(getWorldPosition() + getWorldOffset(), 1.0); 24 | float angle = atan(wpos.z, wpos.x); 25 | float ythickness = 0.025 + 5.0 * fwidth(wpos.y); 26 | float athickness = 0.025 + 5.0 * fwidth(angle); 27 | float y = fract(wpos.y * 10.0); 28 | float y2 = abs(y - 0.5); 29 | float alpha = smoothstep(0.0, ythickness, y2); 30 | float theta = fract(30.0 * angle / 6.28); 31 | float theta2 = abs(theta - 0.5); 32 | alpha *= smoothstep(0.0, athickness, theta2); 33 | material.baseColor.rgb *= 1.0 - (materialParams.gridlines * (1.0 - alpha)); 34 | 35 | material.roughness = materialParams.roughness; 36 | material.clearCoat = materialParams.clearCoat; 37 | material.clearCoatRoughness = materialParams.clearCoatRoughness; 38 | } 39 | } -------------------------------------------------------------------------------- /materials/step1_cylinder_back.mat: -------------------------------------------------------------------------------- 1 | material { 2 | name : step1CylinderBack, 3 | shadingModel : lit, 4 | blending: transparent, 5 | culling: front, 6 | parameters : [ 7 | { type : float4, name : baseColor }, 8 | { type : float, name : roughness }, 9 | { type : float, name : progress }, 10 | { type : float, name : clearCoat }, 11 | { type : float, name : clearCoatRoughness } 12 | ], 13 | } 14 | 15 | fragment { 16 | 17 | void material(inout MaterialInputs material) { 18 | prepareMaterial(material); 19 | material.baseColor = materialParams.baseColor; 20 | material.roughness = materialParams.roughness; 21 | material.clearCoat = materialParams.clearCoat; 22 | material.clearCoatRoughness = materialParams.clearCoatRoughness; 23 | } 24 | } -------------------------------------------------------------------------------- /materials/step1_cylinder_front.mat: -------------------------------------------------------------------------------- 1 | material { 2 | name : step1CylinderFront, 3 | shadingModel : lit, 4 | blending: transparent, 5 | culling: back, 6 | depthCulling: false, 7 | parameters : [ 8 | { type : float4, name : baseColor }, 9 | { type : float, name : roughness }, 10 | { type : float, name : gridlines }, 11 | { type : float, name : clearCoat }, 12 | { type : float, name : clearCoatRoughness } 13 | ], 14 | } 15 | 16 | fragment { 17 | 18 | void material(inout MaterialInputs material) { 19 | prepareMaterial(material); 20 | 21 | // Gridlines 22 | vec4 wpos = vec4(getWorldPosition() + getWorldOffset(), 1.0); 23 | float angle = atan(wpos.z, wpos.x); 24 | float ythickness = 0.025 + 5.0 * fwidth(wpos.y); 25 | float athickness = 0.025 + 5.0 * fwidth(angle); 26 | material.baseColor = vec4(0); 27 | float y = fract(wpos.y * 10.0); 28 | material.baseColor.a = smoothstep(0.0, ythickness, abs(y - 0.5)); 29 | float theta = atan(wpos.z, wpos.x) / 6.28; 30 | theta = fract(theta * 30.0); 31 | material.baseColor.a *= smoothstep(0.0, athickness, abs(theta - 0.5)); 32 | material.baseColor.a = 1.0 - materialParams.gridlines * (1.0 - material.baseColor.a); 33 | material.baseColor.a = 1.0 - material.baseColor.a; 34 | 35 | material.roughness = materialParams.roughness; 36 | material.clearCoat = 0.0; 37 | material.metallic = 0.0; 38 | material.clearCoatRoughness = materialParams.clearCoatRoughness; 39 | } 40 | } -------------------------------------------------------------------------------- /materials/step2.mat: -------------------------------------------------------------------------------- 1 | material { 2 | name : step2, 3 | shadingModel : lit, 4 | transparency: twoPassesTwoSides, 5 | blending: fade, 6 | culling: none, 7 | parameters : [ 8 | { type : float3, name : baseColor }, 9 | { type : float, name : roughness }, 10 | 11 | { type : float, name : greatCircle }, 12 | { type : float, name : luneAlpha }, 13 | { type : float, name : luneExpansion }, 14 | { type : float, name : antipodeAlpha }, 15 | 16 | { type : float, name : clearCoat }, 17 | { type : float, name : clearCoatRoughness } 18 | ], 19 | } 20 | 21 | fragment { 22 | 23 | mat4 rotationX(in float angle) { 24 | return mat4(1.0, 0, 0, 0, 0, cos(angle), -sin(angle), 0, 0, sin(angle), cos(angle), 0, 0, 0, 0, 1); 25 | } 26 | 27 | mat4 rotationY(in float angle) { 28 | return mat4( cos(angle), 0, sin(angle), 0, 0, 1.0, 0, 0, -sin(angle), 0, cos(angle), 0, 0, 0, 0, 1); 29 | } 30 | 31 | mat4 rotationZ(in float angle) { 32 | return mat4( cos(angle), -sin(angle), 0, 0, sin(angle), cos(angle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); 33 | } 34 | 35 | vec3 get_plane_normal(float theta) { 36 | vec3 n = vec3(sin(theta), cos(theta), 0.0); 37 | return (vec4(n, 0) * rotationX(0.0)).xyz; 38 | } 39 | 40 | void material(inout MaterialInputs material) { 41 | prepareMaterial(material); 42 | material.baseColor.rgb = materialParams.baseColor; 43 | material.baseColor.a = 1.0; 44 | 45 | vec4 wpos = vec4(getWorldPosition() + getWorldOffset(), 1.0); 46 | 47 | const float D = 0.0; 48 | const float T = 0.01; 49 | 50 | float luneExpansion = 1.0 - pow(1.0 - materialParams.luneExpansion, 8.0); 51 | float A_theta = mix(0.1, -0.3 - 3.14 / 2.0, luneExpansion); 52 | float B_theta = mix(0.4, -0.3 + 3.14 / 2.0, luneExpansion); 53 | 54 | { 55 | float theta = A_theta; 56 | vec3 N = get_plane_normal(theta); 57 | float d1 = dot(wpos, vec4(N, D + T)); 58 | float d2 = dot(wpos, vec4(N, D - T)); 59 | float a = step(d2, 0.0) * step(0.0, d1); 60 | a *= materialParams.greatCircle; 61 | material.baseColor.rgb *= 1.0 - a; 62 | material.baseColor.a *= 1.0 - a; 63 | } 64 | 65 | { 66 | float theta = B_theta; 67 | vec3 N = get_plane_normal(theta); 68 | float d1 = dot(wpos, vec4(N, D + T)); 69 | float d2 = dot(wpos, vec4(N, D - T)); 70 | float a = step(d2, 0.0) * step(0.0, d1); 71 | a *= materialParams.luneAlpha; 72 | material.baseColor.rgb *= 1.0 - a; 73 | material.baseColor.a *= 1.0 - a; 74 | } 75 | 76 | material.baseColor.b *= 0.5; 77 | 78 | { 79 | float theta0 = A_theta; 80 | float theta1 = B_theta; 81 | vec3 N0 = get_plane_normal(theta0); 82 | vec3 N1 = get_plane_normal(theta1); 83 | float d1 = dot(wpos, vec4(N0, D)); 84 | float d2 = dot(wpos, vec4(N1, D)); 85 | float a = step(d2, 0.0) * step(0.0, d1); 86 | a *= materialParams.luneAlpha; 87 | material.baseColor.b *= 1.0 - a; 88 | } 89 | 90 | { 91 | float theta1 = A_theta; 92 | float theta0 = B_theta; 93 | vec3 N0 = get_plane_normal(theta0); 94 | vec3 N1 = get_plane_normal(theta1); 95 | float d1 = dot(wpos, vec4(N0, D)); 96 | float d2 = dot(wpos, vec4(N1, D)); 97 | float a = step(d2, 0.0) * step(0.0, d1); 98 | a *= materialParams.luneAlpha * materialParams.antipodeAlpha; 99 | material.baseColor.b *= 1.0 - a; 100 | } 101 | 102 | float fadeIn = materialParams.greatCircle; 103 | material.baseColor.a = mix(1.0, 0.7 + 0.3 * (1.0 - material.baseColor.a), fadeIn); 104 | 105 | material.roughness = materialParams.roughness; 106 | material.clearCoat = materialParams.clearCoat; 107 | material.clearCoatRoughness = materialParams.clearCoatRoughness; 108 | } 109 | } -------------------------------------------------------------------------------- /materials/step3.mat: -------------------------------------------------------------------------------- 1 | material { 2 | name : step3, 3 | shadingModel : lit, 4 | transparency: twoPassesTwoSides, 5 | blending: fade, 6 | culling: none, 7 | parameters : [ 8 | { type : float3, name : baseColor }, 9 | { type : float, name : roughness }, 10 | 11 | { type : float, name : fadeInTriangle }, 12 | { type : float, name : triangleExpansion }, 13 | { type : float, name : fadeInLuneA }, 14 | { type : float, name : fadeInLuneB }, 15 | { type : float, name : fadeInLuneC }, 16 | { type : float, name : rotation }, 17 | 18 | { type : float, name : clearCoat }, 19 | { type : float, name : clearCoatRoughness } 20 | ], 21 | } 22 | 23 | fragment { 24 | 25 | vec4 getPerp(vec4 planeEqn, float blend) { 26 | float theta = blend * PI; 27 | vec3 a = vec3(cos(theta), sin(theta), 0); 28 | vec3 Z = normalize(cross(planeEqn.xyz, vec3(0, 0, 1))); 29 | vec3 X = normalize(cross(Z, planeEqn.xyz)); 30 | vec3 Y = normalize(cross(Z, X)); 31 | return vec4(a * mat3(X, Y, Z), 0.0); 32 | } 33 | 34 | mat4 rotationX(in float angle) { 35 | return mat4(1.0, 0, 0, 0, 0, cos(angle), -sin(angle), 0, 0, sin(angle), cos(angle), 0, 0, 0, 0, 1); 36 | } 37 | 38 | mat4 rotationY(in float angle) { 39 | return mat4( cos(angle), 0, sin(angle), 0, 0, 1.0, 0, 0, -sin(angle), 0, cos(angle), 0, 0, 0, 0, 1); 40 | } 41 | 42 | mat4 rotationZ(in float angle) { 43 | return mat4( cos(angle), -sin(angle), 0, 0, sin(angle), cos(angle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); 44 | } 45 | 46 | const float T = 0.01; 47 | 48 | float getEndcaps(vec4 wpos, vec4 planeA, vec4 planeB, float t) { 49 | planeA.w = T; 50 | planeB.w = T; 51 | float a = step(0.0, dot(wpos, planeA)); 52 | planeB = normalize(mix(planeA, -planeB, t)); 53 | float b = step(dot(wpos, planeB), 0.0); 54 | return a * b; 55 | } 56 | 57 | void material(inout MaterialInputs material) { 58 | prepareMaterial(material); 59 | material.baseColor.rgb = materialParams.baseColor; 60 | material.baseColor.b *= 0.5; 61 | material.baseColor.a = 1.0; 62 | 63 | vec4 wpos = vec4(getWorldPosition() + getWorldOffset(), 1.0); 64 | mat4 mat = rotationY(materialParams.rotation); 65 | vec4 kPlaneA = mat * vec4(normalize(vec3(-0.1, -0.7, +0.5)), 0.0); 66 | vec4 kPlaneB = mat * vec4(normalize(vec3(+0.7, +0.7, +0.3)), 0.0); 67 | vec4 kPlaneC = mat * vec4(normalize(vec3(-0.6, +0.6, +0.1)), 0.0); 68 | 69 | kPlaneA.xyz = normalize(mix(kPlaneA.xyz, vec3(0, 0, 1), materialParams.triangleExpansion)); 70 | kPlaneB.xyz = normalize(mix(kPlaneB.xyz, vec3(1, 0, 0), materialParams.triangleExpansion)); 71 | kPlaneC.xyz = normalize(mix(kPlaneC.xyz, vec3(0, 1, 0), materialParams.triangleExpansion)); 72 | 73 | const vec4 kLineColor = vec4(0.0, 0.0, 0.0, 1.0); 74 | 75 | bool insideA = dot(wpos, kPlaneA) > 0.0; 76 | bool insideB = dot(wpos, kPlaneB) > 0.0; 77 | bool insideC = dot(wpos, kPlaneC) > 0.0; 78 | 79 | vec3 totalLuneColor = vec3(0, 0, 0); 80 | 81 | // Double Lune formed by Angle A 82 | float redLune1 = (insideA && insideB) ? 1.0 : 0.0; 83 | float redLune2 = (!insideA && !insideB) ? 1.0 : 0.0; 84 | totalLuneColor += vec3(materialParams.fadeInLuneA, 0, 0) * redLune1; 85 | totalLuneColor += vec3(materialParams.fadeInLuneA, 0, 0) * redLune2; 86 | 87 | // Double Lune formed by Angle B 88 | float grnLune1 = (insideA && insideC) ? 1.0 : 0.0; 89 | float grnLune2 = (!insideA && !insideC) ? 1.0 : 0.0; 90 | totalLuneColor += vec3(0, materialParams.fadeInLuneB, 0) * grnLune1; 91 | totalLuneColor += vec3(0, materialParams.fadeInLuneB, 0) * grnLune2; 92 | 93 | // Double Lune formed by Angle C 94 | float bluLune1 = (insideB && insideC) ? 1.0 : 0.0; 95 | float bluLune2 = (!insideB && !insideC) ? 1.0 : 0.0; 96 | totalLuneColor += vec3(0, 0, materialParams.fadeInLuneC) * bluLune1; 97 | totalLuneColor += vec3(0, 0, materialParams.fadeInLuneC) * bluLune2; 98 | 99 | float maxLune = max(max(totalLuneColor.r, totalLuneColor.g), totalLuneColor.b); 100 | material.baseColor.rgb = mix(material.baseColor.rgb, totalLuneColor, 0.8 * maxLune); 101 | 102 | float fadeIn = materialParams.fadeInTriangle; 103 | float aDrawTime = clamp((fadeIn * 3.0 - 0.0), 0.0, 1.0); 104 | float bDrawTime = clamp((fadeIn * 3.0 - 1.0), 0.0, 1.0); 105 | float cDrawTime = clamp((fadeIn * 3.0 - 2.0), 0.0, 1.0); 106 | 107 | { 108 | float d1 = dot(wpos, vec4(kPlaneA.xyz, kPlaneA.w + T)); 109 | float d2 = dot(wpos, vec4(kPlaneA.xyz, kPlaneA.w - T)); 110 | float d3 = getEndcaps(wpos, kPlaneB, kPlaneC, aDrawTime); 111 | float a = step(d2, 0.0) * step(0.0, d1) * d3; 112 | material.baseColor = mix(material.baseColor, kLineColor, a); 113 | } 114 | 115 | { 116 | float d1 = dot(wpos, vec4(kPlaneB.xyz, kPlaneB.w + T)); 117 | float d2 = dot(wpos, vec4(kPlaneB.xyz, kPlaneB.w - T)); 118 | float d3 = getEndcaps(wpos, kPlaneA, kPlaneC, bDrawTime); 119 | float a = step(d2, 0.0) * step(0.0, d1) * d3; 120 | material.baseColor = mix(material.baseColor, kLineColor, a); 121 | } 122 | 123 | { 124 | float d1 = dot(wpos, vec4(kPlaneC.xyz, kPlaneC.w + T)); 125 | float d2 = dot(wpos, vec4(kPlaneC.xyz, kPlaneC.w - T)); 126 | float d3 = getEndcaps(wpos, kPlaneA, kPlaneB, cDrawTime); 127 | float a = step(d2, 0.0) * step(0.0, d1) * d3; 128 | material.baseColor = mix(material.baseColor, kLineColor, a); 129 | } 130 | 131 | // Dark filled area of triangle 132 | float alpha = (insideA && insideB && insideC) ? 0.5 : 1.0; 133 | material.baseColor.rgb *= mix(1.0, alpha, cDrawTime); 134 | 135 | material.roughness = materialParams.roughness; 136 | material.clearCoat = materialParams.clearCoat; 137 | material.clearCoatRoughness = materialParams.clearCoatRoughness; 138 | } 139 | } -------------------------------------------------------------------------------- /materials/step4.mat: -------------------------------------------------------------------------------- 1 | material { 2 | name : step4, 3 | shadingModel : lit, 4 | transparency: twoPassesTwoSides, 5 | blending: fade, 6 | culling: none, 7 | parameters : [ 8 | { type : float3, name : baseColor }, 9 | { type : float, name : roughness }, 10 | { type : float, name : clearCoat }, 11 | { type : float, name : clearCoatRoughness }, 12 | 13 | { type : float, name : fadeInPolygon }, 14 | { type : float, name : fadeInTriangle } 15 | ], 16 | } 17 | 18 | fragment { 19 | 20 | const float kLineThickness = 0.01; 21 | const vec4 kLineColor = vec4(0.0, 0.0, 0.0, 1.0); 22 | 23 | vec4 rotate(vec4 pt, vec3 axis, float angle) { 24 | float s = sin(angle); 25 | float c = cos(angle); 26 | float oc = 1.0 - c; 27 | return pt * mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0, 28 | oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0, 29 | oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0, 30 | 0.0, 0.0, 0.0, 1.0); 31 | } 32 | 33 | // Returns 0 or 1. The latter is returned only if the given point is both inside the half-space of planeA and inside 34 | // the reverse half-space of planeB. The given t value is a fraction in [0,1] used to animate planeB to create the 35 | // illusion that the line is being gradually inscribed. 36 | float between(vec4 pos, vec4 planeA, vec4 planeB, float t) { 37 | planeA.w = kLineThickness; 38 | planeB.w = kLineThickness; 39 | float a = step(0.0, dot(pos, planeA)); 40 | planeB = normalize(mix(planeA, -planeB, t)); 41 | float b = step(dot(pos, planeB), 0.0); 42 | return a * b; 43 | } 44 | 45 | // Returns a number in [0,1] where 0 is outside a thick geodesic line segment and 1 is inside. Fractional values may 46 | // be returned to achieve smooth antialiasing. The geodesic is defined as the intersection of a great circle with 47 | // two bounding half-spaces (lowerClip and upperClip). The great circle is the intersection of "plane" with the unit 48 | // sphere. 49 | // 50 | // To draw a thick lines, thick function splits "plane" into two planes by shifting it along its normal and checking 51 | // that the given point "pos" is between the shifted planes. The given t value is a fraction in [0,1] used to 52 | // animate the bounding planes to create the illusion that the line is being gradually inscribed. 53 | float geodesic(vec4 pos, vec4 plane, vec4 lowerClip, vec4 upperClip, float t) { 54 | float d1 = dot(pos, vec4(plane.xyz, +kLineThickness)); 55 | float d2 = dot(pos, vec4(plane.xyz, -kLineThickness)); 56 | float d3 = between(pos, lowerClip, upperClip, t); 57 | return step(0.0, d1) * step(d2, 0.0) * d3; 58 | } 59 | 60 | void material(inout MaterialInputs material) { 61 | prepareMaterial(material); 62 | 63 | vec4 color = vec4(materialParams.baseColor, 1.0); 64 | color.b *= 0.5; 65 | 66 | vec4 wpos = vec4(getWorldPosition() + getWorldOffset(), 1.0); 67 | const float outwardTheta = 70.0 * PI / 180.0; // <== This angle determines how large the polygon is. 68 | 69 | vec4 plane[5]; 70 | float fadeInPoly = materialParams.fadeInPolygon; 71 | float fadeInCrossbar = materialParams.fadeInTriangle; 72 | float inside = 1.0; 73 | 74 | for (int i = 0; i < 5; i++) { 75 | float f = float(i); 76 | float theta = 2.0 * PI * f / 5.0; 77 | vec3 axis = vec3(sin(theta), cos(theta), 0.0); 78 | plane[i] = rotate(vec4(0, 0, 1, 0), axis, outwardTheta); 79 | inside *= step(0.0, dot(wpos, plane[i])); 80 | } 81 | 82 | for (int i = 0; i < 5; i++) { 83 | int j = (i + 1) % 5; 84 | int k = (i + 5 - 1) % 5; 85 | float f = float(i); 86 | float inscribe = clamp((fadeInPoly * 5.0 - 5.0 + 1.0 + f), 0.0, 1.0); 87 | color = mix(color, kLineColor, geodesic(wpos, plane[i], plane[j], plane[k], inscribe)); 88 | } 89 | color.rgb *= mix(1.0, 1.0 - 0.5 * inside, fadeInPoly); 90 | 91 | { 92 | float theta0 = 2.0 * PI * 0.0 / 5.0; 93 | float theta3 = 2.0 * PI * 3.0 / 5.0; 94 | float theta = mix(theta0, theta3, 0.5); 95 | vec3 axis = vec3(sin(theta), cos(theta), 0.0); 96 | vec4 crossbarPlane = rotate(vec4(0, 0, 1, 0), axis, 1.44); 97 | color = mix(color, kLineColor, geodesic(wpos, crossbarPlane, plane[0], plane[3], fadeInCrossbar)); 98 | } 99 | 100 | { 101 | float theta0 = 2.0 * PI * 0.0 / 5.0; 102 | float theta4 = 2.0 * PI * 4.0 / 5.0; 103 | float theta = mix(theta0, theta4, 0.5); 104 | vec3 axis = vec3(sin(theta), cos(theta), 0.0); 105 | vec4 crossbarPlane = rotate(vec4(0, 0, 1, 0), axis, 1.725); 106 | color = mix(color, kLineColor, geodesic(wpos, crossbarPlane, plane[0], plane[4], fadeInCrossbar)); 107 | } 108 | 109 | material.baseColor = color; 110 | material.roughness = materialParams.roughness; 111 | material.clearCoat = materialParams.clearCoat; 112 | material.clearCoatRoughness = materialParams.clearCoatRoughness; 113 | } 114 | } -------------------------------------------------------------------------------- /materials/step5.mat: -------------------------------------------------------------------------------- 1 | material { 2 | name : step5, 3 | shadingModel : lit, 4 | blending: fade, 5 | culling: back, 6 | parameters : [ 7 | { type : float3, name : baseColor }, 8 | { type : float, name : roughness }, 9 | { type : float, name : opacity }, 10 | { type : float, name : clearCoat }, 11 | { type : float, name : clearCoatRoughness } 12 | ], 13 | } 14 | 15 | fragment { 16 | 17 | void material(inout MaterialInputs material) { 18 | prepareMaterial(material); 19 | material.baseColor.rgb = materialParams.baseColor; 20 | material.baseColor.b *= 0.5; 21 | material.baseColor *= materialParams.opacity; 22 | material.roughness = materialParams.roughness; 23 | material.clearCoat = materialParams.clearCoat; 24 | material.clearCoatRoughness = materialParams.clearCoatRoughness; 25 | } 26 | } -------------------------------------------------------------------------------- /materials/step5_poly.mat: -------------------------------------------------------------------------------- 1 | material { 2 | name : step5Poly, 3 | shadingModel : lit, 4 | culling: back, 5 | parameters : [ 6 | { type : float, name : inflation } 7 | ], 8 | } 9 | 10 | vertex { 11 | void materialVertex(inout MaterialVertexInputs material) { 12 | vec3 worldPosition = material.worldPosition.xyz + getWorldOffset(); 13 | vec3 spherePt = normalize(worldPosition); 14 | worldPosition = mix(worldPosition, spherePt, materialParams.inflation); 15 | worldPosition *= 1.01; 16 | material.worldPosition.xyz = worldPosition - getWorldOffset(); 17 | 18 | material.worldNormal = normalize(mix(material.worldNormal, spherePt, materialParams.inflation)); 19 | } 20 | } 21 | 22 | fragment { 23 | void material(inout MaterialInputs material) { 24 | prepareMaterial(material); 25 | material.baseColor = vec4(0.8, 0.8, 0.8, 1.0); 26 | 27 | // vec4 wpos = vec4(getWorldPosition() + getWorldOffset(), 1.0); 28 | 29 | material.roughness = 0.2; // + 0.8 * materialParams.inflation; 30 | material.clearCoat = 0.0; 31 | material.metallic = 1.0; 32 | } 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "euler_filament", 3 | "main": "index.js", 4 | "version": "1.0.0", 5 | "config": { 6 | "matc": "./materials/matc" 7 | }, 8 | "scripts": { 9 | "build": "webpack --mode development", 10 | "build:release": "webpack --mode production", 11 | "build:html": "ts-node 'src/markdownToHtml.ts' 'src/verbiage.md' 'docs/index.html'", 12 | "build:shaders": "ls -1 materials/*.mat | sed -e 's/\\.mat$//' | xargs -I % ${npm_package_config_matc} -o docs/%.filamat.bmp %.mat" 13 | }, 14 | "devDependencies": { 15 | "@types/gl-matrix": "^2.4.4", 16 | "ts-loader": "^5.0.0", 17 | "tslint": "^5.11.0", 18 | "typescript": "^3.0.0", 19 | "webpack": "^4.0.0", 20 | "webpack-cli": "^3.1.2" 21 | }, 22 | "dependencies": { 23 | "@types/marked": "^0.6.5", 24 | "@types/node": "^12.7.2", 25 | "filament": "1.3.2", 26 | "gl-matrix": "^2.8.1", 27 | "html-loader": "^0.5.5", 28 | "http-server": "^0.11.1", 29 | "markdown-loader": "^5.1.0", 30 | "marked": "^4.0.10", 31 | "ts-node": "^8.3.0" 32 | }, 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as Filament from "filament"; 2 | 3 | import { Display } from "./display"; 4 | import { Scene } from "./scene"; 5 | import { Story } from "./scrollytell"; 6 | import { Timeline } from "./timeline"; 7 | import * as urls from "./urls"; 8 | 9 | declare const BUILD_COMMAND: string; 10 | 11 | export class App { 12 | public readonly scene = new Scene(); 13 | public readonly story: Story; 14 | 15 | private display: Display; 16 | private readonly production: boolean; 17 | private timeline: Timeline; 18 | 19 | public constructor() { 20 | this.production = BUILD_COMMAND.indexOf("release") > -1; 21 | console.info(this.production ? "Production mode" : "Development mode"); 22 | 23 | this.story = new Story({ 24 | chartSelector: ".chart", 25 | containerSelector: ".container", 26 | panelSelector: ".panel", 27 | segmentSelector: "segment", 28 | developerHud: !this.production, 29 | fullsizeChart: true, 30 | progressHandler: (story) => { 31 | if (this.display) { 32 | const panel = story.getActivePanelIndex(); 33 | const progress = story.getProgressValue(); 34 | this.render(panel, progress); 35 | } 36 | }, 37 | }); 38 | 39 | Filament.init(urls.initialAssets, () => { 40 | this.display = new Display(this.production, this.scene); 41 | this.timeline = new Timeline(this.scene); 42 | this.timeline.update(0, 0); 43 | this.display.update(0, 0); 44 | this.display.resize(); 45 | this.story.refresh(); 46 | }); 47 | 48 | // Ideally this would allow up/down arrows to work but it doesn't seem reliable: 49 | const el: HTMLElement = document.querySelector(".container"); 50 | el.focus(); 51 | } 52 | 53 | private render(panel: number, progress: number) { 54 | if (panel > -1) { 55 | this.timeline.update(panel, progress); 56 | this.display.update(panel, progress); 57 | } 58 | this.display.render(); 59 | } 60 | } 61 | 62 | /* tslint:disable */ 63 | 64 | window.onload = () => { 65 | window["app"] = new App(); 66 | }; 67 | -------------------------------------------------------------------------------- /src/display.ts: -------------------------------------------------------------------------------- 1 | import * as Filament from "filament"; 2 | import * as glm from "gl-matrix"; 3 | 4 | import * as polyhedron from "./polyhedron"; 5 | import { Scene } from "./scene"; 6 | import * as urls from "./urls"; 7 | 8 | const kFallbackImages = [ 9 | [0, 0.05], 10 | [0, 0.40], 11 | [0, 0.65], 12 | [1, 0.00], 13 | [1, 0.24], 14 | [1, 0.45], 15 | [1, 0.82], 16 | [2, 0.00], 17 | [2, 0.20], 18 | [2, 0.35], 19 | [2, 0.57], 20 | [2, 0.62], 21 | [2, 0.75], 22 | [3, 0.16], 23 | [3, 0.41], 24 | [3, 0.89], 25 | [4, 0.13], 26 | [4, 0.29], 27 | [4, 0.44], 28 | ]; 29 | 30 | function getFallbackUrl(panel: number, progress: number): string { 31 | let url = ""; 32 | for (const pair of kFallbackImages) { 33 | const p0 = pair[0].toString(); 34 | const p1 = Math.round(pair[1] * 100).toString().padStart(2, "0"); 35 | if (pair[0] <= panel && pair[1] <= progress) { 36 | url = `images/img${p0}_${p1}.png`; 37 | } 38 | } 39 | return url; 40 | } 41 | 42 | export class Display { 43 | private backCylinderEntity: Filament.Entity; 44 | private readonly camera: Filament.Camera; 45 | private readonly canvas2d: HTMLCanvasElement; 46 | private readonly canvas3d: HTMLCanvasElement; 47 | private readonly context2d: CanvasRenderingContext2D; 48 | private currentProgress = 0; 49 | private currentStep = 0; 50 | private cylinderIndexBuffer: Filament.IndexBuffer; 51 | private cylinderVertexBuffer: Filament.VertexBuffer; 52 | private readonly engine: Filament.Engine; 53 | private readonly fallbackImage: HTMLImageElement; 54 | private readonly filamentScene: Filament.Scene; 55 | private frontCylinderEntity: Filament.Entity; 56 | private readonly indirectLight: Filament.IndirectLight; 57 | private polyhedronEntities: Filament.Entity[]; 58 | private readonly production: boolean; 59 | private readonly renderer: Filament.Renderer; 60 | private readonly scene: Scene; 61 | private readonly skybox: Filament.Skybox; 62 | private sphereEntity: Filament.Entity; 63 | private readonly step1CylinderBackMaterial: Filament.MaterialInstance; 64 | private readonly step1CylinderFrontMaterial: Filament.MaterialInstance; 65 | private readonly step1SphereMaterial: Filament.MaterialInstance; 66 | private readonly step2SphereMaterial: Filament.MaterialInstance; 67 | private readonly step3SphereMaterial: Filament.MaterialInstance; 68 | private readonly step4SphereMaterial: Filament.MaterialInstance; 69 | private readonly step5PolyhedronMaterial: Filament.MaterialInstance; 70 | private readonly step5SphereMaterial: Filament.MaterialInstance; 71 | private readonly swapChain: Filament.SwapChain; 72 | private readonly view: Filament.View; 73 | 74 | public constructor(production: boolean, scene: Scene) { 75 | 76 | glm.glMatrix.setMatrixArrayType(Array); 77 | 78 | // tslint:disable-next-line: no-string-literal 79 | window["vec3"] = glm.vec3; 80 | 81 | this.currentStep = -1; 82 | this.production = production; 83 | this.scene = scene; 84 | this.canvas2d = document.getElementById("canvas2d") as HTMLCanvasElement; 85 | this.canvas3d = document.getElementById("canvas3d") as HTMLCanvasElement; 86 | 87 | try { 88 | this.engine = Filament.Engine.create(this.canvas3d); 89 | } catch (e) { 90 | console.error("WebGL 2.0 is not supported."); 91 | const parent = this.canvas3d.parentElement; 92 | this.canvas3d.remove(); 93 | this.canvas3d = undefined; 94 | this.canvas2d.remove(); 95 | this.canvas2d = undefined; 96 | 97 | const pair = kFallbackImages[0]; 98 | this.fallbackImage = document.createElement("img"); 99 | this.fallbackImage.src = getFallbackUrl(pair[0], pair[1]); 100 | this.fallbackImage.style.height = "auto"; 101 | 102 | parent.appendChild(this.fallbackImage); 103 | } 104 | 105 | if (!this.canvas3d) { 106 | return; 107 | } 108 | 109 | this.context2d = this.canvas2d.getContext("2d"); 110 | this.filamentScene = this.engine.createScene(); 111 | this.swapChain = this.engine.createSwapChain(); 112 | this.renderer = this.engine.createRenderer(); 113 | this.camera = this.engine.createCamera(); 114 | this.view = this.engine.createView(); 115 | this.view.setCamera(this.camera); 116 | this.view.setScene(this.filamentScene); 117 | 118 | const step1SphereMaterial = this.engine.createMaterial(urls.step1SphereMaterial); 119 | const step1CylinderBackMaterial = this.engine.createMaterial(urls.step1CylinderBackMaterial); 120 | const step1CylinderFrontMaterial = this.engine.createMaterial(urls.step1CylinderFrontMaterial); 121 | const step2SphereMaterial = this.engine.createMaterial(urls.step2SphereMaterial); 122 | const step3SphereMaterial = this.engine.createMaterial(urls.step3SphereMaterial); 123 | const step4SphereMaterial = this.engine.createMaterial(urls.step4SphereMaterial); 124 | const step5PolyhedronMaterial = this.engine.createMaterial(urls.step5PolyhedronMaterial); 125 | const step5SphereMaterial = this.engine.createMaterial(urls.step5SphereMaterial); 126 | 127 | const mats: Filament.MaterialInstance[] = [ 128 | this.step1SphereMaterial = step1SphereMaterial.createInstance(), 129 | this.step1CylinderBackMaterial = step1CylinderBackMaterial.createInstance(), 130 | this.step1CylinderFrontMaterial = step1CylinderFrontMaterial.createInstance(), 131 | this.step2SphereMaterial = step2SphereMaterial.createInstance(), 132 | this.step3SphereMaterial = step3SphereMaterial.createInstance(), 133 | this.step4SphereMaterial = step4SphereMaterial.createInstance(), 134 | this.step5SphereMaterial = step5SphereMaterial.createInstance(), 135 | ]; 136 | 137 | this.step5PolyhedronMaterial = step5PolyhedronMaterial.createInstance(); 138 | 139 | const sRGB = Filament.RgbType.sRGB; 140 | for (const mat of mats) { 141 | mat.setColor3Parameter("baseColor", sRGB, [0.0, 0.4, 0.8]); 142 | mat.setFloatParameter("roughness", 0.5); 143 | mat.setFloatParameter("clearCoat", 0.0); 144 | mat.setFloatParameter("clearCoatRoughness", 0.8); 145 | } 146 | 147 | this.createCylinders(); 148 | this.createCentralSphere(); 149 | this.createPolyhedron(); 150 | 151 | this.filamentScene.addEntity(this.sphereEntity); 152 | 153 | this.skybox = this.engine.createSkyFromKtx(urls.sky); 154 | this.indirectLight = this.engine.createIblFromKtx(urls.ibl); 155 | this.indirectLight.setIntensity(50000); 156 | 157 | const iblDirection = this.indirectLight.getDirectionEstimate(); 158 | const iblColor = this.indirectLight.getColorEstimate(iblDirection); 159 | 160 | this.filamentScene.setSkybox(this.skybox); 161 | this.filamentScene.setIndirectLight(this.indirectLight); 162 | 163 | const sunlight: Filament.Entity = Filament.EntityManager.get().create(); 164 | Filament.LightManager.Builder(Filament.LightManager$Type.SUN) 165 | .color(iblColor.slice(0, 3) as number[]) 166 | .castShadows(false) 167 | .intensity(iblColor[3]) 168 | .direction([0.5, -1, 0]) 169 | .build(this.engine, sunlight); 170 | this.filamentScene.addEntity(sunlight); 171 | 172 | this.update(0, 0); 173 | } 174 | 175 | public render() { 176 | if (!this.canvas3d) { 177 | const url = getFallbackUrl(this.currentStep, this.currentProgress); 178 | if (this.currentStep === -1 || this.currentProgress === -1) { 179 | return; 180 | } 181 | if (url && this.fallbackImage.src !== url) { 182 | this.fallbackImage.src = url; 183 | } 184 | return; 185 | } 186 | 187 | const vp = this.scene.viewpoint; 188 | this.camera.lookAt(vp.eye, vp.center, vp.up); 189 | this.renderer.render(this.swapChain, this.view); 190 | 191 | const width = this.canvas2d.width; 192 | const height = this.canvas2d.height; 193 | const fontSize = width / 36; 194 | 195 | this.context2d.setTransform(1, 0, 0, 1, 0, 0); 196 | this.context2d.clearRect(0, 0, width, height); 197 | this.context2d.font = `${fontSize}px 'circular-bold', sans-serif`; 198 | this.context2d.textAlign = "center"; 199 | 200 | for (const span of this.scene.textSpans) { 201 | const x = width / 2 + span.x * width / 2; 202 | const y = height / 2 + span.y * width / 2; 203 | this.context2d.fillStyle = `rgba(0, 0, 0, ${span.opacity})`; 204 | this.context2d.fillText(span.text, x, y); 205 | } 206 | 207 | this.context2d.fillStyle = "rgba(0, 0, 0, 1)"; 208 | } 209 | 210 | public resize() { 211 | if (!this.canvas3d) { 212 | return; 213 | } 214 | const dpr = window.devicePixelRatio; 215 | const width = this.canvas2d.width = this.canvas2d.clientWidth * dpr; 216 | const height = this.canvas2d.height = this.canvas2d.clientHeight * dpr; 217 | this.context2d.setTransform(1, 0, 0, 1, 0, 0); 218 | this.context2d.translate(width / 2.0, height / 2.0); 219 | this.context2d.scale(width / 2.0, width / 2.0); 220 | this.canvas3d.width = width; 221 | this.canvas3d.height = height; 222 | this.view.setViewport([0, 0, width, height]); 223 | const aspect: number = width / height; 224 | const fov = 45; 225 | const near = 1.0; 226 | const far = 20000.0; 227 | this.camera.setProjectionFov(fov, aspect, near, far, Filament.Camera$Fov.HORIZONTAL); 228 | } 229 | 230 | public update(step: number, progress: number) { 231 | this.currentProgress = progress; 232 | 233 | if (!this.canvas3d) { 234 | this.currentStep = step; 235 | return; 236 | } 237 | 238 | const rm = this.engine.getRenderableManager(); 239 | const tcm = this.engine.getTransformManager(); 240 | if (this.currentStep !== step) { 241 | const sphere = rm.getInstance(this.sphereEntity); 242 | this.filamentScene.remove(this.frontCylinderEntity); 243 | this.filamentScene.remove(this.backCylinderEntity); 244 | 245 | if (this.currentStep === 4) { 246 | for (const entity of this.polyhedronEntities) { 247 | this.filamentScene.remove(entity); 248 | } 249 | } 250 | 251 | let currentMaterial: Filament.MaterialInstance; 252 | switch (step) { 253 | case 0: 254 | this.filamentScene.addEntity(this.backCylinderEntity); 255 | this.filamentScene.addEntity(this.frontCylinderEntity); 256 | default: 257 | currentMaterial = this.step1SphereMaterial; 258 | break; 259 | case 1: 260 | currentMaterial = this.step2SphereMaterial; 261 | break; 262 | case 2: 263 | currentMaterial = this.step3SphereMaterial; 264 | break; 265 | case 3: 266 | currentMaterial = this.step4SphereMaterial; 267 | break; 268 | case 4: 269 | currentMaterial = this.step5SphereMaterial; 270 | for (const entity of this.polyhedronEntities) { 271 | this.filamentScene.addEntity(entity); 272 | } 273 | } 274 | rm.setMaterialInstanceAt(sphere, 0, currentMaterial); 275 | this.currentStep = step; 276 | } 277 | 278 | const sRGBA = Filament.RgbaType.sRGB; 279 | 280 | switch (step) { 281 | default: 282 | case 0: 283 | this.step1SphereMaterial.setFloatParameter("gridlines", this.scene.sphereGridlines); 284 | this.step1CylinderFrontMaterial.setFloatParameter("gridlines", this.scene.cylinderGridlines); 285 | this.step1CylinderFrontMaterial.setColor4Parameter("baseColor", sRGBA, this.scene.baseColor); 286 | this.step1CylinderBackMaterial.setColor4Parameter("baseColor", sRGBA, this.scene.baseColor); 287 | const front = tcm.getInstance(this.frontCylinderEntity); 288 | tcm.setTransform(front, this.scene.cylinderTransform); 289 | front.delete(); 290 | const back = tcm.getInstance(this.backCylinderEntity); 291 | tcm.setTransform(back, this.scene.cylinderTransform); 292 | back.delete(); 293 | break; 294 | case 1: 295 | this.step2SphereMaterial.setFloatParameter("greatCircle", this.scene.greatCircle); 296 | this.step2SphereMaterial.setFloatParameter("luneAlpha", this.scene.luneAlpha); 297 | this.step2SphereMaterial.setFloatParameter("luneExpansion", this.scene.luneExpansion); 298 | this.step2SphereMaterial.setFloatParameter("antipodeAlpha", this.scene.antipodeAlpha); 299 | break; 300 | case 2: 301 | this.step3SphereMaterial.setFloatParameter("rotation", this.scene.rotation); 302 | this.step3SphereMaterial.setFloatParameter("fadeInTriangle", this.scene.fadeInTriangle); 303 | this.step3SphereMaterial.setFloatParameter("triangleExpansion", this.scene.triangleExpansion); 304 | this.step3SphereMaterial.setFloatParameter("fadeInLuneA", this.scene.fadeInLuneA); 305 | this.step3SphereMaterial.setFloatParameter("fadeInLuneB", this.scene.fadeInLuneB); 306 | this.step3SphereMaterial.setFloatParameter("fadeInLuneC", this.scene.fadeInLuneC); 307 | break; 308 | case 3: 309 | this.step4SphereMaterial.setFloatParameter("fadeInPolygon", this.scene.fadeInPolygon); 310 | this.step4SphereMaterial.setFloatParameter("fadeInTriangle", this.scene.fadeInTriangle); 311 | break; 312 | case 4: 313 | this.step5SphereMaterial.setFloatParameter("opacity", this.scene.opacity); 314 | this.step5PolyhedronMaterial.setFloatParameter("inflation", this.scene.inflation); 315 | } 316 | } 317 | 318 | private createCentralSphere() { 319 | const AttributeType = Filament.VertexBuffer$AttributeType; 320 | const IndexType = Filament.IndexBuffer$IndexType; 321 | const PrimitiveType = Filament.RenderableManager$PrimitiveType; 322 | const VertexAttribute = Filament.VertexAttribute; 323 | 324 | const icosphere = new Filament.IcoSphere(5); 325 | 326 | const vb: Filament.VertexBuffer = Filament.VertexBuffer.Builder() 327 | .vertexCount(icosphere.vertices.length / 3) 328 | .bufferCount(2) 329 | .attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT3, 0, 0) 330 | .attribute(VertexAttribute.TANGENTS, 1, AttributeType.SHORT4, 0, 0) 331 | .normalized(VertexAttribute.TANGENTS) 332 | .build(this.engine); 333 | 334 | const ib: Filament.IndexBuffer = Filament.IndexBuffer.Builder() 335 | .indexCount(icosphere.triangles.length) 336 | .bufferType(IndexType.USHORT) 337 | .build(this.engine); 338 | 339 | vb.setBufferAt(this.engine, 0, icosphere.vertices); 340 | vb.setBufferAt(this.engine, 1, icosphere.tangents); 341 | ib.setBuffer(this.engine, icosphere.triangles); 342 | 343 | this.sphereEntity = Filament.EntityManager.get().create(); 344 | 345 | Filament.RenderableManager.Builder(1) 346 | .boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] }) 347 | .material(0, this.step1SphereMaterial) 348 | .geometry(0, PrimitiveType.TRIANGLES, vb, ib) 349 | .build(this.engine, this.sphereEntity); 350 | } 351 | 352 | private createCylinders() { 353 | const AttributeType = Filament.VertexBuffer$AttributeType; 354 | const IndexType = Filament.IndexBuffer$IndexType; 355 | const PrimitiveType = Filament.RenderableManager$PrimitiveType; 356 | const VertexAttribute = Filament.VertexAttribute; 357 | 358 | const kSlicesCount = 50; 359 | const kRingsCount = 12; 360 | const kVertCount = kSlicesCount * kRingsCount; 361 | const kThetaInc = Math.PI * 2 / kSlicesCount; 362 | 363 | const cylinder = { 364 | normals: new Float32Array(kVertCount * 3), 365 | tangents: undefined, 366 | triangles: new Uint16Array(kSlicesCount * 3 * 2 * (kRingsCount - 1)), 367 | vertices: new Float32Array(kVertCount * 3), 368 | }; 369 | 370 | let t = 0; 371 | let v = 0; 372 | for (let j = 0; j < kRingsCount - 1; j += 1) { 373 | for (let i = 0; i < kSlicesCount; i += 1) { 374 | const k = (i + 1) % kSlicesCount; 375 | cylinder.triangles[t + 0] = v + i; 376 | cylinder.triangles[t + 1] = v + k; 377 | cylinder.triangles[t + 2] = v + i + kSlicesCount; 378 | cylinder.triangles[t + 3] = v + i + kSlicesCount; 379 | cylinder.triangles[t + 4] = v + k; 380 | cylinder.triangles[t + 5] = v + k + kSlicesCount; 381 | t += 6; 382 | } 383 | v += kSlicesCount; 384 | } 385 | 386 | v = 0; 387 | let z = 0; 388 | const deltaz = 1.0 / (kRingsCount - 1); 389 | 390 | for (let j = 0; j < kRingsCount; j += 1, z += deltaz) { 391 | let theta = 0; 392 | for (let i = 0; i < kSlicesCount; i += 1, theta += kThetaInc) { 393 | const c = Math.cos(theta); 394 | const s = Math.sin(theta); 395 | cylinder.normals[v + 0] = c; 396 | cylinder.normals[v + 1] = s; 397 | cylinder.normals[v + 2] = 0; 398 | cylinder.vertices[v + 0] = c; 399 | cylinder.vertices[v + 1] = s; 400 | cylinder.vertices[v + 2] = z; 401 | v += 3; 402 | } 403 | } 404 | 405 | const normals = Filament._malloc(cylinder.normals.length * cylinder.normals.BYTES_PER_ELEMENT); 406 | Filament.HEAPU8.set(new Uint8Array(cylinder.normals.buffer), normals); 407 | 408 | /* tslint:disable */ 409 | const sob = new Filament.SurfaceOrientation$Builder(); 410 | sob.vertexCount(kVertCount); 411 | sob.normals(normals, 0); 412 | const orientation = sob.build(); 413 | Filament._free(normals); 414 | const quatsBufferSize = kVertCount * 8; 415 | const quatsBuffer = Filament._malloc(quatsBufferSize); 416 | orientation.getQuats(quatsBuffer, kVertCount, Filament.VertexBuffer$AttributeType.SHORT4); 417 | const tangentsMemory = Filament.HEAPU8.subarray(quatsBuffer, quatsBuffer + quatsBufferSize).slice().buffer; 418 | Filament._free(quatsBuffer); 419 | cylinder.tangents = new Int16Array(tangentsMemory); 420 | /* tslint:enable */ 421 | 422 | const vb = this.cylinderVertexBuffer = Filament.VertexBuffer.Builder() 423 | .vertexCount(kVertCount) 424 | .bufferCount(2) 425 | .attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT3, 0, 0) 426 | .attribute(VertexAttribute.TANGENTS, 1, AttributeType.SHORT4, 0, 0) 427 | .normalized(VertexAttribute.TANGENTS) 428 | .build(this.engine); 429 | 430 | const ib = this.cylinderIndexBuffer = Filament.IndexBuffer.Builder() 431 | .indexCount(cylinder.triangles.length) 432 | .bufferType(IndexType.USHORT) 433 | .build(this.engine); 434 | 435 | vb.setBufferAt(this.engine, 0, cylinder.vertices); 436 | vb.setBufferAt(this.engine, 1, cylinder.tangents); 437 | ib.setBuffer(this.engine, cylinder.triangles); 438 | 439 | const m1 = glm.mat4.fromRotation(glm.mat4.create(), Math.PI / 2, [1, 0, 0]); 440 | const m2 = glm.mat4.fromTranslation(glm.mat4.create(), [0, 0, -0.5]); 441 | const m3 = glm.mat4.fromScaling(glm.mat4.create(), [1, 1, 2]); 442 | glm.mat4.multiply(m1, m1, m3); 443 | glm.mat4.multiply(m1, m1, m2); 444 | 445 | this.frontCylinderEntity = Filament.EntityManager.get().create(); 446 | 447 | Filament.RenderableManager.Builder(1) 448 | .boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] }) 449 | .material(0, this.step1CylinderFrontMaterial) 450 | .geometry(0, PrimitiveType.TRIANGLES, vb, ib) 451 | .culling(false) 452 | .build(this.engine, this.frontCylinderEntity); 453 | 454 | this.backCylinderEntity = Filament.EntityManager.get().create(); 455 | 456 | Filament.RenderableManager.Builder(1) 457 | .boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] }) 458 | .material(0, this.step1CylinderBackMaterial) 459 | .geometry(0, PrimitiveType.TRIANGLES, vb, ib) 460 | .culling(false) 461 | .build(this.engine, this.backCylinderEntity); 462 | 463 | const tcm = this.engine.getTransformManager(); 464 | tcm.create(this.frontCylinderEntity); 465 | let inst = tcm.getInstance(this.frontCylinderEntity); 466 | tcm.setTransform(inst, m1); 467 | inst.delete(); 468 | 469 | tcm.create(this.backCylinderEntity); 470 | inst = tcm.getInstance(this.backCylinderEntity); 471 | tcm.setTransform(inst, m1); 472 | inst.delete(); 473 | } 474 | 475 | private createPolyhedron() { 476 | const PrimitiveType = Filament.RenderableManager$PrimitiveType; 477 | 478 | const edges = polyhedron.truncated_icosahedron.edges; 479 | const verts = polyhedron.truncated_icosahedron.verts; 480 | 481 | const vb = this.cylinderVertexBuffer; 482 | const ib = this.cylinderIndexBuffer; 483 | 484 | this.polyhedronEntities = []; 485 | this.polyhedronEntities.length = edges.length; 486 | 487 | const zed = glm.vec3.fromValues(0, 0, 1); 488 | const axis = glm.vec3.create(); 489 | const dir = glm.vec3.create(); 490 | const v0 = glm.vec3.create(); 491 | const v1 = glm.vec3.create(); 492 | const rad = 0.015; 493 | 494 | for (let i = 0; i < edges.length; i += 1) { 495 | const entity = this.polyhedronEntities[i] = Filament.EntityManager.get().create(); 496 | 497 | Filament.RenderableManager.Builder(1) 498 | .boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] }) 499 | .material(0, this.step5PolyhedronMaterial) 500 | .geometry(0, PrimitiveType.TRIANGLES, vb, ib) 501 | .culling(false) 502 | .build(this.engine, entity); 503 | 504 | // Each cylinder has radius 1 and stretches from z = 0 to z = +1. 505 | 506 | glm.vec3.scale(v0, verts[edges[i][0]], 0.9); 507 | glm.vec3.scale(v1, verts[edges[i][1]], 0.9); 508 | 509 | glm.vec3.sub(dir, v1, v0); 510 | glm.vec3.normalize(dir, dir); 511 | glm.vec3.cross(axis, zed, dir); 512 | glm.vec3.normalize(axis, axis); 513 | 514 | const theta = Math.acos(glm.vec3.dot(zed, dir)); 515 | const length = glm.vec3.distance(v0, v1); 516 | 517 | const m1 = glm.mat4.fromTranslation(glm.mat4.create(), v0); 518 | const m2 = glm.mat4.fromRotation(glm.mat4.create(), theta, axis); 519 | const m3 = glm.mat4.fromTranslation(glm.mat4.create(), [0, 0, -rad / 2]); 520 | const m4 = glm.mat4.fromScaling(glm.mat4.create(), [rad, rad, length + rad]); 521 | 522 | glm.mat4.multiply(m1, m1, m2); 523 | glm.mat4.multiply(m1, m1, m3); 524 | glm.mat4.multiply(m1, m1, m4); 525 | 526 | const tcm = this.engine.getTransformManager(); 527 | tcm.create(entity); 528 | const inst = tcm.getInstance(entity); 529 | tcm.setTransform(inst, m1); 530 | inst.delete(); 531 | } 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /src/markdownToHtml.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as marked from "marked"; 3 | 4 | const source = process.argv[2]; 5 | const target = process.argv[3]; 6 | 7 | let isIntro = true; 8 | let inSegment = false; 9 | 10 | const renderer = new marked.Renderer(); 11 | 12 | // Detect if we're inside a fancy segment. 13 | renderer.html = (html) => { 14 | const trimmed = html.toString().trim(); 15 | if (trimmed.startsWith("`; 18 | } 19 | if (trimmed === "") { 20 | inSegment = false; 21 | return `${trimmed}`; 22 | } 23 | return trimmed; 24 | }; 25 | 26 | // Check if the given HTML snippet is enclosed in the given tag. 27 | const isWrapping = (text: string, tag: string): boolean => { 28 | const begin = `<${tag}>`; 29 | const end = ``; 30 | if (!text.startsWith(begin) || !text.endsWith(end)) { 31 | return false; 32 | } 33 | // NOTE: We're not checking that there is only 1 instance of begin / end. 34 | return true; 35 | }; 36 | 37 | // Wrap each h2 section with a panel div. 38 | let h2index = 0; 39 | renderer.heading = (text, level) => { 40 | const d = '\n
'; 41 | if (level !== 2) { 42 | return `${text}\n`; 43 | } 44 | const h = `

${text}\n`; 45 | h2index = h2index + 1; 46 | if (text === "last") { 47 | return "

"; 48 | } 49 | if (!isIntro) { 50 | return `\n${d}\n${h}`; 51 | } 52 | isIntro = false; 53 | return `\n${d}\n${h}`; 54 | }; 55 | 56 | // Add center tags to all-italic or all-bold paragraphs. 57 | // Also wrap each paragraph with a fixed-height segment. 58 | renderer.paragraph = (text) => { 59 | text = text.replace(/\n/g, " "); 60 | if (isWrapping(text, "em") || isWrapping(text, "strong")) { 61 | return `

${text}

\n`; 62 | } 63 | if (!isIntro && !inSegment) { 64 | return `

${text}

\n`; 65 | } 66 | return `

${text}

\n`; 67 | }; 68 | 69 | const markdownSource = fs.readFileSync(source, "utf8"); 70 | 71 | const innerHtml = marked(markdownSource, { renderer }) + "\n"; 72 | 73 | const generatedHtml = ` 74 | 75 | 76 | Euler's Polyhedron Formula 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |
87 | ${innerHtml} 88 |
89 |
90 | 91 | 92 | `; 93 | 94 | fs.writeFileSync(target, generatedHtml, "utf8"); 95 | 96 | console.info(`Generated ${target}`); 97 | -------------------------------------------------------------------------------- /src/polyhedron.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | export const truncated_icosahedron = { 4 | "faces": [[0,3,8,5,1],[2,7,15,13,6],[4,10,18,20,11],[9,14,23,27,17],[12,21,31,29,19],[16,26,36,35,25],[22,32,42,43,33],[24,30,40,44,34],[28,39,49,48,38],[37,47,55,54,46],[41,45,53,57,50],[51,52,56,59,58],[0,1,4,11,7,2],[0,2,6,14,9,3],[1,5,12,19,10,4],[3,9,17,26,16,8],[5,8,16,25,21,12],[6,13,22,33,23,14],[7,11,20,30,24,15],[10,19,29,39,28,18],[13,15,24,34,32,22],[17,27,37,46,36,26],[18,28,38,40,30,20],[21,25,35,45,41,31],[23,33,43,47,37,27],[29,31,41,50,49,39],[32,34,44,52,51,42],[35,36,46,54,53,45],[38,48,56,52,44,40],[42,51,58,55,47,43],[48,49,50,57,59,56],[53,54,55,58,59,57]], 5 | "edges": [[13,15],[19,29],[20,30],[41,50],[35,45],[32,22],[56,52],[12,5],[48,56],[58,51],[40,30],[57,53],[16,26],[33,43],[33,22],[21,31],[8,5],[0,3],[2,6],[2,7],[13,6],[10,18],[46,54],[13,22],[49,50],[58,59],[45,53],[12,21],[57,59],[1,5],[28,39],[36,46],[41,31],[35,36],[26,36],[32,42],[17,26],[0,2],[57,50],[17,9],[54,55],[10,19],[16,8],[1,4],[25,21],[40,44],[18,20],[10,4],[42,51],[24,30],[34,44],[24,34],[11,7],[0,1],[25,35],[14,23],[41,45],[8,3],[55,47],[56,59],[49,39],[24,15],[48,38],[37,46],[40,38],[19,12],[48,49],[15,7],[58,55],[27,23],[29,39],[29,31],[11,4],[9,3],[51,52],[18,28],[11,20],[28,38],[9,14],[16,25],[33,23],[17,27],[27,37],[52,44],[37,47],[43,47],[42,43],[32,34],[14,6],[53,54]], 6 | "verts": [[0,0,1.021],[0.4035482,0,0.9378643],[-0.2274644,0.3333333,0.9378643],[-0.1471226,-0.375774,0.9378643],[0.579632,0.3333333,0.7715933],[0.5058321,-0.375774,0.8033483],[-0.6020514,0.2908927,0.7715933],[-0.05138057,0.6666667,0.7715933],[0.1654988,-0.6080151,0.8033483],[-0.5217096,-0.4182147,0.7715933],[0.8579998,0.2908927,0.4708062],[0.3521676,0.6666667,0.6884578],[0.7841999,-0.4182147,0.5025612],[-0.657475,0.5979962,0.5025612],[-0.749174,-0.08488134,0.6884578],[-0.3171418,0.8302373,0.5025612],[0.1035333,-0.8826969,0.5025612],[-0.5836751,-0.6928964,0.4708062],[0.8025761,0.5979962,0.2017741],[0.9602837,-0.08488134,0.3362902],[0.4899547,0.8302373,0.3362902],[0.7222343,-0.6928964,0.2017741],[-0.8600213,0.5293258,0.1503935],[-0.9517203,-0.1535518,0.3362902],[-0.1793548,0.993808,0.1503935],[0.381901,-0.9251375,0.2017741],[-0.2710537,-0.9251375,0.3362902],[-0.8494363,-0.5293258,0.2017741],[0.8494363,0.5293258,-0.2017741],[1.007144,-0.1535518,-0.06725804],[0.2241935,0.993808,0.06725804],[0.8600213,-0.5293258,-0.1503935],[-0.7222343,0.6928964,-0.2017741],[-1.007144,0.1535518,0.06725804],[-0.381901,0.9251375,-0.2017741],[0.1793548,-0.993808,-0.1503935],[-0.2241935,-0.993808,-0.06725804],[-0.8025761,-0.5979962,-0.2017741],[0.5836751,0.6928964,-0.4708062],[0.9517203,0.1535518,-0.3362902],[0.2710537,0.9251375,-0.3362902],[0.657475,-0.5979962,-0.5025612],[-0.7841999,0.4182147,-0.5025612],[-0.9602837,0.08488134,-0.3362902],[-0.1035333,0.8826969,-0.5025612],[0.3171418,-0.8302373,-0.5025612],[-0.4899547,-0.8302373,-0.3362902],[-0.8579998,-0.2908927,-0.4708062],[0.5217096,0.4182147,-0.7715933],[0.749174,0.08488134,-0.6884578],[0.6020514,-0.2908927,-0.7715933],[-0.5058321,0.375774,-0.8033483],[-0.1654988,0.6080151,-0.8033483],[0.05138057,-0.6666667,-0.7715933],[-0.3521676,-0.6666667,-0.6884578],[-0.579632,-0.3333333,-0.7715933],[0.1471226,0.375774,-0.9378643],[0.2274644,-0.3333333,-0.9378643],[-0.4035482,0,-0.9378643],[0,0,-1.021]] 7 | }; 8 | -------------------------------------------------------------------------------- /src/scene.ts: -------------------------------------------------------------------------------- 1 | import * as glm from "gl-matrix"; 2 | 3 | export class Viewpoint { 4 | public center: glm.vec3 = glm.vec3.fromValues(0, 0, 0); 5 | public eye: glm.vec3 = glm.vec3.fromValues(0, 1, 3); 6 | public up: glm.vec3 = glm.vec3.fromValues(0, 1, 0); 7 | } 8 | 9 | export class TextSpan { 10 | public opacity: number; 11 | public text: string; 12 | public x: number; 13 | public y: number; 14 | } 15 | 16 | export class Scene { 17 | public antipodeAlpha = 0; 18 | public baseColor = [0, 0, 0, 0]; 19 | public cylinderGridlines = 0; 20 | public readonly cylinderTransform = glm.mat4.create(); 21 | public fadeInLuneA = 0; 22 | public fadeInLuneB = 0; 23 | public fadeInLuneC = 0; 24 | public fadeInPolygon = 0; 25 | public fadeInTriangle = 0; 26 | public greatCircle = 0; 27 | public inflation = 0; 28 | public luneAlpha = 0; 29 | public luneExpansion = 0; 30 | public opacity = 0; 31 | public rotation = 0; 32 | public sphereGridlines = 0; 33 | public readonly textSpans: TextSpan[] = []; 34 | public triangleExpansion = 0; 35 | public readonly viewpoint = new Viewpoint(); 36 | } 37 | -------------------------------------------------------------------------------- /src/scrollytell.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * The immutable configuration that describes a single story. 20 | */ 21 | interface IConfig { 22 | readonly chartSelector?: string; 23 | readonly containerSelector: string; 24 | readonly developerHud?: boolean; 25 | readonly enterHandler?: (story: Story, panel: number) => void; 26 | readonly exitHandler?: (story: Story, panel: number) => void; 27 | readonly fullsizeChart?: boolean; 28 | readonly panelSelector: string; 29 | readonly progressHandler?: (story: Story, progress: number) => void; 30 | readonly segmentSelector?: string; 31 | } 32 | 33 | /** 34 | * The context for all animation corresponding to a container element. 35 | */ 36 | export class Story { 37 | private activePanelIndex: number; 38 | private readonly chart: HTMLElement; 39 | private readonly config: IConfig; 40 | private readonly container: HTMLElement; 41 | private developerHudCanvas: HTMLCanvasElement; 42 | private developerHudContext: CanvasRenderingContext2D; 43 | private frameCount: number; 44 | private readonly panels: NodeListOf; 45 | private progressValue: number; 46 | private scrollTop: number; 47 | private readonly tick: () => void; 48 | 49 | public constructor(config: IConfig) { 50 | this.config = config; 51 | this.activePanelIndex = -1; 52 | this.progressValue = -1; 53 | this.frameCount = 0; 54 | 55 | this.container = document.querySelector(config.containerSelector); 56 | if (!this.container) { 57 | throw Error("Scrollytell container not found."); 58 | } 59 | 60 | const cstyle = window.getComputedStyle(this.container); 61 | if (cstyle.getPropertyValue("overflow-y") !== "scroll") { 62 | throw Error("Scrollytell container must have overflow-y:scroll."); 63 | } 64 | if (cstyle.getPropertyValue("position") !== "relative") { 65 | throw Error("Scrollytell container must have position:relative."); 66 | } 67 | 68 | this.panels = document.querySelectorAll(config.panelSelector); 69 | if (!this.panels) { 70 | throw Error("Scrollytell panels not found."); 71 | } 72 | 73 | this.chart = document.querySelector(config.chartSelector); 74 | if (config.fullsizeChart && !this.chart) { 75 | throw Error("Scrollytell chart not found."); 76 | } 77 | 78 | if (config.fullsizeChart) { 79 | const height = this.container.getBoundingClientRect().height; 80 | this.chart.style.height = `${height}px`; 81 | this.chart.style.top = "0"; 82 | const segments: NodeListOf = document.querySelectorAll(config.segmentSelector); 83 | for (const segment of segments) { 84 | segment.style.height = `${height}px`; 85 | } 86 | } 87 | 88 | if (config.developerHud) { 89 | this.showDeveloperHud(true); 90 | } 91 | 92 | this.tick = this.render.bind(this) as (() => void); 93 | 94 | window.requestAnimationFrame(this.tick); 95 | } 96 | 97 | /** 98 | * Returns the zero-based index of the panel that is currently overlapping 99 | * the guideline. Returns -1 if no such panel exists. 100 | */ 101 | public getActivePanelIndex() { 102 | return this.activePanelIndex; 103 | } 104 | 105 | /** 106 | * Returns a percentage in the range [0, 1] that represents the position of 107 | * the active panel relative to the guideline. Returns 0 when the top of the 108 | * panel aligns with the guideline, +1 when the bottom of the panel aligns 109 | * with the guideline, and -1 if no panel is overlapping the guideline. 110 | */ 111 | public getProgressValue() { 112 | return this.progressValue; 113 | } 114 | 115 | /** 116 | * Forces a re-assessment of the active panel index and progress value. Also forces 117 | * the progress handler to trigger on the subsequent frame. 118 | */ 119 | public refresh() { 120 | this.scrollTop = undefined; 121 | this.activePanelIndex = -2; 122 | } 123 | 124 | /** 125 | * Toggles the heads-up-display for development purposes. Do not enable 126 | * when your site is in production. 127 | */ 128 | public showDeveloperHud(enable: boolean) { 129 | if (this.developerHudCanvas) { 130 | const visibility = enable ? "visible" : "hidden"; 131 | this.developerHudCanvas.style.visibility = visibility; 132 | return; 133 | } 134 | if (!enable) { 135 | return; 136 | } 137 | const canvas = document.createElement("canvas"); 138 | const style = canvas.style; 139 | style.position = "fixed"; 140 | style.width = "100%"; 141 | style.height = "100%"; 142 | style.left = "0"; 143 | style.top = "0"; 144 | style.zIndex = "100"; 145 | style.pointerEvents = "none"; 146 | this.container.appendChild(canvas); 147 | 148 | const dpr = window.devicePixelRatio; 149 | canvas.width = canvas.clientWidth * dpr; 150 | canvas.height = canvas.clientHeight * dpr; 151 | this.developerHudContext = canvas.getContext("2d"); 152 | this.developerHudContext.scale(dpr, dpr); 153 | this.developerHudCanvas = canvas; 154 | const family = "'Lexend Deca', sans-serif"; 155 | this.developerHudContext.font = `bold 14px ${family}`; 156 | 157 | // Force a redraw on the next frame. 158 | this.scrollTop = undefined; 159 | } 160 | 161 | private render() { 162 | // Take care not to do work if no scrolling has occurred. This is an 163 | // important optimization because it can save power on mobile devices. 164 | const scrollTop = this.container.scrollTop; 165 | if (scrollTop === this.scrollTop) { 166 | window.requestAnimationFrame(this.tick); 167 | return; 168 | } 169 | this.scrollTop = scrollTop; 170 | 171 | // Determine the guideline Y coordinate. 172 | const cbox = this.container.getBoundingClientRect(); 173 | const guideline = (cbox.top + cbox.bottom) / 2; 174 | 175 | // Determine the active panel and progress value. 176 | const prevActivePanel = this.activePanelIndex; 177 | const prevProgressValue = this.progressValue; 178 | this.activePanelIndex = -1; 179 | this.progressValue = -1; 180 | for (const [index, panel] of this.panels.entries()) { 181 | const pbox = panel.getBoundingClientRect(); 182 | const outside = pbox.top > guideline || pbox.bottom < guideline; 183 | const active = !outside; 184 | const ratio = (guideline - pbox.top) / pbox.height; 185 | if (active) { 186 | this.activePanelIndex = index; 187 | this.progressValue = ratio; 188 | break; 189 | } 190 | } 191 | 192 | const panelChanged = prevActivePanel !== this.activePanelIndex; 193 | const progressChanged = prevProgressValue !== this.progressValue; 194 | 195 | // Trigger scrollytelling events. 196 | if (panelChanged) { 197 | if (this.config.exitHandler) { 198 | this.config.exitHandler(this, prevActivePanel); 199 | } 200 | if (this.config.enterHandler) { 201 | this.config.enterHandler(this, this.activePanelIndex); 202 | } 203 | } 204 | 205 | // Do not update the frame count when scrolling between panels (i.e. 206 | // when there is no active panel). 207 | if (progressChanged || panelChanged) { 208 | this.frameCount += 1; 209 | if (this.config.progressHandler) { 210 | this.config.progressHandler(this, this.progressValue); 211 | } 212 | } 213 | 214 | // Render the developer HUD even when the frame count has not been 215 | // incremented (i.e. activePanelIndex == -1) because the relative 216 | // position of the panel bounding / boxes may have changed. 217 | if (this.developerHudContext) { 218 | this.renderDeveloperHud(cbox); 219 | } 220 | 221 | window.requestAnimationFrame(this.tick); 222 | } 223 | 224 | private renderDeveloperHud(containerBox: ClientRect) { 225 | const cbox = containerBox; 226 | const ctx = this.developerHudContext; 227 | const guideline = (cbox.top + cbox.bottom) / 2; 228 | const width = this.developerHudCanvas.width; 229 | const height = this.developerHudCanvas.height; 230 | 231 | // Make the canvas transparent before drawing anything. 232 | ctx.clearRect(0, 0, width, height); 233 | 234 | // Draw semitransparent gray rectangles over each panel. 235 | ctx.beginPath(); 236 | ctx.fillStyle = "rgba(128, 128, 128, 0.125)"; 237 | for (const [index, panel] of this.panels.entries()) { 238 | const pbox = panel.getBoundingClientRect(); 239 | ctx.rect(pbox.left, pbox.top, pbox.width, pbox.height); 240 | } 241 | ctx.fill(); 242 | 243 | // Draw a semitransparent white background rect under the text. 244 | ctx.beginPath(); 245 | ctx.fillStyle = "rgba(255, 255, 255, 0.5)"; 246 | ctx.rect(0, 0, 200, 100); 247 | ctx.fill(); 248 | 249 | // Draw the text. 250 | ctx.fillStyle = "rgba(50, 50, 0, 1)"; 251 | const pvText = `progress value = ${this.progressValue.toFixed(2)}`; 252 | ctx.fillText(pvText, 10, 20); 253 | const panelText = `active panel = ${this.activePanelIndex}`; 254 | ctx.fillText(panelText, 10, 40); 255 | const frameText = `frame count = ${this.frameCount}`; 256 | ctx.fillText(frameText, 10, 60); 257 | 258 | // Draw the guideline. 259 | ctx.strokeStyle = "rgba(255, 255, 255, 1.0)"; 260 | ctx.beginPath(); 261 | ctx.moveTo(cbox.left, guideline - 1); 262 | ctx.lineTo(cbox.right, guideline - 1); 263 | ctx.moveTo(cbox.left, guideline + 1); 264 | ctx.lineTo(cbox.right, guideline + 1); 265 | ctx.stroke(); 266 | ctx.strokeStyle = "rgba(0, 0, 0, 1.0)"; 267 | ctx.setLineDash([10, 1]); 268 | ctx.beginPath(); 269 | ctx.moveTo(cbox.left, guideline); 270 | ctx.lineTo(cbox.right, guideline); 271 | ctx.stroke(); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/timeline.ts: -------------------------------------------------------------------------------- 1 | import * as glm from "gl-matrix"; 2 | 3 | import { Scene } from "./scene"; 4 | 5 | const clamp = (val: number, lower: number, upper: number): number => Math.max(Math.min(val, upper), lower); 6 | const mix = (a: number, b: number, t: number): number => a * (1 - t) + b * t; 7 | 8 | const smoothstep = (edge0: number, edge1: number, x: number): number => { 9 | const t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); 10 | return t * t * (3.0 - t * 2.0); 11 | }; 12 | 13 | export class Timeline { 14 | private previousStep = -1; 15 | private readonly scene: Scene; 16 | 17 | public constructor(scene: Scene) { 18 | this.scene = scene; 19 | } 20 | 21 | public enterStep3() { 22 | this.scene.textSpans.length = 3; 23 | this.scene.textSpans[0] = { opacity: 1.0, text: "a", x: -0.61, y: -0.20 }; 24 | this.scene.textSpans[1] = { opacity: 1.0, text: "b", x: +0.48, y: -0.13 }; 25 | this.scene.textSpans[2] = { opacity: 1.0, text: "c", x: -0.13, y: +0.54 }; 26 | } 27 | 28 | public enterStep4() { 29 | this.scene.textSpans.length = 11; 30 | const a = this.scene.textSpans[0] = { opacity: 1.0, text: "a", x: 0.13, y: -0.03}; 31 | const b = this.scene.textSpans[1] = { opacity: 1.0, text: "b", x: 0.37, y: 0.31}; 32 | const c = this.scene.textSpans[2] = { opacity: 1.0, text: "c", x: 0.12, y: 0.63}; 33 | const d = this.scene.textSpans[3] = { opacity: 1.0, text: "d", x: -0.3, y: 0.52}; 34 | const e = this.scene.textSpans[4] = { opacity: 1.0, text: "e", x: -0.325, y: 0.12}; 35 | 36 | this.scene.textSpans[5] = { opacity: 1.0, text: "-π", x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }; 37 | this.scene.textSpans[6] = { opacity: 1.0, text: "-π", x: (b.x + c.x) / 2, y: (b.y + c.y) / 2 }; 38 | this.scene.textSpans[7] = { opacity: 1.0, text: "-π", x: (c.x + d.x) / 2, y: (c.y + d.y) / 2 }; 39 | this.scene.textSpans[8] = { opacity: 1.0, text: "-π", x: (d.x + e.x) / 2, y: (d.y + e.y) / 2 }; 40 | this.scene.textSpans[9] = { opacity: 1.0, text: "-π", x: (e.x + a.x) / 2, y: (e.y + a.y) / 2 }; 41 | 42 | const x = (a.x + b.x + c.x + d.x + e.x) / 5; 43 | const y = (a.y + b.y + c.y + d.y + e.y) / 5; 44 | this.scene.textSpans[10] = { opacity: 1.0, text: "2π", x, y }; 45 | } 46 | 47 | public enterStep5() { 48 | this.scene.textSpans.length = (1 + 5 * 2) + (1 + 6 * 2) + (1 * 6 * 2); 49 | 50 | // Lower-right pentagon 51 | this.scene.textSpans[0] = { opacity: 1.0, text: "a", x: 0.03640982, y: 0.0797629 }; 52 | this.scene.textSpans[1] = { opacity: 1.0, text: "b", x: 0.42658763, y: 0.1014394 }; 53 | this.scene.textSpans[2] = { opacity: 1.0, text: "c", x: 0.46994072, y: 0.3832345 }; 54 | this.scene.textSpans[3] = { opacity: 1.0, text: "d", x: 0.18814563, y: 0.6216765 }; 55 | this.scene.textSpans[4] = { opacity: 1.0, text: "e", x: -0.0719729, y: 0.4049110 }; 56 | this.scene.textSpans[5] = { opacity: 1.0, text: "-π", x: 0.23149872, y: 0.0797629 }; 57 | this.scene.textSpans[6] = { opacity: 1.0, text: "-π", x: 0.36155800, y: 0.4916172 }; 58 | this.scene.textSpans[7] = { opacity: 1.0, text: "-π", x: 0.05808636, y: 0.4916172 }; 59 | this.scene.textSpans[8] = { opacity: 1.0, text: "-π", x: 0.44826418, y: 0.2314987 }; 60 | this.scene.textSpans[9] = { opacity: 1.0, text: "-π", x: -0.0286198, y: 0.2314987 }; 61 | this.scene.textSpans[10] = { opacity: 1.0, text: "2π", x: 0.23149872, y: 0.2965283 }; 62 | 63 | // Upper-right hexagon 64 | this.scene.textSpans[11] = { opacity: 1.0, text: "a", x: -0.028619, y: -0.6572396 }; 65 | this.scene.textSpans[12] = { opacity: 1.0, text: "b", x: 0.3398814, y: -0.6138865 }; 66 | this.scene.textSpans[13] = { opacity: 1.0, text: "c", x: 0.5349703, y: -0.3537679 }; 67 | this.scene.textSpans[14] = { opacity: 1.0, text: "d", x: 0.4049110, y: -0.0502963 }; 68 | this.scene.textSpans[15] = { opacity: 1.0, text: "e", x: 0.0580863, y: -0.0502963 }; 69 | this.scene.textSpans[16] = { opacity: 1.0, text: "f", x: -0.180355, y: -0.3754445 }; 70 | this.scene.textSpans[17] = { opacity: 1.0, text: "-π", x: 0.1664690, y: -0.6572396 }; 71 | this.scene.textSpans[18] = { opacity: 1.0, text: "-π", x: 0.2098221, y: -0.0502963 }; 72 | this.scene.textSpans[19] = { opacity: 1.0, text: "-π", x: 0.4699407, y: -0.2020321 }; 73 | this.scene.textSpans[20] = { opacity: 1.0, text: "-π", x: 0.4482641, y: -0.4838272 }; 74 | this.scene.textSpans[21] = { opacity: 1.0, text: "-π", x: -0.071972, y: -0.2237087 }; 75 | this.scene.textSpans[22] = { opacity: 1.0, text: "-π", x: -0.093649, y: -0.5055038 }; 76 | this.scene.textSpans[23] = { opacity: 1.0, text: "2π", x: 0.2098221, y: -0.3754445 }; 77 | 78 | // Leftmost hexagon 79 | this.scene.textSpans[24] = { opacity: 1.0, text: "a", x: -0.6138, y: -0.2453 }; 80 | this.scene.textSpans[25] = { opacity: 1.0, text: "b", x: -0.2670, y: -0.2887 }; 81 | this.scene.textSpans[26] = { opacity: 1.0, text: "c", x: -0.0719, y: 0.01473 }; 82 | this.scene.textSpans[27] = { opacity: 1.0, text: "d", x: -0.2020, y: 0.36155 }; 83 | this.scene.textSpans[28] = { opacity: 1.0, text: "e", x: -0.5488, y: 0.36155 }; 84 | this.scene.textSpans[29] = { opacity: 1.0, text: "f", x: -0.7222, y: 0.10143 }; 85 | this.scene.textSpans[30] = { opacity: 1.0, text: "-π", x: -0.4621, y: -0.2887 }; 86 | this.scene.textSpans[31] = { opacity: 1.0, text: "-π", x: -0.2020, y: -0.1586 }; 87 | this.scene.textSpans[32] = { opacity: 1.0, text: "-π", x: -0.1370, y: 0.16646 }; 88 | this.scene.textSpans[33] = { opacity: 1.0, text: "-π", x: -0.3537, y: 0.38323 }; 89 | this.scene.textSpans[34] = { opacity: 1.0, text: "-π", x: -0.6572, y: 0.23149 }; 90 | this.scene.textSpans[35] = { opacity: 1.0, text: "-π", x: -0.6572, y: -0.1153 }; 91 | this.scene.textSpans[36] = { opacity: 1.0, text: "2π", x: -0.4187, y: 0.03640 }; 92 | } 93 | 94 | public exitStep3() { 95 | this.scene.textSpans.length = 0; 96 | } 97 | 98 | public exitStep4() { 99 | this.scene.textSpans.length = 0; 100 | } 101 | 102 | public exitStep5() { 103 | this.scene.textSpans.length = 0; 104 | } 105 | 106 | // Returns true to force a redraw. 107 | public update(step: number, progress: number): boolean { 108 | const updateFn = this[`updateStep${step + 1}`] as (progress: number) => boolean; 109 | const exitFn = this[`exitStep${this.previousStep + 1}`] as () => void; 110 | const enterFn = this[`enterStep${step + 1}`] as () => void; 111 | if (step !== this.previousStep) { 112 | if (exitFn) { 113 | exitFn.apply(this); 114 | } 115 | if (enterFn) { 116 | enterFn.apply(this); 117 | } 118 | this.previousStep = step; 119 | } 120 | if (updateFn) { 121 | return updateFn.apply(this, [progress]) as boolean; 122 | } 123 | return this.updateStep1(0); 124 | } 125 | 126 | public updateStep1(progress: number): boolean { 127 | const A = smoothstep(0.16, 0.29, progress); // Move cylinder up and camera out. 128 | const B = smoothstep(0.28, 0.42, progress); // Fade in cylinder gridlines. 129 | const C = smoothstep(0.42, 0.66, progress); // Crossfade gridlines from cylinder to sphere. 130 | const D = smoothstep(0.66, 0.90, progress); // Move cylinder down and camera in. 131 | const E = smoothstep(0.95, 1.00, progress); // Fade out gridlines, move cylinder up & out. 132 | 133 | glm.vec3.lerp(this.scene.viewpoint.eye, [0, 0, 3], [0, 1.5, 3.5], A * (1 - D)); 134 | 135 | const cylinderZ = mix(-0.5 + (1.0 - A * (1 - D)), -4.0, E); 136 | const m1 = glm.mat4.fromRotation(glm.mat4.create(), Math.PI / 2, [1, 0, 0]); 137 | const m2 = glm.mat4.fromTranslation(glm.mat4.create(), [0, 0, cylinderZ]); 138 | const m3 = glm.mat4.fromScaling(glm.mat4.create(), [1, 1, 2]); 139 | glm.mat4.multiply(m1, m1, m3); 140 | glm.mat4.multiply(m1, m1, m2); 141 | glm.mat4.copy(this.scene.cylinderTransform, m1); 142 | 143 | this.scene.sphereGridlines = C * (1 - E); 144 | this.scene.cylinderGridlines = B * (1 - C); 145 | this.scene.baseColor = [0.0, 0.0, 0.0, 0.0]; 146 | return false; 147 | } 148 | 149 | public updateStep2(progress: number) { 150 | const A = smoothstep(0.00, 0.11, progress); // Fade in the great circle and change the camera 151 | const B = smoothstep(0.18, 0.22, progress); // Fade in the second great circle and lune 152 | const C = smoothstep(0.31, 0.51, progress); // Widen lune to entire sphere 153 | const D = smoothstep(0.61, 0.71, progress); // Narrow lune back down 154 | const E = smoothstep(0.72, 0.81, progress); // Fade in antipode 155 | const F = smoothstep(0.90, 1.00, progress); // Fade out the double-lune and change the camera 156 | 157 | glm.vec3.lerp(this.scene.viewpoint.eye, [0, 0, 3], [0, 1, 3], A * (1 - F)); 158 | 159 | this.scene.greatCircle = A * (1 - F); 160 | this.scene.luneAlpha = B * (1 - F); 161 | this.scene.luneExpansion = C * (1 - D); 162 | this.scene.antipodeAlpha = E * (1 - F); 163 | return false; 164 | } 165 | 166 | public updateStep3(progress: number) { 167 | const A = smoothstep(0.00, 0.08, progress); // Draw the geodesic triangle and change the camera 168 | const B2 = smoothstep(0.17, 0.23, progress); // Expand triangle to 90-90-90 169 | const B1 = smoothstep(0.23, 0.26, progress); // Change the camera to see polar triangle 170 | const C = smoothstep(0.35, 0.40, progress); // Shrink triangle back and revert the cam 171 | const D = smoothstep(0.40, 0.42, progress); // Fade in letters A B C 172 | const E = smoothstep(0.43, 0.70, progress); // Fade in and out three double lunes sequentially 173 | const F = smoothstep(0.70, 0.75, progress); // Fade in three double lunes simultaneously 174 | const G = smoothstep(0.75, 0.77, progress); // Rotate to see antipode 175 | const H = smoothstep(0.77, 0.91, progress); // Rotate back to normal 176 | const H2 = smoothstep(0.80, 0.91, progress); // Fade in letters A B C again 177 | const I = smoothstep(0.92, 1.00, progress); // Fade everything out and change the camera 178 | 179 | const cam0 = glm.vec3.create(); 180 | const cam1 = glm.vec3.create(); 181 | 182 | glm.vec3.lerp(cam0, [0, 0, 3], [0, 1, 3], A * (1 - I)); 183 | glm.vec3.lerp(cam1, cam0, [2, 2, 2], B1 * (1 - C)); 184 | glm.vec3.lerp(this.scene.viewpoint.eye, cam1, [0, -1, 3], G * (1 - H)); 185 | 186 | this.scene.rotation = G * (1 - H) * Math.PI; 187 | this.scene.fadeInTriangle = A * (1 - I); 188 | this.scene.triangleExpansion = B2 * (1 - C); 189 | 190 | const EA = Math.sin(clamp(E * 3 - 0, 0, 1) * Math.PI); 191 | const EB = Math.sin(clamp(E * 3 - 1, 0, 1) * Math.PI); 192 | const EC = Math.sin(clamp(E * 3 - 2, 0, 1) * Math.PI); 193 | 194 | this.scene.fadeInLuneA = EA; 195 | this.scene.fadeInLuneB = EB; 196 | this.scene.fadeInLuneC = EC; 197 | 198 | if (F > 0 && F <= 1) { 199 | this.scene.fadeInLuneA = F * (1 - I); 200 | this.scene.fadeInLuneB = F * (1 - I); 201 | this.scene.fadeInLuneC = F * (1 - I); 202 | } 203 | 204 | const textOpacity = D * (1 - F) + H2 * (1 - I); 205 | this.scene.textSpans[0].opacity = textOpacity; 206 | this.scene.textSpans[1].opacity = textOpacity; 207 | this.scene.textSpans[2].opacity = textOpacity; 208 | 209 | return false; 210 | } 211 | 212 | public updateStep4(progress: number) { 213 | const A = smoothstep(0.00, 0.14, progress); // Draw the geodesic polygon and change the camera 214 | const B = smoothstep(0.27, 0.39, progress); // Fade in the constituent triangles. 215 | const C = smoothstep(0.65, 0.80, progress); // Fade out the constituent triangles. 216 | const D = smoothstep(0.80, 0.87, progress); // Fade in the text. 217 | const I = smoothstep(0.93, 1.00, progress); // Revert camera and undraw the polygon. 218 | 219 | glm.vec3.lerp(this.scene.viewpoint.eye, [0, 0, 3], [0, 1, 3], A * (1 - I)); 220 | 221 | this.scene.fadeInPolygon = A * (1 - I); 222 | this.scene.fadeInTriangle = B * (1 - C); 223 | const fadeInLabels = D * (1 - I); 224 | 225 | for (const span of this.scene.textSpans) { 226 | span.opacity = fadeInLabels; 227 | } 228 | 229 | return false; 230 | } 231 | 232 | public updateStep5(progress: number) { 233 | const A = smoothstep(0.00, 0.07, progress); // Fade out the enclosing sphere 234 | const B = smoothstep(0.19, 0.26, progress); // Inflate the polyhedron and fade in sphere 235 | const C = smoothstep(0.35, 0.42, progress); // Fade in the labels 236 | const D = smoothstep(0.62, 0.75, progress); // Fade out the labels 237 | 238 | const fadeInLabels = C * (1 - D); 239 | 240 | for (const span of this.scene.textSpans) { 241 | span.opacity = fadeInLabels; 242 | } 243 | 244 | glm.vec3.copy(this.scene.viewpoint.eye, [0, 0, 3]); 245 | 246 | this.scene.opacity = 1.0 - (A * 1.0 - B); 247 | this.scene.inflation = B; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/urls.ts: -------------------------------------------------------------------------------- 1 | const environ = "env/pillars"; 2 | 3 | export const ibl = `${environ}_ibl.ktx.bmp`; 4 | export const sky = `${environ}_skybox.ktx.bmp`; 5 | 6 | export const step1SphereMaterial = "materials/step1.filamat.bmp"; 7 | export const step1CylinderBackMaterial = "materials/step1_cylinder_back.filamat.bmp"; 8 | export const step1CylinderFrontMaterial = "materials/step1_cylinder_front.filamat.bmp"; 9 | export const step2SphereMaterial = "materials/step2.filamat.bmp"; 10 | export const step3SphereMaterial = "materials/step3.filamat.bmp"; 11 | export const step4SphereMaterial = "materials/step4.filamat.bmp"; 12 | export const step5PolyhedronMaterial = "materials/step5_poly.filamat.bmp"; 13 | export const step5SphereMaterial = "materials/step5.filamat.bmp"; 14 | 15 | export const initialAssets = [ 16 | sky, 17 | ibl, 18 | step1SphereMaterial, 19 | step1CylinderBackMaterial, 20 | step1CylinderFrontMaterial, 21 | step2SphereMaterial, 22 | step3SphereMaterial, 23 | step4SphereMaterial, 24 | step5PolyhedronMaterial, 25 | step5SphereMaterial, 26 | ]; 27 | -------------------------------------------------------------------------------- /src/verbiage.md: -------------------------------------------------------------------------------- 1 | # Euler's Polyhedron Formula 2 | 3 | *Chrome / Android is recommended for viewing this page because it uses WebGL 2.* 4 | 5 |
6 | 7 | This page uses continuous scrollytelling to present a variation of Legendre's proof for the following formula. 8 | 9 | **V - E + F = 2** 10 | 11 | For any convex polyhedron (or planar graph), the number of vertices minus the number of edges plus the number of faces 12 | is 2. Leonhard Euler discovered this in 1752 although Descartes discovered a variation over 100 years earlier. 13 | 14 | This equation makes it easy to prove that there only 5 Platonic solids, but perhaps its real beauty lies in how it 15 | connects disparate fields of mathematics. These connections are suggested by Legendre's proof, which is purely 16 | geometrical and leverages some interesting properties of geodesic triangles. 17 | 18 | Scroll down at your own pace, but try not to go too fast, otherwise you'll skip over the animation. The proof is divided 19 | into 5 steps: 20 | 21 | 1. [Surface Area of Sphere](#h0) 22 | 1. [Area of Double Lune](#h1) 23 | 1. [Girard's Theorem](#h2) 24 | 1. [Spherical Polygons](#h3) 25 | 1. [The Conclusion](#h4) 26 | 27 |
28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | ## Part 1: Surface Area of Sphere 37 | 38 | The surface area of a sphere can be derived with freshman calculus, but it was discovered by Archimedes long before the 39 | invention of calculus. 40 | 41 | Archimedes realized that the surface area of a sphere is equal to the area of its smallest enclosing cylinder, which is 42 | **4πr2**. This is somewhat intuitive if you think about lat-long rectangles. 43 | 44 | At the equator, lat-long rectangles are fat and short. Closer to the poles they are tall and thin. As they move closer 45 | to the poles, they become tall at the same rate at which they become thin, so their area remains constant. 46 | 47 | Going forward, we'll keep things simple by focusing on a sphere whose radius is 1. The surface area of this sphere is 48 | **4π**. 49 | 50 | ## Part 2: Area of a Double Lune 51 | 52 | *Great circles* are lines on the surface of a sphere that divide the sphere in half. If you place your car anywhere on a 53 | sphere's surface, then continuously drive without turning the steering wheel, you'll always inscribe a great circle. Any 54 | portion of a great circle is called a *geodesic path*. 55 | 56 | The region on a sphere between two great circles is called a "lune". Let's figure out the surface area of a lune bounded 57 | by **θ** radians. 58 | 59 | If **θ** is **π** radians, the lune encompasses one hemisphere. Since the area of a hemisphere is **2π**, the area of 60 | the lune is **2θ**. 61 | 62 | This is one reason why radians are more elegant than degrees! 63 | 64 | The surface area of a lune plus its antipode is **4θ**. Let's call this a *double lune*. 65 | 66 | ## Part 3: Girard's Theorem 67 | 68 | Spherical triangles are inscribed by geodesic lines (i.e. portions of great circles). Unlike a planar triangle, the area 69 | of a spherical triangle can be determined solely from its angles. With planar triangles, the sum of the three angles is 70 | always **π** radians. Not so with spherical triangles! 71 | 72 | For example, consider the triangle that encompasses one-eighth of the sphere surface, which has three 90° angles, or 73 | **π/2** radians each. Clearly these do not add up to **π**. 74 | 75 | Let's figure out the area of any geodesic triangle with angles **a**, **b**, and **c**. 76 | 77 | Each corner in the triangle corresponds to a double lune on the surface. We can visualize each double lune with one of 78 | the additive primary colors. 79 | 80 | The sum of the lune areas can be visualized by adding up their respective colors. 81 |

82 | Notice that the total area of the lunes is equivalent to the surface of the entire sphere, except that the triangle and 83 | its antipode are each counted an additional 2x times. 84 | 85 | 86 | 87 | Recall that: 88 | - The surface area of the unit sphere is **4π**. 89 | - The surface area of each double lune is **4θ**. 90 | 91 | Moreover, we now know that: 92 | - The surface area of the sphere is equal to the area of all the triangle's double lunes, minus 4x the area of the 93 | triangle. 94 | 95 | Therefore, if the area of geodesic triangle **abc** is **A**, then: 96 | - **4π = 4a + 4b + 4c - 4A** 97 | 98 | Or, more simply stated: 99 | - **A = a + b + c - π** 100 | 101 | This formula was independently discovered by Albert Girard (1595-1632) and Thomas Harriot (1560-1621). 102 | 103 | 104 | 105 | ## Part 4: Spherical Polygons 106 | 107 | Now that we know how to compute the area of a geodesic triangle, can we figure out how to compute 108 | the area of a geodesic polygon? 109 | 110 | Yes, we can! Every n-gon can be decomposed into **(n-2)** triangles. 111 | 112 | 113 | 114 | The sum of all the angles in a polygon is equal to the sum of all the angles in its constituent triangles. And, 115 | we now know that each of those **(n-2)** constituent triangles has an area of: 116 | 117 | **<angle sum> - π** 118 | 119 | Therefore, the area of the polygon must be: 120 | 121 | **<angle sum> - (n-2) π** 122 | 123 | 124 | 125 | 126 | 127 | Stated another way: 128 | 129 | **A = (a + b + c + d + ...) - nπ + 2π**. 130 | 131 | To portray this formula visually, we've inscribed the components of the sum onto the sphere. 132 | 133 | Note that each vertex and edge correspond to a component of the sum, as well as the face itself. This will be useful 134 | later in the proof. 135 | 136 | 137 | 138 | ## Part 5: The Conclusion 139 | 140 | Now that we have a few tools under our belt, let's consider a convex polyhedron. 141 | 142 | What happens when we inflate the polyhedron to meet its enclosing sphere? The sum of the areas of all the resulting 143 | geodesic polygons should be equivalent to the surface area of the sphere! 144 | 145 | Next, apply the visual method for computing the area sum across all polygons. Each vertex contributes a total of 146 | **2π** radians, each edge contributes **-2π** (one for each side), and each face contributes **2π**. 147 | 148 | 149 | 150 | Putting it all together: 151 | - **Surface area of unit sphere =
2πV - 2πE + 2πF** 152 | 153 | Or, simply stated: 154 | - **4π = 2πV - 2πE + 2πF** 155 | 156 | Therefore: 157 | - **V - E + F = 2** 158 | 159 | Et Voilà! 160 | 161 |
162 | 163 | 164 | 165 | The polyhedron formula is also known as *Euler's Characteristic Formula* because the right-hand side of the equation is 166 | actually a "characteristic" of the sphere's topology. If we were to inscribe the graph on a torus instead of a sphere, 167 | the Euler characteristic would be 0 rather than 2. 168 | 169 | To learn more about this, I recommend David Richeson's excellent book [*Euler's Gem*][1], which was the inspiration for 170 | this page. 171 | 172 | Also the inimitable 3blue1brown has a [great video on the polyhedron formula][2], as well as [a video about 173 | the surface area of a sphere][3]. 174 | 175 | 176 | 177 | 178 | 179 | Thanks for reading the proof! Take a look at the code if you're interested in how this was made. 180 |
181 |
182 | GitHub Projects: 183 | - prideout/euler 184 | - google/filament 185 | - google/scrollytell 186 | 187 | 188 | The Little Grasshopper 189 | 190 | 191 |
192 | 193 | [1]: https://www.amazon.com/Eulers-Gem-Polyhedron-Formula-Topology/dp/0691154570 194 | [2]: https://www.youtube.com/watch?v=-9OUyo8NFZg 195 | [3]: https://www.youtube.com/watch?v=GNcFjFmqEc8 196 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["src/old_app.ts"], 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "target": "es2015" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:all" 5 | ], 6 | "rules": { 7 | "object-literal-sort-keys": false, 8 | "prefer-method-signature": false, 9 | "comment-format": false, 10 | "no-console" : false, 11 | "no-this-assignment": false, 12 | "max-classes-per-file": false, 13 | "completed-docs": false, 14 | "newline-per-chained-call": false, 15 | "no-magic-numbers": false, 16 | "no-parameter-reassignment": false, 17 | "number-literal-format": false, 18 | "typedef": false, 19 | "newline-before-return": false, 20 | "prefer-template": false, 21 | "variable-name": false, 22 | "binary-expression-operand-order": false, 23 | "strict-boolean-expressions": {"options": ["allow-undefined-union"]} 24 | }, 25 | "rulesDirectory": [] 26 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | 'use strict'; 5 | 6 | module.exports = { 7 | devtool: 'source-map', 8 | entry: { 9 | main: './src/app.ts' 10 | }, 11 | 12 | // We do not actually use the following modules, but emscripten emits JS bindings that 13 | // conditionally uses them. Therefore we need to tell webpack to skip over their "require" 14 | // statements. 15 | externals: { 16 | fs: 'fs', 17 | crypto: 'crypto', 18 | path: 'path' 19 | }, 20 | 21 | output: { 22 | path: path.resolve(__dirname, 'docs') 23 | }, 24 | 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | loader: 'ts-loader' 30 | } 31 | ] 32 | }, 33 | 34 | resolve: { 35 | extensions: [ '.ts', '.tsx', '.js' ] 36 | }, 37 | 38 | performance: { 39 | assetFilter: function(assetFilename) { 40 | return false; 41 | } 42 | }, 43 | 44 | plugins: [ 45 | new webpack.DefinePlugin({ 46 | "BUILD_COMMAND": JSON.stringify(process.env.npm_lifecycle_event), 47 | }) 48 | ] 49 | }; 50 | --------------------------------------------------------------------------------