/* * 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 * 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 = `
It doesn't appear your browser supports WebGL.
Click here for more information.
`; 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 = '
Context Lost: Click To Reload
'; 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 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, }; } ) );