Repository navigation
Expand file tree
/
Copy pathTypeScript with WebGL.js
More file actions
275 lines (219 loc) · 9.27 KB
/
Copy pathTypeScript with WebGL.js
File metadata and controls
275 lines (219 loc) · 9.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//// { "order": 5, "isJavaScript": true }
// This example creates an HTML canvas which uses WebGL to
// render spinning confetti using JavaScript. We're going
// to walk through the code to understand how it works, and
// see how TypeScript's tooling provides useful insight.
// This example builds off: example:working-with-the-dom
// First up, we need to create an HTML canvas element, which
// we do via the DOM API and set some inline style attributes:
const canvas = document.createElement("canvas");
canvas.id = "spinning-canvas";
canvas.style.backgroundColor = "#0078D4";
canvas.style.position = "fixed";
canvas.style.bottom = "10px";
canvas.style.right = "20px";
canvas.style.width = "500px";
canvas.style.height = "400px";
canvas.style.zIndex = "100";
// Next, to make it easy to make changes, we remove any older
// versions of the canvas when hitting "Run" - now you can
// make changes and see them reflected when you press "Run"
// or (cmd + enter):
const existingCanvas = document.getElementById(canvas.id);
if (existingCanvas && existingCanvas.parentElement) {
existingCanvas.parentElement.removeChild(existingCanvas);
}
// Tell the canvas element that we will use WebGL to draw
// inside the element (and not the default raster engine):
const gl = canvas.getContext("webgl");
// Next we need to create vertex shaders - these roughly are
// small programs that apply maths to a set of incoming
// array of vertices (numbers).
// You can see the large set of attributes at the top of the shader,
// these are passed into the compiled shader further down the example.
// There's a great overview on how they work here:
// https://webglfundamentals.org/webgl/lessons/webgl-how-it-works.html
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(
vertexShader,
`
precision lowp float;
attribute vec2 a_position; // Flat square on XY plane
attribute float a_startAngle;
attribute float a_angularVelocity;
attribute float a_rotationAxisAngle;
attribute float a_particleDistance;
attribute float a_particleAngle;
attribute float a_particleY;
uniform float u_time; // Global state
varying vec2 v_position;
varying vec3 v_color;
varying float v_overlight;
void main() {
float angle = a_startAngle + a_angularVelocity * u_time;
float vertPosition = 1.1 - mod(u_time * .25 + a_particleY, 2.2);
float viewAngle = a_particleAngle + mod(u_time * .25, 6.28);
mat4 vMatrix = mat4(
1.3, 0.0, 0.0, 0.0,
0.0, 1.3, 0.0, 0.0,
0.0, 0.0, 1.0, 1.0,
0.0, 0.0, 0.0, 1.0
);
mat4 shiftMatrix = mat4(
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
a_particleDistance * sin(viewAngle), vertPosition, a_particleDistance * cos(viewAngle), 1.0
);
mat4 pMatrix = mat4(
cos(a_rotationAxisAngle), sin(a_rotationAxisAngle), 0.0, 0.0,
-sin(a_rotationAxisAngle), cos(a_rotationAxisAngle), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
) * mat4(
1.0, 0.0, 0.0, 0.0,
0.0, cos(angle), sin(angle), 0.0,
0.0, -sin(angle), cos(angle), 0.0,
0.0, 0.0, 0.0, 1.0
);
gl_Position = vMatrix * shiftMatrix * pMatrix * vec4(a_position * 0.03, 0.0, 1.0);
vec4 normal = vec4(0.0, 0.0, 1.0, 0.0);
vec4 transformedNormal = normalize(pMatrix * normal);
float dotNormal = abs(dot(normal.xyz, transformedNormal.xyz));
float regularLighting = dotNormal / 2.0 + 0.5;
float glanceLighting = smoothstep(0.92, 0.98, dotNormal);
v_color = vec3(
mix((0.5 - transformedNormal.z / 2.0) * regularLighting, 1.0, glanceLighting),
mix(0.5 * regularLighting, 1.0, glanceLighting),
mix((0.5 + transformedNormal.z / 2.0) * regularLighting, 1.0, glanceLighting)
);
v_position = a_position;
v_overlight = 0.9 + glanceLighting * 0.1;
}
`
);
gl.compileShader(vertexShader);
// This example also uses fragment shaders - a fragment
// shader is another small program that runs through every
// pixel in the canvas and sets its color.
// In this case, if you play around with the numbers you can see how
// this affects the lighting in the scene, as well as the border
// radius on the confetti:
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(
fragmentShader,
`
precision lowp float;
varying vec2 v_position;
varying vec3 v_color;
varying float v_overlight;
void main() {
gl_FragColor = vec4(v_color, 1.0 - smoothstep(0.8, v_overlight, length(v_position)));
}
`
);
gl.compileShader(fragmentShader);
// Takes the compiled shaders and adds them to the canvas'
// WebGL context so that can be used:
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
// We need to get/set the input variables into the shader in a
// memory-safe way, so the order and the length of their
// values needs to be stored.
const attrs = [
{ name: "a_position", length: 2, offset: 0 }, // e.g. x and y represent 2 spaces in memory
{ name: "a_startAngle", length: 1, offset: 2 }, // but angle is just 1 value
{ name: "a_angularVelocity", length: 1, offset: 3 },
{ name: "a_rotationAxisAngle", length: 1, offset: 4 },
{ name: "a_particleDistance", length: 1, offset: 5 },
{ name: "a_particleAngle", length: 1, offset: 6 },
{ name: "a_particleY", length: 1, offset: 7 },
];
const STRIDE = Object.keys(attrs).length + 1;
// Loop through our known attributes and create pointers in memory for the JS side
// to be able to fill into the shader.
// To understand this API a little bit: WebGL is based on OpenGL
// which is a state-machine styled API. You pass in commands in a
// particular order to render things to the screen.
// So, the intended usage is often not passing objects to every WebGL
// API call, but instead passing one thing to one function, then passing
// another to the next. So, here we prime WebGL to create an array of
// vertex pointers:
for (var i = 0; i < attrs.length; i++) {
const name = attrs[i].name;
const length = attrs[i].length;
const offset = attrs[i].offset;
const attribLocation = gl.getAttribLocation(shaderProgram, name);
gl.vertexAttribPointer(attribLocation, length, gl.FLOAT, false, STRIDE * 4, offset * 4);
gl.enableVertexAttribArray(attribLocation);
}
// Then on this line they are bound to an array in memory:
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
// Set up some constants for rendering:
const NUM_PARTICLES = 200;
const NUM_VERTICES = 4;
// Try reducing this one and hitting "Run" again,
// it represents how many points should exist on
// each confetti and having an odd number sends
// it way out of whack.
const NUM_INDICES = 6;
// Create the arrays of inputs for the vertex shaders
const vertices = new Float32Array(NUM_PARTICLES * STRIDE * NUM_VERTICES);
const indices = new Uint16Array(NUM_PARTICLES * NUM_INDICES);
for (let i = 0; i < NUM_PARTICLES; i++) {
const axisAngle = Math.random() * Math.PI * 2;
const startAngle = Math.random() * Math.PI * 2;
const groupPtr = i * STRIDE * NUM_VERTICES;
const particleDistance = Math.sqrt(Math.random());
const particleAngle = Math.random() * Math.PI * 2;
const particleY = Math.random() * 2.2;
const angularVelocity = Math.random() * 2 + 1;
for (let j = 0; j < 4; j++) {
const vertexPtr = groupPtr + j * STRIDE;
vertices[vertexPtr + 2] = startAngle; // Start angle
vertices[vertexPtr + 3] = angularVelocity; // Angular velocity
vertices[vertexPtr + 4] = axisAngle; // Angle diff
vertices[vertexPtr + 5] = particleDistance; // Distance of the particle from the (0,0,0)
vertices[vertexPtr + 6] = particleAngle; // Angle around Y axis
vertices[vertexPtr + 7] = particleY; // Angle around Y axis
}
// Coordinates
vertices[groupPtr] = vertices[groupPtr + STRIDE * 2] = -1;
vertices[groupPtr + STRIDE] = vertices[groupPtr + STRIDE * 3] = +1;
vertices[groupPtr + 1] = vertices[groupPtr + STRIDE + 1] = -1;
vertices[groupPtr + STRIDE * 2 + 1] = vertices[groupPtr + STRIDE * 3 + 1] = +1;
const indicesPtr = i * NUM_INDICES;
const vertexPtr = i * NUM_VERTICES;
indices[indicesPtr] = vertexPtr;
indices[indicesPtr + 4] = indices[indicesPtr + 1] = vertexPtr + 1;
indices[indicesPtr + 3] = indices[indicesPtr + 2] = vertexPtr + 2;
indices[indicesPtr + 5] = vertexPtr + 3;
}
// Pass in the data to the WebGL context
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
const timeUniformLocation = gl.getUniformLocation(shaderProgram, "u_time");
const startTime = (window.performance || Date).now();
// Start the background colour as black
gl.clearColor(0, 0, 0, 1);
// Allow alpha channels on in the vertex shader
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
// Set the WebGL context to be the full size of the canvas
gl.viewport(0, 0, canvas.width, canvas.height);
// Create a run-loop to draw all of the confetti
(function frame() {
gl.uniform1f(timeUniformLocation, ((window.performance || Date).now() - startTime) / 1000);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, NUM_INDICES * NUM_PARTICLES, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(frame);
})();
// Add the new canvas element into the bottom left
// of the playground
document.body.appendChild(canvas);
// Credit: based on this JSFiddle by Subzey
// https://jsfiddle.net/subzey/52sowezj/