summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDuckonaut <duckonaut@#gmail.com>2026-11-09 15:27:03 +0100
committerDuckonaut <duckonaut@#gmail.com>2026-11-09 15:27:03 +0100
commit5efada68310a073a9ebe2398d676cdfb0f4e428a (patch)
tree6d6237de196666c5da0560944bb5aeb0dc6e054f
initial commit
-rw-r--r--.gitignore1
-rw-r--r--common.luax71
-rw-r--r--components/animated_bg.luax3
-rw-r--r--components/footer.luax21
-rw-r--r--not_found.luax3
-rw-r--r--pages/home.luax25
-rw-r--r--public/CNAME1
-rw-r--r--public/assets/GlslCanvas.js2064
-rw-r--r--public/assets/app.css289
-rw-r--r--public/assets/index.css112
-rw-r--r--public/assets/index.js91
-rw-r--r--public/assets/project.css126
-rw-r--r--reluax.luax38
13 files changed, 2845 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1cd791b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+result/
diff --git a/common.luax b/common.luax
new file mode 100644
index 0000000..2887a46
--- /dev/null
+++ b/common.luax
@@ -0,0 +1,71 @@
+return function(props)
+ local use_cdn = reluax.dev_mode == false
+ local asset_prefix = use_cdn and "https://cdn.duckonaut.dev" or ""
+
+ local thumbnail = props.attrs.thumbnail or
+ asset_prefix .. "/assets/images/thumbnail.png"
+ local title = props.attrs.title or "rem.duckonaut.dev"
+ local description = props.attrs.description or "zzz"
+ local stylesheets = props.attrs.stylesheets or
+ { 'app.css',
+ 'index.css'
+ }
+ local scripts = props.attrs.scripts or
+ { 'index.js', 'GlslCanvas.js' }
+
+ local style_tags = {}
+ for _, stylesheet in ipairs(stylesheets) do
+ style_tags[#style_tags + 1] =
+ <link rel="stylesheet" href={asset_prefix .. "/assets/" .. stylesheet} />
+ end
+
+ local script_tags = {}
+ for _, script in ipairs(scripts) do
+ script_tags[#script_tags + 1] =
+ <script src={asset_prefix .. "/assets/" .. script}></script>
+ end
+
+ return <html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <link rel="icon" type="image/svg+xml" href={asset_prefix .. '/assets/images/favicon.svg'} />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="stylesheet"
+ href="//cdn.jsdelivr.net/npm/hack-font@3.3.0/build/web/hack-subset.css" />
+
+ <link rel="stylesheet"
+ type="text/css"
+ href="https://cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.css" />
+ <link rel="stylesheet"
+ type="text/css"
+ href="https://cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick-theme.css" />
+
+ <script type="text/javascript"
+ src="https://code.jquery.com/jquery-1.11.0.min.js"></script>
+ <script type="text/javascript"
+ src="https://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
+ <script type="text/javascript"
+ src="https://cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.min.js"></script>
+
+ {$ script_tags $}
+
+ {$ style_tags $}
+
+ <meta property="og:image" content={thumbnail} />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={description} />
+ <meta property="og:url" content="https://rem.duckonaut.dev" />
+ <meta property="og:type" content="website" />
+ <meta name="theme-color" content="#9191ff" />
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta name="twitter:site" content="@duckonaut" />
+ <meta name="twitter:creator" content="@duckonaut" />
+ <title>{$ title $}</title>
+</head>
+
+<body>
+ <div id="app">{$ props.children $}</div>
+</body>
+
+</html>
+end
diff --git a/components/animated_bg.luax b/components/animated_bg.luax
new file mode 100644
index 0000000..0c5105c
--- /dev/null
+++ b/components/animated_bg.luax
@@ -0,0 +1,3 @@
+return function()
+ return <div id="animated-bg" />
+end
diff --git a/components/footer.luax b/components/footer.luax
new file mode 100644
index 0000000..50a3540
--- /dev/null
+++ b/components/footer.luax
@@ -0,0 +1,21 @@
+return function()
+ return
+ <div class="footer-container">
+ <div class="footer">
+ <div class="footer-begin">
+ </div>
+ <div class="footer-middle">
+ <p class="footer-item">
+ <strong>© {$ os.date("%Y ") $}</strong>
+ <a class="footer-link" href="https://duckonaut.dev">duckonaut.dev</a>
+ </p>
+ <p class="footer-item">
+ Made with <span class="footer-heart">♥ </span> in
+ <a class="footer-link" href="https://github.com/Duckonaut/reluax">Reluax</a>
+ </p>
+ </div>
+ <div class="footer-end">
+ </div>
+ </div>
+ </div>
+end
diff --git a/not_found.luax b/not_found.luax
new file mode 100644
index 0000000..9ad3806
--- /dev/null
+++ b/not_found.luax
@@ -0,0 +1,3 @@
+return function()
+ return "404 Not Found"
+end
diff --git a/pages/home.luax b/pages/home.luax
new file mode 100644
index 0000000..0e7bcb8
--- /dev/null
+++ b/pages/home.luax
@@ -0,0 +1,25 @@
+local art = [[
+ _/\
+ _/ \
+ /__/ \
+ / \
+ _____ /_______\ ______
+ \ / \ /
+ \/ ## ## \ /
+ | #### #### |
+ \ ## ## /
+ \ /
+ |- -|
+ __| |__
+]]
+
+return function()
+ return <div class="main-info">
+ <div class="main-info-title">
+ they do lots of things!
+ </div>
+ <div class="ascii-art">
+ {$ art $}
+ </div>
+ </div>
+end
diff --git a/public/CNAME b/public/CNAME
new file mode 100644
index 0000000..8a0042d
--- /dev/null
+++ b/public/CNAME
@@ -0,0 +1 @@
+rem.duckonaut.dev
diff --git a/public/assets/GlslCanvas.js b/public/assets/GlslCanvas.js
new file mode 100644
index 0000000..ab29380
--- /dev/null
+++ b/public/assets/GlslCanvas.js
@@ -0,0 +1,2064 @@
+// MIT, from https://github.com/patriciogonzalezvivo/glslCanvas/blob/master/dist/GlslCanvas.js
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.GlslCanvas = factory());
+}(this, (function () { 'use strict';
+
+var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
+
+var win;
+
+if (typeof window !== "undefined") {
+ win = window;
+} else if (typeof commonjsGlobal !== "undefined") {
+ win = commonjsGlobal;
+} else if (typeof self !== "undefined"){
+ win = self;
+} else {
+ win = {};
+}
+
+var window_1 = win;
+
+var isFunction_1 = isFunction;
+
+var toString = Object.prototype.toString;
+
+function isFunction (fn) {
+ if (!fn) {
+ return false
+ }
+ var string = toString.call(fn);
+ return string === '[object Function]' ||
+ (typeof fn === 'function' && string !== '[object RegExp]') ||
+ (typeof window !== 'undefined' &&
+ // IE8 and below
+ (fn === window.setTimeout ||
+ fn === window.alert ||
+ fn === window.confirm ||
+ fn === window.prompt))
+}
+
+var trim = function(string) {
+ return string.replace(/^\s+|\s+$/g, '');
+};
+var isArray = function(arg) {
+ return Object.prototype.toString.call(arg) === '[object Array]';
+ };
+
+var parseHeaders = function (headers) {
+ if (!headers)
+ return {}
+
+ var result = {};
+
+ var headersArr = trim(headers).split('\n');
+
+ for (var i = 0; i < headersArr.length; i++) {
+ var row = headersArr[i];
+ var index = row.indexOf(':')
+ , key = trim(row.slice(0, index)).toLowerCase()
+ , value = trim(row.slice(index + 1));
+
+ if (typeof(result[key]) === 'undefined') {
+ result[key] = value;
+ } else if (isArray(result[key])) {
+ result[key].push(value);
+ } else {
+ result[key] = [ result[key], value ];
+ }
+ }
+
+ return result
+};
+
+var immutable = extend;
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+function extend() {
+ var target = {};
+
+ for (var i = 0; i < arguments.length; i++) {
+ var source = arguments[i];
+
+ for (var key in source) {
+ if (hasOwnProperty.call(source, key)) {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ return target
+}
+
+"use strict";
+
+
+
+
+
+var xhr = createXHR;
+// Allow use of default import syntax in TypeScript
+var default_1 = createXHR;
+createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop;
+createXHR.XDomainRequest = "withCredentials" in (new createXHR.XMLHttpRequest()) ? createXHR.XMLHttpRequest : window_1.XDomainRequest;
+
+forEachArray(["get", "put", "post", "patch", "head", "delete"], function(method) {
+ createXHR[method === "delete" ? "del" : method] = function(uri, options, callback) {
+ options = initParams(uri, options, callback);
+ options.method = method.toUpperCase();
+ return _createXHR(options)
+ };
+});
+
+function forEachArray(array, iterator) {
+ for (var i = 0; i < array.length; i++) {
+ iterator(array[i]);
+ }
+}
+
+function isEmpty(obj){
+ for(var i in obj){
+ if(obj.hasOwnProperty(i)) return false
+ }
+ return true
+}
+
+function initParams(uri, options, callback) {
+ var params = uri;
+
+ if (isFunction_1(options)) {
+ callback = options;
+ if (typeof uri === "string") {
+ params = {uri:uri};
+ }
+ } else {
+ params = immutable(options, {uri: uri});
+ }
+
+ params.callback = callback;
+ return params
+}
+
+function createXHR(uri, options, callback) {
+ options = initParams(uri, options, callback);
+ return _createXHR(options)
+}
+
+function _createXHR(options) {
+ if(typeof options.callback === "undefined"){
+ throw new Error("callback argument missing")
+ }
+
+ var called = false;
+ var callback = function cbOnce(err, response, body){
+ if(!called){
+ called = true;
+ options.callback(err, response, body);
+ }
+ };
+
+ function readystatechange() {
+ if (xhr.readyState === 4) {
+ setTimeout(loadFunc, 0);
+ }
+ }
+
+ function getBody() {
+ // Chrome with requestType=blob throws errors arround when even testing access to responseText
+ var body = undefined;
+
+ if (xhr.response) {
+ body = xhr.response;
+ } else {
+ body = xhr.responseText || getXml(xhr);
+ }
+
+ if (isJson) {
+ try {
+ body = JSON.parse(body);
+ } catch (e) {}
+ }
+
+ return body
+ }
+
+ function errorFunc(evt) {
+ clearTimeout(timeoutTimer);
+ if(!(evt instanceof Error)){
+ evt = new Error("" + (evt || "Unknown XMLHttpRequest Error") );
+ }
+ evt.statusCode = 0;
+ return callback(evt, failureResponse)
+ }
+
+ // will load the data & process the response in a special response object
+ function loadFunc() {
+ if (aborted) return
+ var status;
+ clearTimeout(timeoutTimer);
+ if(options.useXDR && xhr.status===undefined) {
+ //IE8 CORS GET successful response doesn't have a status field, but body is fine
+ status = 200;
+ } else {
+ status = (xhr.status === 1223 ? 204 : xhr.status);
+ }
+ var response = failureResponse;
+ var err = null;
+
+ if (status !== 0){
+ response = {
+ body: getBody(),
+ statusCode: status,
+ method: method,
+ headers: {},
+ url: uri,
+ rawRequest: xhr
+ };
+ if(xhr.getAllResponseHeaders){ //remember xhr can in fact be XDR for CORS in IE
+ response.headers = parseHeaders(xhr.getAllResponseHeaders());
+ }
+ } else {
+ err = new Error("Internal XMLHttpRequest Error");
+ }
+ return callback(err, response, response.body)
+ }
+
+ var xhr = options.xhr || null;
+
+ if (!xhr) {
+ if (options.cors || options.useXDR) {
+ xhr = new createXHR.XDomainRequest();
+ }else{
+ xhr = new createXHR.XMLHttpRequest();
+ }
+ }
+
+ var key;
+ var aborted;
+ var uri = xhr.url = options.uri || options.url;
+ var method = xhr.method = options.method || "GET";
+ var body = options.body || options.data;
+ var headers = xhr.headers = options.headers || {};
+ var sync = !!options.sync;
+ var isJson = false;
+ var timeoutTimer;
+ var failureResponse = {
+ body: undefined,
+ headers: {},
+ statusCode: 0,
+ method: method,
+ url: uri,
+ rawRequest: xhr
+ };
+
+ if ("json" in options && options.json !== false) {
+ isJson = true;
+ headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user
+ if (method !== "GET" && method !== "HEAD") {
+ headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user
+ body = JSON.stringify(options.json === true ? body : options.json);
+ }
+ }
+
+ xhr.onreadystatechange = readystatechange;
+ xhr.onload = loadFunc;
+ xhr.onerror = errorFunc;
+ // IE9 must have onprogress be set to a unique function.
+ xhr.onprogress = function () {
+ // IE must die
+ };
+ xhr.onabort = function(){
+ aborted = true;
+ };
+ xhr.ontimeout = errorFunc;
+ xhr.open(method, uri, !sync, options.username, options.password);
+ //has to be after open
+ if(!sync) {
+ xhr.withCredentials = !!options.withCredentials;
+ }
+ // Cannot set timeout with sync request
+ // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly
+ // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent
+ if (!sync && options.timeout > 0 ) {
+ timeoutTimer = setTimeout(function(){
+ if (aborted) return
+ aborted = true;//IE9 may still call readystatechange
+ xhr.abort("timeout");
+ var e = new Error("XMLHttpRequest timeout");
+ e.code = "ETIMEDOUT";
+ errorFunc(e);
+ }, options.timeout );
+ }
+
+ if (xhr.setRequestHeader) {
+ for(key in headers){
+ if(headers.hasOwnProperty(key)){
+ xhr.setRequestHeader(key, headers[key]);
+ }
+ }
+ } else if (options.headers && !isEmpty(options.headers)) {
+ throw new Error("Headers cannot be set on an XDomainRequest object")
+ }
+
+ if ("responseType" in options) {
+ xhr.responseType = options.responseType;
+ }
+
+ if ("beforeSend" in options &&
+ typeof options.beforeSend === "function"
+ ) {
+ options.beforeSend(xhr);
+ }
+
+ // Microsoft Edge browser sends "undefined" when send is called with undefined value.
+ // XMLHttpRequest spec says to pass null as body to indicate no body
+ // See https://github.com/naugtur/xhr/issues/100.
+ xhr.send(body || null);
+
+ return xhr
+
+
+}
+
+function getXml(xhr) {
+ // xhr.responseXML will throw Exception "InvalidStateError" or "DOMException"
+ // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML.
+ try {
+ if (xhr.responseType === "document") {
+ return xhr.responseXML
+ }
+ var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror";
+ if (xhr.responseType === "" && !firefoxBugTakenEffect) {
+ return xhr.responseXML
+ }
+ } catch (e) {}
+
+ return null
+}
+
+function noop() {}
+
+xhr.default = default_1;
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
+ return typeof obj;
+} : function (obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+};
+
+
+
+
+
+var asyncGenerator = function () {
+ function AwaitValue(value) {
+ this.value = value;
+ }
+
+ function AsyncGenerator(gen) {
+ var front, back;
+
+ function send(key, arg) {
+ return new Promise(function (resolve, reject) {
+ var request = {
+ key: key,
+ arg: arg,
+ resolve: resolve,
+ reject: reject,
+ next: null
+ };
+
+ if (back) {
+ back = back.next = request;
+ } else {
+ front = back = request;
+ resume(key, arg);
+ }
+ });
+ }
+
+ function resume(key, arg) {
+ try {
+ var result = gen[key](arg);
+ var value = result.value;
+
+ if (value instanceof AwaitValue) {
+ Promise.resolve(value.value).then(function (arg) {
+ resume("next", arg);
+ }, function (arg) {
+ resume("throw", arg);
+ });
+ } else {
+ settle(result.done ? "return" : "normal", result.value);
+ }
+ } catch (err) {
+ settle("throw", err);
+ }
+ }
+
+ function settle(type, value) {
+ switch (type) {
+ case "return":
+ front.resolve({
+ value: value,
+ done: true
+ });
+ break;
+
+ case "throw":
+ front.reject(value);
+ break;
+
+ default:
+ front.resolve({
+ value: value,
+ done: false
+ });
+ break;
+ }
+
+ front = front.next;
+
+ if (front) {
+ resume(front.key, front.arg);
+ } else {
+ back = null;
+ }
+ }
+
+ this._invoke = send;
+
+ if (typeof gen.return !== "function") {
+ this.return = undefined;
+ }
+ }
+
+ if (typeof Symbol === "function" && Symbol.asyncIterator) {
+ AsyncGenerator.prototype[Symbol.asyncIterator] = function () {
+ return this;
+ };
+ }
+
+ AsyncGenerator.prototype.next = function (arg) {
+ return this._invoke("next", arg);
+ };
+
+ AsyncGenerator.prototype.throw = function (arg) {
+ return this._invoke("throw", arg);
+ };
+
+ AsyncGenerator.prototype.return = function (arg) {
+ return this._invoke("return", arg);
+ };
+
+ return {
+ wrap: function (fn) {
+ return function () {
+ return new AsyncGenerator(fn.apply(this, arguments));
+ };
+ },
+ await: function (value) {
+ return new AwaitValue(value);
+ }
+ };
+}();
+
+
+
+
+
+var classCallCheck = function (instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+};
+
+var createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+}();
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+var toConsumableArray = function (arr) {
+ if (Array.isArray(arr)) {
+ for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
+
+ return arr2;
+ } else {
+ return Array.from(arr);
+ }
+};
+
+var lastError = '';
+
+/**
+ * Creates the HTLM for a failure message
+ * @param {string} canvasContainerId id of container of th
+ * canvas.
+ * @return {string} The html.
+ */
+function makeFailHTML(msg) {
+ return '\n<table style="background-color: #8CE; width: 100%; height: 100%;"><tr>\n<td align="center">\n<div style="display: table-cell; vertical-align: middle;">\n<div style="">' + msg + '</div>\n</div>\n</td></tr></table>\n';
+}
+
+/**
+ * Message for getting a webgl browser
+ * @type {string}
+ */
+var GET_A_WEBGL_BROWSER = '\n\tThis page requires a browser that supports WebGL.<br/>\n\t<a href="http://get.webgl.org">Click here to upgrade your browser.</a>\n';
+
+/**
+ * Message for need better hardware
+ * @type {string}
+ */
+var OTHER_PROBLEM = '\n\tIt does not appear your computer can support WebGL.<br/>\n\t<a href="http://get.webgl.org/troubleshooting/">Click here for more information.</a>\n';
+
+/**
+ * Code to return in `onError` callback when the browser doesn't support webgl
+ * @type {number}
+ */
+var ERROR_BROWSER_SUPPORT = 1;
+
+/**
+ * Code to return in `onError` callback there's any other problem related to webgl
+ * @type {number}
+ */
+var ERROR_OTHER = 2;
+
+/**
+ * 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,
+ * unless `onError` option is defined to a callback,
+ * which allows for custom error handling..
+ * @param {Element} canvas. The canvas element to create a
+ * context from.
+ * @param {WebGLContextCreationAttributes} optAttribs Any
+ * creation attributes you want to pass in.
+ * @return {WebGLRenderingContext} The created context.
+ */
+function setupWebGL(canvas, optAttribs, onError) {
+ function showLink(str) {
+ var container = canvas.parentNode;
+ if (container) {
+ container.innerHTML = makeFailHTML(str);
+ }
+ }
+
+ function handleError(errorCode, msg) {
+ if (typeof onError === 'function') {
+ onError(errorCode);
+ } else {
+ showLink(msg);
+ }
+ }
+
+ if (!window.WebGLRenderingContext) {
+ handleError(ERROR_BROWSER_SUPPORT, GET_A_WEBGL_BROWSER);
+ return null;
+ }
+
+ var context = create3DContext(canvas, optAttribs);
+ if (!context) {
+ handleError(ERROR_OTHER, OTHER_PROBLEM);
+ } else {
+ context.getExtension('OES_standard_derivatives');
+ }
+ return context;
+}
+
+/**
+ * Creates a webgl context.
+ * @param {!Canvas} canvas The canvas tag to get context
+ * from. If one is not passed in one will be created.
+ * @return {!WebGLContext} The created context.
+ */
+function create3DContext(canvas, optAttribs) {
+ var names = ['webgl', 'experimental-webgl'];
+ var context = null;
+ for (var ii = 0; ii < names.length; ++ii) {
+ try {
+ context = canvas.getContext(names[ii], optAttribs);
+ } catch (e) {
+ if (context) {
+ break;
+ }
+ }
+ }
+ return context;
+}
+
+/*
+ * Create a Vertex of a specific type (gl.VERTEX_SHADER/)
+ */
+function createShader(main, source, type, offset) {
+ var gl = main.gl;
+
+ var shader = gl.createShader(type);
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
+
+ if (!compiled) {
+ // Something went wrong during compilation; get the error
+ lastError = gl.getShaderInfoLog(shader);
+ console.error('*** Error compiling shader ' + shader + ':' + lastError);
+ main.trigger('error', {
+ shader: shader,
+ source: source,
+ type: type,
+ error: lastError,
+ offset: offset || 0
+ });
+ gl.deleteShader(shader);
+ return null;
+ }
+
+ return shader;
+}
+
+/**
+ * Loads a shader.
+ * @param {!WebGLContext} gl The WebGLContext to use.
+ * @param {string} shaderSource The shader source.
+ * @param {number} shaderType The type of shader.
+ * @param {function(string): void) opt_errorCallback callback for errors.
+ * @return {!WebGLShader} The created shader.
+ */
+function createProgram(main, shaders, optAttribs, optLocations) {
+ var gl = main.gl;
+
+ var program = gl.createProgram();
+ for (var ii = 0; ii < shaders.length; ++ii) {
+ gl.attachShader(program, shaders[ii]);
+ }
+ if (optAttribs) {
+ for (var _ii = 0; _ii < optAttribs.length; ++_ii) {
+ gl.bindAttribLocation(program, optLocations ? optLocations[_ii] : _ii, optAttribs[_ii]);
+ }
+ }
+ gl.linkProgram(program);
+
+ // Check the link status
+ var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
+ if (!linked) {
+ // something went wrong with the link
+ lastError = gl.getProgramInfoLog(program);
+ console.log('Error in program linking:' + lastError);
+ gl.deleteProgram(program);
+ return null;
+ }
+ return program;
+}
+
+// By Brett Camber on
+// https://github.com/tangrams/tangram/blob/master/src/gl/glsl.js
+function parseUniforms(uniforms) {
+ var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+
+ var parsed = [];
+
+ for (var name in uniforms) {
+ var uniform = uniforms[name];
+ var u = void 0;
+
+ if (prefix) {
+ name = prefix + '.' + name;
+ }
+
+ // Single float
+ if (typeof uniform === 'number') {
+ parsed.push({
+ type: 'float',
+ method: '1f',
+ name: name,
+ value: uniform
+ });
+ }
+ // Array: vector, array of floats, array of textures, or array of structs
+ else if (Array.isArray(uniform)) {
+ // Numeric values
+ if (typeof uniform[0] === 'number') {
+ // float vectors (vec2, vec3, vec4)
+ if (uniform.length === 1) {
+ parsed.push({
+ type: 'float',
+ method: '1f',
+ name: name,
+ value: uniform
+ });
+ }
+ // float vectors (vec2, vec3, vec4)
+ else if (uniform.length >= 2 && uniform.length <= 4) {
+ parsed.push({
+ type: 'vec' + uniform.length,
+ method: uniform.length + 'fv',
+ name: name,
+ value: uniform
+ });
+ }
+ // float array
+ else if (uniform.length > 4) {
+ parsed.push({
+ type: 'float[]',
+ method: '1fv',
+ name: name + '[0]',
+ value: uniform
+ });
+ }
+ // TODO: assume matrix for (typeof == Float32Array && length == 16)?
+ }
+ // Array of textures
+ else if (typeof uniform[0] === 'string') {
+ parsed.push({
+ type: 'sampler2D',
+ method: '1i',
+ name: name,
+ value: uniform
+ });
+ }
+ // Array of arrays - but only arrays of vectors are allowed in this case
+ else if (Array.isArray(uniform[0]) && typeof uniform[0][0] === 'number') {
+ // float vectors (vec2, vec3, vec4)
+ if (uniform[0].length >= 2 && uniform[0].length <= 4) {
+ // Set each vector in the array
+ for (u = 0; u < uniform.length; u++) {
+ parsed.push({
+ type: 'vec' + uniform[0].length,
+ method: uniform[u].length + 'fv',
+ name: name + '[' + u + ']',
+ value: uniform[u]
+ });
+ }
+ }
+ // else error?
+ }
+ // Array of structures
+ else if (_typeof(uniform[0]) === 'object') {
+ for (u = 0; u < uniform.length; u++) {
+ // Set each struct in the array
+ parsed.push.apply(parsed, toConsumableArray(parseUniforms(uniform[u], name + '[' + u + ']')));
+ }
+ }
+ }
+ // Boolean
+ else if (typeof uniform === 'boolean') {
+ parsed.push({
+ type: 'bool',
+ method: '1i',
+ name: name,
+ value: uniform
+ });
+ }
+ // Texture
+ else if (typeof uniform === 'string') {
+ parsed.push({
+ type: 'sampler2D',
+ method: '1i',
+ name: name,
+ value: uniform
+ });
+ }
+ // Structure
+ else if ((typeof uniform === 'undefined' ? 'undefined' : _typeof(uniform)) === 'object') {
+ // Set each field in the struct
+ parsed.push.apply(parsed, toConsumableArray(parseUniforms(uniform, name)));
+ }
+ // TODO: support other non-float types? (int, etc.)
+ }
+ return parsed;
+}
+
+function isCanvasVisible(canvas) {
+ var bound = canvas.getBoundingClientRect();
+ return bound.top + bound.height > 0 && bound.top < (window.innerHeight || document.documentElement.clientHeight);
+}
+
+function isPowerOf2(value) {
+ return (value & value - 1) === 0;
+}
+
+function isSafari() {
+ return (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
+ );
+}
+
+
+
+
+
+
+
+function isDiff(a, b) {
+ if (a && b) {
+ return a.toString() !== b.toString();
+ }
+ return false;
+}
+
+function getFile(url) {
+ var httpRequest = new XMLHttpRequest();
+ httpRequest.open("GET", url, false);
+ httpRequest.send();
+ if (httpRequest.status == 200) return httpRequest.responseText;else return "";
+}
+
+function subscribeMixin$1(target) {
+ var listeners = new Set();
+
+ return Object.assign(target, {
+ on: function on(type, f) {
+ var listener = {};
+ listener[type] = f;
+ listeners.add(listener);
+ },
+ off: function off(type, f) {
+ if (f) {
+ var listener = {};
+ listener[type] = f;
+ listeners.delete(listener);
+ } else {
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = listeners[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var item = _step.value;
+ var _iteratorNormalCompletion2 = true;
+ var _didIteratorError2 = false;
+ var _iteratorError2 = undefined;
+
+ try {
+ for (var _iterator2 = Object.keys(item)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+ var key = _step2.value;
+
+ if (key === type) {
+ listeners.delete(item);
+ return;
+ }
+ }
+ } catch (err) {
+ _didIteratorError2 = true;
+ _iteratorError2 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion2 && _iterator2.return) {
+ _iterator2.return();
+ }
+ } finally {
+ if (_didIteratorError2) {
+ throw _iteratorError2;
+ }
+ }
+ }
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator.return) {
+ _iterator.return();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+ }
+ },
+ listSubscriptions: function listSubscriptions() {
+ var _iteratorNormalCompletion3 = true;
+ var _didIteratorError3 = false;
+ var _iteratorError3 = undefined;
+
+ try {
+ for (var _iterator3 = listeners[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
+ var item = _step3.value;
+
+ console.log(item);
+ }
+ } catch (err) {
+ _didIteratorError3 = true;
+ _iteratorError3 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion3 && _iterator3.return) {
+ _iterator3.return();
+ }
+ } finally {
+ if (_didIteratorError3) {
+ throw _iteratorError3;
+ }
+ }
+ }
+ },
+ subscribe: function subscribe(listener) {
+ listeners.add(listener);
+ },
+ unsubscribe: function unsubscribe(listener) {
+ listeners.delete(listener);
+ },
+ unsubscribeAll: function unsubscribeAll() {
+ listeners.clear();
+ },
+ trigger: function trigger(event) {
+ for (var _len = arguments.length, data = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ data[_key - 1] = arguments[_key];
+ }
+
+ var _iteratorNormalCompletion4 = true;
+ var _didIteratorError4 = false;
+ var _iteratorError4 = undefined;
+
+ try {
+ for (var _iterator4 = listeners[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
+ var listener = _step4.value;
+
+ if (typeof listener[event] === 'function') {
+ listener[event].apply(listener, toConsumableArray(data));
+ }
+ }
+ } catch (err) {
+ _didIteratorError4 = true;
+ _iteratorError4 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion4 && _iterator4.return) {
+ _iterator4.return();
+ }
+ } finally {
+ if (_didIteratorError4) {
+ throw _iteratorError4;
+ }
+ }
+ }
+ }
+ });
+}
+
+// Texture management
+// GL texture wrapper object for keeping track of a global set of textures, keyed by a unique user-defined name
+
+var Texture = function () {
+ function Texture(gl, name) {
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+ classCallCheck(this, Texture);
+
+ subscribeMixin$1(this);
+
+ this.gl = gl;
+ this.texture = gl.createTexture();
+ if (this.texture) {
+ this.valid = true;
+ }
+ this.bind();
+
+ this.name = name;
+ this.source = null;
+ this.sourceType = null;
+ this.loading = null; // a Promise object to track the loading state of this texture
+
+ // Default to a 1-pixel black texture so we can safely render while we wait for an image to load
+ // See: http://stackoverflow.com/questions/19722247/webgl-wait-for-texture-to-load
+ this.setData(1, 1, new Uint8Array([0, 0, 0, 255]), { filtering: 'linear' });
+ this.setFiltering(options.filtering);
+
+ this.load(options);
+ }
+
+ // Destroy a single texture instance
+
+
+ createClass(Texture, [{
+ key: 'destroy',
+ value: function destroy() {
+ if (!this.valid) {
+ return;
+ }
+ this.gl.deleteTexture(this.texture);
+ this.texture = null;
+ delete this.data;
+ this.data = null;
+ this.valid = false;
+ }
+ }, {
+ key: 'bind',
+ value: function bind(unit) {
+ if (!this.valid) {
+ return;
+ }
+ if (typeof unit === 'number') {
+ if (Texture.activeUnit !== unit) {
+ this.gl.activeTexture(this.gl.TEXTURE0 + unit);
+ Texture.activeUnit = unit;
+ }
+ }
+ if (Texture.activeTexture !== this.texture) {
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
+ Texture.activeTexture = this.texture;
+ }
+ }
+ }, {
+ key: 'load',
+ value: function load() {
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ this.loading = null;
+
+ if (typeof options.url === 'string') {
+ if (this.url === undefined || options.url !== this.url) {
+ this.setUrl(options.url, options);
+ }
+ } else if (options.element) {
+ this.setElement(options.element, options);
+ } else if (options.data && options.width && options.height) {
+ this.setData(options.width, options.height, options.data, options);
+ }
+ }
+
+ // Sets texture from an url
+
+ }, {
+ key: 'setUrl',
+ value: function setUrl(url) {
+ var _this = this;
+
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ if (!this.valid) {
+ return;
+ }
+
+ this.url = url; // save URL reference (will be overwritten when element is loaded below)
+ this.source = this.url;
+ this.sourceType = 'url';
+
+ this.loading = new Promise(function (resolve, reject) {
+ var ext = url.split('.').pop().toLowerCase();
+ var isVideo = ext === 'ogv' || ext === 'webm' || ext === 'mp4';
+
+ var element = undefined;
+ if (isVideo) {
+ element = document.createElement('video');
+ element.autoplay = true;
+
+ element.muted = true; /* required for modern browsers to autoplay video */
+ setTimeout(function () {
+ element.play(); /* doesn't block promise but needs a more elegant solution */
+ }, 1);
+
+ options.filtering = 'nearest';
+ // element.preload = 'auto';
+ // element.style.display = 'none';
+ // document.body.appendChild(element);
+ } else {
+ element = new Image();
+ }
+
+ element.onload = function () {
+ try {
+ _this.setElement(element, options);
+ } catch (e) {
+ console.log('Texture \'' + _this.name + '\': failed to load url: \'' + _this.source + '\'', e, options);
+ }
+ resolve(_this);
+ };
+ element.onerror = function (e) {
+ // Warn and resolve on error
+ console.log('Texture \'' + _this.name + '\': failed to load url: \'' + _this.source + '\'', e, options);
+ resolve(_this);
+ };
+
+ // Safari has a bug loading data-URL elements with CORS enabled, so it must be disabled in that case
+ // https://bugs.webkit.org/show_bug.cgi?id=123978
+ if (!(isSafari() && _this.source.slice(0, 5) === 'data:')) {
+ element.crossOrigin = 'anonymous';
+ }
+
+ element.src = _this.source;
+ if (isVideo) {
+ _this.setElement(element, options);
+ }
+ });
+ return this.loading;
+ }
+
+ // Sets texture to a raw image buffer
+
+ }, {
+ key: 'setData',
+ value: function setData(width, height, data) {
+ var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
+
+ this.width = width;
+ this.height = height;
+
+ this.source = data;
+ this.sourceType = 'data';
+
+ this.update(options);
+ this.setFiltering(options);
+
+ this.loading = Promise.resolve(this);
+ return this.loading;
+ }
+
+ // Sets the texture to track a element (canvas/image)
+
+ }, {
+ key: 'setElement',
+ value: function setElement(element, options) {
+ var _this2 = this;
+
+ var el = element;
+
+ // a string element is interpeted as a CSS selector
+ if (typeof element === 'string') {
+ element = document.querySelector(element);
+ }
+
+ if (element instanceof HTMLCanvasElement || element instanceof HTMLImageElement || element instanceof HTMLVideoElement) {
+ this.source = element;
+ this.sourceType = 'element';
+
+ if (element instanceof HTMLVideoElement) {
+ this.width = this.source.videoWidth;
+ this.height = this.source.videoHeight;
+ element.addEventListener('canplaythrough', function () {
+ _this2.intervalID = setInterval(function () {
+ _this2.update(options);
+ }, 15);
+ }, true);
+ element.addEventListener('ended', function () {
+ element.currentTime = 0;
+ element.play();
+ }, true);
+ } else {
+ this.update(options);
+ }
+ this.setFiltering(options);
+ } else {
+ var msg = 'the \'element\' parameter (`element: ' + JSON.stringify(el) + '`) must be a CSS ';
+ msg += 'selector string, or a <canvas>, <image> or <video> object';
+ console.log('Texture \'' + this.name + '\': ' + msg, options);
+ }
+
+ this.loading = Promise.resolve(this);
+ return this.loading;
+ }
+
+ // Uploads current image or buffer to the GPU (can be used to update animated textures on the fly)
+
+ }, {
+ key: 'update',
+ value: function update() {
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ if (!this.valid) {
+ return;
+ }
+
+ this.bind();
+ this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, options.UNPACK_FLIP_Y_WEBGL === false ? false : true);
+ this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, options.UNPACK_PREMULTIPLY_ALPHA_WEBGL || false);
+
+ // Image or Canvas element
+ if (this.sourceType === 'element' && (this.source instanceof HTMLCanvasElement || this.source instanceof HTMLVideoElement || this.source instanceof HTMLImageElement && this.source.complete)) {
+ if (this.source instanceof HTMLVideoElement) {
+ this.width = this.source.videoWidth;
+ this.height = this.source.videoHeight;
+ } else {
+ this.width = this.source.width;
+ this.height = this.source.height;
+ }
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.source);
+ }
+ // Raw image buffer
+ else if (this.sourceType === 'data') {
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.width, this.height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.source);
+ }
+ this.trigger('loaded', this);
+ }
+
+ // Determines appropriate filtering mode
+
+ }, {
+ key: 'setFiltering',
+ value: function setFiltering() {
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ if (!this.valid) {
+ return;
+ }
+
+ this.powerOf2 = isPowerOf2(this.width) && isPowerOf2(this.height);
+ // let defualtFilter = (this.powerOf2 ? 'mipmap' : 'linear');
+ // this.filtering = options.filtering || defualtFilter;
+ this.filtering = options.filtering || 'linear';
+
+ var gl = this.gl;
+ this.bind();
+
+ // For power-of-2 textures, the following presets are available:
+ // mipmap: linear blend from nearest mip
+ // linear: linear blend from original image (no mips)
+ // nearest: nearest pixel from original image (no mips, 'blocky' look)
+ if (this.powerOf2) {
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, options.TEXTURE_WRAP_S || gl.REPEAT);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, options.TEXTURE_WRAP_T || gl.REPEAT);
+
+ if (this.filtering === 'mipmap') {
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); // TODO: use trilinear filtering by defualt instead?
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ gl.generateMipmap(gl.TEXTURE_2D);
+ } else if (this.filtering === 'linear') {
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ } else if (this.filtering === 'nearest') {
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ }
+ } else {
+ // WebGL has strict requirements on non-power-of-2 textures:
+ // No mipmaps and must clamp to edge
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+
+ if (this.filtering === 'mipmap') {
+ this.filtering = 'linear';
+ }
+
+ if (this.filtering === 'nearest') {
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ } else {
+ // default to linear for non-power-of-2 textures
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ }
+ }
+ }
+ }]);
+ return Texture;
+}();
+
+Texture.getMaxTextureSize = function (gl) {
+ return gl.getParameter(gl.MAX_TEXTURE_SIZE);
+};
+
+// Global set of textures, by name
+Texture.activeUnit = -1;
+
+/*
+The MIT License (MIT)
+
+Copyright (c) 2015 Patricio Gonzalez Vivo ( http://www.patriciogonzalezvivo.com )
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the 'Software'), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+var GlslCanvas = function () {
+ function GlslCanvas(canvas, contextOptions, options) {
+ var _this = this;
+
+ classCallCheck(this, GlslCanvas);
+
+ subscribeMixin$1(this);
+
+ contextOptions = contextOptions || {};
+ options = options || {};
+
+ if (canvas.hasAttribute('data-fullscreen') && (canvas.getAttribute('data-fullscreen') == "1" || canvas.getAttribute('data-fullscreen') == "true")) {
+ this.width = window.innerWidth;
+ this.height = window.innerHeight;
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ } else {
+ this.width = canvas.clientWidth;
+ this.height = canvas.clientHeight;
+ }
+
+ this.canvas = canvas;
+ this.gl = undefined;
+ this.deps = {};
+ this.program = undefined;
+ this.textures = {};
+ this.buffers = {};
+ this.uniforms = {};
+ this.vbo = {};
+ this.isValid = false;
+ this.animationFrameRequest = undefined;
+
+ this.BUFFER_COUNT = 0;
+ // this.TEXTURE_COUNT = 0;
+
+ this.vertexString = contextOptions.vertexString || '\n#ifdef GL_ES\nprecision mediump float;\n#endif\n\nattribute vec2 a_position;\nattribute vec2 a_texcoord;\n\nvarying vec2 v_texcoord;\n\nvoid main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n v_texcoord = a_texcoord;\n}\n';
+ this.fragmentString = contextOptions.fragmentString || '\n#ifdef GL_ES\nprecision mediump float;\n#endif\n\nvarying vec2 v_texcoord;\n\nvoid main(){\n gl_FragColor = vec4(0.0);\n}\n';
+
+ // GL Context
+ var gl = setupWebGL(canvas, contextOptions, options.onError);
+ if (!gl) {
+ return;
+ }
+ this.gl = gl;
+ this.timeLoad = this.timePrev = performance.now();
+ this.timeDelta = 0.0;
+ this.forceRender = true;
+ this.paused = false;
+ this.realToCSSPixels = window.devicePixelRatio || 1;
+
+ // Allow alpha
+ canvas.style.backgroundColor = contextOptions.backgroundColor || 'rgba(1,1,1,0)';
+
+ // Load shader
+ if (canvas.hasAttribute('data-fragment')) {
+ this.fragmentString = canvas.getAttribute('data-fragment');
+ } else if (canvas.hasAttribute('data-fragment-url')) {
+ var source = canvas.getAttribute('data-fragment-url');
+ xhr.get(source, function (error, response, body) {
+ _this.load(body, _this.vertexString);
+ });
+ }
+
+ // Load shader
+ if (canvas.hasAttribute('data-vertex')) {
+ this.vertexString = canvas.getAttribute('data-vertex');
+ } else if (canvas.hasAttribute('data-vertex-url')) {
+ var _source = canvas.getAttribute('data-vertex-url');
+ xhr.get(_source, function (error, response, body) {
+ _this.load(_this.fragmentString, body);
+ });
+ }
+
+ this.load();
+
+ if (!this.program) {
+ return;
+ }
+
+ // Define Vertex buffer
+ var texCoordsLoc = gl.getAttribLocation(this.program, 'a_texcoord');
+ this.vbo.texCoords = gl.createBuffer();
+ this.gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo.texCoords);
+ this.gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]), gl.STATIC_DRAW);
+ this.gl.enableVertexAttribArray(texCoordsLoc);
+ this.gl.vertexAttribPointer(texCoordsLoc, 2, gl.FLOAT, false, 0, 0);
+
+ var verticesLoc = gl.getAttribLocation(this.program, 'a_position');
+ this.vbo.vertices = gl.createBuffer();
+ this.gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo.vertices);
+ this.gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), gl.STATIC_DRAW);
+ this.gl.enableVertexAttribArray(verticesLoc);
+ this.gl.vertexAttribPointer(verticesLoc, 2, gl.FLOAT, false, 0, 0);
+
+ // load TEXTURES
+ if (canvas.hasAttribute('data-textures')) {
+ var imgList = canvas.getAttribute('data-textures').split(',');
+ for (var nImg in imgList) {
+ this.setUniform('u_tex' + nImg, imgList[nImg]);
+ }
+ }
+
+ // ========================== EVENTS
+ var mouse = {
+ x: 0,
+ y: 0
+ };
+ document.addEventListener('mousemove', function (e) {
+ mouse.x = e.clientX || e.pageX;
+ mouse.y = e.clientY || e.pageY;
+ }, false);
+
+ var sandbox = this;
+ function RenderLoop() {
+ if (sandbox.nMouse > 1) {
+ sandbox.setMouse(mouse);
+ }
+
+ if (sandbox.resize()) {
+ sandbox.forceRender = true;
+ }
+
+ sandbox.render();
+ sandbox.animationFrameRequest = window.requestAnimationFrame(RenderLoop);
+ }
+
+ // Start
+ this.setMouse({ x: 0, y: 0 });
+ RenderLoop();
+ return this;
+ }
+
+ createClass(GlslCanvas, [{
+ key: 'destroy',
+ value: function destroy() {
+ // Stop the animation
+ cancelAnimationFrame(this.animationFrameRequest);
+
+ this.animated = false;
+ this.isValid = false;
+ for (var tex in this.textures) {
+ if (tex.destroy) {
+ tex.destroy();
+ }
+ }
+ this.textures = {};
+ for (var att in this.attribs) {
+ this.gl.deleteBuffer(this.attribs[att]);
+ }
+ this.gl.useProgram(null);
+ this.gl.deleteProgram(this.program);
+ for (var key in this.buffers) {
+ var buffer = this.buffers[key];
+ this.gl.deleteProgram(buffer.program);
+ }
+
+ this.program = null;
+ this.gl = null;
+ }
+ }, {
+ key: 'load',
+ value: function load(fragString, vertString) {
+ var _this2 = this;
+
+ // Load vertex shader if there is one
+ if (vertString) {
+ this.vertexString = vertString;
+ }
+
+ // Load fragment shader if there is one
+ if (fragString) {
+ this.fragmentString = fragString;
+ }
+
+ var lines = this.fragmentString.split(/\r?\n/);
+ this.fragmentString = "#define PLATFORM_WEBGL\n#line 0\n";
+
+ lines.forEach(function (line, i) {
+ var line_trim = line.trim();
+ if (line_trim.startsWith('#include \"lygia')) {
+ var dep = line_trim.substring(15).replace(/\'|\"|\;|\s/g, '');
+ if (dep.endsWith('glsl')) {
+ if (_this2.deps[dep] === undefined) {
+ var url = "https://lygia.xyz" + dep;
+ _this2.deps[dep] = getFile(url);
+ }
+ _this2.fragmentString += _this2.deps[dep] + '\n#line ' + (i + 1) + '\n';
+ }
+ } else _this2.fragmentString += line + '\n';
+ });
+
+ // console.log(this.fragmentString);
+
+ this.animated = false;
+ this.nDelta = (this.fragmentString.match(/u_delta/g) || []).length;
+ this.nTime = (this.fragmentString.match(/u_time/g) || []).length;
+ this.nDate = (this.fragmentString.match(/u_date/g) || []).length;
+ this.nMouse = (this.fragmentString.match(/u_mouse/g) || []).length;
+ this.animated = this.nDate > 1 || this.nTime > 1 || this.nMouse > 1;
+
+ var nTextures = this.fragmentString.search(/sampler2D/g);
+ if (nTextures) {
+ var _lines = this.fragmentString.split('\n');
+ for (var i = 0; i < _lines.length; i++) {
+ var match = _lines[i].match(/uniform\s*sampler2D\s*([\w]*);\s*\/\/\s*([\w|\:\/\/|\.|\-|\_]*)/i);
+ if (match) {
+ var ext = match[2].split('.').pop().toLowerCase();
+ if (match[1] && match[2] && (ext === 'jpg' || ext === 'jpeg' || ext === 'png' || ext === 'ogv' || ext === 'webm' || ext === 'mp4')) {
+ this.setUniform(match[1], match[2]);
+ }
+ }
+ var main = _lines[i].match(/\s*void\s*main\s*/g);
+ if (main) {
+ break;
+ }
+ }
+ }
+
+ var vertexShader = createShader(this, this.vertexString, this.gl.VERTEX_SHADER);
+ var fragmentShader = createShader(this, this.fragmentString, this.gl.FRAGMENT_SHADER);
+
+ // If Fragment shader fails load a empty one to sign the error
+ if (!fragmentShader) {
+ fragmentShader = createShader(this, 'void main(){\n\tgl_FragColor = vec4(1.0);\n}', this.gl.FRAGMENT_SHADER);
+ this.isValid = false;
+ } else {
+ this.isValid = true;
+ }
+
+ // Create and use program
+ var program = createProgram(this, [vertexShader, fragmentShader]); //, [0,1],['a_texcoord','a_position']);
+ this.gl.useProgram(program);
+
+ // Delete shaders
+ // this.gl.detachShader(program, vertexShader);
+ // this.gl.detachShader(program, fragmentShader);
+ this.gl.deleteShader(vertexShader);
+ this.gl.deleteShader(fragmentShader);
+
+ this.program = program;
+ this.change = true;
+
+ this.BUFFER_COUNT = 0;
+ var buffers = this.getBuffers(this.fragmentString);
+ if (Object.keys(buffers).length) {
+ this.loadPrograms(buffers);
+ }
+ this.buffers = buffers;
+ this.texureIndex = this.BUFFER_COUNT;
+
+ // Trigger event
+ this.trigger('load', {});
+
+ this.forceRender = true;
+ this.render();
+ }
+ }, {
+ key: 'test',
+ value: function test(callback, fragString, vertString) {
+ // Thanks to @thespite for the help here
+ // https://www.khronos.org/registry/webgl/extensions/EXT_disjoint_timer_query/
+ var pre_test_vert = this.vertexString;
+ var pre_test_frag = this.fragmentString;
+ var pre_test_paused = this.paused;
+
+ var ext = this.gl.getExtension('EXT_disjoint_timer_query');
+ var query = ext.createQueryEXT();
+ var wasValid = this.isValid;
+
+ if (fragString || vertString) {
+ this.load(fragString, vertString);
+ wasValid = this.isValid;
+ this.forceRender = true;
+ this.render();
+ }
+
+ this.paused = true;
+ ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);
+ this.forceRender = true;
+ this.render();
+ ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
+
+ var sandbox = this;
+ function finishTest() {
+ // Revert changes... go back to normal
+ sandbox.paused = pre_test_paused;
+ if (fragString || vertString) {
+ sandbox.load(pre_test_frag, pre_test_vert);
+ }
+ }
+ function waitForTest() {
+ sandbox.forceRender = true;
+ sandbox.render();
+ var available = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT);
+ var disjoint = sandbox.gl.getParameter(ext.GPU_DISJOINT_EXT);
+ if (available && !disjoint) {
+ var ret = {
+ wasValid: wasValid,
+ frag: fragString || sandbox.fragmentString,
+ vert: vertString || sandbox.vertexString,
+ timeElapsedMs: ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT) / 1000000.0
+ };
+ finishTest();
+ callback(ret);
+ } else {
+ window.requestAnimationFrame(waitForTest);
+ }
+ }
+ waitForTest();
+ }
+ }, {
+ key: 'loadTexture',
+ value: function loadTexture(name, urlElementOrData, options) {
+ var _this3 = this;
+
+ if (!options) {
+ options = {};
+ }
+
+ if (typeof urlElementOrData === 'string') {
+ options.url = urlElementOrData;
+ } else if ((typeof urlElementOrData === 'undefined' ? 'undefined' : _typeof(urlElementOrData)) === 'object' && urlElementOrData.data && urlElementOrData.width && urlElementOrData.height) {
+ options.data = urlElementOrData.data;
+ options.width = urlElementOrData.width;
+ options.height = urlElementOrData.height;
+ } else if ((typeof urlElementOrData === 'undefined' ? 'undefined' : _typeof(urlElementOrData)) === 'object') {
+ options.element = urlElementOrData;
+ }
+
+ if (this.textures[name]) {
+ if (this.textures[name]) {
+ this.textures[name].load(options);
+ this.textures[name].on('loaded', function (args) {
+ _this3.forceRender = true;
+ });
+ }
+ } else {
+ this.textures[name] = new Texture(this.gl, name, options);
+ this.textures[name].on('loaded', function (args) {
+ _this3.forceRender = true;
+ });
+ }
+ }
+ }, {
+ key: 'refreshUniforms',
+ value: function refreshUniforms() {
+ this.uniforms = {};
+ }
+ }, {
+ key: 'setUniform',
+ value: function setUniform(name) {
+ var u = {};
+
+ for (var _len = arguments.length, value = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ value[_key - 1] = arguments[_key];
+ }
+
+ u[name] = value;
+ this.setUniforms(u);
+ }
+ }, {
+ key: 'setUniforms',
+ value: function setUniforms(uniforms) {
+ var parsed = parseUniforms(uniforms);
+ // Set each uniform
+ for (var u in parsed) {
+ if (parsed[u].type === 'sampler2D') {
+ // For textures, we need to track texture units, so we have a special setter
+ // this.uniformTexture(parsed[u].name, parsed[u].value[0]);
+ this.loadTexture(parsed[u].name, parsed[u].value[0]);
+ } else {
+ this.uniform(parsed[u].method, parsed[u].type, parsed[u].name, parsed[u].value);
+ }
+ }
+ this.forceRender = true;
+ }
+ }, {
+ key: 'setMouse',
+ value: function setMouse(mouse) {
+ // set the mouse uniform
+ var rect = this.canvas.getBoundingClientRect();
+ if (mouse && mouse.x && mouse.x >= rect.left && mouse.x <= rect.right && mouse.y && mouse.y >= rect.top && mouse.y <= rect.bottom) {
+
+ var mouse_x = (mouse.x - rect.left) * this.realToCSSPixels;
+ var mouse_y = this.canvas.height - (mouse.y - rect.top) * this.realToCSSPixels;
+
+ this.uniform('2f', 'vec2', 'u_mouse', mouse_x, mouse_y);
+ }
+ }
+
+ // ex: program.uniform('3f', 'position', x, y, z);
+
+ }, {
+ key: 'uniform',
+ value: function uniform(method, type, name) {
+ // 'value' is a method-appropriate arguments list
+ this.uniforms[name] = this.uniforms[name] || {};
+ var uniform = this.uniforms[name];
+
+ for (var _len2 = arguments.length, value = Array(_len2 > 3 ? _len2 - 3 : 0), _key2 = 3; _key2 < _len2; _key2++) {
+ value[_key2 - 3] = arguments[_key2];
+ }
+
+ var change = isDiff(uniform.value, value);
+
+ // remember and keep track of uniforms location to save calls
+ if (change || this.change || !uniform.location || !uniform.value) {
+ uniform.name = name;
+ uniform.type = type;
+ uniform.value = value;
+ uniform.method = 'uniform' + method;
+ this.gl.useProgram(this.program);
+ uniform.location = this.gl.getUniformLocation(this.program, name);
+ this.gl[uniform.method].apply(this.gl, [uniform.location].concat(uniform.value));
+ // If there is change update and there is buffer update manually one by one
+ for (var key in this.buffers) {
+ var buffer = this.buffers[key];
+ this.gl.useProgram(buffer.program);
+ var location = this.gl.getUniformLocation(buffer.program, name);
+ this.gl[uniform.method].apply(this.gl, [location].concat(uniform.value));
+ }
+ }
+ }
+ }, {
+ key: 'uniformTexture',
+ value: function uniformTexture(name, texture, options) {
+ if (this.textures[name] === undefined) {
+ this.loadTexture(name, texture, options);
+ } else {
+ this.uniform('1i', 'sampler2D', name, this.texureIndex);
+
+ for (var key in this.buffers) {
+ var buffer = this.buffers[key];
+ this.gl.useProgram(buffer.program);
+ this.gl.activeTexture(this.gl.TEXTURE0 + this.texureIndex);
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[name].texture);
+ }
+ this.gl.useProgram(this.program);
+ this.gl.activeTexture(this.gl.TEXTURE0 + this.texureIndex);
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[name].texture);
+ this.uniform('2f', 'vec2', name + 'Resolution', this.textures[name].width, this.textures[name].height);
+ }
+ }
+ }, {
+ key: 'resize',
+ value: function resize() {
+ if (this.width !== this.canvas.clientWidth || this.height !== this.canvas.clientHeight) {
+ this.realToCSSPixels = window.devicePixelRatio || 1;
+
+ // Lookup the size the browser is displaying the canvas in CSS pixels
+ // and compute a size needed to make our drawingbuffer match it in
+ // device pixels.
+ var displayWidth = Math.floor(this.gl.canvas.clientWidth * this.realToCSSPixels);
+ var displayHeight = Math.floor(this.gl.canvas.clientHeight * this.realToCSSPixels);
+
+ // Check if the canvas is not the same size.
+ if (this.gl.canvas.width !== displayWidth || this.gl.canvas.height !== displayHeight) {
+ // Make the canvas the same size
+ this.gl.canvas.width = displayWidth;
+ this.gl.canvas.height = displayHeight;
+ // Set the viewport to match
+ this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
+ }
+ this.width = this.canvas.clientWidth;
+ this.height = this.canvas.clientHeight;
+ this.resizeSwappableBuffers();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }, {
+ key: 'render',
+ value: function render() {
+ this.visible = isCanvasVisible(this.canvas);
+ if (this.forceRender || this.change || this.animated && this.visible && !this.paused) {
+
+ // Update Uniforms when are need
+ var date = new Date();
+ var now = performance.now();
+ this.timeDelta = (now - this.timePrev) / 1000.0;
+ this.timePrev = now;
+ if (this.nDelta > 1) {
+ // set the delta time uniform
+ this.uniform('1f', 'float', 'u_delta', this.timeDelta);
+ }
+
+ if (this.nTime > 1) {
+ // set the elapsed time uniform
+ this.uniform('1f', 'float', 'u_time', (now - this.timeLoad) / 1000.0);
+ }
+
+ if (this.nDate) {
+ // Set date uniform: year/month/day/time_in_sec
+ this.uniform('4f', 'float', 'u_date', date.getFullYear(), date.getMonth(), date.getDate(), date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds() * 0.001);
+ }
+
+ // set the resolution uniform
+ this.uniform('2f', 'vec2', 'u_resolution', this.canvas.width, this.canvas.height);
+
+ for (var key in this.buffers) {
+ var buffer = this.buffers[key];
+ this.uniform('1i', 'sampler2D', buffer.name, buffer.bundle.input.index);
+ }
+
+ this.texureIndex = this.BUFFER_COUNT;
+ for (var tex in this.textures) {
+ this.uniformTexture(tex);
+ this.texureIndex++;
+ }
+
+ this.renderPrograms();
+
+ // Trigger event
+ this.trigger('render', {});
+ this.change = false;
+ this.forceRender = false;
+ }
+ }
+ }, {
+ key: 'pause',
+ value: function pause() {
+ this.paused = true;
+ }
+ }, {
+ key: 'play',
+ value: function play() {
+ this.paused = false;
+ }
+
+ // render main and buffers programs
+
+ }, {
+ key: 'renderPrograms',
+ value: function renderPrograms() {
+ var gl = this.gl;
+ var W = gl.canvas.width;
+ var H = gl.canvas.height;
+
+ gl.viewport(0, 0, W, H);
+
+ for (var key in this.buffers) {
+ var buffer = this.buffers[key];
+ buffer.bundle.render(W, H, buffer.program, buffer.name);
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ }
+
+ gl.useProgram(this.program);
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+ }
+
+ // parse input strings
+
+ }, {
+ key: 'getBuffers',
+ value: function getBuffers(fragString) {
+ var buffers = {};
+ if (fragString) {
+ fragString.replace(/(?:^\s*)((?:#if|#elif)(?:\s*)(defined\s*\(\s*BUFFER_)(\d+)(?:\s*\))|(?:#ifdef)(?:\s*BUFFER_)(\d+)(?:\s*))/gm, function () {
+ var i = arguments[3] || arguments[4];
+ buffers['u_buffer' + i] = {
+ fragment: '#define BUFFER_' + i + '\n' + fragString
+ };
+ });
+ }
+ return buffers;
+ }
+
+ // load buffers programs
+
+ }, {
+ key: 'loadPrograms',
+ value: function loadPrograms(buffers) {
+ var glsl = this;
+ var gl = this.gl;
+ var vertex = createShader(glsl, glsl.vertexString, gl.VERTEX_SHADER);
+ for (var key in buffers) {
+ var buffer = buffers[key];
+ var fragment = createShader(glsl, buffer.fragment, gl.FRAGMENT_SHADER, 1);
+ if (!fragment) {
+ fragment = createShader(glsl, 'void main(){\n\tgl_FragColor = vec4(1.0);\n}', gl.FRAGMENT_SHADER);
+ glsl.isValid = false;
+ } else {
+ glsl.isValid = true;
+ }
+ var program = createProgram(glsl, [vertex, fragment]);
+ buffer.name = key;
+ buffer.program = program;
+ buffer.bundle = glsl.createSwappableBuffer(glsl.canvas.width, glsl.canvas.height, program);
+ gl.deleteShader(fragment);
+ }
+ gl.deleteShader(vertex);
+ }
+
+ // create an input / output swappable buffer
+
+ }, {
+ key: 'createSwappableBuffer',
+ value: function createSwappableBuffer(W, H, program) {
+ var input = this.createBuffer(W, H, program);
+ var output = this.createBuffer(W, H, program);
+ var gl = this.gl;
+ return {
+ input: input,
+ output: output,
+ swap: function swap() {
+ var temp = input;
+ input = output;
+ output = temp;
+ this.input = input;
+ this.output = output;
+ },
+ render: function render(W, H, program, name) {
+ gl.useProgram(program);
+ gl.viewport(0, 0, W, H);
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.input.buffer);
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.output.texture, 0);
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+ this.swap();
+ },
+ resize: function resize(W, H, program, name) {
+ gl.useProgram(program);
+ gl.viewport(0, 0, W, H);
+ this.input.resize(W, H);
+ this.output.resize(W, H);
+ }
+ };
+ }
+
+ // create a buffers
+
+ }, {
+ key: 'createBuffer',
+ value: function createBuffer(W, H, program) {
+ var gl = this.gl;
+ var index = this.BUFFER_COUNT;
+ this.BUFFER_COUNT += 2;
+ gl.getExtension('OES_texture_float');
+ var texture = gl.createTexture();
+ gl.activeTexture(gl.TEXTURE0 + index);
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, W, H, 0, gl.RGBA, gl.FLOAT, null);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ var buffer = gl.createFramebuffer();
+ return {
+ index: index,
+ texture: texture,
+ buffer: buffer,
+ W: W,
+ H: H,
+ resize: function resize(W, H) {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, buffer);
+ var minW = Math.min(W, this.W);
+ var minH = Math.min(H, this.H);
+ var pixels = new Float32Array(minW * minH * 4);
+ gl.readPixels(0, 0, minW, minH, gl.RGBA, gl.FLOAT, pixels);
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ var newIndex = index + 1;
+ var newTexture = gl.createTexture();
+ gl.activeTexture(gl.TEXTURE0 + newIndex);
+ gl.bindTexture(gl.TEXTURE_2D, newTexture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, W, H, 0, gl.RGBA, gl.FLOAT, null);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, minW, minH, gl.RGBA, gl.FLOAT, pixels);
+ var newBuffer = gl.createFramebuffer();
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ gl.deleteTexture(texture);
+ gl.activeTexture(gl.TEXTURE0 + index);
+ gl.bindTexture(gl.TEXTURE_2D, newTexture);
+ index = this.index = index;
+ texture = this.texture = newTexture;
+ buffer = this.buffer = newBuffer;
+ this.W = W;
+ this.H = H;
+ }
+ };
+ }
+
+ // resize buffers on canvas resize
+ // consider applying a throttle of 50 ms on canvas resize
+ // to avoid requestAnimationFrame and Gl violations
+
+ }, {
+ key: 'resizeSwappableBuffers',
+ value: function resizeSwappableBuffers() {
+ var gl = this.gl;
+ var W = gl.canvas.width,
+ H = gl.canvas.height;
+ gl.viewport(0, 0, W, H);
+ for (var key in this.buffers) {
+ var buffer = this.buffers[key];
+ buffer.bundle.resize(W, H, buffer.program, buffer.name);
+ }
+ gl.useProgram(this.program);
+ }
+ }, {
+ key: 'version',
+ value: function version() {
+ return '0.1.7';
+ }
+ }]);
+ return GlslCanvas;
+}();
+
+function loadAllGlslCanvas() {
+ var list = document.getElementsByClassName('glslCanvas');
+ if (list.length > 0) {
+ window.glslCanvases = [];
+ for (var i = 0; i < list.length; i++) {
+ var sandbox = new GlslCanvas(list[i]);
+ if (sandbox.isValid) {
+ window.glslCanvases.push(sandbox);
+ }
+ }
+ }
+}
+
+window.addEventListener('load', function () {
+ loadAllGlslCanvas();
+});
+
+return GlslCanvas;
+
+})));
diff --git a/public/assets/app.css b/public/assets/app.css
new file mode 100644
index 0000000..19ac418
--- /dev/null
+++ b/public/assets/app.css
@@ -0,0 +1,289 @@
+#app-main {
+ font-family: 'Hack', monospace;
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 1rem 1rem 1rem 1rem;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-content: center;
+ align-items: center;
+ flex-grow: 1;
+}
+
+#app {
+ margin: 0 auto;
+ text-align: center;
+ align-content: center;
+ justify-content: center;
+ min-height: 100% !important;
+ display: flex;
+ flex-direction: column;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.preact:hover {
+ filter: drop-shadow(0 0 2em #673ab8aa);
+}
+
+.ascii-art {
+ font-family: 'Hack', monospace;
+ text-align: center;
+ background-color: #161f30;
+ padding: 2px;
+ margin: 8px;
+ font-family: Hack, monospace;
+ font-size: 10pt;
+ overflow-x: auto;
+ white-space: pre
+}
+
+@media (max-width: 480px) {
+ #app-main {
+ max-width: 100%;
+ padding: 0;
+ }
+ .main-info {
+ max-width: 90%;
+ }
+}
+
+@media (max-width: 1150px) and (min-width: 480px) {
+ #app-main {
+ max-width: 100%;
+ padding: 0;
+ }
+ .main-info {
+ max-width: 80%;
+ }
+}
+
+@media (min-width: 1150px) {
+ .main-info {
+ max-width: 1280px;
+ margin-top: 72px;
+ }
+}
+
+.main-info-image {
+ width: 128px;
+ height: 128px;
+ border-radius: 100%;
+ margin: 6px 6px 6px 6px;
+}
+
+.main-info-title {
+ font-size: 42pt;
+}
+
+.main-info-subtitle {
+ font-size: 26pt;
+ font-style: italic;
+ color: #5c6370;
+}
+
+.quiet {
+ color: var(--color-gray);
+ font-style: italic;
+}
+
+.section {
+ font-size: 22pt;
+}
+
+.highlight {
+ color: #e5c07b;
+}
+
+.back-shadows {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: -1;
+ background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, var(--color-bg) 10%, var(--color-bg) 90%, rgba(0, 0, 0, 0) 100%);
+}
+
+#animated-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: -10;
+}
+
+
+@media (max-width: 480px) {
+ .back-shadows {
+ margin: auto;
+ max-width: 80%;
+ }
+ .main-info-title {
+ font-size: 32pt;
+ }
+
+ .main-info-subtitle {
+ font-size: 18pt;
+ }
+}
+
+@media (max-width: 1150px) and (min-width: 480px) {
+ .back-shadows {
+ margin: auto;
+ max-width: 60%;
+ }
+ .main-info-title {
+ font-size: 42pt;
+ }
+
+ .main-info-subtitle {
+ font-size: 26pt;
+ }
+}
+
+@media (min-width: 1150px) {
+ .back-shadows {
+ margin: auto;
+ max-width: 50%;
+ }
+ .main-info-title {
+ font-size: 42pt;
+ }
+
+ .main-info-subtitle {
+ font-size: 26pt;
+ }
+}
+
+
+@media (max-width: 480px) {
+ .about-paragraph {
+ margin: auto;
+ font-size: 16pt;
+ max-width: 90%;
+ }
+}
+
+@media (max-width: 1150px) and (min-width: 480px) {
+ .about-paragraph {
+ margin: auto;
+ font-size: 18pt;
+ max-width: 80%;
+ }
+}
+
+@media (min-width: 1150px) {
+ .about-paragraph {
+ margin: auto;
+ font-size: 18pt;
+ max-width: 60%;
+ }
+}
+
+/* footer */
+/* if #app-main is taller than the viewport, push the footer to the bottom of the page */
+.footer-container {
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ z-index: 9999;
+ margin: auto;
+}
+
+.footer-container {
+ position: static;
+}
+
+.footer {
+ max-width: 1280px;
+ margin-top: 2em;
+ margin-left: auto;
+ margin-right: auto;
+ padding-top: 20pt;
+ padding-bottom: 12pt;
+ align-items: center;
+ justify-content: center;
+ align-self: center;
+ border-top: 2px solid #3e4451;
+ display: flex;
+ flex-direction: row;
+}
+
+@media (max-width: 480px) {
+ .footer {
+ max-width: 90%;
+ }
+}
+
+@media (max-width: 1300px) and (min-width: 480px) {
+ .footer {
+ max-width: 80%;
+ }
+}
+
+@media (min-width: 1300px) {
+ .footer {
+ max-width: 1280px;
+ }
+}
+
+.footer-begin {
+ align-self: left;
+ align-items: flex-start;
+ align-content: flex-start;
+}
+
+.footer-middle {
+ align-self: center;
+ align-items: center;
+ align-content: center;
+ flex-grow: 1;
+}
+
+.footer-end {
+ align-self: right;
+ align-items: flex-end;
+ align-content: flex-end;
+}
+
+.footer-item {
+ font-family: Hack, monospace;
+ font-size: 12pt;
+ color: #647089;
+ text-align: center;
+ float: none;
+ clear: left;
+ display: inline-block;
+ margin: 0 0.25em;
+ transition: all 0.2s ease-in-out;
+}
+
+.footer-link:link {
+ color: #647089;
+ text-decoration: underline;
+}
+
+.footer-link:visited {
+ color: #647089;
+ text-decoration: underline;
+}
+
+.footer-icon {
+ transition: all 0.2s ease-in-out;
+ width: 1.5em;
+ height: 1.5em;
+ filter: brightness(50%);
+}
+
+.footer-icon:hover {
+ transition: all 0.2s ease-in-out;
+ filter: brightness(100%) drop-shadow(0 0 0.5em #647089);
+}
diff --git a/public/assets/index.css b/public/assets/index.css
new file mode 100644
index 0000000..7bffb22
--- /dev/null
+++ b/public/assets/index.css
@@ -0,0 +1,112 @@
+:root {
+ font-weight: 400;
+
+ color-scheme: dark;
+ color: var(--color-fg);
+ background-color: var(--color-bg);
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ /* variables */
+ --color-bg: #161f30;
+ --color-fg: #a3b3d9;
+ --color-search-blue: #083C5A;
+ --color-white: #a3b3d9;
+ --color-gray: #647089;
+ --color-dark-gray: #3e4451;
+ --color-context: #6b737f;
+ --color-light-gray: #c8ccd4;
+ --color-tree-gray: #1b1f27;
+ --color-blue: #6080dd;
+ --color-vivid-blue: #61afef;
+ --color-dark-blue: #223E55;
+ --color-folder-blue: #61afef;
+ --color-light-blue: #a3b3d9;
+ --color-green: #449871;
+ --color-cyan: #56b6c2;
+ --color-light-green: #98c379;
+ --color-red: #c13251;
+ --color-orange: #c18a56;
+ --color-light-red: #e06c75;
+ --color-yellow: #e5c07b;
+ --color-purple: #604bd8;
+ --color-magenta: #b071fc;
+}
+
+html, body {
+ height: 100%;
+}
+
+a:link {
+ font-weight: 500;
+ color: #4fc1ff;
+ text-decoration: inherit;
+}
+
+a:visited {
+ color: #4fc1ff;
+}
+
+a:hover {
+ color: #60d2ff;
+}
+
+a:active {
+ color: #66d8ff;
+}
+
+
+body {
+ margin: 0;
+ /* display: flex; */
+ /* place-items: center; */
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+span {
+ display: inline;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+.hack {
+ font-family: Hack, monospace;
+ font-weight: 500;
+}
+
+.grow {
+ transition: all .2s ease-in-out;
+ transform: scale(1);
+}
+
+.grow:hover {
+ transition: all .2s ease-in-out;
+ transform: scale(1.1);
+}
diff --git a/public/assets/index.js b/public/assets/index.js
new file mode 100644
index 0000000..6dd6ab6
--- /dev/null
+++ b/public/assets/index.js
@@ -0,0 +1,91 @@
+$(document).ready(function() {
+ $('.carousel-wrapper').slick({
+ dots: true,
+ infinite: true,
+ speed: 500,
+ slidesToShow: 1,
+ slidesToScroll: 1
+ });
+
+ var canvas_parent = document.getElementById('animated-bg');
+ var canvas = document.createElement('canvas');
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight / 2;
+ window.sandbox_canvas = canvas;
+
+ canvas_parent.appendChild(canvas, null, { filtering: 'nearest' });
+ var sandbox = new GlslCanvas(canvas);
+ // randomize time
+ sandbox.timeLoad = Math.random() * 100000;
+
+ window.sandbox = sandbox; // for access
+
+ const shader_code = `
+precision highp float;
+uniform vec3 color;
+uniform sampler2D t;
+uniform vec2 tiles;
+uniform float u_time;
+uniform vec2 u_resolution;
+uniform float symbol_count;
+
+vec2 map_to_symbol_uv(vec2 image_coord, float symbol_index) {
+ float symbol_width = 1.0 / symbol_count;
+ float symbol_height = 1.0;
+ float symbol_x = symbol_index * symbol_width;
+ return vec2(symbol_x + image_coord.x * symbol_width, image_coord.y * symbol_height);
+}
+
+vec2 tile_index(vec2 p, vec2 tiles) {
+ return vec2(floor(p.x * tiles.x), floor(p.y * tiles.y));
+}
+
+vec2 tilefy (vec2 p, vec2 tiles) {
+ return vec2(fract(p.x * tiles.x), fract(p.y * tiles.y));
+}
+
+float randf(float n) {
+ return fract(sin(n) * 43758.5453123);
+}
+
+void main() {
+ vec2 uv = gl_FragCoord.xy / u_resolution.xy;
+ vec2 tile = tile_index(uv, tiles);
+ float tilerand = randf(tile.x + tile.y * tiles.x);
+ float v = tilerand + randf(floor(tilerand * u_time));
+ float diff = fract(tilerand * u_time);
+ float s = floor(v * 9.0);
+ vec2 tp = tilefy(uv, tiles);
+ vec2 tp_center = vec2(0.5, 0.5);
+ float d = distance(tp, tp_center);
+ float scale = min(sin(diff * 3.1415) * 1.2, 1.0);
+ tp = tp_center + (tp - tp_center) / scale;
+ if (tp.x < 0.0 || tp.x > 1.0 || tp.y < 0.0 || tp.y > 1.0) {
+ gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
+ return;
+ }
+ vec2 p = map_to_symbol_uv(tp, mod(s, symbol_count));
+ vec4 c = texture2D(t, p);
+ c.rgb *= color;
+ c.a *= uv.y * 4.0;
+ gl_FragColor = vec4(c.rgb * c.a, c.a);
+}
+`
+
+ sandbox.load(shader_code);
+
+ sandbox.setUniform('t', "https://cdn.duckonaut.dev/assets/images/symbols.png");
+
+ sandbox.setUniform('color', 42 / 255, 42 / 255, 80 / 255);
+
+ sandbox.setUniform('symbol_count', 9);
+
+ sandbox.setUniform('tiles', Math.ceil(window.innerWidth / 32), Math.ceil(window.innerHeight / 64));
+
+ addEventListener('resize', function() {
+ window.sandbox_canvas.width = window.innerWidth;
+ window.sandbox_canvas.height = window.innerHeight / 2;
+ window.sandbox.setUniform('tiles', Math.ceil(window.innerWidth / 32), Math.ceil(window.innerHeight / 64));
+ });
+});
+
diff --git a/public/assets/project.css b/public/assets/project.css
new file mode 100644
index 0000000..742c072
--- /dev/null
+++ b/public/assets/project.css
@@ -0,0 +1,126 @@
+.icon {
+ display: inline;
+ width: 32px;
+ height: 32px;
+ transition: 0.3s;
+ transform-origin: center;
+ filter: brightness(70%)
+}
+
+.icon:hover {
+ filter: brightness(80%);
+}
+
+.icon-row {
+ width: 320px;
+ height: 36px;
+}
+
+.icon-container {
+ height: 36px;
+ float: right;
+}
+
+.icon-text {
+ width: auto;
+ height: 36px;
+ line-height: 36px;
+ float: left;
+ vertical-align: middle;
+ margin-right: 8px;
+ color: #5c6370;
+}
+
+.project {
+ display: table;
+ align-self: center;
+ clear: both;
+ margin-top: 24pt;
+}
+
+.project-info {
+ font-size: 14pt;
+}
+
+.project-description {
+ font-size: 16pt;
+ text-align: justify;
+}
+
+.project-image-gallery {
+ margin-left: 24px;
+ margin-right: 10px;
+}
+
+@media (max-width: 1150px) {
+ .project-title {
+ text-align: center;
+ font-size: 24pt;
+ margin-bottom: 8px;
+ text-decoration-line: underline;
+ }
+
+ .project-image-gallery {
+ margin: 60px auto;
+ width: 330px;
+ height: 220px;
+ }
+
+ .project-info {
+ float: none;
+ width: 90%;
+ margin: auto;
+ }
+
+ .project-description {
+ font-size: 14pt;
+ }
+}
+
+@media (max-width: 1150px) and (min-width: 480px) {
+ .project-title {
+ text-align: center;
+ font-size: 24pt;
+ margin-bottom: 8px;
+ text-decoration-line: underline;
+ }
+ .project-image-gallery {
+ margin: 60px auto;
+ width: 480px;
+ height: 360px;
+ }
+
+ .project-info {
+ float: none;
+ width: 90%;
+ margin: auto;
+ }
+
+ .project-description {
+ font-size: 14pt;
+ }
+}
+
+@media (min-width: 1150px) {
+ .project-title {
+ text-align: left;
+ font-size: 32pt;
+ margin-bottom: 8px;
+ text-decoration-line: underline;
+ }
+ .project-image-gallery {
+ float: right;
+ width: 480px;
+ height: 360px;
+ }
+ .project-info {
+ float: left;
+ max-width: 600px;
+ }
+
+ .project-description {
+ font-size: 16pt;
+ }
+}
+
+
diff --git a/reluax.luax b/reluax.luax
new file mode 100644
index 0000000..6845de6
--- /dev/null
+++ b/reluax.luax
@@ -0,0 +1,38 @@
+local Common = require("common")
+local Footer = require("components.footer")
+local AnimatedBg = require("components.animated_bg")
+
+local Home = require("pages.home")
+local NotFound = require("not_found")
+
+math.randomseed(os.time())
+
+local wrap_component = function(component)
+ return
+ <Common>
+ <div class="back-shadows"></div>
+ <div id="app-main">
+ {$ component $}
+ <AnimatedBg />
+ </div>
+ <Footer />
+ </Common>
+end
+
+local route = function(path, method, headers, body)
+ if method ~= 'GET' then
+ return 405, "Method not allowed"
+ end
+
+ if path == '/' then
+ return 200, wrap_component(<Home />)
+ end
+
+ return 404, NotFound()
+end
+
+return {
+ name = "rem.duckonaut.dev",
+ route = route
+}
+