lessons-helper.js 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
  1. /*
  2. * Copyright 2012, Gregg Tavares.
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are
  7. * met:
  8. *
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above
  12. * copyright notice, this list of conditions and the following disclaimer
  13. * in the documentation and/or other materials provided with the
  14. * distribution.
  15. * * Neither the name of Gregg Tavares. nor the names of his
  16. * contributors may be used to endorse or promote products derived from
  17. * this software without specific prior written permission.
  18. *
  19. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  20. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  21. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  22. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  23. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  24. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  25. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  26. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  27. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  28. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  29. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30. */
  31. /* global define */
  32. (function(root, factory) { // eslint-disable-line
  33. if (typeof define === 'function' && define.amd) {
  34. // AMD. Register as an anonymous module.
  35. define([], function() {
  36. return factory.call(root);
  37. });
  38. } else {
  39. // Browser globals
  40. root.lessonsHelper = factory.call(root);
  41. }
  42. }(this, function() {
  43. 'use strict'; // eslint-disable-line
  44. const lessonSettings = window.lessonSettings || {};
  45. const topWindow = this;
  46. /**
  47. * Check if the page is embedded.
  48. * @param {Window?) w window to check
  49. * @return {boolean} True of we are in an iframe
  50. */
  51. function isInIFrame(w) {
  52. w = w || topWindow;
  53. return w !== w.top;
  54. }
  55. function updateCSSIfInIFrame() {
  56. if (isInIFrame()) {
  57. try {
  58. document.getElementsByTagName('html')[0].className = 'iframe';
  59. } catch (e) {
  60. // eslint-disable-line
  61. }
  62. try {
  63. document.body.className = 'iframe';
  64. } catch (e) {
  65. // eslint-disable-line
  66. }
  67. }
  68. }
  69. function isInEditor() {
  70. return window.location.href.substring(0, 4) === 'blob';
  71. }
  72. /**
  73. * Creates a webgl context. If creation fails it will
  74. * change the contents of the container of the <canvas>
  75. * tag to an error message with the correct links for WebGL.
  76. * @param {HTMLCanvasElement} canvas. The canvas element to
  77. * create a context from.
  78. * @param {WebGLContextCreationAttirbutes} opt_attribs Any
  79. * creation attributes you want to pass in.
  80. * @return {WebGLRenderingContext} The created context.
  81. * @memberOf module:webgl-utils
  82. */
  83. function showNeedWebGL(canvas) {
  84. const doc = canvas.ownerDocument;
  85. if (doc) {
  86. const temp = doc.createElement('div');
  87. temp.innerHTML = `
  88. <div style="
  89. position: absolute;
  90. left: 0;
  91. top: 0;
  92. background-color: #DEF;
  93. width: 100vw;
  94. height: 100vh;
  95. display: flex;
  96. flex-flow: column;
  97. justify-content: center;
  98. align-content: center;
  99. align-items: center;
  100. ">
  101. <div style="text-align: center;">
  102. It doesn't appear your browser supports WebGL.<br/>
  103. <a href="http://get.webgl.org" target="_blank">Click here for more information.</a>
  104. </div>
  105. </div>
  106. `;
  107. const div = temp.querySelector('div');
  108. doc.body.appendChild(div);
  109. }
  110. }
  111. const origConsole = {};
  112. function setupConsole() {
  113. const parent = document.createElement('div');
  114. parent.className = 'console';
  115. Object.assign(parent.style, {
  116. fontFamily: 'monospace',
  117. fontSize: 'medium',
  118. maxHeight: '50%',
  119. position: 'fixed',
  120. bottom: 0,
  121. left: 0,
  122. width: '100%',
  123. overflow: 'auto',
  124. background: 'rgba(221, 221, 221, 0.9)',
  125. });
  126. const toggle = document.createElement('div');
  127. let show = false;
  128. Object.assign(toggle.style, {
  129. position: 'absolute',
  130. right: 0,
  131. bottom: 0,
  132. background: '#EEE',
  133. 'font-size': 'smaller',
  134. cursor: 'pointer',
  135. });
  136. toggle.addEventListener('click', showHideConsole);
  137. function showHideConsole() {
  138. show = !show;
  139. toggle.textContent = show ? '☒' : '☐';
  140. parent.style.display = show ? '' : 'none';
  141. }
  142. showHideConsole();
  143. const maxLines = 100;
  144. const lines = [];
  145. let added = false;
  146. function addLine(type, str, color, prefix) {
  147. const div = document.createElement('div');
  148. div.textContent = prefix + str;
  149. div.className = type;
  150. div.style.color = color;
  151. parent.appendChild(div);
  152. lines.push(div);
  153. if (!added) {
  154. added = true;
  155. document.body.appendChild(parent);
  156. document.body.appendChild(toggle);
  157. }
  158. // scrollIntoView only works in Chrome
  159. // In Firefox and Safari scrollIntoView inside an iframe moves
  160. // that element into the view. It should argably only move that
  161. // element inside the iframe itself, otherwise that's giving
  162. // any random iframe control to bring itself into view against
  163. // the parent's wishes.
  164. //
  165. // note that even if we used a solution (which is to manually set
  166. // scrollTop) there's a UI issue that if the user manaully scrolls
  167. // we want to stop scrolling automatically and if they move back
  168. // to the bottom we want to pick up scrolling automatically.
  169. // Kind of a PITA so TBD
  170. //
  171. // div.scrollIntoView();
  172. }
  173. function addLines(type, str, color, prefix) {
  174. while (lines.length > maxLines) {
  175. const div = lines.shift();
  176. div.parentNode.removeChild(div);
  177. }
  178. addLine(type, str, color, prefix);
  179. }
  180. function wrapFunc(obj, funcName, color, prefix) {
  181. const oldFn = obj[funcName];
  182. origConsole[funcName] = oldFn.bind(obj);
  183. return function(...args) {
  184. addLines(funcName, [...args].join(' '), color, prefix);
  185. oldFn.apply(obj, arguments);
  186. };
  187. }
  188. window.console.log = wrapFunc(window.console, 'log', 'black', '');
  189. window.console.warn = wrapFunc(window.console, 'warn', 'black', '⚠');
  190. window.console.error = wrapFunc(window.console, 'error', 'red', '❌');
  191. }
  192. function reportJSError(url, lineNo, colNo, msg) {
  193. try {
  194. const {origUrl, actualLineNo} = window.parent.getActualLineNumberAndMoveTo(url, lineNo, colNo);
  195. url = origUrl;
  196. lineNo = actualLineNo;
  197. } catch (ex) {
  198. origConsole.error(ex);
  199. }
  200. console.error(url, "line:", lineNo, ":", msg); // eslint-disable-line
  201. }
  202. /**
  203. * @typedef {Object} StackInfo
  204. * @property {string} url Url of line
  205. * @property {number} lineNo line number of error
  206. * @property {number} colNo column number of error
  207. * @property {string} [funcName] name of function
  208. */
  209. /**
  210. * @parameter {string} stack A stack string as in `(new Error()).stack`
  211. * @returns {StackInfo}
  212. */
  213. const parseStack = function() {
  214. const browser = getBrowser();
  215. let lineNdx;
  216. let matcher;
  217. if ((/chrome|opera/i).test(browser.name)) {
  218. lineNdx = 3;
  219. matcher = function(line) {
  220. const m = /at ([^(]+)*\(*(.*?):(\d+):(\d+)/.exec(line);
  221. if (m) {
  222. let userFnName = m[1];
  223. let url = m[2];
  224. const lineNo = parseInt(m[3]);
  225. const colNo = parseInt(m[4]);
  226. if (url === '') {
  227. url = userFnName;
  228. userFnName = '';
  229. }
  230. return {
  231. url: url,
  232. lineNo: lineNo,
  233. colNo: colNo,
  234. funcName: userFnName,
  235. };
  236. }
  237. return undefined;
  238. };
  239. } else if ((/firefox|safari/i).test(browser.name)) {
  240. lineNdx = 2;
  241. matcher = function(line) {
  242. const m = /@(.*?):(\d+):(\d+)/.exec(line);
  243. if (m) {
  244. const url = m[1];
  245. const lineNo = parseInt(m[2]);
  246. const colNo = parseInt(m[3]);
  247. return {
  248. url: url,
  249. lineNo: lineNo,
  250. colNo: colNo,
  251. };
  252. }
  253. return undefined;
  254. };
  255. }
  256. return function stackParser(stack) {
  257. if (matcher) {
  258. try {
  259. const lines = stack.split('\n');
  260. // window.fooLines = lines;
  261. // lines.forEach(function(line, ndx) {
  262. // origConsole.log("#", ndx, line);
  263. // });
  264. return matcher(lines[lineNdx]);
  265. } catch (e) {
  266. // do nothing
  267. }
  268. }
  269. return undefined;
  270. };
  271. }();
  272. function setupWorkerSupport() {
  273. function log(data) {
  274. const {logType, msg} = data;
  275. console[logType]('[Worker]', msg); /* eslint-disable-line no-console */
  276. }
  277. function lostContext(/* data */) {
  278. addContextLostHTML();
  279. }
  280. function jsError(data) {
  281. const {url, lineNo, colNo, msg} = data;
  282. reportJSError(url, lineNo, colNo, msg);
  283. }
  284. function jsErrorWithStack(data) {
  285. const {url, stack, msg} = data;
  286. const errorInfo = parseStack(stack);
  287. if (errorInfo) {
  288. reportJSError(errorInfo.url || url, errorInfo.lineNo, errorInfo.colNo, msg);
  289. } else {
  290. console.error(errorMsg) // eslint-disable-line
  291. }
  292. }
  293. const handlers = {
  294. log,
  295. lostContext,
  296. jsError,
  297. jsErrorWithStack,
  298. };
  299. const OrigWorker = self.Worker;
  300. class WrappedWorker extends OrigWorker {
  301. constructor(url) {
  302. super(url);
  303. let listener;
  304. this.onmessage = function(e) {
  305. if (!e || !e.data || !e.data.type === '___editor___') {
  306. if (listener) {
  307. listener(e);
  308. }
  309. return;
  310. }
  311. e.stopImmediatePropagation();
  312. const data = e.data.data;
  313. const fn = handlers[data.type];
  314. if (!fn) {
  315. origConsole.error('unknown editor msg:', data.type);
  316. } else {
  317. fn(data);
  318. }
  319. return;
  320. };
  321. Object.defineProperty(this, 'onmessage', {
  322. get() {
  323. return listener;
  324. },
  325. set(fn) {
  326. listener = fn;
  327. },
  328. });
  329. }
  330. }
  331. self.Worker = WrappedWorker;
  332. }
  333. function addContextLostHTML() {
  334. const div = document.createElement('div');
  335. div.className = 'contextlost';
  336. div.innerHTML = '<div>Context Lost: Click To Reload</div>';
  337. div.addEventListener('click', function() {
  338. window.location.reload();
  339. });
  340. document.body.appendChild(div);
  341. }
  342. /**
  343. * Gets a WebGL context.
  344. * makes its backing store the size it is displayed.
  345. * @param {HTMLCanvasElement} canvas a canvas element.
  346. * @param {module:webgl-utils.GetWebGLContextOptions} [opt_options] options
  347. * @memberOf module:webgl-utils
  348. */
  349. let setupLesson = function(canvas, opt_options) {
  350. // only once
  351. setupLesson = function() {};
  352. const options = opt_options || {};
  353. if (canvas) {
  354. canvas.addEventListener('webglcontextlost', function(e) {
  355. // the default is to do nothing. Preventing the default
  356. // means allowing context to be restored
  357. e.preventDefault();
  358. addContextLostHTML();
  359. });
  360. canvas.addEventListener('webglcontextrestored', function() {
  361. // just reload the page. Easiest.
  362. window.location.reload();
  363. });
  364. }
  365. if (isInIFrame()) {
  366. updateCSSIfInIFrame();
  367. }
  368. };
  369. /**
  370. * Get's the iframe in the parent document
  371. * that is displaying the specified window .
  372. * @param {Window} window window to check.
  373. * @return {HTMLIFrameElement?) the iframe element if window is in an iframe
  374. */
  375. function getIFrameForWindow(window) {
  376. if (!isInIFrame(window)) {
  377. return;
  378. }
  379. const iframes = window.parent.document.getElementsByTagName('iframe');
  380. for (let ii = 0; ii < iframes.length; ++ii) {
  381. const iframe = iframes[ii];
  382. if (iframe.contentDocument === window.document) {
  383. return iframe; // eslint-disable-line
  384. }
  385. }
  386. }
  387. /**
  388. * Returns true if window is on screen. The main window is
  389. * always on screen windows in iframes might not be.
  390. * @param {Window} window the window to check.
  391. * @return {boolean} true if window is on screen.
  392. */
  393. function isFrameVisible(window) {
  394. try {
  395. const iframe = getIFrameForWindow(window);
  396. if (!iframe) {
  397. return true;
  398. }
  399. const bounds = iframe.getBoundingClientRect();
  400. const isVisible = bounds.top < window.parent.innerHeight && bounds.bottom >= 0 &&
  401. bounds.left < window.parent.innerWidth && bounds.right >= 0;
  402. return isVisible && isFrameVisible(window.parent);
  403. } catch (e) {
  404. return true; // We got a security error?
  405. }
  406. }
  407. /**
  408. * Returns true if element is on screen.
  409. * @param {HTMLElement} element the element to check.
  410. * @return {boolean} true if element is on screen.
  411. */
  412. function isOnScreen(element) {
  413. let isVisible = true;
  414. if (element) {
  415. const bounds = element.getBoundingClientRect();
  416. isVisible = bounds.top < topWindow.innerHeight && bounds.bottom >= 0;
  417. }
  418. return isVisible && isFrameVisible(topWindow);
  419. }
  420. // Replace requestAnimationFrame.
  421. if (topWindow.requestAnimationFrame) {
  422. topWindow.requestAnimationFrame = (function(oldRAF) {
  423. return function(callback, element) {
  424. const handler = function() {
  425. return oldRAF(isOnScreen(element) ? callback : handler, element);
  426. };
  427. return handler();
  428. };
  429. }(topWindow.requestAnimationFrame));
  430. }
  431. updateCSSIfInIFrame();
  432. //------------ [ from https://github.com/KhronosGroup/WebGLDeveloperTools ]
  433. /*
  434. ** Copyright (c) 2012 The Khronos Group Inc.
  435. **
  436. ** Permission is hereby granted, free of charge, to any person obtaining a
  437. ** copy of this software and/or associated documentation files (the
  438. ** "Materials"), to deal in the Materials without restriction, including
  439. ** without limitation the rights to use, copy, modify, merge, publish,
  440. ** distribute, sublicense, and/or sell copies of the Materials, and to
  441. ** permit persons to whom the Materials are furnished to do so, subject to
  442. ** the following conditions:
  443. **
  444. ** The above copyright notice and this permission notice shall be included
  445. ** in all copies or substantial portions of the Materials.
  446. **
  447. ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  448. ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  449. ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  450. ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  451. ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  452. ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  453. ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
  454. */
  455. /**
  456. * Types of contexts we have added to map
  457. */
  458. const mappedContextTypes = {};
  459. /**
  460. * Map of numbers to names.
  461. * @type {Object}
  462. */
  463. const glEnums = {};
  464. /**
  465. * Map of names to numbers.
  466. * @type {Object}
  467. */
  468. const enumStringToValue = {};
  469. /**
  470. * Initializes this module. Safe to call more than once.
  471. * @param {!WebGLRenderingContext} ctx A WebGL context. If
  472. * you have more than one context it doesn't matter which one
  473. * you pass in, it is only used to pull out constants.
  474. */
  475. function addEnumsForContext(ctx, type) {
  476. if (!mappedContextTypes[type]) {
  477. mappedContextTypes[type] = true;
  478. for (const propertyName in ctx) {
  479. if (typeof ctx[propertyName] === 'number') {
  480. glEnums[ctx[propertyName]] = propertyName;
  481. enumStringToValue[propertyName] = ctx[propertyName];
  482. }
  483. }
  484. }
  485. }
  486. function enumArrayToString(enums) {
  487. const enumStrings = [];
  488. if (enums.length) {
  489. for (let i = 0; i < enums.length; ++i) {
  490. enums.push(glEnumToString(enums[i])); // eslint-disable-line
  491. }
  492. return '[' + enumStrings.join(', ') + ']';
  493. }
  494. return enumStrings.toString();
  495. }
  496. function makeBitFieldToStringFunc(enums) {
  497. return function(value) {
  498. let orResult = 0;
  499. const orEnums = [];
  500. for (let i = 0; i < enums.length; ++i) {
  501. const enumValue = enumStringToValue[enums[i]];
  502. if ((value & enumValue) !== 0) {
  503. orResult |= enumValue;
  504. orEnums.push(glEnumToString(enumValue)); // eslint-disable-line
  505. }
  506. }
  507. if (orResult === value) {
  508. return orEnums.join(' | ');
  509. } else {
  510. return glEnumToString(value); // eslint-disable-line
  511. }
  512. };
  513. }
  514. const destBufferBitFieldToString = makeBitFieldToStringFunc([
  515. 'COLOR_BUFFER_BIT',
  516. 'DEPTH_BUFFER_BIT',
  517. 'STENCIL_BUFFER_BIT',
  518. ]);
  519. /**
  520. * Which arguments are enums based on the number of arguments to the function.
  521. * So
  522. * 'texImage2D': {
  523. * 9: { 0:true, 2:true, 6:true, 7:true },
  524. * 6: { 0:true, 2:true, 3:true, 4:true },
  525. * },
  526. *
  527. * means if there are 9 arguments then 6 and 7 are enums, if there are 6
  528. * arguments 3 and 4 are enums. Maybe a function as well in which case
  529. * value is passed to function and returns a string
  530. *
  531. * @type {!Object.<number, (!Object.<number, string>|function)}
  532. */
  533. const glValidEnumContexts = {
  534. // Generic setters and getters
  535. 'enable': {1: { 0:true }},
  536. 'disable': {1: { 0:true }},
  537. 'getParameter': {1: { 0:true }},
  538. // Rendering
  539. 'drawArrays': {3:{ 0:true }},
  540. 'drawElements': {4:{ 0:true, 2:true }},
  541. 'drawArraysInstanced': {4: { 0:true }},
  542. 'drawElementsInstanced': {5: {0:true, 2: true }},
  543. 'drawRangeElements': {6: {0:true, 4: true }},
  544. // Shaders
  545. 'createShader': {1: { 0:true }},
  546. 'getShaderParameter': {2: { 1:true }},
  547. 'getProgramParameter': {2: { 1:true }},
  548. 'getShaderPrecisionFormat': {2: { 0: true, 1:true }},
  549. // Vertex attributes
  550. 'getVertexAttrib': {2: { 1:true }},
  551. 'vertexAttribPointer': {6: { 2:true }},
  552. 'vertexAttribIPointer': {5: { 2:true }}, // WebGL2
  553. // Textures
  554. 'bindTexture': {2: { 0:true }},
  555. 'activeTexture': {1: { 0:true }},
  556. 'getTexParameter': {2: { 0:true, 1:true }},
  557. 'texParameterf': {3: { 0:true, 1:true }},
  558. 'texParameteri': {3: { 0:true, 1:true, 2:true }},
  559. 'texImage2D': {
  560. 9: { 0:true, 2:true, 6:true, 7:true },
  561. 6: { 0:true, 2:true, 3:true, 4:true },
  562. 10: { 0:true, 2:true, 6:true, 7:true }, // WebGL2
  563. },
  564. 'texImage3D': {
  565. 10: { 0:true, 2:true, 7:true, 8:true }, // WebGL2
  566. 11: { 0:true, 2:true, 7:true, 8:true }, // WebGL2
  567. },
  568. 'texSubImage2D': {
  569. 9: { 0:true, 6:true, 7:true },
  570. 7: { 0:true, 4:true, 5:true },
  571. 10: { 0:true, 6:true, 7:true }, // WebGL2
  572. },
  573. 'texSubImage3D': {
  574. 11: { 0:true, 8:true, 9:true }, // WebGL2
  575. 12: { 0:true, 8:true, 9:true }, // WebGL2
  576. },
  577. 'texStorage2D': { 5: { 0:true, 2:true }}, // WebGL2
  578. 'texStorage3D': { 6: { 0:true, 2:true }}, // WebGL2
  579. 'copyTexImage2D': {8: { 0:true, 2:true }},
  580. 'copyTexSubImage2D': {8: { 0:true }},
  581. 'copyTexSubImage3D': {9: { 0:true }}, // WebGL2
  582. 'generateMipmap': {1: { 0:true }},
  583. 'compressedTexImage2D': {
  584. 7: { 0: true, 2:true },
  585. 8: { 0: true, 2:true }, // WebGL2
  586. },
  587. 'compressedTexSubImage2D': {
  588. 8: { 0: true, 6:true },
  589. 9: { 0: true, 6:true }, // WebGL2
  590. },
  591. 'compressedTexImage3D': {
  592. 8: { 0: true, 2: true, }, // WebGL2
  593. 9: { 0: true, 2: true, }, // WebGL2
  594. },
  595. 'compressedTexSubImage3D': {
  596. 9: { 0: true, 8: true, }, // WebGL2
  597. 10: { 0: true, 8: true, }, // WebGL2
  598. },
  599. // Buffer objects
  600. 'bindBuffer': {2: { 0:true }},
  601. 'bufferData': {
  602. 3: { 0:true, 2:true },
  603. 4: { 0:true, 2:true }, // WebGL2
  604. 5: { 0:true, 2:true }, // WebGL2
  605. },
  606. 'bufferSubData': {
  607. 3: { 0:true },
  608. 4: { 0:true }, // WebGL2
  609. 5: { 0:true }, // WebGL2
  610. },
  611. 'copyBufferSubData': {
  612. 5: { 0:true }, // WeBGL2
  613. },
  614. 'getBufferParameter': {2: { 0:true, 1:true }},
  615. 'getBufferSubData': {
  616. 3: { 0: true, }, // WebGL2
  617. 4: { 0: true, }, // WebGL2
  618. 5: { 0: true, }, // WebGL2
  619. },
  620. // Renderbuffers and framebuffers
  621. 'pixelStorei': {2: { 0:true, 1:true }},
  622. 'readPixels': {
  623. 7: { 4:true, 5:true },
  624. 8: { 4:true, 5:true }, // WebGL2
  625. },
  626. 'bindRenderbuffer': {2: { 0:true }},
  627. 'bindFramebuffer': {2: { 0:true }},
  628. 'blitFramebuffer': {10: { 8: destBufferBitFieldToString, 9:true }}, // WebGL2
  629. 'checkFramebufferStatus': {1: { 0:true }},
  630. 'framebufferRenderbuffer': {4: { 0:true, 1:true, 2:true }},
  631. 'framebufferTexture2D': {5: { 0:true, 1:true, 2:true }},
  632. 'framebufferTextureLayer': {5: {0:true, 1:true }}, // WebGL2
  633. 'getFramebufferAttachmentParameter': {3: { 0:true, 1:true, 2:true }},
  634. 'getInternalformatParameter': {3: {0:true, 1:true, 2:true }}, // WebGL2
  635. 'getRenderbufferParameter': {2: { 0:true, 1:true }},
  636. 'invalidateFramebuffer': {2: { 0:true, 1: enumArrayToString, }}, // WebGL2
  637. 'invalidateSubFramebuffer': {6: {0: true, 1: enumArrayToString, }}, // WebGL2
  638. 'readBuffer': {1: {0: true}}, // WebGL2
  639. 'renderbufferStorage': {4: { 0:true, 1:true }},
  640. 'renderbufferStorageMultisample': {5: { 0: true, 2: true }}, // WebGL2
  641. // Frame buffer operations (clear, blend, depth test, stencil)
  642. 'clear': {1: { 0: destBufferBitFieldToString }},
  643. 'depthFunc': {1: { 0:true }},
  644. 'blendFunc': {2: { 0:true, 1:true }},
  645. 'blendFuncSeparate': {4: { 0:true, 1:true, 2:true, 3:true }},
  646. 'blendEquation': {1: { 0:true }},
  647. 'blendEquationSeparate': {2: { 0:true, 1:true }},
  648. 'stencilFunc': {3: { 0:true }},
  649. 'stencilFuncSeparate': {4: { 0:true, 1:true }},
  650. 'stencilMaskSeparate': {2: { 0:true }},
  651. 'stencilOp': {3: { 0:true, 1:true, 2:true }},
  652. 'stencilOpSeparate': {4: { 0:true, 1:true, 2:true, 3:true }},
  653. // Culling
  654. 'cullFace': {1: { 0:true }},
  655. 'frontFace': {1: { 0:true }},
  656. // ANGLE_instanced_arrays extension
  657. 'drawArraysInstancedANGLE': {4: { 0:true }},
  658. 'drawElementsInstancedANGLE': {5: { 0:true, 2:true }},
  659. // EXT_blend_minmax extension
  660. 'blendEquationEXT': {1: { 0:true }},
  661. // Multiple Render Targets
  662. 'drawBuffersWebGL': {1: {0: enumArrayToString, }}, // WEBGL_draw_bufers
  663. 'drawBuffers': {1: {0: enumArrayToString, }}, // WebGL2
  664. 'clearBufferfv': {
  665. 4: {0: true }, // WebGL2
  666. 5: {0: true }, // WebGL2
  667. },
  668. 'clearBufferiv': {
  669. 4: {0: true }, // WebGL2
  670. 5: {0: true }, // WebGL2
  671. },
  672. 'clearBufferuiv': {
  673. 4: {0: true }, // WebGL2
  674. 5: {0: true }, // WebGL2
  675. },
  676. 'clearBufferfi': { 4: {0: true}}, // WebGL2
  677. // QueryObjects
  678. 'beginQuery': { 2: { 0: true }}, // WebGL2
  679. 'endQuery': { 1: { 0: true }}, // WebGL2
  680. 'getQuery': { 2: { 0: true, 1: true }}, // WebGL2
  681. 'getQueryParameter': { 2: { 1: true }}, // WebGL2
  682. // Sampler Objects
  683. 'samplerParameteri': { 3: { 1: true }}, // WebGL2
  684. 'samplerParameterf': { 3: { 1: true }}, // WebGL2
  685. 'getSamplerParameter': { 2: { 1: true }}, // WebGL2
  686. // Sync objects
  687. 'clientWaitSync': { 3: { 1: makeBitFieldToStringFunc(['SYNC_FLUSH_COMMANDS_BIT']) }}, // WebGL2
  688. 'fenceSync': { 2: { 0: true }}, // WebGL2
  689. 'getSyncParameter': { 2: { 1: true }}, // WebGL2
  690. // Transform Feedback
  691. 'bindTransformFeedback': { 2: { 0: true }}, // WebGL2
  692. 'beginTransformFeedback': { 1: { 0: true }}, // WebGL2
  693. // Uniform Buffer Objects and Transform Feedback Buffers
  694. 'bindBufferBase': { 3: { 0: true }}, // WebGL2
  695. 'bindBufferRange': { 5: { 0: true }}, // WebGL2
  696. 'getIndexedParameter': { 2: { 0: true }}, // WebGL2
  697. 'getActiveUniforms': { 3: { 2: true }}, // WebGL2
  698. 'getActiveUniformBlockParameter': { 3: { 2: true }}, // WebGL2
  699. };
  700. /**
  701. * Gets an string version of an WebGL enum.
  702. *
  703. * Example:
  704. * var str = WebGLDebugUtil.glEnumToString(ctx.getError());
  705. *
  706. * @param {number} value Value to return an enum for
  707. * @return {string} The string version of the enum.
  708. */
  709. function glEnumToString(value) {
  710. const name = glEnums[value];
  711. return (name !== undefined)
  712. ? `gl.${name}`
  713. : `/*UNKNOWN WebGL ENUM*/ 0x${value.toString(16)}`;
  714. }
  715. /**
  716. * Returns the string version of a WebGL argument.
  717. * Attempts to convert enum arguments to strings.
  718. * @param {string} functionName the name of the WebGL function.
  719. * @param {number} numArgs the number of arguments passed to the function.
  720. * @param {number} argumentIndx the index of the argument.
  721. * @param {*} value The value of the argument.
  722. * @return {string} The value as a string.
  723. */
  724. function glFunctionArgToString(functionName, numArgs, argumentIndex, value) {
  725. const funcInfos = glValidEnumContexts[functionName];
  726. if (funcInfos !== undefined) {
  727. const funcInfo = funcInfos[numArgs];
  728. if (funcInfo !== undefined) {
  729. const argType = funcInfo[argumentIndex];
  730. if (argType) {
  731. if (typeof argType === 'function') {
  732. return argType(value);
  733. } else {
  734. return glEnumToString(value);
  735. }
  736. }
  737. }
  738. }
  739. if (value === null) {
  740. return 'null';
  741. } else if (value === undefined) {
  742. return 'undefined';
  743. } else {
  744. return value.toString();
  745. }
  746. }
  747. /**
  748. * Converts the arguments of a WebGL function to a string.
  749. * Attempts to convert enum arguments to strings.
  750. *
  751. * @param {string} functionName the name of the WebGL function.
  752. * @param {number} args The arguments.
  753. * @return {string} The arguments as a string.
  754. */
  755. function glFunctionArgsToString(functionName, args) {
  756. // apparently we can't do args.join(",");
  757. const argStrs = [];
  758. const numArgs = args.length;
  759. for (let ii = 0; ii < numArgs; ++ii) {
  760. argStrs.push(glFunctionArgToString(functionName, numArgs, ii, args[ii]));
  761. }
  762. return argStrs.join(', ');
  763. }
  764. function makePropertyWrapper(wrapper, original, propertyName) {
  765. wrapper.__defineGetter__(propertyName, function() { // eslint-disable-line
  766. return original[propertyName];
  767. });
  768. // TODO(gmane): this needs to handle properties that take more than
  769. // one value?
  770. wrapper.__defineSetter__(propertyName, function(value) { // eslint-disable-line
  771. original[propertyName] = value;
  772. });
  773. }
  774. /**
  775. * Given a WebGL context returns a wrapped context that calls
  776. * gl.getError after every command and calls a function if the
  777. * result is not gl.NO_ERROR.
  778. *
  779. * @param {!WebGLRenderingContext} ctx The webgl context to
  780. * wrap.
  781. * @param {!function(err, funcName, args): void} opt_onErrorFunc
  782. * The function to call when gl.getError returns an
  783. * error. If not specified the default function calls
  784. * console.log with a message.
  785. * @param {!function(funcName, args): void} opt_onFunc The
  786. * function to call when each webgl function is called.
  787. * You can use this to log all calls for example.
  788. * @param {!WebGLRenderingContext} opt_err_ctx The webgl context
  789. * to call getError on if different than ctx.
  790. */
  791. function makeDebugContext(ctx, options) {
  792. options = options || {};
  793. const errCtx = options.errCtx || ctx;
  794. const onFunc = options.funcFunc;
  795. const sharedState = options.sharedState || {
  796. numDrawCallsRemaining: options.maxDrawCalls || -1,
  797. wrappers: {},
  798. };
  799. options.sharedState = sharedState;
  800. const errorFunc = options.errorFunc || function(err, functionName, args) {
  801. console.error(`WebGL error ${glEnumToString(err)} in ${functionName}(${glFunctionArgsToString(functionName, args)})`); /* eslint-disable-line no-console */
  802. };
  803. // Holds booleans for each GL error so after we get the error ourselves
  804. // we can still return it to the client app.
  805. const glErrorShadow = { };
  806. const wrapper = {};
  807. function removeChecks() {
  808. Object.keys(sharedState.wrappers).forEach(function(name) {
  809. const pair = sharedState.wrappers[name];
  810. const wrapper = pair.wrapper;
  811. const orig = pair.orig;
  812. for (const propertyName in wrapper) {
  813. if (typeof wrapper[propertyName] === 'function') {
  814. wrapper[propertyName] = orig[propertyName].bind(orig);
  815. }
  816. }
  817. });
  818. }
  819. function checkMaxDrawCalls() {
  820. if (sharedState.numDrawCallsRemaining === 0) {
  821. removeChecks();
  822. }
  823. --sharedState.numDrawCallsRemaining;
  824. }
  825. function noop() {
  826. }
  827. // Makes a function that calls a WebGL function and then calls getError.
  828. function makeErrorWrapper(ctx, functionName) {
  829. const check = functionName.substring(0, 4) === 'draw' ? checkMaxDrawCalls : noop;
  830. return function() {
  831. if (onFunc) {
  832. onFunc(functionName, arguments);
  833. }
  834. const result = ctx[functionName].apply(ctx, arguments);
  835. const err = errCtx.getError();
  836. if (err !== 0) {
  837. glErrorShadow[err] = true;
  838. errorFunc(err, functionName, arguments);
  839. }
  840. check();
  841. return result;
  842. };
  843. }
  844. function makeGetExtensionWrapper(ctx, wrapped) {
  845. return function() {
  846. const extensionName = arguments[0];
  847. let ext = sharedState.wrappers[extensionName];
  848. if (!ext) {
  849. ext = wrapped.apply(ctx, arguments);
  850. if (ext) {
  851. const origExt = ext;
  852. ext = makeDebugContext(ext, Object.assign({}, options, {errCtx: ctx}));
  853. sharedState.wrappers[extensionName] = { wrapper: ext, orig: origExt };
  854. addEnumsForContext(origExt, extensionName);
  855. }
  856. }
  857. return ext;
  858. };
  859. }
  860. // Make a an object that has a copy of every property of the WebGL context
  861. // but wraps all functions.
  862. for (const propertyName in ctx) {
  863. if (typeof ctx[propertyName] === 'function') {
  864. if (propertyName !== 'getExtension') {
  865. wrapper[propertyName] = makeErrorWrapper(ctx, propertyName);
  866. } else {
  867. const wrapped = makeErrorWrapper(ctx, propertyName);
  868. wrapper[propertyName] = makeGetExtensionWrapper(ctx, wrapped);
  869. }
  870. } else {
  871. makePropertyWrapper(wrapper, ctx, propertyName);
  872. }
  873. }
  874. // Override the getError function with one that returns our saved results.
  875. if (wrapper.getError) {
  876. wrapper.getError = function() {
  877. for (const err in glErrorShadow) {
  878. if (glErrorShadow.hasOwnProperty(err)) {
  879. if (glErrorShadow[err]) {
  880. glErrorShadow[err] = false;
  881. return err;
  882. }
  883. }
  884. }
  885. return ctx.NO_ERROR;
  886. };
  887. }
  888. if (wrapper.bindBuffer) {
  889. sharedState.wrappers['webgl'] = { wrapper: wrapper, orig: ctx };
  890. addEnumsForContext(ctx, ctx.bindBufferBase ? 'WebGL2' : 'WebGL');
  891. }
  892. return wrapper;
  893. }
  894. //------------
  895. function captureJSErrors() {
  896. // capture JavaScript Errors
  897. window.addEventListener('error', function(e) {
  898. const msg = e.message || e.error;
  899. const url = e.filename;
  900. const lineNo = e.lineno || 1;
  901. const colNo = e.colno || 1;
  902. reportJSError(url, lineNo, colNo, msg);
  903. origConsole.error(e.error);
  904. });
  905. }
  906. // adapted from http://stackoverflow.com/a/2401861/128511
  907. function getBrowser() {
  908. const userAgent = navigator.userAgent;
  909. let m = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
  910. if (/trident/i.test(m[1])) {
  911. m = /\brv[ :]+(\d+)/g.exec(userAgent) || [];
  912. return {
  913. name: 'IE',
  914. version: m[1],
  915. };
  916. }
  917. if (m[1] === 'Chrome') {
  918. const temp = userAgent.match(/\b(OPR|Edge)\/(\d+)/);
  919. if (temp) {
  920. return {
  921. name: temp[1].replace('OPR', 'Opera'),
  922. version: temp[2],
  923. };
  924. }
  925. }
  926. m = m[2] ? [m[1], m[2]] : [navigator.appName, navigator.appVersion, '-?'];
  927. const version = userAgent.match(/version\/(\d+)/i);
  928. if (version) {
  929. m.splice(1, 1, version[1]);
  930. }
  931. return {
  932. name: m[0],
  933. version: m[1],
  934. };
  935. }
  936. const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
  937. function installWebGLLessonSetup() {
  938. HTMLCanvasElement.prototype.getContext = (function(oldFn) {
  939. return function() {
  940. const type = arguments[0];
  941. const isWebGL = isWebGLRE.test(type);
  942. if (isWebGL) {
  943. setupLesson(this);
  944. }
  945. const args = [].slice.apply(arguments);
  946. args[1] = Object.assign({
  947. powerPreference: 'low-power',
  948. }, args[1]);
  949. const ctx = oldFn.apply(this, args);
  950. if (!ctx && isWebGL) {
  951. showNeedWebGL(this);
  952. }
  953. return ctx;
  954. };
  955. }(HTMLCanvasElement.prototype.getContext));
  956. }
  957. function installWebGLDebugContextCreator() {
  958. // capture GL errors
  959. HTMLCanvasElement.prototype.getContext = (function(oldFn) {
  960. return function() {
  961. let ctx = oldFn.apply(this, arguments);
  962. // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
  963. // but that might fail if wrapped by debugging extension
  964. if (ctx && ctx.bindTexture) {
  965. ctx = makeDebugContext(ctx, {
  966. maxDrawCalls: 100,
  967. errorFunc: function(err, funcName, args) {
  968. const numArgs = args.length;
  969. const enumedArgs = [].map.call(args, function(arg, ndx) {
  970. let str = glFunctionArgToString(funcName, numArgs, ndx, arg);
  971. // shorten because of long arrays
  972. if (str.length > 200) {
  973. str = str.substring(0, 200) + '...';
  974. }
  975. return str;
  976. });
  977. const errorMsg = `WebGL error ${glEnumToString(err)} in ${funcName}(${enumedArgs.join(', ')})`;
  978. const errorInfo = parseStack((new Error()).stack);
  979. if (errorInfo) {
  980. reportJSError(errorInfo.url, errorInfo.lineNo, errorInfo.colNo, errorMsg);
  981. } else {
  982. console.error(errorMsg) // eslint-disable-line
  983. }
  984. },
  985. });
  986. }
  987. return ctx;
  988. };
  989. }(HTMLCanvasElement.prototype.getContext));
  990. }
  991. installWebGLLessonSetup();
  992. if (isInEditor()) {
  993. setupWorkerSupport();
  994. setupConsole();
  995. captureJSErrors();
  996. if (lessonSettings.glDebug !== false) {
  997. installWebGLDebugContextCreator();
  998. }
  999. }
  1000. return {
  1001. setupLesson: setupLesson,
  1002. showNeedWebGL: showNeedWebGL,
  1003. makeDebugContext: makeDebugContext,
  1004. glFunctionArgsToString: glFunctionArgsToString,
  1005. };
  1006. }));