aframe-gui
Version:
A-Frame GUI components
1,386 lines (1,249 loc) • 232 kB
JavaScript
(function (THREE, aframe) {
'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
function _interopNamespace(e) {
if (e && e.__esModule) { return e; } else {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () {
return e[k];
}
});
}
});
}
n['default'] = e;
return Object.freeze(n);
}
}
var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
var aframe__default = /*#__PURE__*/_interopDefaultLegacy(aframe);
/**
* Lightweight thenable implementation that is entirely self-contained within a single
* function with no external dependencies so it can be easily shipped across to a WorkerModule.
*
* This implementation conforms fully to the Promises/A+ spec so it can safely interoperate
* with other thenable implementations. https://github.com/promises-aplus/promises-spec
*
* *However*, it is _not_ a full implementation of ES2015 Promises, e.g. it does not
* have the same constructor signature and does not expose a `catch` method or the static
* `resolve`/`reject`/`all`/`race` initializer methods. If you need to hand a Thenable
* instance off to consuming code that may expect a true Promise, you'll want to wrap it
* in a native-or-polyfilled Promise first.
*
* (Why yet another Promises/A+ implementation? Great question. We needed a polyfill-like
* thing that was (a) wrapped in a single function for easy serialization across to a Worker,
* and (b) was as small as possible -- at ~900B minified (~500B gzipped) this is the smallest
* implementation I've found. And also, exercises like this are challenging and fun.)
*/
function BespokeThenable() {
var state = 0; // 0=pending, 1=fulfilled, -1=rejected
var queue = [];
var value;
var scheduled = 0;
var completeCalled = 0;
function then(onResolve, onReject) {
var nextThenable = BespokeThenable();
function handleNext() {
var cb = state > 0 ? onResolve : onReject;
if (isFn(cb)) {
try {
var result = cb(value);
if (result === nextThenable) {
recursiveError();
}
var resultThen = getThenableThen(result);
if (resultThen) {
resultThen.call(result, nextThenable.resolve, nextThenable.reject);
} else {
nextThenable.resolve(result);
}
} catch (err) {
nextThenable.reject(err);
}
} else {
nextThenable[state > 0 ? 'resolve' : 'reject'](value);
}
}
queue.push(handleNext);
if (state) {
scheduleQueueFlush();
}
return nextThenable
}
var resolve = oneTime(function (val) {
if (!completeCalled) {
complete(1, val);
}
});
var reject = oneTime(function (reason) {
if (!completeCalled) {
complete(-1, reason);
}
});
function complete(st, val) {
completeCalled++;
var ignoreThrow = 0;
try {
if (val === thenableObj) {
recursiveError();
}
var valThen = st > 0 && getThenableThen(val);
if (valThen) {
valThen.call(val, oneTime(function (v) {
ignoreThrow++;
complete(1, v);
}), oneTime(function (v) {
ignoreThrow++;
complete(-1, v);
}));
} else {
state = st;
value = val;
scheduleQueueFlush();
}
} catch(e) {
if (!state && !ignoreThrow) {
complete(-1, e);
}
}
}
function scheduleQueueFlush() {
if (!scheduled) {
setTimeout(flushQueue, 0); //TODO setImmediate or postMessage approach if available?
scheduled = 1;
}
}
function flushQueue() {
var q = queue;
scheduled = 0;
queue = [];
q.forEach(callIt);
}
function callIt(fn) {
fn();
}
function getThenableThen(val) {
var valThen = val && (isFn(val) || typeof val === 'object') && val.then;
return isFn(valThen) && valThen
}
function oneTime(fn) {
var called = 0;
return function() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
if (!called++) {
fn.apply(this, args);
}
}
}
function recursiveError() {
throw new TypeError('Chaining cycle detected')
}
var isFn = function (v) { return typeof v === 'function'; };
var thenableObj = {
then: then,
resolve: resolve,
reject: reject
};
return thenableObj
}
/**
* Thenable implementation that uses a native Promise under the covers. This implementation
* is preferred if Promise is available, for better performance and dev tools integration.
* @constructor
*/
function NativePromiseThenable() {
var resolve, reject;
var promise = new Promise(function (res, rej) {
resolve = res;
reject = rej;
});
return {
then: promise.then.bind(promise),
resolve: resolve,
reject: reject
}
}
/**
* Promise.all() impl:
*/
BespokeThenable.all = NativePromiseThenable.all = function(items) {
var resultCount = 0;
var results = [];
var out = DefaultThenable();
if (items.length === 0) {
out.resolve([]);
} else {
items.forEach(function (item, i) {
var itemThenable = DefaultThenable();
itemThenable.resolve(item);
itemThenable.then(function (res) {
resultCount++;
results[i] = res;
if (resultCount === items.length) {
out.resolve(results);
}
}, out.reject);
});
}
return out
};
/**
* Choose the best Thenable implementation and export it as the default.
*/
var DefaultThenable = typeof Promise === 'function' ? NativePromiseThenable : BespokeThenable;
/**
* Main content for the worker that handles the loading and execution of
* modules within it.
*/
function workerBootstrap() {
var modules = Object.create(null);
// Handle messages for registering a module
function registerModule(ref, callback) {
var id = ref.id;
var name = ref.name;
var dependencies = ref.dependencies; if ( dependencies === void 0 ) dependencies = [];
var init = ref.init; if ( init === void 0 ) init = function(){};
var getTransferables = ref.getTransferables; if ( getTransferables === void 0 ) getTransferables = null;
// Only register once
if (modules[id]) { return }
try {
// If any dependencies are modules, ensure they're registered and grab their value
dependencies = dependencies.map(function (dep) {
if (dep && dep.isWorkerModule) {
registerModule(dep, function (depResult) {
if (depResult instanceof Error) { throw depResult }
});
dep = modules[dep.id].value;
}
return dep
});
// Rehydrate functions
init = rehydrate(("<" + name + ">.init"), init);
if (getTransferables) {
getTransferables = rehydrate(("<" + name + ">.getTransferables"), getTransferables);
}
// Initialize the module and store its value
var value = null;
if (typeof init === 'function') {
value = init.apply(void 0, dependencies);
} else {
console.error('worker module init function failed to rehydrate');
}
modules[id] = {
id: id,
value: value,
getTransferables: getTransferables
};
callback(value);
} catch(err) {
if (!(err && err.noLog)) {
console.error(err);
}
callback(err);
}
}
// Handle messages for calling a registered module's result function
function callModule(ref, callback) {
var ref$1;
var id = ref.id;
var args = ref.args;
if (!modules[id] || typeof modules[id].value !== 'function') {
callback(new Error(("Worker module " + id + ": not found or its 'init' did not return a function")));
}
try {
var result = (ref$1 = modules[id]).value.apply(ref$1, args);
if (result && typeof result.then === 'function') {
result.then(handleResult, function (rej) { return callback(rej instanceof Error ? rej : new Error('' + rej)); });
} else {
handleResult(result);
}
} catch(err) {
callback(err);
}
function handleResult(result) {
try {
var tx = modules[id].getTransferables && modules[id].getTransferables(result);
if (!tx || !Array.isArray(tx) || !tx.length) {
tx = undefined; //postMessage is very picky about not passing null or empty transferables
}
callback(result, tx);
} catch(err) {
console.error(err);
callback(err);
}
}
}
function rehydrate(name, str) {
var result = void 0;
self.troikaDefine = function (r) { return result = r; };
var url = URL.createObjectURL(
new Blob(
[("/** " + (name.replace(/\*/g, '')) + " **/\n\ntroikaDefine(\n" + str + "\n)")],
{type: 'application/javascript'}
)
);
try {
importScripts(url);
} catch(err) {
console.error(err);
}
URL.revokeObjectURL(url);
delete self.troikaDefine;
return result
}
// Handler for all messages within the worker
self.addEventListener('message', function (e) {
var ref = e.data;
var messageId = ref.messageId;
var action = ref.action;
var data = ref.data;
try {
// Module registration
if (action === 'registerModule') {
registerModule(data, function (result) {
if (result instanceof Error) {
postMessage({
messageId: messageId,
success: false,
error: result.message
});
} else {
postMessage({
messageId: messageId,
success: true,
result: {isCallable: typeof result === 'function'}
});
}
});
}
// Invocation
if (action === 'callModule') {
callModule(data, function (result, transferables) {
if (result instanceof Error) {
postMessage({
messageId: messageId,
success: false,
error: result.message
});
} else {
postMessage({
messageId: messageId,
success: true,
result: result
}, transferables || undefined);
}
});
}
} catch(err) {
postMessage({
messageId: messageId,
success: false,
error: err.stack
});
}
});
}
/**
* Fallback for `defineWorkerModule` that behaves identically but runs in the main
* thread, for when the execution environment doesn't support web workers or they
* are disallowed due to e.g. CSP security restrictions.
*/
function defineMainThreadModule(options) {
var moduleFunc = function() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return moduleFunc._getInitResult().then(function (initResult) {
if (typeof initResult === 'function') {
return initResult.apply(void 0, args)
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
};
moduleFunc._getInitResult = function() {
// We can ignore getTransferables in main thread. TODO workerId?
var dependencies = options.dependencies;
var init = options.init;
// Resolve dependencies
dependencies = Array.isArray(dependencies) ? dependencies.map(function (dep) { return dep && dep._getInitResult ? dep._getInitResult() : dep; }
) : [];
// Invoke init with the resolved dependencies
var initThenable = DefaultThenable.all(dependencies).then(function (deps) {
return init.apply(null, deps)
});
// Cache the resolved promise for subsequent calls
moduleFunc._getInitResult = function () { return initThenable; };
return initThenable
};
return moduleFunc
}
var supportsWorkers = function () {
var supported = false;
// Only attempt worker initialization in browsers; elsewhere it would just be
// noise e.g. loading into a Node environment for SSR.
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
try {
// TODO additional checks for things like importScripts within the worker?
// Would need to be an async check.
var worker = new Worker(
URL.createObjectURL(new Blob([''], { type: 'application/javascript' }))
);
worker.terminate();
supported = true;
} catch (err) {
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') ; else {
console.log(
("Troika createWorkerModule: web workers not allowed; falling back to main thread execution. Cause: [" + (err.message) + "]")
);
}
}
}
// Cached result
supportsWorkers = function () { return supported; };
return supported
};
var _workerModuleId = 0;
var _messageId = 0;
var _allowInitAsString = false;
var workers = Object.create(null);
var openRequests = /*#__PURE__*/(function () {
var obj = Object.create(null);
obj._count = 0;
return obj
})();
/**
* Define a module of code that will be executed with a web worker. This provides a simple
* interface for moving chunks of logic off the main thread, and managing their dependencies
* among one another.
*
* @param {object} options
* @param {function} options.init - The main function that initializes the module. This will be run
* within the worker, and will be passed the resolved dependencies as arguments. Its
* return value becomes the module's content, which can then be used by other modules
* that depend on it. This function can perform any logic using those dependencies, but
* must not depend on anything from its parent closures.
* @param {array} [options.dependencies] - Provides any dependencies required by the init function:
* - Primitives like strings, numbers, booleans
* - Raw functions; these will be stringified and rehydrated within the worker so they
* must not depend on anything from their parent closures
* - Other worker modules; these will be resolved within the worker, and therefore modules
* that provide functions can be called without having to cross the worker/main thread
* boundary.
* @param {function} [options.getTransferables] - An optional function that will be run in the worker
* just before posting the response value from a module call back to the main thread.
* It will be passed that response value, and if it returns an array then that will be
* used as the "transferables" parameter to `postMessage`. Use this if there are values
* in the response that can/should be transfered rather than cloned.
* @param {string} [options.name] - A descriptive name for this module; this can be useful for
* debugging but is not currently used for anything else.
* @param {string} [options.workerId] - By default all modules will run in the same dedicated worker,
* but if you want to use multiple workers you can pass a `workerId` to indicate a specific
* worker to spawn. Note that each worker is completely standalone and no data or state will
* be shared between them. If a worker module is used as a dependency by worker modules
* using different `workerId`s, then that dependency will be re-registered in each worker.
* @return {function(...[*]): {then}}
*/
function defineWorkerModule(options) {
if ((!options || typeof options.init !== 'function') && !_allowInitAsString) {
throw new Error('requires `options.init` function')
}
var dependencies = options.dependencies;
var init = options.init;
var getTransferables = options.getTransferables;
var workerId = options.workerId;
if (!supportsWorkers()) {
return defineMainThreadModule(options)
}
if (workerId == null) {
workerId = '#default';
}
var id = "workerModule" + (++_workerModuleId);
var name = options.name || id;
var registrationThenable = null;
dependencies = dependencies && dependencies.map(function (dep) {
// Wrap raw functions as worker modules with no dependencies
if (typeof dep === 'function' && !dep.workerModuleData) {
_allowInitAsString = true;
dep = defineWorkerModule({
workerId: workerId,
name: ("<" + name + "> function dependency: " + (dep.name)),
init: ("function(){return (\n" + (stringifyFunction(dep)) + "\n)}")
});
_allowInitAsString = false;
}
// Grab postable data for worker modules
if (dep && dep.workerModuleData) {
dep = dep.workerModuleData;
}
return dep
});
function moduleFunc() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
// Register this module if needed
if (!registrationThenable) {
registrationThenable = callWorker(workerId,'registerModule', moduleFunc.workerModuleData);
}
// Invoke the module, returning a thenable
return registrationThenable.then(function (ref) {
var isCallable = ref.isCallable;
if (isCallable) {
return callWorker(workerId,'callModule', {id: id, args: args})
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
}
moduleFunc.workerModuleData = {
isWorkerModule: true,
id: id,
name: name,
dependencies: dependencies,
init: stringifyFunction(init),
getTransferables: getTransferables && stringifyFunction(getTransferables)
};
return moduleFunc
}
/**
* Stringifies a function into a form that can be deserialized in the worker
* @param fn
*/
function stringifyFunction(fn) {
var str = fn.toString();
// If it was defined in object method/property format, it needs to be modified
if (!/^function/.test(str) && /^\w+\s*\(/.test(str)) {
str = 'function ' + str;
}
return str
}
function getWorker(workerId) {
var worker = workers[workerId];
if (!worker) {
// Bootstrap the worker's content
var bootstrap = stringifyFunction(workerBootstrap);
// Create the worker from the bootstrap function content
worker = workers[workerId] = new Worker(
URL.createObjectURL(
new Blob(
[("/** Worker Module Bootstrap: " + (workerId.replace(/\*/g, '')) + " **/\n\n;(" + bootstrap + ")()")],
{type: 'application/javascript'}
)
)
);
// Single handler for response messages from the worker
worker.onmessage = function (e) {
var response = e.data;
var msgId = response.messageId;
var callback = openRequests[msgId];
if (!callback) {
throw new Error('WorkerModule response with empty or unknown messageId')
}
delete openRequests[msgId];
openRequests.count--;
callback(response);
};
}
return worker
}
// Issue a call to the worker with a callback to handle the response
function callWorker(workerId, action, data) {
var thenable = DefaultThenable();
var messageId = ++_messageId;
openRequests[messageId] = function (response) {
if (response.success) {
thenable.resolve(response.result);
} else {
thenable.reject(new Error(("Error in worker " + action + " call: " + (response.error))));
}
};
openRequests._count++;
if (openRequests.count > 1000) { //detect leaks
console.warn('Large number of open WorkerModule requests, some may not be returning');
}
getWorker(workerId).postMessage({
messageId: messageId,
action: action,
data: data
});
return thenable
}
/**
* Just the {@link Thenable} function wrapped as a worker module. If another worker
* module needs Thenable as a dependency, it's better to pass this module rather than
* the raw function in its `dependencies` array so it only gets registered once.
*/
var ThenableWorkerModule = /*#__PURE__*/defineWorkerModule({
name: 'Thenable',
dependencies: [DefaultThenable],
init: function(Thenable) {
return Thenable
}
});
/**
* Regular expression for matching the `void main() {` opener line in GLSL.
* @type {RegExp}
*/
const voidMainRegExp = /\bvoid\s+main\s*\(\s*\)\s*{/g;
/**
* Recursively expands all `#include <xyz>` statements within string of shader code.
* Copied from three's WebGLProgram#parseIncludes for external use.
*
* @param {string} source - The GLSL source code to evaluate
* @return {string} The GLSL code with all includes expanded
*/
function expandShaderIncludes( source ) {
const pattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
function replace(match, include) {
let chunk = THREE.ShaderChunk[include];
return chunk ? expandShaderIncludes(chunk) : match
}
return source.replace( pattern, replace )
}
// Local assign polyfill to avoid importing troika-core
const assign = Object.assign || function(/*target, ...sources*/) {
let target = arguments[0];
for (let i = 1, len = arguments.length; i < len; i++) {
let source = arguments[i];
if (source) {
for (let prop in source) {
if (source.hasOwnProperty(prop)) {
target[prop] = source[prop];
}
}
}
}
return target
};
const epoch = Date.now();
const CONSTRUCTOR_CACHE = new WeakMap();
const SHADER_UPGRADE_CACHE = new Map();
// Material ids must be integers, but we can't access the increment from Three's `Material` module,
// so let's choose a sufficiently large starting value that should theoretically never collide.
let materialInstanceId = 1e10;
/**
* A utility for creating a custom shader material derived from another material's
* shaders. This allows you to inject custom shader logic and transforms into the
* builtin ThreeJS materials without having to recreate them from scratch.
*
* @param {THREE.Material} baseMaterial - the original material to derive from
*
* @param {Object} options - How the base material should be modified.
* @param {Object} options.defines - Custom `defines` for the material
* @param {Object} options.extensions - Custom `extensions` for the material, e.g. `{derivatives: true}`
* @param {Object} options.uniforms - Custom `uniforms` for use in the modified shader. These can
* be accessed and manipulated via the resulting material's `uniforms` property, just like
* in a ShaderMaterial. You do not need to repeat the base material's own uniforms here.
* @param {String} options.timeUniform - If specified, a uniform of this name will be injected into
* both shaders, and it will automatically be updated on each render frame with a number of
* elapsed milliseconds. The "zero" epoch time is not significant so don't rely on this as a
* true calendar time.
* @param {String} options.vertexDefs - Custom GLSL code to inject into the vertex shader's top-level
* definitions, above the `void main()` function.
* @param {String} options.vertexMainIntro - Custom GLSL code to inject at the top of the vertex
* shader's `void main` function.
* @param {String} options.vertexMainOutro - Custom GLSL code to inject at the end of the vertex
* shader's `void main` function.
* @param {String} options.vertexTransform - Custom GLSL code to manipulate the `position`, `normal`,
* and/or `uv` vertex attributes. This code will be wrapped within a standalone function with
* those attributes exposed by their normal names as read/write values.
* @param {String} options.fragmentDefs - Custom GLSL code to inject into the fragment shader's top-level
* definitions, above the `void main()` function.
* @param {String} options.fragmentMainIntro - Custom GLSL code to inject at the top of the fragment
* shader's `void main` function.
* @param {String} options.fragmentMainOutro - Custom GLSL code to inject at the end of the fragment
* shader's `void main` function. You can manipulate `gl_FragColor` here but keep in mind it goes
* after any of ThreeJS's color postprocessing shader chunks (tonemapping, fog, etc.), so if you
* want those to apply to your changes use `fragmentColorTransform` instead.
* @param {String} options.fragmentColorTransform - Custom GLSL code to manipulate the `gl_FragColor`
* output value. Will be injected near the end of the `void main` function, but before any
* of ThreeJS's color postprocessing shader chunks (tonemapping, fog, etc.), and before the
* `fragmentMainOutro`.
* @param {function<{vertexShader,fragmentShader}>:{vertexShader,fragmentShader}} options.customRewriter - A function
* for performing custom rewrites of the full shader code. Useful if you need to do something
* special that's not covered by the other builtin options. This function will be executed before
* any other transforms are applied.
* @param {boolean} options.chained - Set to `true` to prototype-chain the derived material to the base
* material, rather than the default behavior of copying it. This allows the derived material to
* automatically pick up changes made to the base material and its properties. This can be useful
* where the derived material is hidden from the user as an implementation detail, allowing them
* to work with the original material like normal. But it can result in unexpected behavior if not
* handled carefully.
*
* @return {THREE.Material}
*
* The returned material will also have two new methods, `getDepthMaterial()` and `getDistanceMaterial()`,
* which can be called to get a variant of the derived material for use in shadow casting. If the
* target mesh is expected to cast shadows, then you can assign these to the mesh's `customDepthMaterial`
* (for directional and spot lights) and/or `customDistanceMaterial` (for point lights) properties to
* allow the cast shadow to honor your derived shader's vertex transforms and discarded fragments. These
* will also set a custom `#define IS_DEPTH_MATERIAL` or `#define IS_DISTANCE_MATERIAL` that you can look
* for in your derived shaders with `#ifdef` to customize their behavior for the depth or distance
* scenarios, e.g. skipping antialiasing or expensive shader logic.
*/
function createDerivedMaterial(baseMaterial, options) {
// Generate a key that is unique to the content of these `options`. We'll use this
// throughout for caching and for generating the upgraded shader code. This increases
// the likelihood that the resulting shaders will line up across multiple calls so
// their GL programs can be shared and cached.
const optionsKey = getKeyForOptions(options);
// First check to see if we've already derived from this baseMaterial using this
// unique set of options, and if so reuse the constructor to avoid some allocations.
let ctorsByDerivation = CONSTRUCTOR_CACHE.get(baseMaterial);
if (!ctorsByDerivation) {
CONSTRUCTOR_CACHE.set(baseMaterial, (ctorsByDerivation = Object.create(null)));
}
if (ctorsByDerivation[optionsKey]) {
return new ctorsByDerivation[optionsKey]()
}
const privateBeforeCompileProp = `_onBeforeCompile${optionsKey}`;
// Private onBeforeCompile handler that injects the modified shaders and uniforms when
// the renderer switches to this material's program
const onBeforeCompile = function (shaderInfo) {
baseMaterial.onBeforeCompile.call(this, shaderInfo);
// Upgrade the shaders, caching the result by incoming source code
const cacheKey = optionsKey + '|||' + shaderInfo.vertexShader + '|||' + shaderInfo.fragmentShader;
let upgradedShaders = SHADER_UPGRADE_CACHE[cacheKey];
if (!upgradedShaders) {
const upgraded = upgradeShaders(shaderInfo, options, optionsKey);
upgradedShaders = SHADER_UPGRADE_CACHE[cacheKey] = upgraded;
}
// Inject upgraded shaders and uniforms into the program
shaderInfo.vertexShader = upgradedShaders.vertexShader;
shaderInfo.fragmentShader = upgradedShaders.fragmentShader;
assign(shaderInfo.uniforms, this.uniforms);
// Inject auto-updating time uniform if requested
if (options.timeUniform) {
shaderInfo.uniforms[options.timeUniform] = {
get value() {return Date.now() - epoch}
};
}
// Users can still add their own handlers on top of ours
if (this[privateBeforeCompileProp]) {
this[privateBeforeCompileProp](shaderInfo);
}
};
const DerivedMaterial = function DerivedMaterial() {
return derive(options.chained ? baseMaterial : baseMaterial.clone())
};
const derive = function(base) {
// Prototype chain to the base material
const derived = Object.create(base, descriptor);
// Store the baseMaterial for reference; this is always the original even when cloning
Object.defineProperty(derived, 'baseMaterial', { value: baseMaterial });
// Needs its own ids
Object.defineProperty(derived, 'id', { value: materialInstanceId++ });
derived.uuid = THREE.MathUtils.generateUUID();
// Merge uniforms, defines, and extensions
derived.uniforms = assign({}, base.uniforms, options.uniforms);
derived.defines = assign({}, base.defines, options.defines);
derived.defines[`TROIKA_DERIVED_MATERIAL_${optionsKey}`] = ''; //force a program change from the base material
derived.extensions = assign({}, base.extensions, options.extensions);
// Don't inherit EventDispatcher listeners
derived._listeners = undefined;
return derived
};
const descriptor = {
constructor: {value: DerivedMaterial},
isDerivedMaterial: {value: true},
customProgramCacheKey: {
value: function () {
return optionsKey
}
},
onBeforeCompile: {
get() {
return onBeforeCompile
},
set(fn) {
this[privateBeforeCompileProp] = fn;
}
},
copy: {
writable: true,
configurable: true,
value: function (source) {
baseMaterial.copy.call(this, source);
if (!baseMaterial.isShaderMaterial && !baseMaterial.isDerivedMaterial) {
assign(this.extensions, source.extensions);
assign(this.defines, source.defines);
assign(this.uniforms, THREE.UniformsUtils.clone(source.uniforms));
}
return this
}
},
clone: {
writable: true,
configurable: true,
value: function () {
const newBase = new baseMaterial.constructor();
return derive(newBase).copy(this)
}
},
/**
* Utility to get a MeshDepthMaterial that will honor this derived material's vertex
* transformations and discarded fragments.
*/
getDepthMaterial: {
writable: true,
configurable: true,
value: function() {
let depthMaterial = this._depthMaterial;
if (!depthMaterial) {
depthMaterial = this._depthMaterial = createDerivedMaterial(
baseMaterial.isDerivedMaterial
? baseMaterial.getDepthMaterial()
: new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking }),
options
);
depthMaterial.defines.IS_DEPTH_MATERIAL = '';
depthMaterial.uniforms = this.uniforms; //automatically recieve same uniform values
}
return depthMaterial
}
},
/**
* Utility to get a MeshDistanceMaterial that will honor this derived material's vertex
* transformations and discarded fragments.
*/
getDistanceMaterial: {
writable: true,
configurable: true,
value: function() {
let distanceMaterial = this._distanceMaterial;
if (!distanceMaterial) {
distanceMaterial = this._distanceMaterial = createDerivedMaterial(
baseMaterial.isDerivedMaterial
? baseMaterial.getDistanceMaterial()
: new THREE.MeshDistanceMaterial(),
options
);
distanceMaterial.defines.IS_DISTANCE_MATERIAL = '';
distanceMaterial.uniforms = this.uniforms; //automatically recieve same uniform values
}
return distanceMaterial
}
},
dispose: {
writable: true,
configurable: true,
value() {
const {_depthMaterial, _distanceMaterial} = this;
if (_depthMaterial) _depthMaterial.dispose();
if (_distanceMaterial) _distanceMaterial.dispose();
baseMaterial.dispose.call(this);
}
}
};
ctorsByDerivation[optionsKey] = DerivedMaterial;
return new DerivedMaterial()
}
function upgradeShaders({vertexShader, fragmentShader}, options, key) {
let {
vertexDefs,
vertexMainIntro,
vertexMainOutro,
vertexTransform,
fragmentDefs,
fragmentMainIntro,
fragmentMainOutro,
fragmentColorTransform,
customRewriter,
timeUniform
} = options;
vertexDefs = vertexDefs || '';
vertexMainIntro = vertexMainIntro || '';
vertexMainOutro = vertexMainOutro || '';
fragmentDefs = fragmentDefs || '';
fragmentMainIntro = fragmentMainIntro || '';
fragmentMainOutro = fragmentMainOutro || '';
// Expand includes if needed
if (vertexTransform || customRewriter) {
vertexShader = expandShaderIncludes(vertexShader);
}
if (fragmentColorTransform || customRewriter) {
// We need to be able to find postprocessing chunks after include expansion in order to
// put them after the fragmentColorTransform, so mark them with comments first. Even if
// this particular derivation doesn't have a fragmentColorTransform, other derivations may,
// so we still mark them.
fragmentShader = fragmentShader.replace(
/^[ \t]*#include <((?:tonemapping|encodings|fog|premultiplied_alpha|dithering)_fragment)>/gm,
'\n//!BEGIN_POST_CHUNK $1\n$&\n//!END_POST_CHUNK\n'
);
fragmentShader = expandShaderIncludes(fragmentShader);
}
// Apply custom rewriter function
if (customRewriter) {
let res = customRewriter({vertexShader, fragmentShader});
vertexShader = res.vertexShader;
fragmentShader = res.fragmentShader;
}
// The fragmentColorTransform needs to go before any postprocessing chunks, so extract
// those and re-insert them into the outro in the correct place:
if (fragmentColorTransform) {
let postChunks = [];
fragmentShader = fragmentShader.replace(
/^\/\/!BEGIN_POST_CHUNK[^]+?^\/\/!END_POST_CHUNK/gm, // [^]+? = non-greedy match of any chars including newlines
match => {
postChunks.push(match);
return ''
}
);
fragmentMainOutro = `${fragmentColorTransform}\n${postChunks.join('\n')}\n${fragmentMainOutro}`;
}
// Inject auto-updating time uniform if requested
if (timeUniform) {
const code = `\nuniform float ${timeUniform};\n`;
vertexDefs = code + vertexDefs;
fragmentDefs = code + fragmentDefs;
}
// Inject a function for the vertexTransform and rename all usages of position/normal/uv
if (vertexTransform) {
vertexDefs = `${vertexDefs}
vec3 troika_position_${key};
vec3 troika_normal_${key};
vec2 troika_uv_${key};
void troikaVertexTransform${key}(inout vec3 position, inout vec3 normal, inout vec2 uv) {
${vertexTransform}
}
`;
vertexMainIntro = `
troika_position_${key} = vec3(position);
troika_normal_${key} = vec3(normal);
troika_uv_${key} = vec2(uv);
troikaVertexTransform${key}(troika_position_${key}, troika_normal_${key}, troika_uv_${key});
${vertexMainIntro}
`;
vertexShader = vertexShader.replace(/\b(position|normal|uv)\b/g, (match, match1, index, fullStr) => {
return /\battribute\s+vec[23]\s+$/.test(fullStr.substr(0, index)) ? match1 : `troika_${match1}_${key}`
});
}
// Inject defs and intro/outro snippets
vertexShader = injectIntoShaderCode(vertexShader, key, vertexDefs, vertexMainIntro, vertexMainOutro);
fragmentShader = injectIntoShaderCode(fragmentShader, key, fragmentDefs, fragmentMainIntro, fragmentMainOutro);
return {
vertexShader,
fragmentShader
}
}
function injectIntoShaderCode(shaderCode, id, defs, intro, outro) {
if (intro || outro || defs) {
shaderCode = shaderCode.replace(voidMainRegExp, `
${defs}
void troikaOrigMain${id}() {`
);
shaderCode += `
void main() {
${intro}
troikaOrigMain${id}();
${outro}
}`;
}
return shaderCode
}
function optionsJsonReplacer(key, value) {
return key === 'uniforms' ? undefined : typeof value === 'function' ? value.toString() : value
}
let _idCtr = 0;
const optionsHashesToIds = new Map();
function getKeyForOptions(options) {
const optionsHash = JSON.stringify(options, optionsJsonReplacer);
let id = optionsHashesToIds.get(optionsHash);
if (id == null) {
optionsHashesToIds.set(optionsHash, (id = ++_idCtr));
}
return id
}
/**
* Initializes and returns a function to generate an SDF texture for a given glyph.
* @param {function} createGlyphSegmentsIndex - factory for a GlyphSegmentsIndex implementation.
* @param {number} config.sdfExponent
* @param {number} config.sdfMargin
*
* @return {function(Object): {renderingBounds: [minX, minY, maxX, maxY], textureData: Uint8Array}}
*/
function createSDFGenerator(createGlyphSegmentsIndex, config) {
const { sdfExponent, sdfMargin } = config;
/**
* How many straight line segments to use when approximating a glyph's quadratic/cubic bezier curves.
*/
const CURVE_POINTS = 16;
/**
* Find the point on a quadratic bezier curve at t where t is in the range [0, 1]
*/
function pointOnQuadraticBezier(x0, y0, x1, y1, x2, y2, t) {
const t2 = 1 - t;
return {
x: t2 * t2 * x0 + 2 * t2 * t * x1 + t * t * x2,
y: t2 * t2 * y0 + 2 * t2 * t * y1 + t * t * y2
}
}
/**
* Find the point on a cubic bezier curve at t where t is in the range [0, 1]
*/
function pointOnCubicBezier(x0, y0, x1, y1, x2, y2, x3, y3, t) {
const t2 = 1 - t;
return {
x: t2 * t2 * t2 * x0 + 3 * t2 * t2 * t * x1 + 3 * t2 * t * t * x2 + t * t * t * x3,
y: t2 * t2 * t2 * y0 + 3 * t2 * t2 * t * y1 + 3 * t2 * t * t * y2 + t * t * t * y3
}
}
/**
* Generate an SDF texture segment for a single glyph.
* @param {object} glyphObj
* @param {number} sdfSize - the length of one side of the SDF image.
* Larger images encode more details. Must be a power of 2.
* @return {{textureData: Uint8Array, renderingBounds: *[]}}
*/
function generateSDF(glyphObj, sdfSize) {
//console.time('glyphSDF')
const textureData = new Uint8Array(sdfSize * sdfSize);
// Determine mapping between glyph grid coords and sdf grid coords
const glyphW = glyphObj.xMax - glyphObj.xMin;
const glyphH = glyphObj.yMax - glyphObj.yMin;
// Choose a maximum search distance radius in font units, based on the glyph's max dimensions
const fontUnitsMaxSearchDist = Math.max(glyphW, glyphH);
// Margin - add an extra 0.5 over the configured value because the outer 0.5 doesn't contain
// useful interpolated values and will be ignored anyway.
const fontUnitsMargin = Math.max(glyphW, glyphH) / sdfSize * (sdfMargin * sdfSize + 0.5);
// Metrics of the texture/quad in font units
const textureMinFontX = glyphObj.xMin - fontUnitsMargin;
const textureMinFontY = glyphObj.yMin - fontUnitsMargin;
const textureMaxFontX = glyphObj.xMax + fontUnitsMargin;
const textureMaxFontY = glyphObj.yMax + fontUnitsMargin;
const fontUnitsTextureWidth = textureMaxFontX - textureMinFontX;
const fontUnitsTextureHeight = textureMaxFontY - textureMinFontY;
const fontUnitsTextureMaxDim = Math.max(fontUnitsTextureWidth, fontUnitsTextureHeight);
function textureXToFontX(x) {
return textureMinFontX + fontUnitsTextureWidth * x / sdfSize
}
function textureYToFontY(y) {
return textureMinFontY + fontUnitsTextureHeight * y / sdfSize
}
if (glyphObj.pathCommandCount) { //whitespace chars will have no commands, so we can skip all this
// Decompose all paths into straight line segments and add them to a quadtree
const lineSegmentsIndex = createGlyphSegmentsIndex(glyphObj);
let firstX, firstY, prevX, prevY;
glyphObj.forEachPathCommand((type, x0, y0, x1, y1, x2, y2) => {
switch (type) {
case 'M':
prevX = firstX = x0;
prevY = firstY = y0;
break
case 'L':
if (x0 !== prevX || y0 !== prevY) { //yup, some fonts have zero-length line commands
lineSegmentsIndex.addLineSegment(prevX, prevY, (prevX = x0), (prevY = y0));
}
break
case 'Q': {
let prevPoint = {x: prevX, y: prevY};
for (let i = 1; i < CURVE_POINTS; i++) {
let nextPoint = pointOnQuadraticBezier(
prevX, prevY,
x0, y0,
x1, y1,
i / (CURVE_POINTS - 1)
);
lineSegmentsIndex.addLineSegment(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y);
prevPoint = nextPoint;
}
prevX = x1;
prevY = y1;
break
}
case 'C': {
let prevPoint = {x: prevX, y: prevY};
for (let i = 1; i < CURVE_POINTS; i++) {
let nextPoint = pointOnCubicBezier(
prevX, prevY,
x0, y0,
x1, y1,
x2, y2,
i / (CURVE_POINTS - 1)
);
lineSegmentsIndex.addLineSegment(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y);
prevPoint = nextPoint;
}
prevX = x2;
prevY = y2;
break
}
case 'Z':
if (prevX !== firstX || prevY !== firstY) {
lineSegmentsIndex.addLineSegment(prevX, prevY, firstX, firstY);
}
break
}
});
// For each target SDF texel, find the distance from its center to its nearest line segment,
// map that distance to an alpha value, and write that alpha to the texel
for (let sdfX = 0; sdfX < sdfSize; sdfX++) {
for (let sdfY = 0; sdfY < sdfSize; sdfY++) {
const signedDist = lineSegmentsIndex.findNearestSignedDistance(
textureXToFontX(sdfX + 0.5),
textureYToFontY(sdfY + 0.5),
fontUnitsMaxSearchDist
);
// Use an exponential scale to ensure the texels very near the glyph path have adequate
// precision, while allowing the distance field to cover the entire texture, given that
// there are only 8 bits available. Formula visualized: https://www.desmos.com/calculator/uiaq5aqiam
let alpha = Math.pow((1 - Math.abs(signedDist) / fontUnitsTextureMaxDim), sdfExponent) / 2;
if (signedDist < 0) {
alpha = 1 - alpha;
}
alpha = Math.max(0, Math.min(255, Math.round(alpha * 255))); //clamp
textureData[sdfY * sdfSize + sdfX] = alpha;
}
}
}
//console.timeEnd('glyphSDF')
return {
textureData: textureData,
renderingBounds: [
textureMinFontX,
textureMinFontY,
textureMaxFontX,
textureMaxFontY
]
}
}
return generateSDF
}
/**
* Creates a self-contained environment for processing text rendering requests.
*
* It is important that this function has no closure dependencies, so that it can be easily injected
* into the source for a Worker without requiring a build step or complex dependency loading. All its
* dependencies must be passed in at initialization.
*
* @param {function} fontParser - a function that accepts an ArrayBuffer of the font data and returns
* a standardized structure giving access to the font and its glyphs:
* {
* unitsPerEm: number,
* ascender: number,
* descender: number,
* forEachGlyph(string, fontSize, letterSpacing, callback) {
* //invokes callback for each glyph to render, passing it an object:
* callback({
* index: number,
* advanceWidth: number,
* xMin: number,
* yMin: number,
* xMax: number,
* yMax: number,
* pathCommandCount: number,
* forEachPathCommand(callback) {
* //invokes callback for each path command, with args:
* callback(
* type: 'M|L|C|Q|Z',
* ...args //0 to 6 args depending on the type
* )
* }
* })
* }
* }
* @param {function} sdfGenerator - a function that accepts a glyph object and generates an SDF texture
* from it.
* @param {Object} config
* @return {Object}
*/
function createFontProcessor(fontParser, sdfGenerator, config) {
const {
defaultFontURL
} = config;
/**
* @private
* Holds data about font glyphs and how they relate to SDF atlases
*
* {
* 'fontUrl@sdfSize': {
* fontObj: {}, //result of the fontParser
* glyphs: {
* [glyphIndex]: {
* atlasIndex: 0,
* glyphObj: {}, //glyph object from the fontParser
* renderingBounds: [x0, y0, x1, y1]
* },
* ...
* },
* glyphCount: 123
* }
* }
*/
const fontAtlases = Object.create(null);
/**
* Holds parsed font objects by url
*/
const fonts = Object.create(null);
const INF = Infinity;
/**
* Load a given font url
*/
function doLoadFont(url, callback) {
function tryLoad() {
const onError = err => {
console.error(`Failure loading font ${url}${url === defaultFontURL ? '' : '; trying fallback'}`, err);
if (url !== defaultFontURL) {
url = defaultFontURL;
tryLoad();
}
};
try {
const request = new XMLHttpRequest();
request.open('get', url, true);
request.responseType = 'arraybuffer';
request.onload = function () {
if (request.status >= 400) {
onError(new Error(request.statusText));
}
else if (request.status > 0) {
try {
const fontObj = fontParser(request.response);
callback(fontObj);
} catch (e) {
onError(e);
}
}
};
request.onerror = onError;
request.send();
} catch(err) {
onError(err);
}
}
tryLoad();
}
/**
* Load a given font url if needed, invoking a callback when it's loaded. If already
* loaded, the callback will be called synchronously.
*/
function loadFont(fontUrl, callback) {
if (!fontUrl) fontUrl = defaultFontURL;
let font = fonts[fontUrl];
if (font) {
// if currently loading font, add to callbacks, otherwise execute immediately
if (font.pending) {
font.pending.push(callback);
} else {
callback(font);
}
} else {
fonts[fontUrl] = {pending: [callback]};
doLoadFont(fontUrl, fontObj => {
let callbacks = fonts[fontUrl].pending;
fonts[fontUrl] = fontObj;
callbacks.forEach(cb => cb(fontObj));
});
}
}
/**
* Get the atlas data for a given font url, loading it from the network and initializing
* its atlas data objects if necessary.
*/
function getSdfAtlas(fontUrl, sdfGly