/*
* Copyright 2021, GFXFundamentals.
* 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 GFXFundamentals. 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 {
if (document.body) {
document.body.className = 'iframe';
}
} catch (e) {
// eslint-disable-line
}
}
}
function isInEditor() {
return window.location.href.substring(0, 4) === 'blob';
}
/**
* Changes canvas to message about needing webgl
* @param {HTMLCanvasElement} canvas. The canvas element to
* create a context from.
* @memberOf module:webgl-utils
*/
function showNeedWebGL(canvas, type) {
const doc = canvas.ownerDocument;
if (doc) {
const isWebGL2 = type === 'webgl2';
const temp = doc.createElement('div');
temp.innerHTML = `
`;
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);
}
function wrapFunc(obj, funcName, prefix) {
const oldFn = obj[funcName];
origConsole[funcName] = oldFn.bind(obj);
return function(...args) {
addLines(funcName, [...args].join(' '), 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) {
super(url);
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 (!fn) {
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.
* @param {module:webgl-utils.GetWebGLContextOptions} [opt_options] options
* @memberOf module:webgl-utils
*/
let setupLesson = function(canvas, opt_options) {
// only once
setupLesson = function() {};
const options = opt_options || {};
if (canvas) {
canvas.addEventListener('webglcontextlost', function() {
// the default is to do nothing. Preventing the default
// means allowing context to be restored
addContextLostHTML();
});
/* because of bug in firefox we can't auto restore
canvas.addEventListener('webglcontextrestored', function() {
// just reload the page. Easiest.
window.location.reload();
});
*/
}
if (isInIFrame()) {
updateCSSIfInIFrame();
} else if (!options.noTitle && options.title !== false) {
var titleElem = document.querySelector("title");
if (titleElem && titleElem.getAttribute("addtitletodoc") !== "false") {
var title = document.title;
var h1 = document.createElement("h1");
h1.innerText = title;
document.body.insertBefore(h1, document.body.children[0]);
}
}
};
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 isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
function installWebGLLessonSetup() {
HTMLCanvasElement.prototype.getContext = (function(oldFn) {
return function() {
const type = arguments[0];
const isWebGL = isWebGLRE.test(type);
if (isWebGL) {
setupLesson(this);
}
const args = [].slice.apply(arguments);
args[1] = Object.assign({
powerPreference: 'low-power',
}, args[1]);
const ctx = oldFn.apply(this, args);
if (!ctx && isWebGL) {
showNeedWebGL(this, type);
}
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,
};
}));