123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860 |
- /*
- * Copyright 2012, Gregg Tavares.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following disclaimer
- * in the documentation and/or other materials provided with the
- * distribution.
- * * Neither the name of Gregg Tavares. nor the names of his
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
- /* global define */
- (function(root, factory) { // eslint-disable-line
- if ( typeof define === 'function' && define.amd ) {
- // AMD. Register as an anonymous module.
- define( [], function () {
- return factory.call( root );
- } );
- } else {
- // Browser globals
- root.lessonsHelper = factory.call( root );
- }
- }( this, function () {
- 'use strict'; // eslint-disable-line
- const lessonSettings = window.lessonSettings || {};
- const topWindow = this;
- /**
- * Check if the page is embedded.
- * @param {Window?) w window to check
- * @return {boolean} True of we are in an iframe
- */
- function isInIFrame( w ) {
- w = w || topWindow;
- return w !== w.top;
- }
- function updateCSSIfInIFrame() {
- if ( isInIFrame() ) {
- try {
- document.getElementsByTagName( 'html' )[ 0 ].className = 'iframe';
- } catch ( e ) {
- // eslint-disable-line
- }
- try {
- document.body.className = 'iframe';
- } catch ( e ) {
- // eslint-disable-line
- }
- }
- }
- function isInEditor() {
- return window.location.href.substring( 0, 4 ) === 'blob';
- }
- /**
- * Creates a webgl context. If creation fails it will
- * change the contents of the container of the <canvas>
- * tag to an error message with the correct links for WebGL.
- * @param {HTMLCanvasElement} canvas. The canvas element to
- * create a context from.
- * @param {WebGLContextCreationAttributes} opt_attribs Any
- * creation attributes you want to pass in.
- * @return {WebGLRenderingContext} The created context.
- * @memberOf module:webgl-utils
- */
- function showNeedWebGL( canvas ) {
- const doc = canvas.ownerDocument;
- if ( doc ) {
- const temp = doc.createElement( 'div' );
- temp.innerHTML = `
- <div style="
- position: absolute;
- left: 0;
- top: 0;
- background-color: #DEF;
- width: 100%;
- height: 100%;
- display: flex;
- flex-flow: column;
- justify-content: center;
- align-content: center;
- align-items: center;
- ">
- <div style="text-align: center;">
- It doesn't appear your browser supports WebGL.<br/>
- <a href="http://get.webgl.org" target="_blank">Click here for more information.</a>
- </div>
- </div>
- `;
- const div = temp.querySelector( 'div' );
- doc.body.appendChild( div );
- }
- }
- const origConsole = {};
- function setupConsole() {
- const style = document.createElement( 'style' );
- style.innerText = `
- .console {
- font-family: monospace;
- font-size: medium;
- max-height: 50%;
- position: fixed;
- bottom: 0;
- left: 0;
- width: 100%;
- overflow: auto;
- background: rgba(221, 221, 221, 0.9);
- }
- .console .console-line {
- white-space: pre-line;
- }
- .console .log .warn {
- color: black;
- }
- .console .error {
- color: red;
- }
- `;
- const parent = document.createElement( 'div' );
- parent.className = 'console';
- const toggle = document.createElement( 'div' );
- let show = false;
- Object.assign( toggle.style, {
- position: 'absolute',
- right: 0,
- bottom: 0,
- background: '#EEE',
- 'font-size': 'smaller',
- cursor: 'pointer',
- } );
- toggle.addEventListener( 'click', showHideConsole );
- function showHideConsole() {
- show = ! show;
- toggle.textContent = show ? '☒' : '☐';
- parent.style.display = show ? '' : 'none';
- }
- showHideConsole();
- const maxLines = 100;
- const lines = [];
- let added = false;
- function addLine( type, str, prefix ) {
- const div = document.createElement( 'div' );
- div.textContent = ( prefix + str ) || ' ';
- div.className = `console-line ${type}`;
- parent.appendChild( div );
- lines.push( div );
- if ( ! added ) {
- added = true;
- document.body.appendChild( style );
- document.body.appendChild( parent );
- document.body.appendChild( toggle );
- }
- // scrollIntoView only works in Chrome
- // In Firefox and Safari scrollIntoView inside an iframe moves
- // that element into the view. It should arguably only move that
- // element inside the iframe itself, otherwise that's giving
- // any random iframe control to bring itself into view against
- // the parent's wishes.
- //
- // note that even if we used a solution (which is to manually set
- // scrollTop) there's a UI issue that if the user manually scrolls
- // we want to stop scrolling automatically and if they move back
- // to the bottom we want to pick up scrolling automatically.
- // Kind of a PITA so TBD
- //
- // div.scrollIntoView();
- }
- function addLines( type, str, prefix ) {
- while ( lines.length > maxLines ) {
- const div = lines.shift();
- div.parentNode.removeChild( div );
- }
- addLine( type, str, prefix );
- }
- const threePukeRE = /WebGLRenderer.*?extension not supported/;
- function wrapFunc( obj, funcName, prefix ) {
- const oldFn = obj[ funcName ];
- origConsole[ funcName ] = oldFn.bind( obj );
- return function ( ...args ) {
- // three.js pukes all over so filter here
- const src = [ ...args ].join( ' ' );
- if ( ! threePukeRE.test( src ) ) {
- addLines( funcName, src, prefix );
- }
- oldFn.apply( obj, arguments );
- };
- }
- window.console.log = wrapFunc( window.console, 'log', '' );
- window.console.warn = wrapFunc( window.console, 'warn', '⚠' );
- window.console.error = wrapFunc( window.console, 'error', '❌' );
- }
- function reportJSError( url, lineNo, colNo, msg ) {
- try {
- const { origUrl, actualLineNo } = window.parent.getActualLineNumberAndMoveTo( url, lineNo, colNo );
- url = origUrl;
- lineNo = actualLineNo;
- } catch ( ex ) {
- origConsole.error( ex );
- }
- console.error(url, "line:", lineNo, ":", msg); // eslint-disable-line
- }
- /**
- * @typedef {Object} StackInfo
- * @property {string} url Url of line
- * @property {number} lineNo line number of error
- * @property {number} colNo column number of error
- * @property {string} [funcName] name of function
- */
- /**
- * @parameter {string} stack A stack string as in `(new Error()).stack`
- * @returns {StackInfo}
- */
- const parseStack = function () {
- const browser = getBrowser();
- let lineNdx;
- let matcher;
- if ( ( /chrome|opera/i ).test( browser.name ) ) {
- lineNdx = 3;
- matcher = function ( line ) {
- const m = /at ([^(]*?)\(*(.*?):(\d+):(\d+)/.exec( line );
- if ( m ) {
- let userFnName = m[ 1 ];
- let url = m[ 2 ];
- const lineNo = parseInt( m[ 3 ] );
- const colNo = parseInt( m[ 4 ] );
- if ( url === '' ) {
- url = userFnName;
- userFnName = '';
- }
- return {
- url: url,
- lineNo: lineNo,
- colNo: colNo,
- funcName: userFnName,
- };
- }
- return undefined;
- };
- } else if ( ( /firefox|safari/i ).test( browser.name ) ) {
- lineNdx = 2;
- matcher = function ( line ) {
- const m = /@(.*?):(\d+):(\d+)/.exec( line );
- if ( m ) {
- const url = m[ 1 ];
- const lineNo = parseInt( m[ 2 ] );
- const colNo = parseInt( m[ 3 ] );
- return {
- url: url,
- lineNo: lineNo,
- colNo: colNo,
- };
- }
- return undefined;
- };
- }
- return function stackParser( stack ) {
- if ( matcher ) {
- try {
- const lines = stack.split( '\n' );
- // window.fooLines = lines;
- // lines.forEach(function(line, ndx) {
- // origConsole.log("#", ndx, line);
- // });
- return matcher( lines[ lineNdx ] );
- } catch ( e ) {
- // do nothing
- }
- }
- return undefined;
- };
- }();
- function setupWorkerSupport() {
- function log( data ) {
- const { logType, msg } = data;
- console[ logType ]( '[Worker]', msg ); /* eslint-disable-line no-console */
- }
- function lostContext( /* data */ ) {
- addContextLostHTML();
- }
- function jsError( data ) {
- const { url, lineNo, colNo, msg } = data;
- reportJSError( url, lineNo, colNo, msg );
- }
- function jsErrorWithStack( data ) {
- const { url, stack, msg } = data;
- const errorInfo = parseStack( stack );
- if ( errorInfo ) {
- reportJSError( errorInfo.url || url, errorInfo.lineNo, errorInfo.colNo, msg );
- } else {
- console.error(errorMsg) // eslint-disable-line
- }
- }
- const handlers = {
- log,
- lostContext,
- jsError,
- jsErrorWithStack,
- };
- const OrigWorker = self.Worker;
- class WrappedWorker extends OrigWorker {
- constructor( url, ...args ) {
- super( url, ...args );
- let listener;
- this.onmessage = function ( e ) {
- if ( ! e || ! e.data || e.data.type !== '___editor___' ) {
- if ( listener ) {
- listener( e );
- }
- return;
- }
- e.stopImmediatePropagation();
- const data = e.data.data;
- const fn = handlers[ data.type ];
- if ( typeof fn !== 'function' ) {
- origConsole.error( 'unknown editor msg:', data.type );
- } else {
- fn( data );
- }
- return;
- };
- Object.defineProperty( this, 'onmessage', {
- get() {
- return listener;
- },
- set( fn ) {
- listener = fn;
- },
- } );
- }
- }
- self.Worker = WrappedWorker;
- }
- function addContextLostHTML() {
- const div = document.createElement( 'div' );
- div.className = 'contextlost';
- div.innerHTML = '<div>Context Lost: Click To Reload</div>';
- div.addEventListener( 'click', function () {
- window.location.reload();
- } );
- document.body.appendChild( div );
- }
- /**
- * Gets a WebGL context.
- * makes its backing store the size it is displayed.
- * @param {HTMLCanvasElement} canvas a canvas element.
- * @memberOf module:webgl-utils
- */
- let setupLesson = function ( canvas ) {
- // only once
- setupLesson = function () {};
- if ( canvas ) {
- canvas.addEventListener( 'webglcontextlost', function () {
- // the default is to do nothing. Preventing the default
- // means allowing context to be restored
- // e.preventDefault(); // can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
- addContextLostHTML();
- } );
- /* can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
- canvas.addEventListener('webglcontextrestored', function() {
- // just reload the page. Easiest.
- window.location.reload();
- });
- */
- }
- if ( isInIFrame() ) {
- updateCSSIfInIFrame();
- }
- };
- // Replace requestAnimationFrame and cancelAnimationFrame with one
- // that only executes when the body is visible (we're in an iframe).
- // It's frustrating that th browsers don't do this automatically.
- // It's half of the point of rAF that it shouldn't execute when
- // content is not visible but browsers execute rAF in iframes even
- // if they are not visible.
- if ( topWindow.requestAnimationFrame ) {
- topWindow.requestAnimationFrame = ( function ( oldRAF, oldCancelRAF ) {
- let nextFakeRAFId = 1;
- const fakeRAFIdToCallbackMap = new Map();
- let rafRequestId;
- let isBodyOnScreen;
- function rAFHandler( time ) {
- rafRequestId = undefined;
- const ids = [ ...fakeRAFIdToCallbackMap.keys() ]; // WTF! Map.keys() iterates over live keys!
- for ( const id of ids ) {
- const callback = fakeRAFIdToCallbackMap.get( id );
- fakeRAFIdToCallbackMap.delete( id );
- if ( callback ) {
- callback( time );
- }
- }
- }
- function startRAFIfIntersectingAndNeeded() {
- if ( ! rafRequestId && isBodyOnScreen && fakeRAFIdToCallbackMap.size > 0 ) {
- rafRequestId = oldRAF( rAFHandler );
- }
- }
- function stopRAF() {
- if ( rafRequestId ) {
- oldCancelRAF( rafRequestId );
- rafRequestId = undefined;
- }
- }
- function initIntersectionObserver() {
- const intersectionObserver = new IntersectionObserver( ( entries ) => {
- entries.forEach( entry => {
- isBodyOnScreen = entry.isIntersecting;
- } );
- if ( isBodyOnScreen ) {
- startRAFIfIntersectingAndNeeded();
- } else {
- stopRAF();
- }
- } );
- intersectionObserver.observe( document.body );
- }
- function betterRAF( callback ) {
- const fakeRAFId = nextFakeRAFId ++;
- fakeRAFIdToCallbackMap.set( fakeRAFId, callback );
- startRAFIfIntersectingAndNeeded();
- return fakeRAFId;
- }
- function betterCancelRAF( id ) {
- fakeRAFIdToCallbackMap.delete( id );
- }
- topWindow.cancelAnimationFrame = betterCancelRAF;
- return function ( callback ) {
- // we need to lazy init this because this code gets parsed
- // before body exists. We could fix it by moving lesson-helper.js
- // after <body> but that would require changing 100s of examples
- initIntersectionObserver();
- topWindow.requestAnimationFrame = betterRAF;
- return betterRAF( callback );
- };
- }( topWindow.requestAnimationFrame, topWindow.cancelAnimationFrame ) );
- }
- updateCSSIfInIFrame();
- function captureJSErrors() {
- // capture JavaScript Errors
- window.addEventListener( 'error', function ( e ) {
- const msg = e.message || e.error;
- const url = e.filename;
- const lineNo = e.lineno || 1;
- const colNo = e.colno || 1;
- reportJSError( url, lineNo, colNo, msg );
- origConsole.error( e.error );
- } );
- }
- // adapted from http://stackoverflow.com/a/2401861/128511
- function getBrowser() {
- const userAgent = navigator.userAgent;
- let m = userAgent.match( /(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i ) || [];
- if ( /trident/i.test( m[ 1 ] ) ) {
- m = /\brv[ :]+(\d+)/g.exec( userAgent ) || [];
- return {
- name: 'IE',
- version: m[ 1 ],
- };
- }
- if ( m[ 1 ] === 'Chrome' ) {
- const temp = userAgent.match( /\b(OPR|Edge)\/(\d+)/ );
- if ( temp ) {
- return {
- name: temp[ 1 ].replace( 'OPR', 'Opera' ),
- version: temp[ 2 ],
- };
- }
- }
- m = m[ 2 ] ? [ m[ 1 ], m[ 2 ] ] : [ navigator.appName, navigator.appVersion, '-?' ];
- const version = userAgent.match( /version\/(\d+)/i );
- if ( version ) {
- m.splice( 1, 1, version[ 1 ] );
- }
- return {
- name: m[ 0 ],
- version: m[ 1 ],
- };
- }
- const canvasesToTimeoutMap = new Map();
- const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
- const isWebGL2RE = /^webgl2$/i;
- function installWebGLLessonSetup() {
- HTMLCanvasElement.prototype.getContext = ( function ( oldFn ) {
- return function () {
- const timeoutId = canvasesToTimeoutMap.get( this );
- if ( timeoutId ) {
- clearTimeout( timeoutId );
- }
- const type = arguments[ 0 ];
- const isWebGL1or2 = isWebGLRE.test( type );
- const isWebGL2 = isWebGL2RE.test( type );
- if ( isWebGL1or2 ) {
- setupLesson( this );
- }
- const args = [].slice.apply( arguments );
- args[ 1 ] = {
- powerPreference: 'low-power',
- ...args[ 1 ],
- };
- const ctx = oldFn.apply( this, args );
- if ( ! ctx ) {
- if ( isWebGL2 ) {
- // three tries webgl2 then webgl1
- // so wait 1/2 a second before showing the failure
- // message. If we get success on the same canvas
- // we'll cancel this.
- canvasesToTimeoutMap.set( this, setTimeout( () => {
- canvasesToTimeoutMap.delete( this );
- showNeedWebGL( this );
- }, 500 ) );
- } else {
- showNeedWebGL( this );
- }
- }
- return ctx;
- };
- }( HTMLCanvasElement.prototype.getContext ) );
- }
- function installWebGLDebugContextCreator() {
- if ( ! self.webglDebugHelper ) {
- return;
- }
- const {
- makeDebugContext,
- glFunctionArgToString,
- glEnumToString,
- } = self.webglDebugHelper;
- // capture GL errors
- HTMLCanvasElement.prototype.getContext = ( function ( oldFn ) {
- return function () {
- let ctx = oldFn.apply( this, arguments );
- // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
- // but that might fail if wrapped by debugging extension
- if ( ctx && ctx.bindTexture ) {
- ctx = makeDebugContext( ctx, {
- maxDrawCalls: 100,
- errorFunc: function ( err, funcName, args ) {
- const numArgs = args.length;
- const enumedArgs = [].map.call( args, function ( arg, ndx ) {
- let str = glFunctionArgToString( funcName, numArgs, ndx, arg );
- // shorten because of long arrays
- if ( str.length > 200 ) {
- str = str.substring( 0, 200 ) + '...';
- }
- return str;
- } );
- const errorMsg = `WebGL error ${glEnumToString( err )} in ${funcName}(${enumedArgs.join( ', ' )})`;
- const errorInfo = parseStack( ( new Error() ).stack );
- if ( errorInfo ) {
- reportJSError( errorInfo.url, errorInfo.lineNo, errorInfo.colNo, errorMsg );
- } else {
- console.error(errorMsg) // eslint-disable-line
- }
- },
- } );
- }
- return ctx;
- };
- }( HTMLCanvasElement.prototype.getContext ) );
- }
- installWebGLLessonSetup();
- if ( isInEditor() ) {
- setupWorkerSupport();
- setupConsole();
- captureJSErrors();
- if ( lessonSettings.glDebug !== false ) {
- installWebGLDebugContextCreator();
- }
- }
- return {
- setupLesson: setupLesson,
- showNeedWebGL: showNeedWebGL,
- };
- } ) );
|