lessons-helper.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  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. * @memberOf module:webgl-utils
  347. */
  348. let setupLesson = function(canvas) {
  349. // only once
  350. setupLesson = function() {};
  351. if (canvas) {
  352. canvas.addEventListener('webglcontextlost', function(e) {
  353. // the default is to do nothing. Preventing the default
  354. // means allowing context to be restored
  355. e.preventDefault();
  356. addContextLostHTML();
  357. });
  358. canvas.addEventListener('webglcontextrestored', function() {
  359. // just reload the page. Easiest.
  360. window.location.reload();
  361. });
  362. }
  363. if (isInIFrame()) {
  364. updateCSSIfInIFrame();
  365. }
  366. };
  367. /**
  368. * Get's the iframe in the parent document
  369. * that is displaying the specified window .
  370. * @param {Window} window window to check.
  371. * @return {HTMLIFrameElement?) the iframe element if window is in an iframe
  372. */
  373. function getIFrameForWindow(window) {
  374. if (!isInIFrame(window)) {
  375. return;
  376. }
  377. const iframes = window.parent.document.getElementsByTagName('iframe');
  378. for (let ii = 0; ii < iframes.length; ++ii) {
  379. const iframe = iframes[ii];
  380. if (iframe.contentDocument === window.document) {
  381. return iframe; // eslint-disable-line
  382. }
  383. }
  384. }
  385. /**
  386. * Returns true if window is on screen. The main window is
  387. * always on screen windows in iframes might not be.
  388. * @param {Window} window the window to check.
  389. * @return {boolean} true if window is on screen.
  390. */
  391. function isFrameVisible(window) {
  392. try {
  393. const iframe = getIFrameForWindow(window);
  394. if (!iframe) {
  395. return true;
  396. }
  397. const bounds = iframe.getBoundingClientRect();
  398. const isVisible = bounds.top < window.parent.innerHeight && bounds.bottom >= 0 &&
  399. bounds.left < window.parent.innerWidth && bounds.right >= 0;
  400. return isVisible && isFrameVisible(window.parent);
  401. } catch (e) {
  402. return true; // We got a security error?
  403. }
  404. }
  405. /**
  406. * Returns true if element is on screen.
  407. * @param {HTMLElement} element the element to check.
  408. * @return {boolean} true if element is on screen.
  409. */
  410. function isOnScreen(element) {
  411. let isVisible = true;
  412. if (element) {
  413. const bounds = element.getBoundingClientRect();
  414. isVisible = bounds.top < topWindow.innerHeight && bounds.bottom >= 0;
  415. }
  416. return isVisible && isFrameVisible(topWindow);
  417. }
  418. // Replace requestAnimationFrame.
  419. if (topWindow.requestAnimationFrame) {
  420. topWindow.requestAnimationFrame = (function(oldRAF) {
  421. return function(callback, element) {
  422. const handler = function() {
  423. return oldRAF(isOnScreen(element) ? callback : handler, element);
  424. };
  425. return handler();
  426. };
  427. }(topWindow.requestAnimationFrame));
  428. }
  429. updateCSSIfInIFrame();
  430. function captureJSErrors() {
  431. // capture JavaScript Errors
  432. window.addEventListener('error', function(e) {
  433. const msg = e.message || e.error;
  434. const url = e.filename;
  435. const lineNo = e.lineno || 1;
  436. const colNo = e.colno || 1;
  437. reportJSError(url, lineNo, colNo, msg);
  438. origConsole.error(e.error);
  439. });
  440. }
  441. // adapted from http://stackoverflow.com/a/2401861/128511
  442. function getBrowser() {
  443. const userAgent = navigator.userAgent;
  444. let m = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
  445. if (/trident/i.test(m[1])) {
  446. m = /\brv[ :]+(\d+)/g.exec(userAgent) || [];
  447. return {
  448. name: 'IE',
  449. version: m[1],
  450. };
  451. }
  452. if (m[1] === 'Chrome') {
  453. const temp = userAgent.match(/\b(OPR|Edge)\/(\d+)/);
  454. if (temp) {
  455. return {
  456. name: temp[1].replace('OPR', 'Opera'),
  457. version: temp[2],
  458. };
  459. }
  460. }
  461. m = m[2] ? [m[1], m[2]] : [navigator.appName, navigator.appVersion, '-?'];
  462. const version = userAgent.match(/version\/(\d+)/i);
  463. if (version) {
  464. m.splice(1, 1, version[1]);
  465. }
  466. return {
  467. name: m[0],
  468. version: m[1],
  469. };
  470. }
  471. const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
  472. function installWebGLLessonSetup() {
  473. HTMLCanvasElement.prototype.getContext = (function(oldFn) {
  474. return function() {
  475. const type = arguments[0];
  476. const isWebGL = isWebGLRE.test(type);
  477. if (isWebGL) {
  478. setupLesson(this);
  479. }
  480. const args = [].slice.apply(arguments);
  481. args[1] = Object.assign({
  482. powerPreference: 'low-power',
  483. }, args[1]);
  484. const ctx = oldFn.apply(this, args);
  485. if (!ctx && isWebGL) {
  486. showNeedWebGL(this);
  487. }
  488. return ctx;
  489. };
  490. }(HTMLCanvasElement.prototype.getContext));
  491. }
  492. function installWebGLDebugContextCreator() {
  493. if (!self.webglDebugHelper) {
  494. return;
  495. }
  496. const {
  497. makeDebugContext,
  498. glFunctionArgToString,
  499. glEnumToString,
  500. } = self.webglDebugHelper;
  501. // capture GL errors
  502. HTMLCanvasElement.prototype.getContext = (function(oldFn) {
  503. return function() {
  504. let ctx = oldFn.apply(this, arguments);
  505. // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
  506. // but that might fail if wrapped by debugging extension
  507. if (ctx && ctx.bindTexture) {
  508. ctx = makeDebugContext(ctx, {
  509. maxDrawCalls: 100,
  510. errorFunc: function(err, funcName, args) {
  511. const numArgs = args.length;
  512. const enumedArgs = [].map.call(args, function(arg, ndx) {
  513. let str = glFunctionArgToString(funcName, numArgs, ndx, arg);
  514. // shorten because of long arrays
  515. if (str.length > 200) {
  516. str = str.substring(0, 200) + '...';
  517. }
  518. return str;
  519. });
  520. const errorMsg = `WebGL error ${glEnumToString(err)} in ${funcName}(${enumedArgs.join(', ')})`;
  521. const errorInfo = parseStack((new Error()).stack);
  522. if (errorInfo) {
  523. reportJSError(errorInfo.url, errorInfo.lineNo, errorInfo.colNo, errorMsg);
  524. } else {
  525. console.error(errorMsg) // eslint-disable-line
  526. }
  527. },
  528. });
  529. }
  530. return ctx;
  531. };
  532. }(HTMLCanvasElement.prototype.getContext));
  533. }
  534. installWebGLLessonSetup();
  535. if (isInEditor()) {
  536. setupWorkerSupport();
  537. setupConsole();
  538. captureJSErrors();
  539. if (lessonSettings.glDebug !== false) {
  540. installWebGLDebugContextCreator();
  541. }
  542. }
  543. return {
  544. setupLesson: setupLesson,
  545. showNeedWebGL: showNeedWebGL,
  546. };
  547. }));