UNPKG

6.13 kBJavaScriptView Raw
1//@flow
2import createShader from "gl-shader";
3import createTexture from "gl-texture2d";
4import now from "performance-now";
5import ndarray from "ndarray";
6
7type Color = [number, number, number, number];
8
9function colorMatches(actual: Color, expected: Color): boolean {
10 const dr = actual[0] - expected[0];
11 const dg = actual[1] - expected[1];
12 const db = actual[2] - expected[2];
13 const da = actual[3] - expected[3];
14 // we need to be fuzzy because implementation precision can differ
15 return dr * dr + dg * dg + db * db + da * da < 20; // euclidian distance < sqrt(20)
16}
17
18type CompilerResult = {
19 data: {
20 compileTime: number,
21 drawTime: number,
22 },
23 errors: Array<{
24 type: string,
25 message: string,
26 code: string,
27 line?: number,
28 }>,
29};
30
31const VERTEX_SHADER = `attribute vec2 _p;
32varying vec2 uv;
33void main() {
34gl_Position = vec4(_p,0.0,1.0);
35uv = vec2(0.5, 0.5) * (_p+vec2(1.0, 1.0));
36}`;
37
38export default (gl: WebGLRenderingContext) => {
39 const { drawingBufferWidth: w, drawingBufferHeight: h } = gl;
40 const pixels = new Uint8Array(w * h * 4);
41
42 function colorAt(x, y) {
43 const i = (x + y * w) * 4;
44 const r = pixels[i + 0];
45 const g = pixels[i + 1];
46 const b = pixels[i + 2];
47 const a = pixels[i + 3];
48 return [r, g, b, a];
49 }
50
51 const buffer = gl.createBuffer();
52 gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
53 gl.bufferData(
54 gl.ARRAY_BUFFER,
55 new Float32Array([-1, -1, -1, 4, 4, -1]), // see a-big-triangle
56 gl.STATIC_DRAW
57 );
58 gl.viewport(0, 0, w, h);
59
60 const genericTexture = createTexture(
61 gl,
62 ndarray(
63 // prettier-ignore
64 [ 0.1, 0.1, 0.1, 1.0,
65 0.5, 0.5, 0.5, 1.0,
66 0.5, 0.5, 0.5, 1.0,
67 0.9, 0.9, 0.9, 1.0 ],
68 [2, 2, 4]
69 )
70 );
71 genericTexture.minFilter = gl.LINEAR;
72 genericTexture.magFilter = gl.LINEAR;
73
74 return (glsl: string): CompilerResult => {
75 let data = {
76 compileTime: 0,
77 drawTime: 0,
78 };
79 const errors = [];
80 let shader;
81 try {
82 const beforeCompilation = now();
83 shader = createShader(
84 gl,
85 VERTEX_SHADER,
86 `precision highp float;varying vec2 uv;uniform float progress, ratio;vec4 getFromColor (vec2 uv) { return vec4(uv, 0.0, 1.0); } vec4 getToColor (vec2 uv) { return vec4(uv, 1.0, 1.0); } ${glsl}
87 void main () {
88 gl_FragColor = transition(uv);
89 }`
90 );
91 gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); // this is just to force trigger a flush
92 const afterCompilation = now();
93 data.compileTime = afterCompilation - beforeCompilation;
94
95 // We now will check the transition is correctly rendering the {from, to} images in progress={0, 1}
96 // leaving all transition params to default zero value should not affect a transition to "work"
97
98 shader.bind();
99 shader.attributes._p.pointer();
100
101 Object.keys(shader.types.uniforms).forEach(key => {
102 if (shader.types.uniforms[key] === "sampler2D") {
103 shader.uniforms[key] = genericTexture.bind();
104 }
105 });
106
107 shader.uniforms.ratio = w / h;
108
109 gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); // this is just to force trigger a flush
110 const beforeDraw1 = now();
111 shader.uniforms.progress = 0;
112 gl.drawArrays(gl.TRIANGLES, 0, 3);
113 gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); // we need to put this in the scope because impl like Chrome are lazy and this really trigger the work.
114 const afterDraw1 = now();
115 const draw1matches = [
116 colorMatches(colorAt(0, 0), [0, 0, 0, 255]),
117 colorMatches(colorAt(w - 1, 0), [255, 0, 0, 255]),
118 colorMatches(colorAt(0, h - 1), [0, 255, 0, 255]),
119 colorMatches(colorAt(w - 1, h - 1), [255, 255, 0, 255]),
120 ];
121
122 const beforeDraw2 = now();
123 shader.uniforms.progress = 1;
124 gl.drawArrays(gl.TRIANGLES, 0, 3);
125 gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
126 const afterDraw2 = now();
127 const draw2matches = [
128 colorMatches(colorAt(0, 0), [0, 0, 255, 255]),
129 colorMatches(colorAt(w - 1, 0), [255, 0, 255, 255]),
130 colorMatches(colorAt(0, h - 1), [0, 255, 255, 255]),
131 colorMatches(colorAt(w - 1, h - 1), [255, 255, 255, 255]),
132 ];
133
134 const drawErrorMessages = [];
135 if (draw1matches.some(t => !t)) {
136 drawErrorMessages.push("render getFromColor(uv) when progress=0.0");
137 }
138 if (draw2matches.some(t => !t)) {
139 drawErrorMessages.push("render getToColor(uv) when progress=1.0");
140 }
141 if (drawErrorMessages.length > 0) {
142 errors.push({
143 code: "Transition_draw_invalid",
144 type: "error",
145 message: "invalid transition, it must: " +
146 drawErrorMessages.join(", "),
147 });
148 }
149
150 const drawTime1 = afterDraw1 - beforeDraw1;
151 const drawTime2 = afterDraw2 - beforeDraw2;
152 // average of the 2 draws for even better precision
153 data.drawTime = (drawTime1 + drawTime2) / 2;
154 } catch (e) {
155 let error;
156 const lines = e.message.split("\n");
157 for (let _i = 0, _len = lines.length; _i < _len; _i++) {
158 const i = lines[_i];
159 if (i.substr(0, 5) === "ERROR") {
160 error = i;
161 }
162 }
163 if (!error) {
164 errors.push({
165 line: 0,
166 code: "WebGL_unknown_error",
167 type: "error",
168 message: "Unknown error: " + e.message,
169 });
170 } else {
171 const details = error.split(":");
172 if (details.length < 4) {
173 errors.push({
174 line: 0,
175 code: "WebGL_error",
176 type: "error",
177 message: error,
178 });
179 } else {
180 const lineStr = details[2];
181 let line = parseInt(lineStr, 10);
182 if (isNaN(line)) line = 0;
183 const message = details.splice(3).join(":");
184 errors.push({
185 line,
186 code: "WebGL_error",
187 type: "error",
188 message,
189 });
190 }
191 }
192 } finally {
193 if (shader) shader.dispose();
194 }
195 return {
196 data,
197 errors,
198 };
199 };
200};