123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641 |
- /*
- * 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,
- };
- }));
|