lessons-helper.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  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. *
  49. * @param {Window?} w - window to check.
  50. * @return {boolean} True of we are in an iframe
  51. */
  52. function isInIFrame( w ) {
  53. w = w || topWindow;
  54. return w !== w.top;
  55. }
  56. function updateCSSIfInIFrame() {
  57. if ( isInIFrame() ) {
  58. try {
  59. document.getElementsByTagName( 'html' )[ 0 ].className = 'iframe';
  60. } catch ( e ) { } // eslint-disable-line no-unused-vars
  61. try {
  62. document.body.className = 'iframe';
  63. } catch ( e ) { } // eslint-disable-line no-unused-vars
  64. }
  65. }
  66. function isInEditor() {
  67. return window.location.href.substring( 0, 4 ) === 'blob';
  68. }
  69. /**
  70. * Creates a webgl context. If creation fails it will
  71. * change the contents of the container of the <canvas>
  72. * tag to an error message with the correct links for WebGL.
  73. * @param {HTMLCanvasElement} canvas. The canvas element to
  74. * create a context from.
  75. * @param {WebGLContextCreationAttributes} opt_attribs Any
  76. * creation attributes you want to pass in.
  77. * @return {WebGLRenderingContext} The created context.
  78. * @memberOf module:webgl-utils
  79. */
  80. function showNeedWebGL( canvas ) {
  81. const doc = canvas.ownerDocument;
  82. if ( doc ) {
  83. const temp = doc.createElement( 'div' );
  84. temp.innerHTML = `
  85. <div style="
  86. position: absolute;
  87. left: 0;
  88. top: 0;
  89. background-color: #DEF;
  90. width: 100%;
  91. height: 100%;
  92. display: flex;
  93. flex-flow: column;
  94. justify-content: center;
  95. align-content: center;
  96. align-items: center;
  97. ">
  98. <div style="text-align: center;">
  99. It doesn't appear your browser supports WebGL.<br/>
  100. <a href="http://get.webgl.org" target="_blank">Click here for more information.</a>
  101. </div>
  102. </div>
  103. `;
  104. const div = temp.querySelector( 'div' );
  105. doc.body.appendChild( div );
  106. }
  107. }
  108. const origConsole = {};
  109. function setupConsole() {
  110. const style = document.createElement( 'style' );
  111. style.innerText = `
  112. .console {
  113. font-family: monospace;
  114. font-size: medium;
  115. max-height: 50%;
  116. position: fixed;
  117. bottom: 0;
  118. left: 0;
  119. width: 100%;
  120. overflow: auto;
  121. background: rgba(221, 221, 221, 0.9);
  122. }
  123. .console .console-line {
  124. white-space: pre-line;
  125. }
  126. .console .log .warn {
  127. color: black;
  128. }
  129. .console .error {
  130. color: red;
  131. }
  132. `;
  133. const parent = document.createElement( 'div' );
  134. parent.className = 'console';
  135. const toggle = document.createElement( 'div' );
  136. let show = false;
  137. Object.assign( toggle.style, {
  138. position: 'absolute',
  139. right: 0,
  140. bottom: 0,
  141. background: '#EEE',
  142. 'font-size': 'smaller',
  143. cursor: 'pointer',
  144. } );
  145. toggle.addEventListener( 'click', showHideConsole );
  146. function showHideConsole() {
  147. show = ! show;
  148. toggle.textContent = show ? '☒' : '☐';
  149. parent.style.display = show ? '' : 'none';
  150. }
  151. showHideConsole();
  152. const maxLines = 100;
  153. const lines = [];
  154. let added = false;
  155. function addLine( type, str, prefix ) {
  156. const div = document.createElement( 'div' );
  157. div.textContent = ( prefix + str ) || ' ';
  158. div.className = `console-line ${type}`;
  159. parent.appendChild( div );
  160. lines.push( div );
  161. if ( ! added ) {
  162. added = true;
  163. document.body.appendChild( style );
  164. document.body.appendChild( parent );
  165. document.body.appendChild( toggle );
  166. }
  167. // scrollIntoView only works in Chrome
  168. // In Firefox and Safari scrollIntoView inside an iframe moves
  169. // that element into the view. It should arguably only move that
  170. // element inside the iframe itself, otherwise that's giving
  171. // any random iframe control to bring itself into view against
  172. // the parent's wishes.
  173. //
  174. // note that even if we used a solution (which is to manually set
  175. // scrollTop) there's a UI issue that if the user manually scrolls
  176. // we want to stop scrolling automatically and if they move back
  177. // to the bottom we want to pick up scrolling automatically.
  178. // Kind of a PITA so TBD
  179. //
  180. // div.scrollIntoView();
  181. }
  182. function addLines( type, str, prefix ) {
  183. while ( lines.length > maxLines ) {
  184. const div = lines.shift();
  185. div.parentNode.removeChild( div );
  186. }
  187. addLine( type, str, prefix );
  188. }
  189. const threePukeRE = /WebGLRenderer.*?extension not supported/;
  190. function wrapFunc( obj, funcName, prefix ) {
  191. const oldFn = obj[ funcName ];
  192. origConsole[ funcName ] = oldFn.bind( obj );
  193. return function ( ...args ) {
  194. // three.js pukes all over so filter here
  195. const src = [ ...args ].join( ' ' );
  196. if ( ! threePukeRE.test( src ) ) {
  197. addLines( funcName, src, prefix );
  198. }
  199. oldFn.apply( obj, arguments );
  200. };
  201. }
  202. window.console.log = wrapFunc( window.console, 'log', '' );
  203. window.console.warn = wrapFunc( window.console, 'warn', '⚠' );
  204. window.console.error = wrapFunc( window.console, 'error', '❌' );
  205. }
  206. function reportJSError( url, lineNo, colNo, msg ) {
  207. try {
  208. const { origUrl, actualLineNo } = window.parent.getActualLineNumberAndMoveTo( url, lineNo, colNo );
  209. url = origUrl;
  210. lineNo = actualLineNo;
  211. } catch ( ex ) {
  212. origConsole.error( ex );
  213. }
  214. console.error(url, "line:", lineNo, ":", msg); // eslint-disable-line
  215. }
  216. /**
  217. * @typedef {Object} StackInfo
  218. * @property {string} url Url of line
  219. * @property {number} lineNo line number of error
  220. * @property {number} colNo column number of error
  221. * @property {string} [funcName] name of function
  222. */
  223. /**
  224. * @parameter {string} stack A stack string as in `(new Error()).stack`
  225. * @returns {StackInfo}
  226. */
  227. const parseStack = function () {
  228. const browser = getBrowser();
  229. let lineNdx;
  230. let matcher;
  231. if ( ( /chrome|opera/i ).test( browser.name ) ) {
  232. lineNdx = 3;
  233. matcher = function ( line ) {
  234. const m = /at ([^(]*?)\(*(.*?):(\d+):(\d+)/.exec( line );
  235. if ( m ) {
  236. let userFnName = m[ 1 ];
  237. let url = m[ 2 ];
  238. const lineNo = parseInt( m[ 3 ] );
  239. const colNo = parseInt( m[ 4 ] );
  240. if ( url === '' ) {
  241. url = userFnName;
  242. userFnName = '';
  243. }
  244. return {
  245. url: url,
  246. lineNo: lineNo,
  247. colNo: colNo,
  248. funcName: userFnName,
  249. };
  250. }
  251. return undefined;
  252. };
  253. } else if ( ( /firefox|safari/i ).test( browser.name ) ) {
  254. lineNdx = 2;
  255. matcher = function ( line ) {
  256. const m = /@(.*?):(\d+):(\d+)/.exec( line );
  257. if ( m ) {
  258. const url = m[ 1 ];
  259. const lineNo = parseInt( m[ 2 ] );
  260. const colNo = parseInt( m[ 3 ] );
  261. return {
  262. url: url,
  263. lineNo: lineNo,
  264. colNo: colNo,
  265. };
  266. }
  267. return undefined;
  268. };
  269. }
  270. return function stackParser( stack ) {
  271. if ( matcher ) {
  272. try {
  273. const lines = stack.split( '\n' );
  274. // window.fooLines = lines;
  275. // lines.forEach(function(line, ndx) {
  276. // origConsole.log("#", ndx, line);
  277. // });
  278. return matcher( lines[ lineNdx ] );
  279. } catch ( e ) {} // eslint-disable-line no-unused-vars
  280. }
  281. return undefined;
  282. };
  283. }();
  284. function setupWorkerSupport() {
  285. function log( data ) {
  286. const { logType, msg } = data;
  287. console[ logType ]( '[Worker]', msg );
  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, ...args ) {
  314. super( url, ...args );
  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 ( typeof fn !== 'function' ) {
  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 canvasesToTimeoutMap = new Map();
  491. const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
  492. const isWebGL2RE = /^webgl2$/i;
  493. function installWebGLLessonSetup() {
  494. HTMLCanvasElement.prototype.getContext = ( function ( oldFn ) {
  495. return function () {
  496. const timeoutId = canvasesToTimeoutMap.get( this );
  497. if ( timeoutId ) {
  498. clearTimeout( timeoutId );
  499. }
  500. const type = arguments[ 0 ];
  501. const isWebGL1or2 = isWebGLRE.test( type );
  502. const isWebGL2 = isWebGL2RE.test( type );
  503. if ( isWebGL1or2 ) {
  504. setupLesson( this );
  505. }
  506. const args = [].slice.apply( arguments );
  507. args[ 1 ] = {
  508. powerPreference: 'low-power',
  509. ...args[ 1 ],
  510. };
  511. const ctx = oldFn.apply( this, args );
  512. if ( ! ctx ) {
  513. if ( isWebGL2 ) {
  514. // three tries webgl2 then webgl1
  515. // so wait 1/2 a second before showing the failure
  516. // message. If we get success on the same canvas
  517. // we'll cancel this.
  518. canvasesToTimeoutMap.set( this, setTimeout( () => {
  519. canvasesToTimeoutMap.delete( this );
  520. showNeedWebGL( this );
  521. }, 500 ) );
  522. } else {
  523. showNeedWebGL( this );
  524. }
  525. }
  526. return ctx;
  527. };
  528. }( HTMLCanvasElement.prototype.getContext ) );
  529. }
  530. function installWebGLDebugContextCreator() {
  531. if ( ! self.webglDebugHelper ) {
  532. return;
  533. }
  534. const {
  535. makeDebugContext,
  536. glFunctionArgToString,
  537. glEnumToString,
  538. } = self.webglDebugHelper;
  539. // capture GL errors
  540. HTMLCanvasElement.prototype.getContext = ( function ( oldFn ) {
  541. return function () {
  542. let ctx = oldFn.apply( this, arguments );
  543. // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
  544. // but that might fail if wrapped by debugging extension
  545. if ( ctx && ctx.bindTexture ) {
  546. ctx = makeDebugContext( ctx, {
  547. maxDrawCalls: 100,
  548. errorFunc: function ( err, funcName, args ) {
  549. const numArgs = args.length;
  550. const enumedArgs = [].map.call( args, function ( arg, ndx ) {
  551. let str = glFunctionArgToString( funcName, numArgs, ndx, arg );
  552. // shorten because of long arrays
  553. if ( str.length > 200 ) {
  554. str = str.substring( 0, 200 ) + '...';
  555. }
  556. return str;
  557. } );
  558. const errorMsg = `WebGL error ${glEnumToString( err )} in ${funcName}(${enumedArgs.join( ', ' )})`;
  559. const errorInfo = parseStack( ( new Error() ).stack );
  560. if ( errorInfo ) {
  561. reportJSError( errorInfo.url, errorInfo.lineNo, errorInfo.colNo, errorMsg );
  562. } else {
  563. console.error(errorMsg) // eslint-disable-line
  564. }
  565. },
  566. } );
  567. }
  568. return ctx;
  569. };
  570. }( HTMLCanvasElement.prototype.getContext ) );
  571. }
  572. installWebGLLessonSetup();
  573. if ( isInEditor() ) {
  574. setupWorkerSupport();
  575. setupConsole();
  576. captureJSErrors();
  577. if ( lessonSettings.glDebug !== false ) {
  578. installWebGLDebugContextCreator();
  579. }
  580. }
  581. return {
  582. setupLesson: setupLesson,
  583. showNeedWebGL: showNeedWebGL,
  584. };
  585. } ) );
粤ICP备19079148号