You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

641 lines
19 KiB

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