lessons-helper.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  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 {WebGLContextCreationAttributes} 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 style = document.createElement('style');
  114. style.innerText = `
  115. .console {
  116. font-family: monospace;
  117. font-size: medium;
  118. max-height: 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. .console .console-line {
  127. white-space: pre-line;
  128. }
  129. .console .log .warn {
  130. color: black;
  131. }
  132. .console .error {
  133. color: red;
  134. }
  135. `;
  136. const parent = document.createElement('div');
  137. parent.className = 'console';
  138. const toggle = document.createElement('div');
  139. let show = false;
  140. Object.assign(toggle.style, {
  141. position: 'absolute',
  142. right: 0,
  143. bottom: 0,
  144. background: '#EEE',
  145. 'font-size': 'smaller',
  146. cursor: 'pointer',
  147. });
  148. toggle.addEventListener('click', showHideConsole);
  149. function showHideConsole() {
  150. show = !show;
  151. toggle.textContent = show ? '☒' : '☐';
  152. parent.style.display = show ? '' : 'none';
  153. }
  154. showHideConsole();
  155. const maxLines = 100;
  156. const lines = [];
  157. let added = false;
  158. function addLine(type, str, prefix) {
  159. const div = document.createElement('div');
  160. div.textContent = (prefix + str) || ' ';
  161. div.className = `console-line ${type}`;
  162. parent.appendChild(div);
  163. lines.push(div);
  164. if (!added) {
  165. added = true;
  166. document.body.appendChild(style);
  167. document.body.appendChild(parent);
  168. document.body.appendChild(toggle);
  169. }
  170. // scrollIntoView only works in Chrome
  171. // In Firefox and Safari scrollIntoView inside an iframe moves
  172. // that element into the view. It should arguably only move that
  173. // element inside the iframe itself, otherwise that's giving
  174. // any random iframe control to bring itself into view against
  175. // the parent's wishes.
  176. //
  177. // note that even if we used a solution (which is to manually set
  178. // scrollTop) there's a UI issue that if the user manually scrolls
  179. // we want to stop scrolling automatically and if they move back
  180. // to the bottom we want to pick up scrolling automatically.
  181. // Kind of a PITA so TBD
  182. //
  183. // div.scrollIntoView();
  184. }
  185. function addLines(type, str, prefix) {
  186. while (lines.length > maxLines) {
  187. const div = lines.shift();
  188. div.parentNode.removeChild(div);
  189. }
  190. addLine(type, str, prefix);
  191. }
  192. function wrapFunc(obj, funcName, prefix) {
  193. const oldFn = obj[funcName];
  194. origConsole[funcName] = oldFn.bind(obj);
  195. return function(...args) {
  196. addLines(funcName, [...args].join(' '), prefix);
  197. oldFn.apply(obj, arguments);
  198. };
  199. }
  200. window.console.log = wrapFunc(window.console, 'log', '');
  201. window.console.warn = wrapFunc(window.console, 'warn', '⚠');
  202. window.console.error = wrapFunc(window.console, 'error', '❌');
  203. }
  204. function reportJSError(url, lineNo, colNo, msg) {
  205. try {
  206. const {origUrl, actualLineNo} = window.parent.getActualLineNumberAndMoveTo(url, lineNo, colNo);
  207. url = origUrl;
  208. lineNo = actualLineNo;
  209. } catch (ex) {
  210. origConsole.error(ex);
  211. }
  212. console.error(url, "line:", lineNo, ":", msg); // eslint-disable-line
  213. }
  214. /**
  215. * @typedef {Object} StackInfo
  216. * @property {string} url Url of line
  217. * @property {number} lineNo line number of error
  218. * @property {number} colNo column number of error
  219. * @property {string} [funcName] name of function
  220. */
  221. /**
  222. * @parameter {string} stack A stack string as in `(new Error()).stack`
  223. * @returns {StackInfo}
  224. */
  225. const parseStack = function() {
  226. const browser = getBrowser();
  227. let lineNdx;
  228. let matcher;
  229. if ((/chrome|opera/i).test(browser.name)) {
  230. lineNdx = 3;
  231. matcher = function(line) {
  232. const m = /at ([^(]+)*\(*(.*?):(\d+):(\d+)/.exec(line);
  233. if (m) {
  234. let userFnName = m[1];
  235. let url = m[2];
  236. const lineNo = parseInt(m[3]);
  237. const colNo = parseInt(m[4]);
  238. if (url === '') {
  239. url = userFnName;
  240. userFnName = '';
  241. }
  242. return {
  243. url: url,
  244. lineNo: lineNo,
  245. colNo: colNo,
  246. funcName: userFnName,
  247. };
  248. }
  249. return undefined;
  250. };
  251. } else if ((/firefox|safari/i).test(browser.name)) {
  252. lineNdx = 2;
  253. matcher = function(line) {
  254. const m = /@(.*?):(\d+):(\d+)/.exec(line);
  255. if (m) {
  256. const url = m[1];
  257. const lineNo = parseInt(m[2]);
  258. const colNo = parseInt(m[3]);
  259. return {
  260. url: url,
  261. lineNo: lineNo,
  262. colNo: colNo,
  263. };
  264. }
  265. return undefined;
  266. };
  267. }
  268. return function stackParser(stack) {
  269. if (matcher) {
  270. try {
  271. const lines = stack.split('\n');
  272. // window.fooLines = lines;
  273. // lines.forEach(function(line, ndx) {
  274. // origConsole.log("#", ndx, line);
  275. // });
  276. return matcher(lines[lineNdx]);
  277. } catch (e) {
  278. // do nothing
  279. }
  280. }
  281. return undefined;
  282. };
  283. }();
  284. function setupWorkerSupport() {
  285. function log(data) {
  286. const {logType, msg} = data;
  287. console[logType]('[Worker]', msg); /* eslint-disable-line no-console */
  288. }
  289. function lostContext(/* data */) {
  290. addContextLostHTML();
  291. }
  292. function jsError(data) {
  293. const {url, lineNo, colNo, msg} = data;
  294. reportJSError(url, lineNo, colNo, msg);
  295. }
  296. function jsErrorWithStack(data) {
  297. const {url, stack, msg} = data;
  298. const errorInfo = parseStack(stack);
  299. if (errorInfo) {
  300. reportJSError(errorInfo.url || url, errorInfo.lineNo, errorInfo.colNo, msg);
  301. } else {
  302. console.error(errorMsg) // eslint-disable-line
  303. }
  304. }
  305. const handlers = {
  306. log,
  307. lostContext,
  308. jsError,
  309. jsErrorWithStack,
  310. };
  311. const OrigWorker = self.Worker;
  312. class WrappedWorker extends OrigWorker {
  313. constructor(url) {
  314. super(url);
  315. let listener;
  316. this.onmessage = function(e) {
  317. if (!e || !e.data || !e.data.type === '___editor___') {
  318. if (listener) {
  319. listener(e);
  320. }
  321. return;
  322. }
  323. e.stopImmediatePropagation();
  324. const data = e.data.data;
  325. const fn = handlers[data.type];
  326. if (!fn) {
  327. origConsole.error('unknown editor msg:', data.type);
  328. } else {
  329. fn(data);
  330. }
  331. return;
  332. };
  333. Object.defineProperty(this, 'onmessage', {
  334. get() {
  335. return listener;
  336. },
  337. set(fn) {
  338. listener = fn;
  339. },
  340. });
  341. }
  342. }
  343. self.Worker = WrappedWorker;
  344. }
  345. function addContextLostHTML() {
  346. const div = document.createElement('div');
  347. div.className = 'contextlost';
  348. div.innerHTML = '<div>Context Lost: Click To Reload</div>';
  349. div.addEventListener('click', function() {
  350. window.location.reload();
  351. });
  352. document.body.appendChild(div);
  353. }
  354. /**
  355. * Gets a WebGL context.
  356. * makes its backing store the size it is displayed.
  357. * @param {HTMLCanvasElement} canvas a canvas element.
  358. * @memberOf module:webgl-utils
  359. */
  360. let setupLesson = function(canvas) {
  361. // only once
  362. setupLesson = function() {};
  363. if (canvas) {
  364. canvas.addEventListener('webglcontextlost', function() {
  365. // the default is to do nothing. Preventing the default
  366. // means allowing context to be restored
  367. // e.preventDefault(); // can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
  368. addContextLostHTML();
  369. });
  370. /* can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
  371. canvas.addEventListener('webglcontextrestored', function() {
  372. // just reload the page. Easiest.
  373. window.location.reload();
  374. });
  375. */
  376. }
  377. if (isInIFrame()) {
  378. updateCSSIfInIFrame();
  379. }
  380. };
  381. // Replace requestAnimationFrame and cancelAnimationFrame with one
  382. // that only executes when the body is visible (we're in an iframe).
  383. // It's frustrating that th browsers don't do this automatically.
  384. // It's half of the point of rAF that it shouldn't execute when
  385. // content is not visible but browsers execute rAF in iframes even
  386. // if they are not visible.
  387. if (topWindow.requestAnimationFrame) {
  388. topWindow.requestAnimationFrame = (function(oldRAF, oldCancelRAF) {
  389. let nextFakeRAFId = 1;
  390. const fakeRAFIdToCallbackMap = new Map();
  391. let rafRequestId;
  392. let isBodyOnScreen;
  393. function rAFHandler(time) {
  394. rafRequestId = undefined;
  395. const ids = [...fakeRAFIdToCallbackMap.keys()]; // WTF! Map.keys() iterates over live keys!
  396. for (const id of ids) {
  397. const callback = fakeRAFIdToCallbackMap.get(id);
  398. fakeRAFIdToCallbackMap.delete(id);
  399. if (callback) {
  400. callback(time);
  401. }
  402. }
  403. }
  404. function startRAFIfIntersectingAndNeeded() {
  405. if (!rafRequestId && isBodyOnScreen && fakeRAFIdToCallbackMap.size > 0) {
  406. rafRequestId = oldRAF(rAFHandler);
  407. }
  408. }
  409. function stopRAF() {
  410. if (rafRequestId) {
  411. oldCancelRAF(rafRequestId);
  412. rafRequestId = undefined;
  413. }
  414. }
  415. function initIntersectionObserver() {
  416. const intersectionObserver = new IntersectionObserver((entries) => {
  417. entries.forEach(entry => {
  418. isBodyOnScreen = entry.isIntersecting;
  419. });
  420. if (isBodyOnScreen) {
  421. startRAFIfIntersectingAndNeeded();
  422. } else {
  423. stopRAF();
  424. }
  425. });
  426. intersectionObserver.observe(document.body);
  427. }
  428. function betterRAF(callback) {
  429. const fakeRAFId = nextFakeRAFId++;
  430. fakeRAFIdToCallbackMap.set(fakeRAFId, callback);
  431. startRAFIfIntersectingAndNeeded();
  432. return fakeRAFId;
  433. }
  434. function betterCancelRAF(id) {
  435. fakeRAFIdToCallbackMap.delete(id);
  436. }
  437. topWindow.cancelAnimationFrame = betterCancelRAF;
  438. return function(callback) {
  439. // we need to lazy init this because this code gets parsed
  440. // before body exists. We could fix it by moving lesson-helper.js
  441. // after <body> but that would require changing 100s of examples
  442. initIntersectionObserver();
  443. topWindow.requestAnimationFrame = betterRAF;
  444. return betterRAF(callback);
  445. };
  446. }(topWindow.requestAnimationFrame, topWindow.cancelAnimationFrame));
  447. }
  448. updateCSSIfInIFrame();
  449. function captureJSErrors() {
  450. // capture JavaScript Errors
  451. window.addEventListener('error', function(e) {
  452. const msg = e.message || e.error;
  453. const url = e.filename;
  454. const lineNo = e.lineno || 1;
  455. const colNo = e.colno || 1;
  456. reportJSError(url, lineNo, colNo, msg);
  457. origConsole.error(e.error);
  458. });
  459. }
  460. // adapted from http://stackoverflow.com/a/2401861/128511
  461. function getBrowser() {
  462. const userAgent = navigator.userAgent;
  463. let m = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
  464. if (/trident/i.test(m[1])) {
  465. m = /\brv[ :]+(\d+)/g.exec(userAgent) || [];
  466. return {
  467. name: 'IE',
  468. version: m[1],
  469. };
  470. }
  471. if (m[1] === 'Chrome') {
  472. const temp = userAgent.match(/\b(OPR|Edge)\/(\d+)/);
  473. if (temp) {
  474. return {
  475. name: temp[1].replace('OPR', 'Opera'),
  476. version: temp[2],
  477. };
  478. }
  479. }
  480. m = m[2] ? [m[1], m[2]] : [navigator.appName, navigator.appVersion, '-?'];
  481. const version = userAgent.match(/version\/(\d+)/i);
  482. if (version) {
  483. m.splice(1, 1, version[1]);
  484. }
  485. return {
  486. name: m[0],
  487. version: m[1],
  488. };
  489. }
  490. const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
  491. function installWebGLLessonSetup() {
  492. HTMLCanvasElement.prototype.getContext = (function(oldFn) {
  493. return function() {
  494. const type = arguments[0];
  495. const isWebGL = isWebGLRE.test(type);
  496. if (isWebGL) {
  497. setupLesson(this);
  498. }
  499. const args = [].slice.apply(arguments);
  500. args[1] = Object.assign({
  501. powerPreference: 'low-power',
  502. }, args[1]);
  503. const ctx = oldFn.apply(this, args);
  504. if (!ctx && isWebGL) {
  505. showNeedWebGL(this);
  506. }
  507. return ctx;
  508. };
  509. }(HTMLCanvasElement.prototype.getContext));
  510. }
  511. function installWebGLDebugContextCreator() {
  512. if (!self.webglDebugHelper) {
  513. return;
  514. }
  515. const {
  516. makeDebugContext,
  517. glFunctionArgToString,
  518. glEnumToString,
  519. } = self.webglDebugHelper;
  520. // capture GL errors
  521. HTMLCanvasElement.prototype.getContext = (function(oldFn) {
  522. return function() {
  523. let ctx = oldFn.apply(this, arguments);
  524. // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
  525. // but that might fail if wrapped by debugging extension
  526. if (ctx && ctx.bindTexture) {
  527. ctx = makeDebugContext(ctx, {
  528. maxDrawCalls: 100,
  529. errorFunc: function(err, funcName, args) {
  530. const numArgs = args.length;
  531. const enumedArgs = [].map.call(args, function(arg, ndx) {
  532. let str = glFunctionArgToString(funcName, numArgs, ndx, arg);
  533. // shorten because of long arrays
  534. if (str.length > 200) {
  535. str = str.substring(0, 200) + '...';
  536. }
  537. return str;
  538. });
  539. const errorMsg = `WebGL error ${glEnumToString(err)} in ${funcName}(${enumedArgs.join(', ')})`;
  540. const errorInfo = parseStack((new Error()).stack);
  541. if (errorInfo) {
  542. reportJSError(errorInfo.url, errorInfo.lineNo, errorInfo.colNo, errorMsg);
  543. } else {
  544. console.error(errorMsg) // eslint-disable-line
  545. }
  546. },
  547. });
  548. }
  549. return ctx;
  550. };
  551. }(HTMLCanvasElement.prototype.getContext));
  552. }
  553. installWebGLLessonSetup();
  554. if (isInEditor()) {
  555. setupWorkerSupport();
  556. setupConsole();
  557. captureJSErrors();
  558. if (lessonSettings.glDebug !== false) {
  559. installWebGLDebugContextCreator();
  560. }
  561. }
  562. return {
  563. setupLesson: setupLesson,
  564. showNeedWebGL: showNeedWebGL,
  565. };
  566. }));