├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.js ├── css ├── bright.css ├── dark.css ├── main.css └── reset.css ├── gif.js ├── gif.js.map ├── gif.worker.js ├── gif.worker.js.map ├── img ├── error.png ├── error.svg └── tsodinClown.png ├── index.html ├── js ├── eval.js ├── filters.js ├── grecha.js └── index.js ├── package-lock.json ├── package.json ├── serviceworker.js ├── serviceworker.ts └── ts ├── eval.ts ├── filters.ts ├── grecha.ts └── index.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | node-20-typescript: 6 | runs-on: ubuntu-18.04 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-node@v2 10 | with: 11 | node-version: '20' 12 | - run: npm install 13 | - run: npm run build 14 | - run: git diff --exit-code 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Alexey Kutepov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # emoteJAM 2 | 3 | `emoteJAM` is a simple website that generates animated [BTTV](https://betterttv.com/) emotes from static images. 4 | 5 | That idea is to apply a well established "meme meta filters" to static emotes. Such as [JAM](https://betterttv.com/emotes/5b77ac3af7bddc567b1d5fb2), [Hop](https://betterttv.com/emotes/5a9578d6dcf3205f57ba294f), etc. 6 | 7 | The most important feature of the website is that it's completely client-side and can be easily deployed to something like [GitHub Pages](https://pages.github.com/). It uses [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API) to animate static images and [gif.js](https://jnordberg.github.io/gif.js/) to generate actual GIF files inside of your browser. 8 | 9 | Official Deployed Instance: [https://tsoding.github.io/emoteJAM/](https://tsoding.github.io/emoteJAM/) 10 | 11 | ## Running Locally 12 | 13 | Nothing particularly special is required. Just serve the folder using HTTP server like Python's SimpleHTTPServer: 14 | 15 | ```console 16 | $ python3 -m http.server 6969 17 | $ iexplore.exe http://localhost:6969/ 18 | ``` 19 | 20 | ## Building 21 | 22 | The whole build is organized so you can just serve the repo via an HTTP server and it just works. This is done to simplify deployment to [GitHub pages](https://pages.github.com/). We just tell GitHub to service this repo as is. The build artifacts are also commited to the repo. So if you want to simply get the website working you don't even have to build anything. Just serve the repo. 23 | 24 | The build is done via the [./build.js](./build.js) script. It is recommended to read it to get an idea on how it works. It is also recommended to check the `"scripts"` section of [./package.json](./package.json) to get an idea on how it is called from `npm run`. 25 | 26 | Before doing any building make sure you installed all the necessary dependencies: 27 | 28 | ```console 29 | $ npm install 30 | ``` 31 | 32 | To build all the artifacts 33 | 34 | ```console 35 | $ npm run build 36 | ``` 37 | 38 | ## Watching 39 | 40 | The [./build.js](./build.js) script enables you to [Watch](https://www.typescriptlang.org/docs/handbook/configuring-watch.html#handbook-content) the source code: 41 | 42 | ```console 43 | $ npm run watch 44 | ``` 45 | 46 | ## Serving and Watching 47 | 48 | ```console 49 | $ npm run service 50 | ``` 51 | 52 | This starts both `python3 -m http.server 6969` and [Watching](#Watching) at the same time, providing a convenient development environment. 53 | 54 | # Filter Development 55 | 56 | **WARNING! Knowledge of [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API) or [OpenGL](https://www.opengl.org/) is required to read this section!** 57 | 58 | ## Uniforms 59 | 60 | | Name | Type | Description | 61 | | --- | --- | --- | 62 | | `time` | `float` | Current time in Seconds (float) since the start of the application. Can be used for animating. | 63 | | `resolution` | `vec2` | Resolution of the emote canvas in Pixels. | 64 | | `emote` | `sampler2D` | The input image as the WebGL texture. | 65 | | `emoteSize` | `vec2` | The input image size in pixels. | 66 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | function cmd(program, args) { 4 | console.log('CMD:', program, args); 5 | const p = spawn(program, args.flat()); // NOTE: flattening the args array enables you to group related arguments for better self-documentation of the running command 6 | p.stdout.on('data', (data) => process.stdout.write(data)); 7 | p.stderr.on('data', (data) => process.stderr.write(data)); 8 | p.on('close', (code) => { 9 | if (code !== 0) { 10 | console.error(program, args, 'exited with', code); 11 | } 12 | }); 13 | return p; 14 | } 15 | 16 | const commonTscFlags = [ 17 | '--strict', 18 | '--removeComments', 19 | '--skipLibCheck', 20 | ]; 21 | 22 | const mainTs = [ 23 | 'ts/eval.ts', 24 | 'ts/filters.ts', 25 | 'ts/grecha.ts', 26 | 'ts/index.ts' 27 | ]; 28 | 29 | function tscMain(...extraParams) { 30 | cmd('tsc', [ 31 | ...commonTscFlags, 32 | ['--outDir', 'js'], 33 | ...extraParams, 34 | mainTs, 35 | ]); 36 | } 37 | 38 | function tscServiceWorker(...extraParams) { 39 | cmd('tsc', [ 40 | ...commonTscFlags, 41 | ['--lib', 'webworker'], 42 | ['--outFile', 'serviceworker.js'], 43 | ...extraParams, 44 | 'serviceworker.ts' 45 | ]); 46 | } 47 | 48 | function build(part, ...args) { 49 | switch (part) { 50 | case undefined: 51 | tscServiceWorker(); 52 | tscMain(); 53 | break; 54 | case 'main': 55 | tscMain(); 56 | break; 57 | case 'serviceworker': 58 | tscServiceWorker(); 59 | break; 60 | default: 61 | throw new Error(`Unknown build part ${part}. Available parts: main, serviceworker.`); 62 | } 63 | } 64 | 65 | function watch(part, ...args) { 66 | switch (part) { 67 | case undefined: 68 | tscMain('-w', '--preserveWatchOutput'); 69 | tscServiceWorker('-w', '--preserveWatchOutput'); 70 | break; 71 | case 'main': 72 | tscMain('-w', '--preserveWatchOutput'); 73 | break; 74 | case 'serviceworker': 75 | tscServiceWorker('-w', '--preserveWatchOutput'); 76 | break; 77 | default: 78 | throw new Error(`Unknown watch part ${part}. Available parts: main, serviceworker.`); 79 | } 80 | } 81 | 82 | const [nodePath, scriptPath, command, ...args] = process.argv; 83 | switch (command) { 84 | case undefined: 85 | case 'build': 86 | build(...args); 87 | break; 88 | case 'watch': 89 | watch(...args); 90 | break; 91 | case 'serve': 92 | // TODO: maybe replace Python with something from Node itself? 93 | // Python is a pretty unreasonable dependency. 94 | cmd('python3', [['-m', 'http.server'], '6969']); 95 | watch(); 96 | break; 97 | default: 98 | throw new Error(`Unknown command ${command}. Available commands: build, watch.`); 99 | } 100 | -------------------------------------------------------------------------------- /css/bright.css: -------------------------------------------------------------------------------- 1 | body { 2 | color:#444; 3 | } 4 | 5 | .progress { 6 | background: #29c; 7 | } 8 | 9 | .spinning-wheel { 10 | border-left: 20px solid #29c; 11 | } 12 | 13 | .github-corner { 14 | fill: #444; 15 | color: #fff; 16 | } 17 | 18 | a { 19 | color: #07a; 20 | text-decoration: none; 21 | border-bottom: #07a 0.0625em solid; 22 | } 23 | -------------------------------------------------------------------------------- /css/dark.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #ddd; 3 | background-color: #222; 4 | } 5 | 6 | .progress { 7 | background: #29c; 8 | } 9 | 10 | .spinning-wheel { 11 | border-left: 20px solid #29c; 12 | } 13 | 14 | .github-corner { 15 | fill: #ddd; 16 | color: #222; 17 | } 18 | 19 | a { 20 | color: #29c; 21 | text-decoration: none; 22 | border-bottom: #29c 0.0625em solid; 23 | } 24 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 40px auto; 3 | max-width: 650px; 4 | line-height: 1.6; 5 | font-size:18px; 6 | padding: 0 10px 7 | } 8 | 9 | h1 { 10 | font-size: 400%; 11 | line-height: 1.2; 12 | } 13 | 14 | h2 { 15 | font-size: 200%; 16 | line-height: 1.2; 17 | padding-top: 30px; 18 | padding-bottom: 10px; 19 | } 20 | 21 | p { 22 | margin-top: 20px; 23 | } 24 | 25 | .widget { 26 | width: 100%; 27 | padding-top: 10px; 28 | } 29 | 30 | .widget-element { 31 | display: table; 32 | margin: 0 auto; 33 | padding-top: 10px; 34 | } 35 | 36 | .progress-bar { 37 | width: 100%; 38 | } 39 | 40 | .progress { 41 | width: 0%; 42 | height: 20px; 43 | } 44 | 45 | .spinning-wheel { 46 | width: 92px; 47 | height: 92px; 48 | border-right: 20px solid #00000000; 49 | border-top: 20px solid #00000000; 50 | border-bottom: 20px solid #00000000; 51 | border-radius: 112px; 52 | animation: spinning 0.5s infinite; 53 | animation-timing-function: linear; 54 | } 55 | 56 | @keyframes spinning { 57 | 0% { 58 | transform: rotate(0deg); 59 | } 60 | 100% { 61 | transform: rotate(360deg); 62 | } 63 | } 64 | 65 | #custom-preview { 66 | width: 112px; 67 | } 68 | -------------------------------------------------------------------------------- /css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /gif.js: -------------------------------------------------------------------------------- 1 | // gif.js 0.2.0 - https://github.com/jnordberg/gif.js 2 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GIF=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i0&&this._events[type].length>m){this._events[type].warned=true;console.error("(node) warning: possible EventEmitter memory "+"leak detected. %d listeners added. "+"Use emitter.setMaxListeners() to increase limit.",this._events[type].length);if(typeof console.trace==="function"){console.trace()}}}return this};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.once=function(type,listener){if(!isFunction(listener))throw TypeError("listener must be a function");var fired=false;function g(){this.removeListener(type,g);if(!fired){fired=true;listener.apply(this,arguments)}}g.listener=listener;this.on(type,g);return this};EventEmitter.prototype.removeListener=function(type,listener){var list,position,length,i;if(!isFunction(listener))throw TypeError("listener must be a function");if(!this._events||!this._events[type])return this;list=this._events[type];length=list.length;position=-1;if(list===listener||isFunction(list.listener)&&list.listener===listener){delete this._events[type];if(this._events.removeListener)this.emit("removeListener",type,listener)}else if(isObject(list)){for(i=length;i-- >0;){if(list[i]===listener||list[i].listener&&list[i].listener===listener){position=i;break}}if(position<0)return this;if(list.length===1){list.length=0;delete this._events[type]}else{list.splice(position,1)}if(this._events.removeListener)this.emit("removeListener",type,listener)}return this};EventEmitter.prototype.removeAllListeners=function(type){var key,listeners;if(!this._events)return this;if(!this._events.removeListener){if(arguments.length===0)this._events={};else if(this._events[type])delete this._events[type];return this}if(arguments.length===0){for(key in this._events){if(key==="removeListener")continue;this.removeAllListeners(key)}this.removeAllListeners("removeListener");this._events={};return this}listeners=this._events[type];if(isFunction(listeners)){this.removeListener(type,listeners)}else if(listeners){while(listeners.length)this.removeListener(type,listeners[listeners.length-1])}delete this._events[type];return this};EventEmitter.prototype.listeners=function(type){var ret;if(!this._events||!this._events[type])ret=[];else if(isFunction(this._events[type]))ret=[this._events[type]];else ret=this._events[type].slice();return ret};EventEmitter.prototype.listenerCount=function(type){if(this._events){var evlistener=this._events[type];if(isFunction(evlistener))return 1;else if(evlistener)return evlistener.length}return 0};EventEmitter.listenerCount=function(emitter,type){return emitter.listenerCount(type)};function isFunction(arg){return typeof arg==="function"}function isNumber(arg){return typeof arg==="number"}function isObject(arg){return typeof arg==="object"&&arg!==null}function isUndefined(arg){return arg===void 0}},{}],2:[function(require,module,exports){var UA,browser,mode,platform,ua;ua=navigator.userAgent.toLowerCase();platform=navigator.platform.toLowerCase();UA=ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0];mode=UA[1]==="ie"&&document.documentMode;browser={name:UA[1]==="version"?UA[3]:UA[1],version:mode||parseFloat(UA[1]==="opera"&&UA[4]?UA[4]:UA[2]),platform:{name:ua.match(/ip(?:ad|od|hone)/)?"ios":(ua.match(/(?:webos|android)/)||platform.match(/mac|win|linux/)||["other"])[0]}};browser[browser.name]=true;browser[browser.name+parseInt(browser.version,10)]=true;browser.platform[browser.platform.name]=true;module.exports=browser},{}],3:[function(require,module,exports){var EventEmitter,GIF,browser,extend=function(child,parent){for(var key in parent){if(hasProp.call(parent,key))child[key]=parent[key]}function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor;child.__super__=parent.prototype;return child},hasProp={}.hasOwnProperty,indexOf=[].indexOf||function(item){for(var i=0,l=this.length;iref;i=0<=ref?++j:--j){results.push(null)}return results}.call(this);numWorkers=this.spawnWorkers();if(this.options.globalPalette===true){this.renderNextFrame()}else{for(i=j=0,ref=numWorkers;0<=ref?jref;i=0<=ref?++j:--j){this.renderNextFrame()}}this.emit("start");return this.emit("progress",0)};GIF.prototype.abort=function(){var worker;while(true){worker=this.activeWorkers.shift();if(worker==null){break}this.log("killing active worker");worker.terminate()}this.running=false;return this.emit("abort")};GIF.prototype.spawnWorkers=function(){var j,numWorkers,ref,results;numWorkers=Math.min(this.options.workers,this.frames.length);(function(){results=[];for(var j=ref=this.freeWorkers.length;ref<=numWorkers?jnumWorkers;ref<=numWorkers?j++:j--){results.push(j)}return results}).apply(this).forEach(function(_this){return function(i){var worker;_this.log("spawning worker "+i);worker=new Worker(_this.options.workerScript);worker.onmessage=function(event){_this.activeWorkers.splice(_this.activeWorkers.indexOf(worker),1);_this.freeWorkers.push(worker);return _this.frameFinished(event.data)};return _this.freeWorkers.push(worker)}}(this));return numWorkers};GIF.prototype.frameFinished=function(frame){var i,j,ref;this.log("frame "+frame.index+" finished - "+this.activeWorkers.length+" active");this.finishedFrames++;this.emit("progress",this.finishedFrames/this.frames.length);this.imageParts[frame.index]=frame;if(this.options.globalPalette===true){this.options.globalPalette=frame.globalPalette;this.log("global palette analyzed");if(this.frames.length>2){for(i=j=1,ref=this.freeWorkers.length;1<=ref?jref;i=1<=ref?++j:--j){this.renderNextFrame()}}}if(indexOf.call(this.imageParts,null)>=0){return this.renderNextFrame()}else{return this.finishRendering()}};GIF.prototype.finishRendering=function(){var data,frame,i,image,j,k,l,len,len1,len2,len3,offset,page,ref,ref1,ref2;len=0;ref=this.imageParts;for(j=0,len1=ref.length;j=this.frames.length){return}frame=this.frames[this.nextFrame++];worker=this.freeWorkers.shift();task=this.getTask(frame);this.log("starting frame "+(task.index+1)+" of "+this.frames.length);this.activeWorkers.push(worker);return worker.postMessage(task)};GIF.prototype.getContextData=function(ctx){return ctx.getImageData(0,0,this.options.width,this.options.height).data};GIF.prototype.getImageData=function(image){var ctx;if(this._canvas==null){this._canvas=document.createElement("canvas");this._canvas.width=this.options.width;this._canvas.height=this.options.height}ctx=this._canvas.getContext("2d");ctx.fillStyle=this.options.background;ctx.fillRect(0,0,this.options.width,this.options.height);ctx.drawImage(image,0,0);return this.getContextData(ctx)};GIF.prototype.getTask=function(frame){var index,task;index=this.frames.indexOf(frame);task={index:index,last:index===this.frames.length-1,delay:frame.delay,dispose:frame.dispose,transparent:frame.transparent,width:this.options.width,height:this.options.height,quality:this.options.quality,dither:this.options.dither,globalPalette:this.options.globalPalette,repeat:this.options.repeat,canTransfer:browser.name==="chrome"};if(frame.data!=null){task.data=frame.data}else if(frame.context!=null){task.data=this.getContextData(frame.context)}else if(frame.image!=null){task.data=this.getImageData(frame.image)}else{throw new Error("Invalid frame")}return task};GIF.prototype.log=function(){var args;args=1<=arguments.length?slice.call(arguments,0):[];if(!this.options.debug){return}return console.log.apply(console,args)};return GIF}(EventEmitter);module.exports=GIF},{"./browser.coffee":2,events:1}]},{},[3])(3)}); 3 | //# sourceMappingURL=gif.js.map 4 | -------------------------------------------------------------------------------- /gif.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["node_modules/browser-pack/_prelude.js","node_modules/events/events.js","src/browser.coffee","src/gif.coffee"],"names":["f","exports","module","define","amd","g","window","global","self","this","GIF","r","e","n","t","o","i","c","require","u","a","Error","code","p","call","length","1","EventEmitter","_events","_maxListeners","undefined","prototype","defaultMaxListeners","setMaxListeners","isNumber","isNaN","TypeError","emit","type","er","handler","len","args","listeners","error","isObject","arguments","err","context","isUndefined","isFunction","Array","slice","apply","addListener","listener","m","newListener","push","warned","console","trace","on","once","fired","removeListener","list","position","splice","removeAllListeners","key","ret","listenerCount","evlistener","emitter","arg","UA","browser","mode","platform","ua","navigator","userAgent","toLowerCase","match","document","documentMode","name","version","parseFloat","parseInt","extend","child","parent","hasProp","ctor","constructor","__super__","superClass","defaults","frameDefaults","workerScript","workers","repeat","background","quality","width","height","transparent","debug","dither","delay","copy","dispose","options","base","value","running","frames","freeWorkers","activeWorkers","setOptions","setOption","_canvas","results","addFrame","image","frame","ImageData","data","CanvasRenderingContext2D","WebGLRenderingContext","getContextData","childNodes","getImageData","render","j","numWorkers","ref","nextFrame","finishedFrames","imageParts","spawnWorkers","globalPalette","renderNextFrame","abort","worker","shift","log","terminate","Math","min","forEach","_this","Worker","onmessage","event","indexOf","frameFinished","index","finishRendering","k","l","len1","len2","len3","offset","page","ref1","ref2","pageSize","cursor","round","Uint8Array","set","Blob","task","getTask","postMessage","ctx","createElement","getContext","fillStyle","fillRect","drawImage","last","canTransfer"],"mappings":"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","sourceRoot":"","sourcesContent":["(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c=\"function\"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error(\"Cannot find module '\"+i+\"'\");throw a.code=\"MODULE_NOT_FOUND\",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u=\"function\"==typeof require&&require,i=0;i 0 && this._events[type].length > m) {\n this._events[type].warned = true;\n console.error('(node) warning: possible EventEmitter memory ' +\n 'leak detected. %d listeners added. ' +\n 'Use emitter.setMaxListeners() to increase limit.',\n this._events[type].length);\n if (typeof console.trace === 'function') {\n // not supported in IE 10\n console.trace();\n }\n }\n }\n\n return this;\n};\n\nEventEmitter.prototype.on = EventEmitter.prototype.addListener;\n\nEventEmitter.prototype.once = function(type, listener) {\n if (!isFunction(listener))\n throw TypeError('listener must be a function');\n\n var fired = false;\n\n function g() {\n this.removeListener(type, g);\n\n if (!fired) {\n fired = true;\n listener.apply(this, arguments);\n }\n }\n\n g.listener = listener;\n this.on(type, g);\n\n return this;\n};\n\n// emits a 'removeListener' event iff the listener was removed\nEventEmitter.prototype.removeListener = function(type, listener) {\n var list, position, length, i;\n\n if (!isFunction(listener))\n throw TypeError('listener must be a function');\n\n if (!this._events || !this._events[type])\n return this;\n\n list = this._events[type];\n length = list.length;\n position = -1;\n\n if (list === listener ||\n (isFunction(list.listener) && list.listener === listener)) {\n delete this._events[type];\n if (this._events.removeListener)\n this.emit('removeListener', type, listener);\n\n } else if (isObject(list)) {\n for (i = length; i-- > 0;) {\n if (list[i] === listener ||\n (list[i].listener && list[i].listener === listener)) {\n position = i;\n break;\n }\n }\n\n if (position < 0)\n return this;\n\n if (list.length === 1) {\n list.length = 0;\n delete this._events[type];\n } else {\n list.splice(position, 1);\n }\n\n if (this._events.removeListener)\n this.emit('removeListener', type, listener);\n }\n\n return this;\n};\n\nEventEmitter.prototype.removeAllListeners = function(type) {\n var key, listeners;\n\n if (!this._events)\n return this;\n\n // not listening for removeListener, no need to emit\n if (!this._events.removeListener) {\n if (arguments.length === 0)\n this._events = {};\n else if (this._events[type])\n delete this._events[type];\n return this;\n }\n\n // emit removeListener for all listeners on all events\n if (arguments.length === 0) {\n for (key in this._events) {\n if (key === 'removeListener') continue;\n this.removeAllListeners(key);\n }\n this.removeAllListeners('removeListener');\n this._events = {};\n return this;\n }\n\n listeners = this._events[type];\n\n if (isFunction(listeners)) {\n this.removeListener(type, listeners);\n } else if (listeners) {\n // LIFO order\n while (listeners.length)\n this.removeListener(type, listeners[listeners.length - 1]);\n }\n delete this._events[type];\n\n return this;\n};\n\nEventEmitter.prototype.listeners = function(type) {\n var ret;\n if (!this._events || !this._events[type])\n ret = [];\n else if (isFunction(this._events[type]))\n ret = [this._events[type]];\n else\n ret = this._events[type].slice();\n return ret;\n};\n\nEventEmitter.prototype.listenerCount = function(type) {\n if (this._events) {\n var evlistener = this._events[type];\n\n if (isFunction(evlistener))\n return 1;\n else if (evlistener)\n return evlistener.length;\n }\n return 0;\n};\n\nEventEmitter.listenerCount = function(emitter, type) {\n return emitter.listenerCount(type);\n};\n\nfunction isFunction(arg) {\n return typeof arg === 'function';\n}\n\nfunction isNumber(arg) {\n return typeof arg === 'number';\n}\n\nfunction isObject(arg) {\n return typeof arg === 'object' && arg !== null;\n}\n\nfunction isUndefined(arg) {\n return arg === void 0;\n}\n","### CoffeeScript version of the browser detection from MooTools ###\n\nua = navigator.userAgent.toLowerCase()\nplatform = navigator.platform.toLowerCase()\nUA = ua.match(/(opera|ie|firefox|chrome|version)[\\s\\/:]([\\w\\d\\.]+)?.*?(safari|version[\\s\\/:]([\\w\\d\\.]+)|$)/) or [null, 'unknown', 0]\nmode = UA[1] == 'ie' && document.documentMode\n\nbrowser =\n name: if UA[1] is 'version' then UA[3] else UA[1]\n version: mode or parseFloat(if UA[1] is 'opera' && UA[4] then UA[4] else UA[2])\n\n platform:\n name: if ua.match(/ip(?:ad|od|hone)/) then 'ios' else (ua.match(/(?:webos|android)/) or platform.match(/mac|win|linux/) or ['other'])[0]\n\nbrowser[browser.name] = true\nbrowser[browser.name + parseInt(browser.version, 10)] = true\nbrowser.platform[browser.platform.name] = true\n\nmodule.exports = browser\n","{EventEmitter} = require 'events'\nbrowser = require './browser.coffee'\n\nclass GIF extends EventEmitter\n\n defaults =\n workerScript: 'gif.worker.js'\n workers: 2\n repeat: 0 # repeat forever, -1 = repeat once\n background: '#fff'\n quality: 10 # pixel sample interval, lower is better\n width: null # size derermined from first frame if possible\n height: null\n transparent: null\n debug: false\n dither: false # see GIFEncoder.js for dithering options\n\n frameDefaults =\n delay: 500 # ms\n copy: false\n dispose: -1\n\n constructor: (options) ->\n @running = false\n\n @options = {}\n @frames = []\n\n @freeWorkers = []\n @activeWorkers = []\n\n @setOptions options\n for key, value of defaults\n @options[key] ?= value\n\n setOption: (key, value) ->\n @options[key] = value\n if @_canvas? and key in ['width', 'height']\n @_canvas[key] = value\n\n setOptions: (options) ->\n @setOption key, value for own key, value of options\n\n addFrame: (image, options={}) ->\n frame = {}\n frame.transparent = @options.transparent\n for key of frameDefaults\n frame[key] = options[key] or frameDefaults[key]\n\n # use the images width and height for options unless already set\n @setOption 'width', image.width unless @options.width?\n @setOption 'height', image.height unless @options.height?\n\n if ImageData? and image instanceof ImageData\n frame.data = image.data\n else if (CanvasRenderingContext2D? and image instanceof CanvasRenderingContext2D) or (WebGLRenderingContext? and image instanceof WebGLRenderingContext)\n if options.copy\n frame.data = @getContextData image\n else\n frame.context = image\n else if image.childNodes?\n if options.copy\n frame.data = @getImageData image\n else\n frame.image = image\n else\n throw new Error 'Invalid image'\n\n @frames.push frame\n\n render: ->\n throw new Error 'Already running' if @running\n\n if not @options.width? or not @options.height?\n throw new Error 'Width and height must be set prior to rendering'\n\n @running = true\n @nextFrame = 0\n @finishedFrames = 0\n\n @imageParts = (null for i in [0...@frames.length])\n numWorkers = @spawnWorkers()\n # we need to wait for the palette\n if @options.globalPalette == true\n @renderNextFrame()\n else\n @renderNextFrame() for i in [0...numWorkers]\n\n @emit 'start'\n @emit 'progress', 0\n\n abort: ->\n loop\n worker = @activeWorkers.shift()\n break unless worker?\n @log 'killing active worker'\n worker.terminate()\n @running = false\n @emit 'abort'\n\n # private\n\n spawnWorkers: ->\n numWorkers = Math.min(@options.workers, @frames.length)\n [@freeWorkers.length...numWorkers].forEach (i) =>\n @log \"spawning worker #{ i }\"\n worker = new Worker @options.workerScript\n worker.onmessage = (event) =>\n @activeWorkers.splice @activeWorkers.indexOf(worker), 1\n @freeWorkers.push worker\n @frameFinished event.data\n @freeWorkers.push worker\n return numWorkers\n\n frameFinished: (frame) ->\n @log \"frame #{ frame.index } finished - #{ @activeWorkers.length } active\"\n @finishedFrames++\n @emit 'progress', @finishedFrames / @frames.length\n @imageParts[frame.index] = frame\n # remember calculated palette, spawn the rest of the workers\n if @options.globalPalette == true\n @options.globalPalette = frame.globalPalette\n @log 'global palette analyzed'\n @renderNextFrame() for i in [1...@freeWorkers.length] if @frames.length > 2\n if null in @imageParts\n @renderNextFrame()\n else\n @finishRendering()\n\n finishRendering: ->\n len = 0\n for frame in @imageParts\n len += (frame.data.length - 1) * frame.pageSize + frame.cursor\n len += frame.pageSize - frame.cursor\n @log \"rendering finished - filesize #{ Math.round(len / 1000) }kb\"\n data = new Uint8Array len\n offset = 0\n for frame in @imageParts\n for page, i in frame.data\n data.set page, offset\n if i is frame.data.length - 1\n offset += frame.cursor\n else\n offset += frame.pageSize\n\n image = new Blob [data],\n type: 'image/gif'\n\n @emit 'finished', image, data\n\n renderNextFrame: ->\n throw new Error 'No free workers' if @freeWorkers.length is 0\n return if @nextFrame >= @frames.length # no new frame to render\n\n frame = @frames[@nextFrame++]\n worker = @freeWorkers.shift()\n task = @getTask frame\n\n @log \"starting frame #{ task.index + 1 } of #{ @frames.length }\"\n @activeWorkers.push worker\n worker.postMessage task#, [task.data.buffer]\n\n getContextData: (ctx) ->\n return ctx.getImageData(0, 0, @options.width, @options.height).data\n\n getImageData: (image) ->\n if not @_canvas?\n @_canvas = document.createElement 'canvas'\n @_canvas.width = @options.width\n @_canvas.height = @options.height\n\n ctx = @_canvas.getContext '2d'\n ctx.fillStyle = @options.background\n ctx.fillRect 0, 0, @options.width, @options.height\n ctx.drawImage image, 0, 0\n\n return @getContextData ctx\n\n getTask: (frame) ->\n index = @frames.indexOf frame\n task =\n index: index\n last: index is (@frames.length - 1)\n delay: frame.delay\n dispose: frame.dispose\n transparent: frame.transparent\n width: @options.width\n height: @options.height\n quality: @options.quality\n dither: @options.dither\n globalPalette: @options.globalPalette\n repeat: @options.repeat\n canTransfer: (browser.name is 'chrome')\n\n if frame.data?\n task.data = frame.data\n else if frame.context?\n task.data = @getContextData frame.context\n else if frame.image?\n task.data = @getImageData frame.image\n else\n throw new Error 'Invalid frame'\n\n return task\n\n log: (args...) ->\n return unless @options.debug\n console.log args...\n\n\nmodule.exports = GIF\n"]} -------------------------------------------------------------------------------- /gif.worker.js: -------------------------------------------------------------------------------- 1 | // gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js 2 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j=0&&x1+x=0&&y1+y>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i=0){disp=this.dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<0)cur_accum|=code<=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<>betashift;var betagamma=intbias<>3;var radiusbiasshift=6;var radiusbias=1<>3);var i,v;for(i=0;i>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(jlo){a=radpower[m++];if(jlo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i>intbiasshift-netbiasshift);if(biasdist>betashift;freq[i]-=betafreq;bias[i]+=betafreq<>1;for(j=previouscol+1;j>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i=0){if(i=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j 2 | 3 | 4 | 19 | 21 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 66 | 72 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /img/tsodinClown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/emoteJAM/e4e7f860bc89fa94a8f3208b89cd82dd9586de54/img/tsodinClown.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | emoteJAM — Generate animated emotes from static images 5 | 6 | 7 | 8 | 9 | 10 |

emoteJAM

11 | 12 |

Welcome to emoteJAM — a simple website that generates animated BTTV emotes from static images. Let's get started!

13 | 14 |

1. Select an Image

15 | 16 |

112x112 static images are prefered but you can try to select any image and see what happens. This does not upload the image anywhere. emoteJAM works entirely in your browser without any server behind it.

17 | 18 |
19 | 20 |

After you selected the image move on the next section to select a Filter!

21 | 22 |

2. Select a Filter

23 |

The image below is not a GIF yet. It's just a preview of how the filter looks like when applied to the image you've selected. Try different filters and pick the one you want.

24 | 25 |
26 | 27 |

Bright green color is going to be transparent when the actual final GIF is rendered. There is no way to change that yet (but we are working on it). If your image contains that color I'm deeply sorry!

28 | 29 |

Let's render your GIF now.

30 | 31 |

3. Render the GIF

32 |

Click "Render" and when the rendering process is done you can just Right Click the final image and Save it as a GIF that you can then upload to BTTV.

33 |
34 |
35 |
36 |
37 | 38 |
39 | 40 |
41 |

By clicking "Render" you confirm that you have all of the necessary permissions from the respective copyright holders to process this image. Since all of the image processing/generation is done on your machine emoteJAM developers disclaim all the responsibility for any copyright violations done using this website.

42 |

Feel free to scroll back and start over. Have fun!

43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /js/eval.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var BinaryPrec; 3 | (function (BinaryPrec) { 4 | BinaryPrec[BinaryPrec["PREC0"] = 0] = "PREC0"; 5 | BinaryPrec[BinaryPrec["PREC1"] = 1] = "PREC1"; 6 | BinaryPrec[BinaryPrec["COUNT_PRECS"] = 2] = "COUNT_PRECS"; 7 | })(BinaryPrec || (BinaryPrec = {})); 8 | var BINARY_OPS = { 9 | '+': { 10 | func: function (lhs, rhs) { return lhs + rhs; }, 11 | prec: BinaryPrec.PREC0 12 | }, 13 | '-': { 14 | func: function (lhs, rhs) { return lhs - rhs; }, 15 | prec: BinaryPrec.PREC0 16 | }, 17 | '*': { 18 | func: function (lhs, rhs) { return lhs * rhs; }, 19 | prec: BinaryPrec.PREC1 20 | }, 21 | '/': { 22 | func: function (lhs, rhs) { return lhs / rhs; }, 23 | prec: BinaryPrec.PREC1 24 | }, 25 | '%': { 26 | func: function (lhs, rhs) { return lhs % rhs; }, 27 | prec: BinaryPrec.PREC1 28 | } 29 | }; 30 | var UNARY_OPS = { 31 | '-': function (arg) { return -arg; } 32 | }; 33 | var Lexer = (function () { 34 | function Lexer(src) { 35 | this.src = src; 36 | } 37 | Lexer.prototype.unnext = function (token) { 38 | this.src = token + this.src; 39 | }; 40 | Lexer.prototype.next = function () { 41 | this.src = this.src.trimStart(); 42 | if (this.src.length == 0) { 43 | return null; 44 | } 45 | function is_token_break(c) { 46 | var syntax = '(),'; 47 | return c in BINARY_OPS || c in UNARY_OPS || syntax.includes(c); 48 | } 49 | if (is_token_break(this.src[0])) { 50 | var token_1 = this.src[0]; 51 | this.src = this.src.slice(1); 52 | return token_1; 53 | } 54 | for (var i = 0; i < this.src.length; ++i) { 55 | if (is_token_break(this.src[i]) || this.src[i] == ' ') { 56 | var token_2 = this.src.slice(0, i); 57 | this.src = this.src.slice(i); 58 | return token_2; 59 | } 60 | } 61 | var token = this.src; 62 | this.src = ''; 63 | return token; 64 | }; 65 | return Lexer; 66 | }()); 67 | function parse_primary(lexer) { 68 | var token = lexer.next(); 69 | if (token !== null) { 70 | if (token in UNARY_OPS) { 71 | var operand = parse_expr(lexer); 72 | return { 73 | "kind": "unary_op", 74 | "payload": { 75 | "op": token, 76 | "operand": operand 77 | } 78 | }; 79 | } 80 | else if (token === '(') { 81 | var expr = parse_expr(lexer); 82 | token = lexer.next(); 83 | if (token !== ')') { 84 | throw new Error("Expected ')' but got '" + token + "'"); 85 | } 86 | return expr; 87 | } 88 | else if (token === ')') { 89 | throw new Error("No primary expression starts with ')'"); 90 | } 91 | else { 92 | var next_token = lexer.next(); 93 | if (next_token === '(') { 94 | var args = []; 95 | next_token = lexer.next(); 96 | if (next_token === ')') { 97 | return { 98 | "kind": "funcall", 99 | "payload": { 100 | "name": token, 101 | "args": args 102 | } 103 | }; 104 | } 105 | if (next_token === null) { 106 | throw Error("Unexpected end of input"); 107 | } 108 | lexer.unnext(next_token); 109 | args.push(parse_expr(lexer)); 110 | next_token = lexer.next(); 111 | while (next_token == ',') { 112 | args.push(parse_expr(lexer)); 113 | next_token = lexer.next(); 114 | } 115 | if (next_token !== ')') { 116 | throw Error("Expected ')' but got '" + next_token + "'"); 117 | } 118 | return { 119 | "kind": "funcall", 120 | "payload": { 121 | "name": token, 122 | "args": args 123 | } 124 | }; 125 | } 126 | else { 127 | if (next_token !== null) { 128 | lexer.unnext(next_token); 129 | } 130 | return { 131 | "kind": "symbol", 132 | "payload": { 133 | "value": token 134 | } 135 | }; 136 | } 137 | } 138 | } 139 | else { 140 | throw new Error('Expected primary expression but reached the end of the input'); 141 | } 142 | } 143 | function parse_expr(lexer, prec) { 144 | if (prec === void 0) { prec = BinaryPrec.PREC0; } 145 | if (prec >= BinaryPrec.COUNT_PRECS) { 146 | return parse_primary(lexer); 147 | } 148 | var lhs = parse_expr(lexer, prec + 1); 149 | var op_token = lexer.next(); 150 | if (op_token !== null) { 151 | if (op_token in BINARY_OPS && BINARY_OPS[op_token].prec == prec) { 152 | var rhs = parse_expr(lexer, prec); 153 | return { 154 | "kind": "binary_op", 155 | "payload": { 156 | "op": op_token, 157 | "lhs": lhs, 158 | "rhs": rhs 159 | } 160 | }; 161 | } 162 | else { 163 | lexer.unnext(op_token); 164 | } 165 | } 166 | return lhs; 167 | } 168 | function compile_expr(src) { 169 | var lexer = new Lexer(src); 170 | var result = parse_expr(lexer); 171 | var token = lexer.next(); 172 | if (token !== null) { 173 | console.log(typeof (token)); 174 | console.log(token); 175 | throw new Error("Unexpected token '" + token + "'"); 176 | } 177 | return result; 178 | } 179 | function run_expr(expr, user_context) { 180 | var _a; 181 | if (user_context === void 0) { user_context = {}; } 182 | console.assert(typeof (expr) === 'object'); 183 | switch (expr.kind) { 184 | case 'symbol': { 185 | var symbol = expr.payload; 186 | var value = symbol.value; 187 | var number = Number(value); 188 | if (isNaN(number)) { 189 | if (user_context.vars && value in user_context.vars) { 190 | return user_context.vars[value]; 191 | } 192 | throw new Error("Unknown variable '" + value + "'"); 193 | } 194 | else { 195 | return number; 196 | } 197 | } 198 | case 'unary_op': { 199 | var unary_op = expr.payload; 200 | if (unary_op.op in UNARY_OPS) { 201 | return UNARY_OPS[unary_op.op](run_expr(unary_op.operand, user_context)); 202 | } 203 | throw new Error("Unknown unary operator '" + unary_op.op + "'"); 204 | } 205 | case 'binary_op': { 206 | var binary_op = expr.payload; 207 | if (binary_op.op in BINARY_OPS) { 208 | return BINARY_OPS[binary_op.op].func(run_expr(binary_op.lhs, user_context), run_expr(binary_op.rhs, user_context)); 209 | } 210 | throw new Error("Unknown binary operator '" + binary_op.op + "'"); 211 | } 212 | case 'funcall': { 213 | var funcall = expr.payload; 214 | if (user_context.funcs && funcall.name in user_context.funcs) { 215 | return (_a = user_context.funcs)[funcall.name].apply(_a, funcall.args.map(function (arg) { return run_expr(arg, user_context); })); 216 | } 217 | throw new Error("Unknown function '" + funcall.name + "'"); 218 | } 219 | default: { 220 | throw new Error("Unexpected AST node '" + expr.kind + "'"); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /js/filters.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var filters = { 3 | "Hop": { 4 | "transparent": 0x00FF00 + "", 5 | "duration": "interval * 2", 6 | "params": { 7 | "interval": { 8 | "label": "Interval", 9 | "type": "float", 10 | "init": 0.85, 11 | "min": 0.01, 12 | "max": 2.00, 13 | "step": 0.01 14 | }, 15 | "ground": { 16 | "label": "Ground", 17 | "type": "float", 18 | "init": 0.5, 19 | "min": -1.0, 20 | "max": 1.0, 21 | "step": 0.01 22 | }, 23 | "scale": { 24 | "label": "Scale", 25 | "type": "float", 26 | "init": 0.40, 27 | "min": 0.0, 28 | "max": 1.0, 29 | "step": 0.01 30 | }, 31 | "jump_height": { 32 | "label": "Jump Height", 33 | "type": "float", 34 | "init": 4.0, 35 | "min": 1.0, 36 | "max": 10.0, 37 | "step": 0.01 38 | }, 39 | "hops": { 40 | "label": "Hops Count", 41 | "type": "float", 42 | "init": 2.0, 43 | "min": 1.0, 44 | "max": 5.0, 45 | "step": 1.0 46 | } 47 | }, 48 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\nuniform float time;\nuniform vec2 emoteSize;\n\nuniform float interval;\nuniform float ground;\nuniform float scale;\nuniform float jump_height;\nuniform float hops;\n\nvarying vec2 uv;\n\nfloat sliding_from_left_to_right(float time_interval) {\n return (mod(time, time_interval) - time_interval * 0.5) / (time_interval * 0.5);\n}\n\nfloat flipping_directions(float time_interval) {\n return 1.0 - 2.0 * mod(floor(time / time_interval), 2.0);\n}\n\nvoid main() {\n float x_time_interval = interval;\n float y_time_interval = x_time_interval / (2.0 * hops);\n vec2 offset = vec2(\n sliding_from_left_to_right(x_time_interval) * flipping_directions(x_time_interval) * (1.0 - scale),\n ((sliding_from_left_to_right(y_time_interval) * flipping_directions(y_time_interval) + 1.0) / jump_height) - ground);\n\n gl_Position = vec4(\n meshPosition * scale + offset,\n 0.0,\n 1.0);\n\n uv = (meshPosition + vec2(1.0, 1.0)) / 2.0;\n\n uv.x = (flipping_directions(x_time_interval) + 1.0) / 2.0 - uv.x * flipping_directions(x_time_interval);\n}\n", 49 | "fragment": "#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 50 | }, 51 | "Hopper": { 52 | "transparent": 0x00FF00 + "", 53 | "duration": "0.85", 54 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\nuniform float time;\n\nvarying vec2 uv;\n\nfloat sliding_from_left_to_right(float time_interval) {\n return (mod(time, time_interval) - time_interval * 0.5) / (time_interval * 0.5);\n}\n\nfloat flipping_directions(float time_interval) {\n return 1.0 - 2.0 * mod(floor(time / time_interval), 2.0);\n}\n\nvoid main() {\n float scale = 0.40;\n float hops = 2.0;\n float x_time_interval = 0.85 / 2.0;\n float y_time_interval = x_time_interval / (2.0 * hops);\n float height = 0.5;\n vec2 offset = vec2(\n sliding_from_left_to_right(x_time_interval) * flipping_directions(x_time_interval) * (1.0 - scale),\n ((sliding_from_left_to_right(y_time_interval) * flipping_directions(y_time_interval) + 1.0) / 4.0) - height);\n\n gl_Position = vec4(\n meshPosition * scale + offset,\n 0.0,\n 1.0);\n\n uv = (meshPosition + vec2(1.0, 1.0)) / 2.0;\n\n uv.x = (flipping_directions(x_time_interval) + 1.0) / 2.0 - uv.x * flipping_directions(x_time_interval);\n}\n", 55 | "fragment": "#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 56 | }, 57 | "Overheat": { 58 | "transparent": 0x00FF00 + "", 59 | "duration": "0.85 / 8.0 * 2.0", 60 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\nuniform float time;\n\nvarying vec2 uv;\n\nfloat sliding_from_left_to_right(float time_interval) {\n return (mod(time, time_interval) - time_interval * 0.5) / (time_interval * 0.5);\n}\n\nfloat flipping_directions(float time_interval) {\n return 1.0 - 2.0 * mod(floor(time / time_interval), 2.0);\n}\n\nvoid main() {\n float scale = 0.40;\n float hops = 2.0;\n float x_time_interval = 0.85 / 8.0;\n float y_time_interval = x_time_interval / (2.0 * hops);\n float height = 0.5;\n vec2 offset = vec2(\n sliding_from_left_to_right(x_time_interval) * flipping_directions(x_time_interval) * (1.0 - scale),\n ((sliding_from_left_to_right(y_time_interval) * flipping_directions(y_time_interval) + 1.0) / 4.0) - height);\n\n gl_Position = vec4(\n meshPosition * scale + offset,\n 0.0,\n 1.0);\n\n uv = (meshPosition + vec2(1.0, 1.0)) / 2.0;\n\n uv.x = (flipping_directions(x_time_interval) + 1.0) / 2.0 - uv.x * flipping_directions(x_time_interval);\n}\n", 61 | "fragment": "#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)) * vec4(1.0, 0.0, 0.0, 1.0);\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 62 | }, 63 | "Bounce": { 64 | "transparent": 0x00FF00 + "", 65 | "duration": "Math.PI / period", 66 | "params": { 67 | "period": { 68 | "type": "float", 69 | "init": 5.0, 70 | "min": 1.0, 71 | "max": 10.0, 72 | "step": 0.1 73 | }, 74 | "scale": { 75 | "type": "float", 76 | "init": 0.30, 77 | "min": 0.0, 78 | "max": 1.0, 79 | "step": 0.01 80 | } 81 | }, 82 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform float period;\nuniform float scale;\n\nvarying vec2 uv;\n\nvoid main() {\n vec2 offset = vec2(0.0, (2.0 * abs(sin(time * period)) - 1.0) * (1.0 - scale));\n gl_Position = vec4(meshPosition * scale + offset, 0.0, 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 83 | "fragment": "\n#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 84 | }, 85 | "Circle": { 86 | "transparent": 0x00FF00 + "", 87 | "duration": "Math.PI / 4.0", 88 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nvarying vec2 uv;\n\nvec2 rotate(vec2 v, float a) {\n\tfloat s = sin(a);\n\tfloat c = cos(a);\n\tmat2 m = mat2(c, -s, s, c);\n\treturn m * v;\n}\n\nvoid main() {\n float scale = 0.30;\n float period_interval = 8.0;\n float pi = 3.141592653589793238;\n vec2 outer_circle = vec2(cos(period_interval * time), sin(period_interval * time)) * (1.0 - scale);\n vec2 inner_circle = rotate(meshPosition * scale, (-period_interval * time) + pi / 2.0);\n gl_Position = vec4(\n inner_circle + outer_circle,\n 0.0,\n 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 89 | "fragment": "\n#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n float speed = 1.0;\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 90 | }, 91 | "Slide": { 92 | "transparent": 0x00FF00 + "", 93 | "duration": "0.85 * 2", 94 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\nuniform float time;\n\nvarying vec2 uv;\n\nfloat sliding_from_left_to_right(float time_interval) {\n return (mod(time, time_interval) - time_interval * 0.5) / (time_interval * 0.5);\n}\n\nfloat flipping_directions(float time_interval) {\n return 1.0 - 2.0 * mod(floor(time / time_interval), 2.0);\n}\n\nvoid main() {\n float scale = 0.40;\n float hops = 2.0;\n float x_time_interval = 0.85;\n float y_time_interval = x_time_interval / (2.0 * hops);\n float height = 0.5;\n vec2 offset = vec2(\n sliding_from_left_to_right(x_time_interval) * flipping_directions(x_time_interval) * (1.0 - scale),\n - height);\n\n gl_Position = vec4(\n meshPosition * scale + offset,\n 0.0,\n 1.0);\n\n uv = (meshPosition + vec2(1.0, 1.0)) / 2.0;\n\n uv.x = (flipping_directions(x_time_interval) + 1.0) / 2.0 - uv.x * flipping_directions(x_time_interval);\n}\n", 95 | "fragment": "#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 96 | }, 97 | "Laughing": { 98 | "transparent": 0x00FF00 + "", 99 | "duration": "Math.PI / 12.0", 100 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\nuniform float time;\n\nvarying vec2 uv;\n\nvoid main() {\n float a = 0.3;\n float t = (sin(24.0 * time) * a + a) / 2.0;\n\n gl_Position = vec4(\n meshPosition - vec2(0.0, t),\n 0.0,\n 1.0);\n uv = (meshPosition + vec2(1.0, 1.0)) / 2.0;\n}\n", 101 | "fragment": "#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 102 | }, 103 | "Blob": { 104 | "transparent": 0x00FF00 + "", 105 | "duration": "Math.PI / 3", 106 | "vertex": "#version 100\n\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nvarying vec2 uv;\n\nvoid main() {\n float stretch = sin(6.0 * time) * 0.5 + 1.0;\n\n vec2 offset = vec2(0.0, 1.0 - stretch);\n gl_Position = vec4(\n meshPosition * vec2(stretch, 2.0 - stretch) + offset,\n 0.0,\n 1.0);\n uv = (meshPosition + vec2(1.0, 1.0)) / 2.0;\n}\n", 107 | "fragment": "#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 108 | }, 109 | "Go": { 110 | "transparent": 0x00FF00 + "", 111 | "duration": "1 / 4", 112 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_Position = vec4(meshPosition, 0.0, 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 113 | "fragment": "\n#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nfloat slide(float speed, float value) {\n return mod(value - speed * time, 1.0);\n}\n\nvoid main() {\n float speed = 4.0;\n gl_FragColor = texture2D(emote, vec2(slide(speed, uv.x), 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 114 | }, 115 | "Elevator": { 116 | "transparent": 0x00FF00 + "", 117 | "duration": "1 / 4", 118 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_Position = vec4(meshPosition, 0.0, 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 119 | "fragment": "\n#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nfloat slide(float speed, float value) {\n return mod(value - speed * time, 1.0);\n}\n\nvoid main() {\n float speed = 4.0;\n gl_FragColor = texture2D(\n emote,\n vec2(uv.x, slide(speed, 1.0 - uv.y)));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 120 | }, 121 | "Rain": { 122 | "transparent": 0x00FF00 + "", 123 | "duration": "1", 124 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_Position = vec4(meshPosition, 0.0, 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 125 | "fragment": "\n#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nfloat slide(float speed, float value) {\n return mod(value - speed * time, 1.0);\n}\n\nvoid main() {\n float speed = 1.0;\n gl_FragColor = texture2D(\n emote,\n vec2(mod(4.0 * slide(speed, uv.x), 1.0),\n mod(4.0 * slide(speed, 1.0 - uv.y), 1.0)));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 126 | }, 127 | "Pride": { 128 | "transparent": null, 129 | "duration": "2.0", 130 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_Position = vec4(meshPosition, 0.0, 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 131 | "fragment": "\n#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvec3 hsl2rgb(vec3 c) {\n vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0);\n return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0));\n}\n\nvoid main() {\n float speed = 1.0;\n\n vec4 pixel = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n pixel.w = floor(pixel.w + 0.5);\n pixel = vec4(mix(vec3(1.0), pixel.xyz, pixel.w), 1.0);\n vec4 rainbow = vec4(hsl2rgb(vec3((time - uv.x - uv.y) * 0.5, 1.0, 0.80)), 1.0);\n gl_FragColor = pixel * rainbow;\n}\n" 132 | }, 133 | "Hard": { 134 | "transparent": 0x00FF00 + "", 135 | "duration": "2.0 * Math.PI / intensity", 136 | "params": { 137 | "zoom": { 138 | "type": "float", 139 | "init": 1.4, 140 | "min": 0.0, 141 | "max": 6.9, 142 | "step": 0.1 143 | }, 144 | "intensity": { 145 | "type": "float", 146 | "init": 32.0, 147 | "min": 1.0, 148 | "max": 42.0, 149 | "step": 1.0 150 | }, 151 | "amplitude": { 152 | "type": "float", 153 | "init": 1.0 / 8.0, 154 | "min": 0.0, 155 | "max": 1.0 / 2.0, 156 | "step": 0.001 157 | } 158 | }, 159 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform float zoom;\nuniform float intensity;\nuniform float amplitude;\n\nvarying vec2 uv;\n\nvoid main() {\n vec2 shaking = vec2(cos(intensity * time), sin(intensity * time)) * amplitude;\n gl_Position = vec4(meshPosition * zoom + shaking, 0.0, 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 160 | "fragment": "\n#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 161 | }, 162 | "Peek": { 163 | "transparent": 0x00FF00 + "", 164 | "duration": "2.0 * Math.PI", 165 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nvarying vec2 uv;\n\nvoid main() {\n float time_clipped= mod(time * 2.0, (4.0 * 3.14));\n\n float s1 = float(time_clipped < (2.0 * 3.14));\n float s2 = 1.0 - s1;\n\n float hold1 = float(time_clipped > (0.5 * 3.14) && time_clipped < (2.0 * 3.14));\n float hold2 = 1.0 - float(time_clipped > (2.5 * 3.14) && time_clipped < (4.0 * 3.14));\n\n float cycle_1 = 1.0 - ((s1 * sin(time_clipped) * (1.0 - hold1)) + hold1);\n float cycle_2 = s2 * hold2 * (sin(time_clipped) - 1.0); \n\n gl_Position = vec4(meshPosition.x + 1.0 + cycle_1 + cycle_2 , meshPosition.y, 0.0, 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 166 | "fragment": "\n#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y));\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 167 | }, 168 | "Matrix": { 169 | "transparent": null, 170 | "duration": "3.0", 171 | "vertex": "\n #version 100\n precision mediump float;\n\n attribute vec2 meshPosition;\n\n uniform vec2 resolution;\n uniform float time;\n\n varying vec2 _uv;\n\n void main()\n {\n _uv = (meshPosition + 1.0) / 2.0;\n gl_Position = vec4(meshPosition.x, meshPosition.y, 0.0, 1.0);\n }\n ", 172 | "fragment": "\n #version 100\n precision mediump float;\n\n uniform vec2 resolution;\n uniform float time;\n uniform sampler2D emote;\n\n varying vec2 _uv;\n\n float clamp01(float value)\n {\n return clamp(value, 0.0, 1.0);\n }\n\n float sdf_zero(vec2 uv)\n {\n float inside = step(0.15, abs(uv.x)) + step(0.3, abs(uv.y));\n float outside = step(0.35, abs(uv.x)) + step(0.4, abs(uv.y));\n return clamp01(inside) - clamp01(outside);\n }\n\n float sdf_one(vec2 uv)\n {\n float top = step(0.25, -uv.y) * step(0.0, uv.x);\n float inside = (step(0.2, uv.x) + top);\n float outside = step(0.35, abs(uv.x)) + step(0.4, abs(uv.y));\n return clamp01(clamp01(inside) - clamp01(outside));\n }\n\n // Random float. No precomputed gradients mean this works for any number of grid coordinates\n float random01(vec2 n)\n {\n float random = 2920.0 * sin(n.x * 21942.0 + n.y * 171324.0 + 8912.0) *\n cos(n.x * 23157.0 * n.y * 217832.0 + 9758.0);\n \n return (sin(random) + 1.0) / 2.0;\n }\n\n float loop_time(float time_frame)\n {\n float times = floor(time / time_frame);\n return time - (times * time_frame);\n }\n\n void main()\n {\n vec2 uv = _uv;\n uv.y = 1.0 - _uv.y;\n \n float number_of_numbers = 8.0;\n float number_change_rate = 2.0;\n float amount_of_numbers = 0.6; // from 0 - 1\n \n vec4 texture_color = texture2D(emote, uv);\n vec4 number_color = vec4(0, 0.7, 0, 1);\n\n float looped_time = loop_time(3.0); \n\n vec2 translation = vec2(0, looped_time * -8.0);\n\n vec2 pos_idx = floor(uv * number_of_numbers + translation);\n float rnd_number = step(0.5, random01(pos_idx + floor(looped_time * number_change_rate)));\n float rnd_show = step(1.0 - amount_of_numbers, random01(pos_idx + vec2(99,99)));\n\n vec2 nuv = uv * number_of_numbers + translation;\n nuv = fract(nuv);\n\n float one = sdf_one(nuv - 0.5) * rnd_number;\n float zero = sdf_zero(nuv - 0.5) * (1.0 - rnd_number);\n float number = (one + zero) * rnd_show;\n\n float is_texture = 1.0 - number;\n float is_number = number;\n\n vec4 col = (texture_color * is_texture) + (number_color * is_number);\n\n gl_FragColor = col;\n gl_FragColor.w = 1.0;\n }\n " 173 | }, 174 | "Flag": { 175 | "transparent": 0x00FF00 + "", 176 | "duration": "Math.PI", 177 | "vertex": "\n #version 100\n precision mediump float;\n\n attribute vec2 meshPosition;\n\n uniform vec2 resolution;\n uniform float time;\n\n varying vec2 _uv;\n\n void main()\n {\n _uv = (meshPosition + 1.0) / 2.0;\n _uv.y = 1.0 - _uv.y;\n gl_Position = vec4(meshPosition.x, meshPosition.y, 0.0, 1.0);\n }\n ", 178 | "fragment": "\n #version 100\n precision mediump float;\n\n varying vec2 _uv;\n uniform sampler2D emote;\n uniform float time;\n\n float sin01(float value)\n {\n return (sin(value) + 1.0) / 2.0;\n }\n\n //pos is left bottom point.\n float sdf_rect(vec2 pos, vec2 size, vec2 uv)\n {\n float left = pos.x;\n float right = pos.x + size.x;\n float bottom = pos.y;\n float top = pos.y + size.y;\n return (step(bottom, uv.y) - step(top, uv.y)) * (step(left, uv.x) - step(right, uv.x)); \n }\n\n void main() {\n float stick_width = 0.1;\n float flag_height = 0.75;\n float wave_size = 0.08;\n vec4 stick_color = vec4(107.0 / 256.0, 59.0 / 256.0, 9.0 / 256.0,1);\n \n vec2 flag_uv = _uv;\n flag_uv.x = (1.0 / (1.0 - stick_width)) * (flag_uv.x - stick_width);\n flag_uv.y *= 1.0 / flag_height;\n\n float flag_close_to_stick = smoothstep(0.0, 0.5, flag_uv.x);\n flag_uv.y += sin((-time * 2.0) + (flag_uv.x * 8.0)) * flag_close_to_stick * wave_size;\n\n float is_flag = sdf_rect(vec2(0,0), vec2(1.0, 1.0), flag_uv);\n float is_flag_stick = sdf_rect(vec2(0.0, 0.0), vec2(stick_width, 1), _uv);\n\n vec4 emote_color = texture2D(emote, flag_uv);\n vec4 texture_color = (emote_color * is_flag) + (stick_color * is_flag_stick);\n\n gl_FragColor = texture_color;\n }\n " 179 | }, 180 | "Thanosed": { 181 | "transparent": 0x00FF00 + "", 182 | "duration": "duration", 183 | "params": { 184 | "duration": { 185 | "type": "float", 186 | "init": 6.0, 187 | "min": 1.0, 188 | "max": 16.0, 189 | "step": 1.0 190 | }, 191 | "delay": { 192 | "type": "float", 193 | "init": 0.2, 194 | "min": 0.0, 195 | "max": 1.0, 196 | "step": 0.1 197 | }, 198 | "pixelization": { 199 | "type": "float", 200 | "init": 1.0, 201 | "min": 1.0, 202 | "max": 3.0, 203 | "step": 1.0 204 | } 205 | }, 206 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_Position = vec4(meshPosition, 0.0, 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 207 | "fragment": "\n#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\nuniform float duration;\nuniform float delay;\nuniform float pixelization;\n\nuniform sampler2D emote;\n\nvarying vec2 uv;\n\n// https://www.aussiedwarf.com/2017/05/09/Random10Bit.html\nfloat rand(vec2 co){\n vec3 product = vec3( sin( dot(co, vec2(0.129898,0.78233))),\n sin( dot(co, vec2(0.689898,0.23233))),\n sin( dot(co, vec2(0.434198,0.51833))) );\n vec3 weighting = vec3(4.37585453723, 2.465973, 3.18438);\n return fract(dot(weighting, product));\n}\n\nvoid main() {\n float pixelated_resolution = 112.0 / pixelization;\n vec2 pixelated_uv = floor(uv * pixelated_resolution);\n float noise = (rand(pixelated_uv) + 1.0) / 2.0;\n float slope = (0.2 + noise * 0.8) * (1.0 - (0.0 + uv.x * 0.5));\n float time_interval = 1.1 + delay * 2.0;\n float progress = 0.2 + delay + slope - mod(time_interval * time / duration, time_interval);\n float mask = progress > 0.1 ? 1.0 : 0.0;\n vec4 pixel = texture2D(emote, vec2(uv.x * (progress > 0.5 ? 1.0 : progress * 2.0), 1.0 - uv.y));\n pixel.w = floor(pixel.w + 0.5);\n gl_FragColor = pixel * vec4(vec3(1.0), mask);\n}\n" 208 | }, 209 | "Ripple": { 210 | "transparent": 0x00FF00 + "", 211 | "duration": "2 * Math.PI / b", 212 | "params": { 213 | "a": { 214 | "label": "Wave Length", 215 | "type": "float", 216 | "init": 12.0, 217 | "min": 0.01, 218 | "max": 24.0, 219 | "step": 0.01 220 | }, 221 | "b": { 222 | "label": "Time Freq", 223 | "type": "float", 224 | "init": 4.0, 225 | "min": 0.01, 226 | "max": 8.0, 227 | "step": 0.01 228 | }, 229 | "c": { 230 | "label": "Amplitude", 231 | "type": "float", 232 | "init": 0.03, 233 | "min": 0.01, 234 | "max": 0.06, 235 | "step": 0.01 236 | } 237 | }, 238 | "vertex": "#version 100\nprecision mediump float;\n\nattribute vec2 meshPosition;\n\nuniform vec2 resolution;\nuniform float time;\n\nvarying vec2 uv;\n\nvoid main() {\n gl_Position = vec4(meshPosition, 0.0, 1.0);\n uv = (meshPosition + 1.0) / 2.0;\n}\n", 239 | "fragment": "#version 100\n\nprecision mediump float;\n\nuniform vec2 resolution;\nuniform float time;\n\nuniform sampler2D emote;\n\nuniform float a;\nuniform float b;\nuniform float c;\n\nvarying vec2 uv;\n\nvoid main() {\n vec2 pos = vec2(uv.x, 1.0 - uv.y);\n vec2 center = vec2(0.5);\n vec2 dir = pos - center;\n float x = length(dir);\n float y = sin(x + time);\n vec4 pixel = texture2D(emote, pos + cos(x*a - time*b)*c*(dir/x));\n gl_FragColor = pixel;\n gl_FragColor.w = floor(gl_FragColor.w + 0.5);\n}\n" 240 | } 241 | }; 242 | -------------------------------------------------------------------------------- /js/grecha.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __spreadArray = (this && this.__spreadArray) || function (to, from) { 3 | for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) 4 | to[j] = from[i]; 5 | return to; 6 | }; 7 | var LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; 8 | function tag(name) { 9 | var children = []; 10 | for (var _i = 1; _i < arguments.length; _i++) { 11 | children[_i - 1] = arguments[_i]; 12 | } 13 | var result = document.createElement(name); 14 | for (var _a = 0, children_1 = children; _a < children_1.length; _a++) { 15 | var child = children_1[_a]; 16 | if (typeof (child) === 'string') { 17 | result.appendChild(document.createTextNode(child)); 18 | } 19 | else { 20 | result.appendChild(child); 21 | } 22 | } 23 | result.att$ = function (name, value) { 24 | this.setAttribute(name, value); 25 | return this; 26 | }; 27 | result.onclick$ = function (callback) { 28 | this.onclick = callback; 29 | return this; 30 | }; 31 | return result; 32 | } 33 | function canvas() { 34 | var children = []; 35 | for (var _i = 0; _i < arguments.length; _i++) { 36 | children[_i] = arguments[_i]; 37 | } 38 | return tag.apply(void 0, __spreadArray(["canvas"], children)); 39 | } 40 | function h1() { 41 | var children = []; 42 | for (var _i = 0; _i < arguments.length; _i++) { 43 | children[_i] = arguments[_i]; 44 | } 45 | return tag.apply(void 0, __spreadArray(["h1"], children)); 46 | } 47 | function h2() { 48 | var children = []; 49 | for (var _i = 0; _i < arguments.length; _i++) { 50 | children[_i] = arguments[_i]; 51 | } 52 | return tag.apply(void 0, __spreadArray(["h2"], children)); 53 | } 54 | function h3() { 55 | var children = []; 56 | for (var _i = 0; _i < arguments.length; _i++) { 57 | children[_i] = arguments[_i]; 58 | } 59 | return tag.apply(void 0, __spreadArray(["h3"], children)); 60 | } 61 | function p() { 62 | var children = []; 63 | for (var _i = 0; _i < arguments.length; _i++) { 64 | children[_i] = arguments[_i]; 65 | } 66 | return tag.apply(void 0, __spreadArray(["p"], children)); 67 | } 68 | function a() { 69 | var children = []; 70 | for (var _i = 0; _i < arguments.length; _i++) { 71 | children[_i] = arguments[_i]; 72 | } 73 | return tag.apply(void 0, __spreadArray(["a"], children)); 74 | } 75 | function div() { 76 | var children = []; 77 | for (var _i = 0; _i < arguments.length; _i++) { 78 | children[_i] = arguments[_i]; 79 | } 80 | return tag.apply(void 0, __spreadArray(["div"], children)); 81 | } 82 | function span() { 83 | var children = []; 84 | for (var _i = 0; _i < arguments.length; _i++) { 85 | children[_i] = arguments[_i]; 86 | } 87 | return tag.apply(void 0, __spreadArray(["span"], children)); 88 | } 89 | function select() { 90 | var children = []; 91 | for (var _i = 0; _i < arguments.length; _i++) { 92 | children[_i] = arguments[_i]; 93 | } 94 | return tag.apply(void 0, __spreadArray(["select"], children)); 95 | } 96 | function img(src) { 97 | return tag("img").att$("src", src); 98 | } 99 | function input(type) { 100 | return tag("input").att$("type", type); 101 | } 102 | function router(routes) { 103 | var result = div(); 104 | function syncHash() { 105 | var hashLocation = document.location.hash.split('#')[1]; 106 | if (!hashLocation) { 107 | hashLocation = '/'; 108 | } 109 | if (!(hashLocation in routes)) { 110 | var route404 = '/404'; 111 | console.assert(route404 in routes); 112 | hashLocation = route404; 113 | } 114 | while (result.firstChild) { 115 | result.removeChild(result.lastChild); 116 | } 117 | result.appendChild(routes[hashLocation]); 118 | return result; 119 | } 120 | ; 121 | syncHash(); 122 | window.addEventListener("hashchange", syncHash); 123 | return result; 124 | } 125 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var feature_params = false; 3 | var vertexAttribs = { 4 | "meshPosition": 0 5 | }; 6 | var TRIANGLE_PAIR = 2; 7 | var TRIANGLE_VERTICIES = 3; 8 | var VEC2_COUNT = 2; 9 | var VEC2_X = 0; 10 | var VEC2_Y = 1; 11 | var CANVAS_WIDTH = 112; 12 | var CANVAS_HEIGHT = 112; 13 | function compileShaderSource(gl, source, shaderType) { 14 | function shaderTypeToString() { 15 | switch (shaderType) { 16 | case gl.VERTEX_SHADER: return 'Vertex'; 17 | case gl.FRAGMENT_SHADER: return 'Fragment'; 18 | default: return shaderType; 19 | } 20 | } 21 | var shader = gl.createShader(shaderType); 22 | if (shader === null) { 23 | throw new Error("Could not create a new shader"); 24 | } 25 | gl.shaderSource(shader, source); 26 | gl.compileShader(shader); 27 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 28 | throw new Error("Could not compile " + shaderTypeToString() + " shader: " + gl.getShaderInfoLog(shader)); 29 | } 30 | return shader; 31 | } 32 | function linkShaderProgram(gl, shaders, vertexAttribs) { 33 | var program = gl.createProgram(); 34 | if (program === null) { 35 | throw new Error('Could not create a new shader program'); 36 | } 37 | for (var _i = 0, shaders_1 = shaders; _i < shaders_1.length; _i++) { 38 | var shader = shaders_1[_i]; 39 | gl.attachShader(program, shader); 40 | } 41 | for (var vertexName in vertexAttribs) { 42 | gl.bindAttribLocation(program, vertexAttribs[vertexName], vertexName); 43 | } 44 | gl.linkProgram(program); 45 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 46 | throw new Error("Could not link shader program: " + gl.getProgramInfoLog(program)); 47 | } 48 | return program; 49 | } 50 | function createTextureFromImage(gl, image) { 51 | var textureId = gl.createTexture(); 52 | if (textureId === null) { 53 | throw new Error('Could not create a new WebGL texture'); 54 | } 55 | gl.bindTexture(gl.TEXTURE_2D, textureId); 56 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 57 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 58 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 59 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 60 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 61 | return textureId; 62 | } 63 | function loadFilterProgram(gl, filter, vertexAttribs) { 64 | var _a; 65 | var vertexShader = compileShaderSource(gl, filter.vertex, gl.VERTEX_SHADER); 66 | var fragmentShader = compileShaderSource(gl, filter.fragment, gl.FRAGMENT_SHADER); 67 | var id = linkShaderProgram(gl, [vertexShader, fragmentShader], vertexAttribs); 68 | gl.deleteShader(vertexShader); 69 | gl.deleteShader(fragmentShader); 70 | gl.useProgram(id); 71 | var uniforms = { 72 | "resolution": gl.getUniformLocation(id, 'resolution'), 73 | "time": gl.getUniformLocation(id, 'time'), 74 | "emoteSize": gl.getUniformLocation(id, 'emoteSize') 75 | }; 76 | var paramsPanel = div().att$("class", "widget-element"); 77 | var paramsInputs = {}; 78 | var _loop_1 = function (paramName) { 79 | if (paramName in uniforms) { 80 | throw new Error("Redefinition of existing uniform parameter " + paramName); 81 | } 82 | switch (filter.params[paramName].type) { 83 | case "float": 84 | { 85 | var valuePreview_1 = span(filter.params[paramName].init.toString()); 86 | var valueInput = input("range"); 87 | if (filter.params[paramName].min !== undefined) { 88 | valueInput.att$("min", filter.params[paramName].min); 89 | } 90 | if (filter.params[paramName].max !== undefined) { 91 | valueInput.att$("max", filter.params[paramName].max); 92 | } 93 | if (filter.params[paramName].step !== undefined) { 94 | valueInput.att$("step", filter.params[paramName].step); 95 | } 96 | if (filter.params[paramName].init !== undefined) { 97 | valueInput.att$("value", filter.params[paramName].init); 98 | } 99 | paramsInputs[paramName] = valueInput; 100 | valueInput.oninput = function () { 101 | valuePreview_1.innerText = this.value; 102 | paramsPanel.dispatchEvent(new CustomEvent("paramsChanged")); 103 | }; 104 | var label = (_a = filter.params[paramName].label) !== null && _a !== void 0 ? _a : paramName; 105 | paramsPanel.appendChild(div(span(label + ": "), valuePreview_1, div(valueInput))); 106 | } 107 | break; 108 | default: { 109 | throw new Error("Filter parameters do not support type " + filter.params[paramName].type); 110 | } 111 | } 112 | uniforms[paramName] = gl.getUniformLocation(id, paramName); 113 | }; 114 | for (var paramName in filter.params) { 115 | _loop_1(paramName); 116 | } 117 | paramsPanel.paramsSnapshot$ = function () { 118 | var snapshot = {}; 119 | for (var paramName in paramsInputs) { 120 | snapshot[paramName] = { 121 | "uniform": uniforms[paramName], 122 | "value": Number(paramsInputs[paramName].value) 123 | }; 124 | } 125 | return snapshot; 126 | }; 127 | return { 128 | "id": id, 129 | "uniforms": uniforms, 130 | "duration": compile_expr(filter.duration), 131 | "transparent": filter.transparent, 132 | "paramsPanel": paramsPanel 133 | }; 134 | } 135 | function ImageSelector() { 136 | var imageInput = input("file"); 137 | var imagePreview = img("img/tsodinClown.png") 138 | .att$("class", "widget-element") 139 | .att$("width", CANVAS_WIDTH); 140 | var root = div(div(imageInput).att$("class", "widget-element"), imagePreview).att$("class", "widget"); 141 | root.selectedImage$ = function () { 142 | return imagePreview; 143 | }; 144 | root.selectedFileName$ = function () { 145 | function removeFileNameExt(fileName) { 146 | if (fileName.includes('.')) { 147 | return fileName.split('.').slice(0, -1).join('.'); 148 | } 149 | else { 150 | return fileName; 151 | } 152 | } 153 | var file = imageInput.files[0]; 154 | return file ? removeFileNameExt(file.name) : 'result'; 155 | }; 156 | root.updateFiles$ = function (files) { 157 | imageInput.files = files; 158 | imageInput.onchange(); 159 | }; 160 | imagePreview.addEventListener('load', function () { 161 | root.dispatchEvent(new CustomEvent("imageSelected", { 162 | detail: { 163 | imageData: this 164 | } 165 | })); 166 | }); 167 | imagePreview.addEventListener('error', function () { 168 | imageInput.value = ''; 169 | this.src = 'img/error.png'; 170 | }); 171 | imageInput.onchange = function () { 172 | imagePreview.src = URL.createObjectURL(this.files[0]); 173 | }; 174 | return root; 175 | } 176 | function FilterList() { 177 | var root = select(); 178 | for (var name_1 in filters) { 179 | root.add(new Option(name_1)); 180 | } 181 | root.selectedFilter$ = function () { 182 | return filters[root.selectedOptions[0].value]; 183 | }; 184 | root.onchange = function () { 185 | root.dispatchEvent(new CustomEvent('filterChanged', { 186 | detail: { 187 | filter: root.selectedFilter$() 188 | } 189 | })); 190 | }; 191 | root.addEventListener('wheel', function (e) { 192 | e.preventDefault(); 193 | if (e.deltaY < 0) { 194 | root.selectedIndex = Math.max(root.selectedIndex - 1, 0); 195 | } 196 | if (e.deltaY > 0) { 197 | root.selectedIndex = Math.min(root.selectedIndex + 1, root.length - 1); 198 | } 199 | root.onchange(); 200 | }); 201 | return root; 202 | } 203 | function FilterSelector() { 204 | var filterList_ = FilterList(); 205 | var filterPreview = canvas() 206 | .att$("width", CANVAS_WIDTH) 207 | .att$("height", CANVAS_HEIGHT); 208 | var root = div(div("Filter: ", filterList_) 209 | .att$("class", "widget-element"), filterPreview.att$("class", "widget-element")).att$("class", "widget"); 210 | var gl = filterPreview.getContext("webgl", { antialias: false, alpha: false }); 211 | if (!gl) { 212 | throw new Error("Could not initialize WebGL context"); 213 | } 214 | { 215 | gl.enable(gl.BLEND); 216 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 217 | { 218 | var meshPositionBufferData = new Float32Array(TRIANGLE_PAIR * TRIANGLE_VERTICIES * VEC2_COUNT); 219 | for (var triangle = 0; triangle < TRIANGLE_PAIR; ++triangle) { 220 | for (var vertex = 0; vertex < TRIANGLE_VERTICIES; ++vertex) { 221 | var quad = triangle + vertex; 222 | var index = triangle * TRIANGLE_VERTICIES * VEC2_COUNT + 223 | vertex * VEC2_COUNT; 224 | meshPositionBufferData[index + VEC2_X] = (2 * (quad & 1) - 1); 225 | meshPositionBufferData[index + VEC2_Y] = (2 * ((quad >> 1) & 1) - 1); 226 | } 227 | } 228 | var meshPositionBuffer = gl.createBuffer(); 229 | gl.bindBuffer(gl.ARRAY_BUFFER, meshPositionBuffer); 230 | gl.bufferData(gl.ARRAY_BUFFER, meshPositionBufferData, gl.STATIC_DRAW); 231 | var meshPositionAttrib = vertexAttribs['meshPosition']; 232 | gl.vertexAttribPointer(meshPositionAttrib, VEC2_COUNT, gl.FLOAT, false, 0, 0); 233 | gl.enableVertexAttribArray(meshPositionAttrib); 234 | } 235 | } 236 | var emoteImage = undefined; 237 | var emoteTexture = undefined; 238 | var program = undefined; 239 | function syncParams() { 240 | if (program) { 241 | var snapshot = program.paramsPanel.paramsSnapshot$(); 242 | for (var paramName in snapshot) { 243 | gl.uniform1f(snapshot[paramName].uniform, snapshot[paramName].value); 244 | } 245 | } 246 | } 247 | program = loadFilterProgram(gl, filterList_.selectedFilter$(), vertexAttribs); 248 | program.paramsPanel.addEventListener('paramsChanged', syncParams); 249 | if (feature_params) { 250 | root.appendChild(program.paramsPanel); 251 | } 252 | syncParams(); 253 | root.updateImage$ = function (newEmoteImage) { 254 | emoteImage = newEmoteImage; 255 | if (emoteTexture) { 256 | gl.deleteTexture(emoteTexture); 257 | } 258 | emoteTexture = createTextureFromImage(gl, emoteImage); 259 | }; 260 | filterList_.addEventListener('filterChanged', function (e) { 261 | if (program) { 262 | gl.deleteProgram(program.id); 263 | program.paramsPanel.removeEventListener('paramsChanged', syncParams); 264 | if (feature_params) { 265 | root.removeChild(program.paramsPanel); 266 | } 267 | } 268 | program = loadFilterProgram(gl, e.detail.filter, vertexAttribs); 269 | program.paramsPanel.addEventListener('paramsChanged', syncParams); 270 | if (feature_params) { 271 | root.appendChild(program.paramsPanel); 272 | } 273 | syncParams(); 274 | }); 275 | root.render$ = function (filename) { 276 | if (program === undefined) { 277 | console.warn('Could not rendering anything because the filter was not selected'); 278 | return undefined; 279 | } 280 | if (emoteImage == undefined) { 281 | console.warn('Could not rendering anything because the image was not selected'); 282 | return undefined; 283 | } 284 | var gif = new GIF({ 285 | workers: 5, 286 | quality: 10, 287 | width: CANVAS_WIDTH, 288 | height: CANVAS_HEIGHT, 289 | transparent: program.transparent 290 | }); 291 | var context = { 292 | "vars": { 293 | "Math.PI": Math.PI 294 | } 295 | }; 296 | if (context.vars !== undefined) { 297 | var snapshot = program.paramsPanel.paramsSnapshot$(); 298 | for (var paramName in snapshot) { 299 | context.vars[paramName] = snapshot[paramName].value; 300 | } 301 | } 302 | var fps = 30; 303 | var dt = 1.0 / fps; 304 | var duration = Math.min(run_expr(program.duration, context), 60); 305 | var renderProgress = document.getElementById("render-progress"); 306 | if (renderProgress === null) { 307 | throw new Error('Could not find "render-progress"'); 308 | } 309 | var renderSpinner = document.getElementById("render-spinner"); 310 | if (renderSpinner === null) { 311 | throw new Error('Could not find "render-spinner"'); 312 | } 313 | var renderPreview = document.getElementById("render-preview"); 314 | if (renderPreview === null) { 315 | throw new Error('Could not find "render-preview"'); 316 | } 317 | var renderDownload = document.getElementById("render-download"); 318 | if (renderDownload === null) { 319 | throw new Error('Could not find "render-download"'); 320 | } 321 | renderPreview.style.display = "none"; 322 | renderSpinner.style.display = "block"; 323 | var t = 0.0; 324 | while (t <= duration) { 325 | gl.uniform1f(program.uniforms.time, t); 326 | gl.uniform2f(program.uniforms.resolution, CANVAS_WIDTH, CANVAS_HEIGHT); 327 | gl.uniform2f(program.uniforms.emoteSize, emoteImage.width, emoteImage.height); 328 | gl.clearColor(0.0, 1.0, 0.0, 1.0); 329 | gl.clear(gl.COLOR_BUFFER_BIT); 330 | gl.drawArrays(gl.TRIANGLES, 0, TRIANGLE_PAIR * TRIANGLE_VERTICIES); 331 | var pixels = new Uint8ClampedArray(4 * CANVAS_WIDTH * CANVAS_HEIGHT); 332 | gl.readPixels(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, gl.RGBA, gl.UNSIGNED_BYTE, pixels); 333 | { 334 | var center = Math.floor(CANVAS_HEIGHT / 2); 335 | for (var y = 0; y < center; ++y) { 336 | var row = 4 * CANVAS_WIDTH; 337 | for (var x = 0; x < row; ++x) { 338 | var ai = y * 4 * CANVAS_WIDTH + x; 339 | var bi = (CANVAS_HEIGHT - y - 1) * 4 * CANVAS_WIDTH + x; 340 | var a_1 = pixels[ai]; 341 | var b = pixels[bi]; 342 | pixels[ai] = b; 343 | pixels[bi] = a_1; 344 | } 345 | } 346 | } 347 | gif.addFrame(new ImageData(pixels, CANVAS_WIDTH, CANVAS_HEIGHT), { 348 | delay: dt * 1000, 349 | dispose: 2 350 | }); 351 | renderProgress.style.width = (t / duration) * 50 + "%"; 352 | t += dt; 353 | } 354 | gif.on('finished', function (blob) { 355 | renderPreview.src = URL.createObjectURL(blob); 356 | renderPreview.style.display = "block"; 357 | renderDownload.href = renderPreview.src; 358 | renderDownload.download = filename; 359 | renderDownload.style.display = "block"; 360 | renderSpinner.style.display = "none"; 361 | }); 362 | gif.on('progress', function (p) { 363 | renderProgress.style.width = 50 + p * 50 + "%"; 364 | }); 365 | gif.render(); 366 | return gif; 367 | }; 368 | { 369 | var step_1 = function (timestamp) { 370 | gl.clearColor(0.0, 1.0, 0.0, 1.0); 371 | gl.clear(gl.COLOR_BUFFER_BIT); 372 | if (program && emoteImage) { 373 | gl.uniform1f(program.uniforms.time, timestamp * 0.001); 374 | gl.uniform2f(program.uniforms.resolution, filterPreview.width, filterPreview.height); 375 | gl.uniform2f(program.uniforms.emoteSize, emoteImage.width, emoteImage.height); 376 | gl.drawArrays(gl.TRIANGLES, 0, TRIANGLE_PAIR * TRIANGLE_VERTICIES); 377 | } 378 | window.requestAnimationFrame(step_1); 379 | }; 380 | window.requestAnimationFrame(step_1); 381 | } 382 | return root; 383 | } 384 | window.onload = function () { 385 | if ("serviceWorker" in navigator) { 386 | navigator.serviceWorker.register('serviceworker.js').then(function (registration) { 387 | console.log("Registered a Service Worker ", registration); 388 | }, function (error) { 389 | console.error("Could not register a Service Worker ", error); 390 | }); 391 | } 392 | else { 393 | console.error("Service Workers are not supported in this browser."); 394 | } 395 | feature_params = new URLSearchParams(document.location.search).has("feature-params"); 396 | var filterSelectorEntry = document.getElementById('filter-selector-entry'); 397 | if (filterSelectorEntry === null) { 398 | throw new Error('Could not find "filter-selector-entry"'); 399 | } 400 | var imageSelectorEntry = document.getElementById('image-selector-entry'); 401 | if (imageSelectorEntry === null) { 402 | throw new Error('Could not find "image-selector-entry"'); 403 | } 404 | var imageSelector = ImageSelector(); 405 | var filterSelector = FilterSelector(); 406 | imageSelector.addEventListener('imageSelected', function (e) { 407 | filterSelector.updateImage$(e.detail.imageData); 408 | }); 409 | filterSelectorEntry.appendChild(filterSelector); 410 | imageSelectorEntry.appendChild(imageSelector); 411 | document.ondrop = function (event) { 412 | var _a; 413 | event.preventDefault(); 414 | imageSelector.updateFiles$((_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files); 415 | }; 416 | document.ondragover = function (event) { 417 | event.preventDefault(); 418 | }; 419 | var gif = undefined; 420 | var renderButton = document.getElementById("render"); 421 | if (renderButton === null) { 422 | throw new Error('Could not find "render"'); 423 | } 424 | renderButton.onclick = function () { 425 | if (gif && gif.running) { 426 | gif.abort(); 427 | } 428 | var fileName = imageSelector.selectedFileName$(); 429 | gif = filterSelector.render$(fileName + ".gif"); 430 | }; 431 | }; 432 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emotejam", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "emotejam", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/gif.js": "^0.2.1", 13 | "@types/node": "^20.9.3", 14 | "typescript": "^4.3.2" 15 | } 16 | }, 17 | "node_modules/@types/gif.js": { 18 | "version": "0.2.1", 19 | "resolved": "https://registry.npmjs.org/@types/gif.js/-/gif.js-0.2.1.tgz", 20 | "integrity": "sha512-oZAPX8pgueiAngu3HfynjdtsDNt4EiD1fs5An//LtBKvOdnc4Wq8/S7GkAKpP80+29WyVwGEGVEUXPyFhbQ2+g==", 21 | "dev": true 22 | }, 23 | "node_modules/@types/node": { 24 | "version": "20.9.3", 25 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz", 26 | "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==", 27 | "dev": true, 28 | "dependencies": { 29 | "undici-types": "~5.26.4" 30 | } 31 | }, 32 | "node_modules/typescript": { 33 | "version": "4.3.2", 34 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz", 35 | "integrity": "sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==", 36 | "dev": true, 37 | "bin": { 38 | "tsc": "bin/tsc", 39 | "tsserver": "bin/tsserver" 40 | }, 41 | "engines": { 42 | "node": ">=4.2.0" 43 | } 44 | }, 45 | "node_modules/undici-types": { 46 | "version": "5.26.5", 47 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 48 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 49 | "dev": true 50 | } 51 | }, 52 | "dependencies": { 53 | "@types/gif.js": { 54 | "version": "0.2.1", 55 | "resolved": "https://registry.npmjs.org/@types/gif.js/-/gif.js-0.2.1.tgz", 56 | "integrity": "sha512-oZAPX8pgueiAngu3HfynjdtsDNt4EiD1fs5An//LtBKvOdnc4Wq8/S7GkAKpP80+29WyVwGEGVEUXPyFhbQ2+g==", 57 | "dev": true 58 | }, 59 | "@types/node": { 60 | "version": "20.9.3", 61 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz", 62 | "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==", 63 | "dev": true, 64 | "requires": { 65 | "undici-types": "~5.26.4" 66 | } 67 | }, 68 | "typescript": { 69 | "version": "4.3.2", 70 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz", 71 | "integrity": "sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==", 72 | "dev": true 73 | }, 74 | "undici-types": { 75 | "version": "5.26.5", 76 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 77 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 78 | "dev": true 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emotejam", 3 | "version": "1.0.0", 4 | "description": "Simple website that generates animated BTTV emotes from static images", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "node build.js build", 9 | "watch": "node build.js watch", 10 | "serve": "node build.js serve", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/tsoding/emoteJAM.git" 16 | }, 17 | "keywords": [ 18 | "bttv", 19 | "twitch", 20 | "emotes", 21 | "emoji", 22 | "webgl", 23 | "gif", 24 | "memes" 25 | ], 26 | "author": "Alexey Kutepov ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/tsoding/emoteJAM/issues" 30 | }, 31 | "homepage": "https://github.com/tsoding/emoteJAM#readme", 32 | "devDependencies": { 33 | "@types/gif.js": "^0.2.1", 34 | "@types/node": "^20.9.3", 35 | "typescript": "^4.3.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /serviceworker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var cacheName = "emoteJAM-v1"; 39 | var assets = [ 40 | 'index.html', 41 | 'css/bright.css', 42 | 'css/main.css', 43 | 'css/reset.css', 44 | 'gif.js', 45 | 'gif.worker.js', 46 | 'img/tsodinClown.png', 47 | 'js/eval.js', 48 | 'js/filters.js', 49 | 'js/grecha.js', 50 | 'js/index.js', 51 | ]; 52 | self.addEventListener("install", function (e) { 53 | console.log("[Service Worker] Install"); 54 | var event = e; 55 | event.waitUntil((function () { return __awaiter(void 0, void 0, void 0, function () { 56 | var cache; 57 | return __generator(this, function (_a) { 58 | switch (_a.label) { 59 | case 0: 60 | console.log("[Service Worker] Caching all the assets"); 61 | return [4, caches.open(cacheName)]; 62 | case 1: 63 | cache = _a.sent(); 64 | cache.addAll(assets); 65 | return [2]; 66 | } 67 | }); 68 | }); })()); 69 | }); 70 | self.addEventListener("activate", function (e) { 71 | console.log("[Service Worker] Activate"); 72 | var event = e; 73 | event.waitUntil((function () { return __awaiter(void 0, void 0, void 0, function () { 74 | var keys, _a, _b, _i, key; 75 | return __generator(this, function (_c) { 76 | switch (_c.label) { 77 | case 0: 78 | console.log("[Service Worker] Cleaning up all caches"); 79 | return [4, caches.keys()]; 80 | case 1: 81 | keys = _c.sent(); 82 | _a = []; 83 | for (_b in keys) 84 | _a.push(_b); 85 | _i = 0; 86 | _c.label = 2; 87 | case 2: 88 | if (!(_i < _a.length)) return [3, 5]; 89 | key = _a[_i]; 90 | if (!(key !== cacheName)) return [3, 4]; 91 | return [4, caches["delete"](key)]; 92 | case 3: 93 | _c.sent(); 94 | _c.label = 4; 95 | case 4: 96 | _i++; 97 | return [3, 2]; 98 | case 5: return [2]; 99 | } 100 | }); 101 | }); })()); 102 | }); 103 | self.addEventListener("fetch", function (e) { 104 | var event = e; 105 | event.respondWith((function () { return __awaiter(void 0, void 0, void 0, function () { 106 | var cache, response; 107 | return __generator(this, function (_a) { 108 | switch (_a.label) { 109 | case 0: 110 | console.log("[Service Worker] Fetch " + event.request.url); 111 | return [4, caches.open(cacheName)]; 112 | case 1: 113 | cache = _a.sent(); 114 | return [4, cache.match(event.request.url)]; 115 | case 2: 116 | response = _a.sent(); 117 | if (!(response === undefined)) return [3, 4]; 118 | console.log("[Service Worker] Response for " + event.request.url + " is not available in cache. Making an actual request..."); 119 | return [4, fetch(event.request.url)]; 120 | case 3: 121 | response = _a.sent(); 122 | cache.put(event.request.url, response.clone()); 123 | _a.label = 4; 124 | case 4: return [2, response]; 125 | } 126 | }); 127 | }); })()); 128 | }); 129 | -------------------------------------------------------------------------------- /serviceworker.ts: -------------------------------------------------------------------------------- 1 | const cacheName = "emoteJAM-v1"; 2 | const assets = [ 3 | 'index.html', 4 | 'css/bright.css', 5 | 'css/main.css', 6 | 'css/reset.css', 7 | 'gif.js', 8 | 'gif.worker.js', 9 | 'img/tsodinClown.png', 10 | 'js/eval.js', 11 | 'js/filters.js', 12 | 'js/grecha.js', 13 | 'js/index.js', 14 | ]; 15 | 16 | self.addEventListener("install", e => { 17 | console.log("[Service Worker] Install"); 18 | const event = e as ExtendableEvent; 19 | event.waitUntil((async () => { 20 | console.log("[Service Worker] Caching all the assets"); 21 | const cache = await caches.open(cacheName); 22 | cache.addAll(assets); 23 | })()); 24 | }); 25 | 26 | self.addEventListener("activate", e => { 27 | console.log("[Service Worker] Activate"); 28 | const event = e as ExtendableEvent; 29 | event.waitUntil((async() => { 30 | console.log("[Service Worker] Cleaning up all caches"); 31 | const keys = await caches.keys(); 32 | for (let key in keys) { 33 | if (key !== cacheName) { 34 | await caches.delete(key); 35 | } 36 | } 37 | })()); 38 | }); 39 | 40 | self.addEventListener("fetch", (e) => { 41 | const event = e as FetchEvent; 42 | event.respondWith((async () => { 43 | console.log(`[Service Worker] Fetch ${event.request.url}`); 44 | const cache = await caches.open(cacheName); 45 | let response = await cache.match(event.request.url); 46 | if (response === undefined) { 47 | console.log(`[Service Worker] Response for ${event.request.url} is not available in cache. Making an actual request...`); 48 | response = await fetch(event.request.url); 49 | cache.put(event.request.url, response.clone()); 50 | } 51 | return response; 52 | })()); 53 | }); 54 | -------------------------------------------------------------------------------- /ts/eval.ts: -------------------------------------------------------------------------------- 1 | type BinaryOp = '+' | '-' | '*' | '/' | '%'; 2 | type BinaryOpFunc = (lhs: number, rhs: number) => number; 3 | 4 | enum BinaryPrec { 5 | PREC0 = 0, 6 | PREC1, 7 | COUNT_PRECS 8 | } 9 | 10 | interface BinaryOpDef { 11 | func: BinaryOpFunc, 12 | prec: BinaryPrec 13 | } 14 | 15 | const BINARY_OPS: {[op in BinaryOp]: BinaryOpDef} = { 16 | '+': { 17 | func: (lhs, rhs) => lhs + rhs, 18 | prec: BinaryPrec.PREC0 19 | }, 20 | '-': { 21 | func: (lhs, rhs) => lhs - rhs, 22 | prec: BinaryPrec.PREC0 23 | }, 24 | '*': { 25 | func: (lhs, rhs) => lhs * rhs, 26 | prec: BinaryPrec.PREC1 27 | }, 28 | '/': { 29 | func: (lhs, rhs) => lhs / rhs, 30 | prec: BinaryPrec.PREC1 31 | }, 32 | '%': { 33 | func: (lhs, rhs) => lhs % rhs, 34 | prec: BinaryPrec.PREC1 35 | } 36 | }; 37 | 38 | type UnaryOp = '-'; 39 | type UnaryOpFunc = (arg: number) => number; 40 | 41 | const UNARY_OPS: {[op in UnaryOp]: UnaryOpFunc} = { 42 | '-': (arg: number) => -arg, 43 | }; 44 | 45 | class Lexer { 46 | src: string 47 | 48 | constructor(src: string) { 49 | this.src = src; 50 | } 51 | 52 | unnext(token: string): void { 53 | this.src = token + this.src; 54 | } 55 | 56 | next(): string | null { 57 | this.src = this.src.trimStart(); 58 | 59 | if (this.src.length == 0) { 60 | return null; 61 | } 62 | 63 | function is_token_break(c: string) { 64 | const syntax = '(),'; 65 | return c in BINARY_OPS || c in UNARY_OPS || syntax.includes(c); 66 | } 67 | 68 | if (is_token_break(this.src[0])) { 69 | const token = this.src[0]; 70 | this.src = this.src.slice(1); 71 | return token; 72 | } 73 | 74 | for (let i = 0; i < this.src.length; ++i) { 75 | if (is_token_break(this.src[i]) || this.src[i] == ' ') { 76 | const token = this.src.slice(0, i); 77 | this.src = this.src.slice(i); 78 | return token; 79 | } 80 | } 81 | 82 | const token = this.src; 83 | this.src = ''; 84 | return token; 85 | } 86 | } 87 | 88 | type ExprKind = 'unary_op' | 'binary_op' | 'funcall' | 'symbol'; 89 | 90 | interface UnaryOpExpr { 91 | op: UnaryOp, 92 | operand: Expr, 93 | } 94 | 95 | interface BinaryOpExpr { 96 | op: BinaryOp, 97 | lhs: Expr, 98 | rhs: Expr, 99 | } 100 | 101 | interface FuncallExpr { 102 | name: string, 103 | args: Array, 104 | } 105 | 106 | interface SymbolExpr { 107 | value: string 108 | } 109 | 110 | interface Expr { 111 | kind: ExprKind, 112 | payload: UnaryOpExpr | BinaryOpExpr | FuncallExpr | SymbolExpr 113 | } 114 | 115 | function parse_primary(lexer: Lexer): Expr { 116 | let token = lexer.next(); 117 | if (token !== null) { 118 | if (token in UNARY_OPS) { 119 | let operand = parse_expr(lexer); 120 | return { 121 | "kind": "unary_op", 122 | "payload": { 123 | "op": token as UnaryOp, 124 | "operand": operand, 125 | }, 126 | }; 127 | } else if (token === '(') { 128 | let expr = parse_expr(lexer); 129 | token = lexer.next(); 130 | if (token !== ')') { 131 | throw new Error(`Expected ')' but got '${token}'`); 132 | } 133 | return expr; 134 | } else if (token === ')') { 135 | throw new Error(`No primary expression starts with ')'`); 136 | } else { 137 | let next_token = lexer.next(); 138 | if (next_token === '(') { 139 | const args: Array = []; 140 | 141 | next_token = lexer.next(); 142 | if (next_token === ')') { 143 | return { 144 | "kind": "funcall", 145 | "payload": { 146 | "name": token, 147 | "args": args, 148 | } 149 | }; 150 | } 151 | 152 | if (next_token === null) { 153 | throw Error(`Unexpected end of input`); 154 | } 155 | 156 | lexer.unnext(next_token); 157 | args.push(parse_expr(lexer)); 158 | 159 | next_token = lexer.next(); 160 | while (next_token == ',') { 161 | args.push(parse_expr(lexer)); 162 | next_token = lexer.next(); 163 | } 164 | 165 | if (next_token !== ')') { 166 | throw Error(`Expected ')' but got '${next_token}'`); 167 | } 168 | 169 | return { 170 | "kind": "funcall", 171 | "payload": { 172 | "name": token, 173 | "args": args, 174 | } 175 | }; 176 | } else { 177 | if (next_token !== null) { 178 | lexer.unnext(next_token); 179 | } 180 | return { 181 | "kind": "symbol", 182 | "payload": { 183 | "value": token 184 | } 185 | }; 186 | } 187 | } 188 | } else { 189 | throw new Error('Expected primary expression but reached the end of the input'); 190 | } 191 | } 192 | 193 | function parse_expr(lexer: Lexer, prec: BinaryPrec = BinaryPrec.PREC0): Expr { 194 | if (prec >= BinaryPrec.COUNT_PRECS) { 195 | return parse_primary(lexer); 196 | } 197 | 198 | let lhs = parse_expr(lexer, prec + 1); 199 | 200 | let op_token = lexer.next(); 201 | if (op_token !== null) { 202 | if (op_token in BINARY_OPS && BINARY_OPS[op_token as BinaryOp].prec == prec) { 203 | let rhs = parse_expr(lexer, prec); 204 | return { 205 | "kind": "binary_op", 206 | "payload": { 207 | "op": op_token as BinaryOp, 208 | "lhs": lhs, 209 | "rhs": rhs, 210 | } 211 | }; 212 | } else { 213 | lexer.unnext(op_token); 214 | } 215 | } 216 | 217 | return lhs; 218 | } 219 | 220 | function compile_expr(src: string): Expr { 221 | const lexer = new Lexer(src); 222 | const result = parse_expr(lexer); 223 | const token = lexer.next(); 224 | if (token !== null) { 225 | console.log(typeof(token)); 226 | console.log(token); 227 | throw new Error(`Unexpected token '${token}'`); 228 | } 229 | return result; 230 | } 231 | 232 | interface UserContext { 233 | vars?: {[name: string]: number}, 234 | funcs?: {[name: string]: (...xs: number[]) => number}, 235 | } 236 | 237 | function run_expr(expr: Expr, user_context: UserContext = {}): number { 238 | console.assert(typeof(expr) === 'object'); 239 | 240 | switch (expr.kind) { 241 | case 'symbol': { 242 | const symbol = expr.payload as SymbolExpr; 243 | const value = symbol.value; 244 | const number = Number(value); 245 | if (isNaN(number)) { 246 | if (user_context.vars && value in user_context.vars) { 247 | return user_context.vars[value]; 248 | } 249 | 250 | throw new Error(`Unknown variable '${value}'`); 251 | } else { 252 | return number; 253 | } 254 | } 255 | 256 | case 'unary_op': { 257 | const unary_op = expr.payload as UnaryOpExpr; 258 | 259 | if (unary_op.op in UNARY_OPS) { 260 | return UNARY_OPS[unary_op.op](run_expr(unary_op.operand, user_context)); 261 | } 262 | 263 | throw new Error(`Unknown unary operator '${unary_op.op}'`); 264 | } 265 | 266 | case 'binary_op': { 267 | const binary_op = expr.payload as BinaryOpExpr; 268 | 269 | if (binary_op.op in BINARY_OPS) { 270 | return BINARY_OPS[binary_op.op].func( 271 | run_expr(binary_op.lhs, user_context), 272 | run_expr(binary_op.rhs, user_context)); 273 | } 274 | 275 | throw new Error(`Unknown binary operator '${binary_op.op}'`); 276 | } 277 | 278 | case 'funcall': { 279 | const funcall = expr.payload as FuncallExpr; 280 | 281 | if (user_context.funcs && funcall.name in user_context.funcs) { 282 | return user_context.funcs[funcall.name](...funcall.args.map((arg) => run_expr(arg, user_context))); 283 | } 284 | 285 | throw new Error(`Unknown function '${funcall.name}'`); 286 | } 287 | 288 | default: { 289 | throw new Error(`Unexpected AST node '${expr.kind}'`); 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /ts/filters.ts: -------------------------------------------------------------------------------- 1 | interface Param { 2 | label?: string, 3 | type: string, 4 | init: number, 5 | min?: number, 6 | max?: number, 7 | step?: number, 8 | } 9 | 10 | interface Filter { 11 | transparent: string | null, 12 | duration: string, 13 | params?: { 14 | [name: string]: Param 15 | }, 16 | vertex: string, 17 | fragment: string 18 | } 19 | 20 | // TODO(#58): add params to all of the filters 21 | // TODO(#61): human readable titles for the filter params 22 | const filters: {[name: string]: Filter} = { 23 | "Hop": { 24 | "transparent": 0x00FF00 + "", 25 | "duration": "interval * 2", 26 | // TODO(#62): when you have too many params the UI gets really cluttered 27 | "params": { 28 | // TODO(#65): filter params should have help tooltips associated with them 29 | "interval": { 30 | "label": "Interval", 31 | "type": "float", 32 | "init": 0.85, 33 | "min": 0.01, 34 | "max": 2.00, 35 | "step": 0.01, 36 | }, 37 | "ground": { 38 | "label": "Ground", 39 | "type": "float", 40 | "init": 0.5, 41 | "min": -1.0, 42 | "max": 1.0, 43 | "step": 0.01, 44 | }, 45 | "scale": { 46 | "label": "Scale", 47 | "type": "float", 48 | "init": 0.40, 49 | "min": 0.0, 50 | "max": 1.0, 51 | "step": 0.01, 52 | }, 53 | // TODO(#63): jump_height in the "Hop" filter does not make any sense 54 | // If it's bigger the emote should jump higher. Right now it is the other way around. 55 | "jump_height": { 56 | "label": "Jump Height", 57 | "type": "float", 58 | "init": 4.0, 59 | "min": 1.0, 60 | "max": 10.0, 61 | "step": 0.01, 62 | }, 63 | "hops": { 64 | "label": "Hops Count", 65 | "type": "float", 66 | "init": 2.0, 67 | "min": 1.0, 68 | "max": 5.0, 69 | "step": 1.0, 70 | } 71 | }, 72 | "vertex": `#version 100 73 | precision mediump float; 74 | 75 | attribute vec2 meshPosition; 76 | uniform float time; 77 | uniform vec2 emoteSize; 78 | 79 | uniform float interval; 80 | uniform float ground; 81 | uniform float scale; 82 | uniform float jump_height; 83 | uniform float hops; 84 | 85 | varying vec2 uv; 86 | 87 | float sliding_from_left_to_right(float time_interval) { 88 | return (mod(time, time_interval) - time_interval * 0.5) / (time_interval * 0.5); 89 | } 90 | 91 | float flipping_directions(float time_interval) { 92 | return 1.0 - 2.0 * mod(floor(time / time_interval), 2.0); 93 | } 94 | 95 | void main() { 96 | float x_time_interval = interval; 97 | float y_time_interval = x_time_interval / (2.0 * hops); 98 | vec2 offset = vec2( 99 | sliding_from_left_to_right(x_time_interval) * flipping_directions(x_time_interval) * (1.0 - scale), 100 | ((sliding_from_left_to_right(y_time_interval) * flipping_directions(y_time_interval) + 1.0) / jump_height) - ground); 101 | 102 | gl_Position = vec4( 103 | meshPosition * scale + offset, 104 | 0.0, 105 | 1.0); 106 | 107 | uv = (meshPosition + vec2(1.0, 1.0)) / 2.0; 108 | 109 | uv.x = (flipping_directions(x_time_interval) + 1.0) / 2.0 - uv.x * flipping_directions(x_time_interval); 110 | } 111 | `, 112 | "fragment": `#version 100 113 | 114 | precision mediump float; 115 | 116 | uniform vec2 resolution; 117 | uniform float time; 118 | 119 | uniform sampler2D emote; 120 | 121 | varying vec2 uv; 122 | 123 | void main() { 124 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 125 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 126 | } 127 | ` 128 | }, 129 | "Hopper": { 130 | "transparent": 0x00FF00 + "", 131 | "duration": "0.85", 132 | "vertex": `#version 100 133 | precision mediump float; 134 | 135 | attribute vec2 meshPosition; 136 | uniform float time; 137 | 138 | varying vec2 uv; 139 | 140 | float sliding_from_left_to_right(float time_interval) { 141 | return (mod(time, time_interval) - time_interval * 0.5) / (time_interval * 0.5); 142 | } 143 | 144 | float flipping_directions(float time_interval) { 145 | return 1.0 - 2.0 * mod(floor(time / time_interval), 2.0); 146 | } 147 | 148 | void main() { 149 | float scale = 0.40; 150 | float hops = 2.0; 151 | float x_time_interval = 0.85 / 2.0; 152 | float y_time_interval = x_time_interval / (2.0 * hops); 153 | float height = 0.5; 154 | vec2 offset = vec2( 155 | sliding_from_left_to_right(x_time_interval) * flipping_directions(x_time_interval) * (1.0 - scale), 156 | ((sliding_from_left_to_right(y_time_interval) * flipping_directions(y_time_interval) + 1.0) / 4.0) - height); 157 | 158 | gl_Position = vec4( 159 | meshPosition * scale + offset, 160 | 0.0, 161 | 1.0); 162 | 163 | uv = (meshPosition + vec2(1.0, 1.0)) / 2.0; 164 | 165 | uv.x = (flipping_directions(x_time_interval) + 1.0) / 2.0 - uv.x * flipping_directions(x_time_interval); 166 | } 167 | `, 168 | "fragment": `#version 100 169 | 170 | precision mediump float; 171 | 172 | uniform vec2 resolution; 173 | uniform float time; 174 | 175 | uniform sampler2D emote; 176 | 177 | varying vec2 uv; 178 | 179 | void main() { 180 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 181 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 182 | } 183 | ` 184 | }, 185 | "Overheat": { 186 | "transparent": 0x00FF00 + "", 187 | "duration": "0.85 / 8.0 * 2.0", 188 | "vertex": `#version 100 189 | precision mediump float; 190 | 191 | attribute vec2 meshPosition; 192 | uniform float time; 193 | 194 | varying vec2 uv; 195 | 196 | float sliding_from_left_to_right(float time_interval) { 197 | return (mod(time, time_interval) - time_interval * 0.5) / (time_interval * 0.5); 198 | } 199 | 200 | float flipping_directions(float time_interval) { 201 | return 1.0 - 2.0 * mod(floor(time / time_interval), 2.0); 202 | } 203 | 204 | void main() { 205 | float scale = 0.40; 206 | float hops = 2.0; 207 | float x_time_interval = 0.85 / 8.0; 208 | float y_time_interval = x_time_interval / (2.0 * hops); 209 | float height = 0.5; 210 | vec2 offset = vec2( 211 | sliding_from_left_to_right(x_time_interval) * flipping_directions(x_time_interval) * (1.0 - scale), 212 | ((sliding_from_left_to_right(y_time_interval) * flipping_directions(y_time_interval) + 1.0) / 4.0) - height); 213 | 214 | gl_Position = vec4( 215 | meshPosition * scale + offset, 216 | 0.0, 217 | 1.0); 218 | 219 | uv = (meshPosition + vec2(1.0, 1.0)) / 2.0; 220 | 221 | uv.x = (flipping_directions(x_time_interval) + 1.0) / 2.0 - uv.x * flipping_directions(x_time_interval); 222 | } 223 | `, 224 | "fragment": `#version 100 225 | 226 | precision mediump float; 227 | 228 | uniform vec2 resolution; 229 | uniform float time; 230 | 231 | uniform sampler2D emote; 232 | 233 | varying vec2 uv; 234 | 235 | void main() { 236 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)) * vec4(1.0, 0.0, 0.0, 1.0); 237 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 238 | } 239 | ` 240 | }, 241 | "Bounce": { 242 | "transparent": 0x00FF00 + "", 243 | "duration": "Math.PI / period", 244 | "params": { 245 | "period": { 246 | "type": "float", 247 | "init": 5.0, 248 | "min": 1.0, 249 | "max": 10.0, 250 | "step": 0.1, 251 | }, 252 | "scale": { 253 | "type": "float", 254 | "init": 0.30, 255 | "min": 0.0, 256 | "max": 1.0, 257 | "step": 0.01, 258 | } 259 | }, 260 | "vertex": `#version 100 261 | precision mediump float; 262 | 263 | attribute vec2 meshPosition; 264 | 265 | uniform vec2 resolution; 266 | uniform float time; 267 | 268 | uniform float period; 269 | uniform float scale; 270 | 271 | varying vec2 uv; 272 | 273 | void main() { 274 | vec2 offset = vec2(0.0, (2.0 * abs(sin(time * period)) - 1.0) * (1.0 - scale)); 275 | gl_Position = vec4(meshPosition * scale + offset, 0.0, 1.0); 276 | uv = (meshPosition + 1.0) / 2.0; 277 | } 278 | `, 279 | "fragment": ` 280 | #version 100 281 | 282 | precision mediump float; 283 | 284 | uniform vec2 resolution; 285 | uniform float time; 286 | 287 | uniform sampler2D emote; 288 | 289 | varying vec2 uv; 290 | 291 | void main() { 292 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 293 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 294 | } 295 | `, 296 | }, 297 | "Circle": { 298 | "transparent": 0x00FF00 + "", 299 | "duration": "Math.PI / 4.0", 300 | "vertex": `#version 100 301 | precision mediump float; 302 | 303 | attribute vec2 meshPosition; 304 | 305 | uniform vec2 resolution; 306 | uniform float time; 307 | 308 | varying vec2 uv; 309 | 310 | vec2 rotate(vec2 v, float a) { 311 | float s = sin(a); 312 | float c = cos(a); 313 | mat2 m = mat2(c, -s, s, c); 314 | return m * v; 315 | } 316 | 317 | void main() { 318 | float scale = 0.30; 319 | float period_interval = 8.0; 320 | float pi = 3.141592653589793238; 321 | vec2 outer_circle = vec2(cos(period_interval * time), sin(period_interval * time)) * (1.0 - scale); 322 | vec2 inner_circle = rotate(meshPosition * scale, (-period_interval * time) + pi / 2.0); 323 | gl_Position = vec4( 324 | inner_circle + outer_circle, 325 | 0.0, 326 | 1.0); 327 | uv = (meshPosition + 1.0) / 2.0; 328 | } 329 | `, 330 | "fragment": ` 331 | #version 100 332 | 333 | precision mediump float; 334 | 335 | uniform vec2 resolution; 336 | uniform float time; 337 | 338 | uniform sampler2D emote; 339 | 340 | varying vec2 uv; 341 | 342 | void main() { 343 | float speed = 1.0; 344 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 345 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 346 | } 347 | `, 348 | }, 349 | "Slide": { 350 | "transparent": 0x00FF00 + "", 351 | "duration": "0.85 * 2", 352 | "vertex": `#version 100 353 | precision mediump float; 354 | 355 | attribute vec2 meshPosition; 356 | uniform float time; 357 | 358 | varying vec2 uv; 359 | 360 | float sliding_from_left_to_right(float time_interval) { 361 | return (mod(time, time_interval) - time_interval * 0.5) / (time_interval * 0.5); 362 | } 363 | 364 | float flipping_directions(float time_interval) { 365 | return 1.0 - 2.0 * mod(floor(time / time_interval), 2.0); 366 | } 367 | 368 | void main() { 369 | float scale = 0.40; 370 | float hops = 2.0; 371 | float x_time_interval = 0.85; 372 | float y_time_interval = x_time_interval / (2.0 * hops); 373 | float height = 0.5; 374 | vec2 offset = vec2( 375 | sliding_from_left_to_right(x_time_interval) * flipping_directions(x_time_interval) * (1.0 - scale), 376 | - height); 377 | 378 | gl_Position = vec4( 379 | meshPosition * scale + offset, 380 | 0.0, 381 | 1.0); 382 | 383 | uv = (meshPosition + vec2(1.0, 1.0)) / 2.0; 384 | 385 | uv.x = (flipping_directions(x_time_interval) + 1.0) / 2.0 - uv.x * flipping_directions(x_time_interval); 386 | } 387 | `, 388 | "fragment": `#version 100 389 | 390 | precision mediump float; 391 | 392 | uniform vec2 resolution; 393 | uniform float time; 394 | 395 | uniform sampler2D emote; 396 | 397 | varying vec2 uv; 398 | 399 | void main() { 400 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 401 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 402 | } 403 | ` 404 | }, 405 | "Laughing": { 406 | "transparent": 0x00FF00 + "", 407 | "duration": "Math.PI / 12.0", 408 | "vertex": `#version 100 409 | precision mediump float; 410 | 411 | attribute vec2 meshPosition; 412 | uniform float time; 413 | 414 | varying vec2 uv; 415 | 416 | void main() { 417 | float a = 0.3; 418 | float t = (sin(24.0 * time) * a + a) / 2.0; 419 | 420 | gl_Position = vec4( 421 | meshPosition - vec2(0.0, t), 422 | 0.0, 423 | 1.0); 424 | uv = (meshPosition + vec2(1.0, 1.0)) / 2.0; 425 | } 426 | `, 427 | "fragment": `#version 100 428 | 429 | precision mediump float; 430 | 431 | uniform vec2 resolution; 432 | uniform float time; 433 | 434 | uniform sampler2D emote; 435 | 436 | varying vec2 uv; 437 | 438 | void main() { 439 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 440 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 441 | } 442 | ` 443 | }, 444 | "Blob": { 445 | "transparent": 0x00FF00 + "", 446 | "duration": "Math.PI / 3", 447 | "vertex": `#version 100 448 | 449 | precision mediump float; 450 | 451 | attribute vec2 meshPosition; 452 | 453 | uniform vec2 resolution; 454 | uniform float time; 455 | 456 | varying vec2 uv; 457 | 458 | void main() { 459 | float stretch = sin(6.0 * time) * 0.5 + 1.0; 460 | 461 | vec2 offset = vec2(0.0, 1.0 - stretch); 462 | gl_Position = vec4( 463 | meshPosition * vec2(stretch, 2.0 - stretch) + offset, 464 | 0.0, 465 | 1.0); 466 | uv = (meshPosition + vec2(1.0, 1.0)) / 2.0; 467 | } 468 | `, 469 | "fragment": `#version 100 470 | 471 | precision mediump float; 472 | 473 | uniform vec2 resolution; 474 | uniform float time; 475 | 476 | uniform sampler2D emote; 477 | 478 | varying vec2 uv; 479 | 480 | void main() { 481 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 482 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 483 | } 484 | ` 485 | }, 486 | "Go": { 487 | "transparent": 0x00FF00 + "", 488 | "duration": "1 / 4", 489 | "vertex": `#version 100 490 | precision mediump float; 491 | 492 | attribute vec2 meshPosition; 493 | 494 | uniform vec2 resolution; 495 | uniform float time; 496 | 497 | varying vec2 uv; 498 | 499 | void main() { 500 | gl_Position = vec4(meshPosition, 0.0, 1.0); 501 | uv = (meshPosition + 1.0) / 2.0; 502 | } 503 | `, 504 | "fragment": ` 505 | #version 100 506 | 507 | precision mediump float; 508 | 509 | uniform vec2 resolution; 510 | uniform float time; 511 | 512 | uniform sampler2D emote; 513 | 514 | varying vec2 uv; 515 | 516 | float slide(float speed, float value) { 517 | return mod(value - speed * time, 1.0); 518 | } 519 | 520 | void main() { 521 | float speed = 4.0; 522 | gl_FragColor = texture2D(emote, vec2(slide(speed, uv.x), 1.0 - uv.y)); 523 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 524 | } 525 | `, 526 | }, 527 | "Elevator": { 528 | "transparent": 0x00FF00 + "", 529 | "duration": "1 / 4", 530 | "vertex": `#version 100 531 | precision mediump float; 532 | 533 | attribute vec2 meshPosition; 534 | 535 | uniform vec2 resolution; 536 | uniform float time; 537 | 538 | varying vec2 uv; 539 | 540 | void main() { 541 | gl_Position = vec4(meshPosition, 0.0, 1.0); 542 | uv = (meshPosition + 1.0) / 2.0; 543 | } 544 | `, 545 | "fragment": ` 546 | #version 100 547 | 548 | precision mediump float; 549 | 550 | uniform vec2 resolution; 551 | uniform float time; 552 | 553 | uniform sampler2D emote; 554 | 555 | varying vec2 uv; 556 | 557 | float slide(float speed, float value) { 558 | return mod(value - speed * time, 1.0); 559 | } 560 | 561 | void main() { 562 | float speed = 4.0; 563 | gl_FragColor = texture2D( 564 | emote, 565 | vec2(uv.x, slide(speed, 1.0 - uv.y))); 566 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 567 | } 568 | `, 569 | }, 570 | "Rain": { 571 | "transparent": 0x00FF00 + "", 572 | "duration": "1", 573 | "vertex": `#version 100 574 | precision mediump float; 575 | 576 | attribute vec2 meshPosition; 577 | 578 | uniform vec2 resolution; 579 | uniform float time; 580 | 581 | varying vec2 uv; 582 | 583 | void main() { 584 | gl_Position = vec4(meshPosition, 0.0, 1.0); 585 | uv = (meshPosition + 1.0) / 2.0; 586 | } 587 | `, 588 | "fragment": ` 589 | #version 100 590 | 591 | precision mediump float; 592 | 593 | uniform vec2 resolution; 594 | uniform float time; 595 | 596 | uniform sampler2D emote; 597 | 598 | varying vec2 uv; 599 | 600 | float slide(float speed, float value) { 601 | return mod(value - speed * time, 1.0); 602 | } 603 | 604 | void main() { 605 | float speed = 1.0; 606 | gl_FragColor = texture2D( 607 | emote, 608 | vec2(mod(4.0 * slide(speed, uv.x), 1.0), 609 | mod(4.0 * slide(speed, 1.0 - uv.y), 1.0))); 610 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 611 | } 612 | `, 613 | }, 614 | "Pride": { 615 | "transparent": null, 616 | "duration": "2.0", 617 | "vertex": `#version 100 618 | precision mediump float; 619 | 620 | attribute vec2 meshPosition; 621 | 622 | uniform vec2 resolution; 623 | uniform float time; 624 | 625 | varying vec2 uv; 626 | 627 | void main() { 628 | gl_Position = vec4(meshPosition, 0.0, 1.0); 629 | uv = (meshPosition + 1.0) / 2.0; 630 | } 631 | `, 632 | "fragment": ` 633 | #version 100 634 | 635 | precision mediump float; 636 | 637 | uniform vec2 resolution; 638 | uniform float time; 639 | 640 | uniform sampler2D emote; 641 | 642 | varying vec2 uv; 643 | 644 | vec3 hsl2rgb(vec3 c) { 645 | vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0); 646 | return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0)); 647 | } 648 | 649 | void main() { 650 | float speed = 1.0; 651 | 652 | vec4 pixel = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 653 | pixel.w = floor(pixel.w + 0.5); 654 | pixel = vec4(mix(vec3(1.0), pixel.xyz, pixel.w), 1.0); 655 | vec4 rainbow = vec4(hsl2rgb(vec3((time - uv.x - uv.y) * 0.5, 1.0, 0.80)), 1.0); 656 | gl_FragColor = pixel * rainbow; 657 | } 658 | `, 659 | }, 660 | "Hard": { 661 | "transparent": 0x00FF00 + "", 662 | "duration": "2.0 * Math.PI / intensity", 663 | "params": { 664 | "zoom": { 665 | "type": "float", 666 | "init": 1.4, 667 | "min": 0.0, 668 | "max": 6.9, 669 | "step": 0.1, 670 | }, 671 | "intensity": { 672 | "type": "float", 673 | "init": 32.0, 674 | "min": 1.0, 675 | "max": 42.0, 676 | "step": 1.0, 677 | }, 678 | "amplitude": { 679 | "type": "float", 680 | "init": 1.0 / 8.0, 681 | "min": 0.0, 682 | "max": 1.0 / 2.0, 683 | "step": 0.001, 684 | }, 685 | }, 686 | "vertex": `#version 100 687 | precision mediump float; 688 | 689 | attribute vec2 meshPosition; 690 | 691 | uniform vec2 resolution; 692 | uniform float time; 693 | 694 | uniform float zoom; 695 | uniform float intensity; 696 | uniform float amplitude; 697 | 698 | varying vec2 uv; 699 | 700 | void main() { 701 | vec2 shaking = vec2(cos(intensity * time), sin(intensity * time)) * amplitude; 702 | gl_Position = vec4(meshPosition * zoom + shaking, 0.0, 1.0); 703 | uv = (meshPosition + 1.0) / 2.0; 704 | } 705 | `, 706 | "fragment": ` 707 | #version 100 708 | 709 | precision mediump float; 710 | 711 | uniform vec2 resolution; 712 | uniform float time; 713 | 714 | uniform sampler2D emote; 715 | 716 | varying vec2 uv; 717 | 718 | void main() { 719 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 720 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 721 | } 722 | `, 723 | }, 724 | "Peek":{ 725 | "transparent": 0x00FF00 + "", 726 | "duration": "2.0 * Math.PI" , 727 | "vertex": `#version 100 728 | precision mediump float; 729 | 730 | attribute vec2 meshPosition; 731 | 732 | uniform vec2 resolution; 733 | uniform float time; 734 | 735 | varying vec2 uv; 736 | 737 | void main() { 738 | float time_clipped= mod(time * 2.0, (4.0 * 3.14)); 739 | 740 | float s1 = float(time_clipped < (2.0 * 3.14)); 741 | float s2 = 1.0 - s1; 742 | 743 | float hold1 = float(time_clipped > (0.5 * 3.14) && time_clipped < (2.0 * 3.14)); 744 | float hold2 = 1.0 - float(time_clipped > (2.5 * 3.14) && time_clipped < (4.0 * 3.14)); 745 | 746 | float cycle_1 = 1.0 - ((s1 * sin(time_clipped) * (1.0 - hold1)) + hold1); 747 | float cycle_2 = s2 * hold2 * (sin(time_clipped) - 1.0); 748 | 749 | gl_Position = vec4(meshPosition.x + 1.0 + cycle_1 + cycle_2 , meshPosition.y, 0.0, 1.0); 750 | uv = (meshPosition + 1.0) / 2.0; 751 | } 752 | `, 753 | "fragment": ` 754 | #version 100 755 | 756 | precision mediump float; 757 | 758 | uniform vec2 resolution; 759 | uniform float time; 760 | 761 | uniform sampler2D emote; 762 | 763 | varying vec2 uv; 764 | 765 | void main() { 766 | gl_FragColor = texture2D(emote, vec2(uv.x, 1.0 - uv.y)); 767 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 768 | } 769 | `, 770 | }, 771 | "Matrix": { 772 | "transparent": null, 773 | "duration": "3.0", 774 | "vertex":` 775 | #version 100 776 | precision mediump float; 777 | 778 | attribute vec2 meshPosition; 779 | 780 | uniform vec2 resolution; 781 | uniform float time; 782 | 783 | varying vec2 _uv; 784 | 785 | void main() 786 | { 787 | _uv = (meshPosition + 1.0) / 2.0; 788 | gl_Position = vec4(meshPosition.x, meshPosition.y, 0.0, 1.0); 789 | } 790 | `, 791 | "fragment": ` 792 | #version 100 793 | precision mediump float; 794 | 795 | uniform vec2 resolution; 796 | uniform float time; 797 | uniform sampler2D emote; 798 | 799 | varying vec2 _uv; 800 | 801 | float clamp01(float value) 802 | { 803 | return clamp(value, 0.0, 1.0); 804 | } 805 | 806 | float sdf_zero(vec2 uv) 807 | { 808 | float inside = step(0.15, abs(uv.x)) + step(0.3, abs(uv.y)); 809 | float outside = step(0.35, abs(uv.x)) + step(0.4, abs(uv.y)); 810 | return clamp01(inside) - clamp01(outside); 811 | } 812 | 813 | float sdf_one(vec2 uv) 814 | { 815 | float top = step(0.25, -uv.y) * step(0.0, uv.x); 816 | float inside = (step(0.2, uv.x) + top); 817 | float outside = step(0.35, abs(uv.x)) + step(0.4, abs(uv.y)); 818 | return clamp01(clamp01(inside) - clamp01(outside)); 819 | } 820 | 821 | // Random float. No precomputed gradients mean this works for any number of grid coordinates 822 | float random01(vec2 n) 823 | { 824 | float random = 2920.0 * sin(n.x * 21942.0 + n.y * 171324.0 + 8912.0) * 825 | cos(n.x * 23157.0 * n.y * 217832.0 + 9758.0); 826 | 827 | return (sin(random) + 1.0) / 2.0; 828 | } 829 | 830 | float loop_time(float time_frame) 831 | { 832 | float times = floor(time / time_frame); 833 | return time - (times * time_frame); 834 | } 835 | 836 | void main() 837 | { 838 | vec2 uv = _uv; 839 | uv.y = 1.0 - _uv.y; 840 | 841 | float number_of_numbers = 8.0; 842 | float number_change_rate = 2.0; 843 | float amount_of_numbers = 0.6; // from 0 - 1 844 | 845 | vec4 texture_color = texture2D(emote, uv); 846 | vec4 number_color = vec4(0, 0.7, 0, 1); 847 | 848 | float looped_time = loop_time(3.0); 849 | 850 | vec2 translation = vec2(0, looped_time * -8.0); 851 | 852 | vec2 pos_idx = floor(uv * number_of_numbers + translation); 853 | float rnd_number = step(0.5, random01(pos_idx + floor(looped_time * number_change_rate))); 854 | float rnd_show = step(1.0 - amount_of_numbers, random01(pos_idx + vec2(99,99))); 855 | 856 | vec2 nuv = uv * number_of_numbers + translation; 857 | nuv = fract(nuv); 858 | 859 | float one = sdf_one(nuv - 0.5) * rnd_number; 860 | float zero = sdf_zero(nuv - 0.5) * (1.0 - rnd_number); 861 | float number = (one + zero) * rnd_show; 862 | 863 | float is_texture = 1.0 - number; 864 | float is_number = number; 865 | 866 | vec4 col = (texture_color * is_texture) + (number_color * is_number); 867 | 868 | gl_FragColor = col; 869 | gl_FragColor.w = 1.0; 870 | } 871 | ` 872 | }, 873 | "Flag":{ 874 | "transparent": 0x00FF00 + "", 875 | "duration": "Math.PI", 876 | "vertex":` 877 | #version 100 878 | precision mediump float; 879 | 880 | attribute vec2 meshPosition; 881 | 882 | uniform vec2 resolution; 883 | uniform float time; 884 | 885 | varying vec2 _uv; 886 | 887 | void main() 888 | { 889 | _uv = (meshPosition + 1.0) / 2.0; 890 | _uv.y = 1.0 - _uv.y; 891 | gl_Position = vec4(meshPosition.x, meshPosition.y, 0.0, 1.0); 892 | } 893 | `, 894 | "fragment" :` 895 | #version 100 896 | precision mediump float; 897 | 898 | varying vec2 _uv; 899 | uniform sampler2D emote; 900 | uniform float time; 901 | 902 | float sin01(float value) 903 | { 904 | return (sin(value) + 1.0) / 2.0; 905 | } 906 | 907 | //pos is left bottom point. 908 | float sdf_rect(vec2 pos, vec2 size, vec2 uv) 909 | { 910 | float left = pos.x; 911 | float right = pos.x + size.x; 912 | float bottom = pos.y; 913 | float top = pos.y + size.y; 914 | return (step(bottom, uv.y) - step(top, uv.y)) * (step(left, uv.x) - step(right, uv.x)); 915 | } 916 | 917 | void main() { 918 | float stick_width = 0.1; 919 | float flag_height = 0.75; 920 | float wave_size = 0.08; 921 | vec4 stick_color = vec4(107.0 / 256.0, 59.0 / 256.0, 9.0 / 256.0,1); 922 | 923 | vec2 flag_uv = _uv; 924 | flag_uv.x = (1.0 / (1.0 - stick_width)) * (flag_uv.x - stick_width); 925 | flag_uv.y *= 1.0 / flag_height; 926 | 927 | float flag_close_to_stick = smoothstep(0.0, 0.5, flag_uv.x); 928 | flag_uv.y += sin((-time * 2.0) + (flag_uv.x * 8.0)) * flag_close_to_stick * wave_size; 929 | 930 | float is_flag = sdf_rect(vec2(0,0), vec2(1.0, 1.0), flag_uv); 931 | float is_flag_stick = sdf_rect(vec2(0.0, 0.0), vec2(stick_width, 1), _uv); 932 | 933 | vec4 emote_color = texture2D(emote, flag_uv); 934 | vec4 texture_color = (emote_color * is_flag) + (stick_color * is_flag_stick); 935 | 936 | gl_FragColor = texture_color; 937 | } 938 | ` 939 | }, 940 | "Thanosed": { 941 | "transparent": 0x00FF00 + "", 942 | "duration": "duration", 943 | "params": { 944 | "duration": { 945 | "type": "float", 946 | "init": 6.0, 947 | "min": 1.0, 948 | "max": 16.0, 949 | "step": 1.0, 950 | }, 951 | "delay": { 952 | "type": "float", 953 | "init": 0.2, 954 | "min": 0.0, 955 | "max": 1.0, 956 | "step": 0.1, 957 | }, 958 | "pixelization": { 959 | "type": "float", 960 | "init": 1.0, 961 | "min": 1.0, 962 | "max": 3.0, 963 | "step": 1.0, 964 | }, 965 | }, 966 | "vertex": `#version 100 967 | precision mediump float; 968 | 969 | attribute vec2 meshPosition; 970 | 971 | uniform vec2 resolution; 972 | uniform float time; 973 | 974 | varying vec2 uv; 975 | 976 | void main() { 977 | gl_Position = vec4(meshPosition, 0.0, 1.0); 978 | uv = (meshPosition + 1.0) / 2.0; 979 | } 980 | `, 981 | "fragment": ` 982 | #version 100 983 | 984 | precision mediump float; 985 | 986 | uniform vec2 resolution; 987 | uniform float time; 988 | uniform float duration; 989 | uniform float delay; 990 | uniform float pixelization; 991 | 992 | uniform sampler2D emote; 993 | 994 | varying vec2 uv; 995 | 996 | // https://www.aussiedwarf.com/2017/05/09/Random10Bit.html 997 | float rand(vec2 co){ 998 | vec3 product = vec3( sin( dot(co, vec2(0.129898,0.78233))), 999 | sin( dot(co, vec2(0.689898,0.23233))), 1000 | sin( dot(co, vec2(0.434198,0.51833))) ); 1001 | vec3 weighting = vec3(4.37585453723, 2.465973, 3.18438); 1002 | return fract(dot(weighting, product)); 1003 | } 1004 | 1005 | void main() { 1006 | float pixelated_resolution = 112.0 / pixelization; 1007 | vec2 pixelated_uv = floor(uv * pixelated_resolution); 1008 | float noise = (rand(pixelated_uv) + 1.0) / 2.0; 1009 | float slope = (0.2 + noise * 0.8) * (1.0 - (0.0 + uv.x * 0.5)); 1010 | float time_interval = 1.1 + delay * 2.0; 1011 | float progress = 0.2 + delay + slope - mod(time_interval * time / duration, time_interval); 1012 | float mask = progress > 0.1 ? 1.0 : 0.0; 1013 | vec4 pixel = texture2D(emote, vec2(uv.x * (progress > 0.5 ? 1.0 : progress * 2.0), 1.0 - uv.y)); 1014 | pixel.w = floor(pixel.w + 0.5); 1015 | gl_FragColor = pixel * vec4(vec3(1.0), mask); 1016 | } 1017 | `, 1018 | }, 1019 | "Ripple": { 1020 | "transparent": 0x00FF00 + "", 1021 | "duration": "2 * Math.PI / b", 1022 | "params": { 1023 | "a": { 1024 | "label": "Wave Length", 1025 | "type": "float", 1026 | "init": 12.0, 1027 | "min": 0.01, 1028 | "max": 24.0, 1029 | "step": 0.01, 1030 | }, 1031 | "b": { 1032 | "label": "Time Freq", 1033 | "type": "float", 1034 | "init": 4.0, 1035 | "min": 0.01, 1036 | "max": 8.0, 1037 | "step": 0.01, 1038 | }, 1039 | "c": { 1040 | "label": "Amplitude", 1041 | "type": "float", 1042 | "init": 0.03, 1043 | "min": 0.01, 1044 | "max": 0.06, 1045 | "step": 0.01, 1046 | } 1047 | }, 1048 | "vertex": `#version 100 1049 | precision mediump float; 1050 | 1051 | attribute vec2 meshPosition; 1052 | 1053 | uniform vec2 resolution; 1054 | uniform float time; 1055 | 1056 | varying vec2 uv; 1057 | 1058 | void main() { 1059 | gl_Position = vec4(meshPosition, 0.0, 1.0); 1060 | uv = (meshPosition + 1.0) / 2.0; 1061 | } 1062 | `, 1063 | "fragment": `#version 100 1064 | 1065 | precision mediump float; 1066 | 1067 | uniform vec2 resolution; 1068 | uniform float time; 1069 | 1070 | uniform sampler2D emote; 1071 | 1072 | uniform float a; 1073 | uniform float b; 1074 | uniform float c; 1075 | 1076 | varying vec2 uv; 1077 | 1078 | void main() { 1079 | vec2 pos = vec2(uv.x, 1.0 - uv.y); 1080 | vec2 center = vec2(0.5); 1081 | vec2 dir = pos - center; 1082 | float x = length(dir); 1083 | float y = sin(x + time); 1084 | vec4 pixel = texture2D(emote, pos + cos(x*a - time*b)*c*(dir/x)); 1085 | gl_FragColor = pixel; 1086 | gl_FragColor.w = floor(gl_FragColor.w + 0.5); 1087 | } 1088 | `, 1089 | } 1090 | }; 1091 | -------------------------------------------------------------------------------- /ts/grecha.ts: -------------------------------------------------------------------------------- 1 | const LOREM: string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; 2 | 3 | type Child = string | HTMLElement; 4 | // TODO(#73): make tag more typesafe 5 | // Essentially get rid of the `any` 6 | type Tag = any; 7 | 8 | function tag(name: string, ...children: Child[]): Tag { 9 | const result: Tag = document.createElement(name); 10 | for (const child of children) { 11 | if (typeof(child) === 'string') { 12 | result.appendChild(document.createTextNode(child)); 13 | } else { 14 | result.appendChild(child); 15 | } 16 | } 17 | 18 | result.att$ = function(name: string, value: string) { 19 | this.setAttribute(name, value); 20 | return this; 21 | }; 22 | 23 | 24 | result.onclick$ = function(callback: (this: GlobalEventHandlers, ev: MouseEvent) => Tag) { 25 | this.onclick = callback; 26 | return this; 27 | }; 28 | 29 | return result; 30 | } 31 | 32 | function canvas(...children: Child[]): Tag { 33 | return tag("canvas", ...children); 34 | } 35 | 36 | function h1(...children: Child[]): Tag { 37 | return tag("h1", ...children); 38 | } 39 | 40 | function h2(...children: Child[]): Tag { 41 | return tag("h2", ...children); 42 | } 43 | 44 | function h3(...children: Child[]): Tag { 45 | return tag("h3", ...children); 46 | } 47 | 48 | function p(...children: Child[]): Tag { 49 | return tag("p", ...children); 50 | } 51 | 52 | function a(...children: Child[]): Tag { 53 | return tag("a", ...children); 54 | } 55 | 56 | function div(...children: Child[]): Tag { 57 | return tag("div", ...children); 58 | } 59 | 60 | function span(...children: Child[]): Tag { 61 | return tag("span", ...children); 62 | } 63 | 64 | function select(...children: Child[]): Tag { 65 | return tag("select", ...children); 66 | } 67 | 68 | 69 | function img(src: string): Tag { 70 | return tag("img").att$("src", src); 71 | } 72 | 73 | function input(type: string): Tag { 74 | return tag("input").att$("type", type); 75 | } 76 | 77 | interface Routes { 78 | [route: string]: Tag 79 | } 80 | 81 | function router(routes: Routes): Tag { 82 | let result = div(); 83 | 84 | function syncHash() { 85 | let hashLocation = document.location.hash.split('#')[1]; 86 | if (!hashLocation) { 87 | hashLocation = '/'; 88 | } 89 | 90 | if (!(hashLocation in routes)) { 91 | const route404 = '/404'; 92 | console.assert(route404 in routes); 93 | hashLocation = route404; 94 | } 95 | 96 | while (result.firstChild) { 97 | result.removeChild(result.lastChild); 98 | } 99 | result.appendChild(routes[hashLocation]); 100 | 101 | return result; 102 | }; 103 | 104 | syncHash(); 105 | window.addEventListener("hashchange", syncHash); 106 | 107 | return result; 108 | } 109 | -------------------------------------------------------------------------------- /ts/index.ts: -------------------------------------------------------------------------------- 1 | let feature_params = false; 2 | 3 | interface VertexAttribs { 4 | [name: string]: number 5 | } 6 | 7 | const vertexAttribs: VertexAttribs = { 8 | "meshPosition": 0 9 | }; 10 | const TRIANGLE_PAIR = 2; 11 | const TRIANGLE_VERTICIES = 3; 12 | const VEC2_COUNT = 2; 13 | const VEC2_X = 0; 14 | const VEC2_Y = 1; 15 | const CANVAS_WIDTH = 112; 16 | const CANVAS_HEIGHT = 112; 17 | 18 | function compileShaderSource(gl: WebGLRenderingContext, source: string, shaderType: GLenum): WebGLShader { 19 | function shaderTypeToString() { 20 | switch (shaderType) { 21 | case gl.VERTEX_SHADER: return 'Vertex'; 22 | case gl.FRAGMENT_SHADER: return 'Fragment'; 23 | default: return shaderType; 24 | } 25 | } 26 | 27 | const shader = gl.createShader(shaderType); 28 | if (shader === null) { 29 | throw new Error(`Could not create a new shader`); 30 | } 31 | 32 | gl.shaderSource(shader, source); 33 | gl.compileShader(shader); 34 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 35 | throw new Error(`Could not compile ${shaderTypeToString()} shader: ${gl.getShaderInfoLog(shader)}`); 36 | } 37 | return shader; 38 | } 39 | 40 | function linkShaderProgram(gl: WebGLRenderingContext, shaders: WebGLShader[], vertexAttribs: VertexAttribs): WebGLProgram { 41 | const program = gl.createProgram(); 42 | if (program === null) { 43 | throw new Error('Could not create a new shader program'); 44 | } 45 | 46 | for (let shader of shaders) { 47 | gl.attachShader(program, shader); 48 | } 49 | 50 | for (let vertexName in vertexAttribs) { 51 | gl.bindAttribLocation(program, vertexAttribs[vertexName], vertexName); 52 | } 53 | 54 | gl.linkProgram(program); 55 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 56 | throw new Error(`Could not link shader program: ${gl.getProgramInfoLog(program)}`); 57 | } 58 | return program; 59 | } 60 | 61 | function createTextureFromImage(gl: WebGLRenderingContext, image: TexImageSource): WebGLTexture { 62 | let textureId = gl.createTexture(); 63 | if (textureId === null) { 64 | throw new Error('Could not create a new WebGL texture'); 65 | } 66 | 67 | gl.bindTexture(gl.TEXTURE_2D, textureId); 68 | gl.texImage2D( 69 | gl.TEXTURE_2D, // target 70 | 0, // level 71 | gl.RGBA, // internalFormat 72 | gl.RGBA, // srcFormat 73 | gl.UNSIGNED_BYTE, // srcType 74 | image // image 75 | ); 76 | 77 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 78 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 79 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 80 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 81 | 82 | return textureId; 83 | } 84 | 85 | interface Uniforms { 86 | [name: string]: WebGLUniformLocation | null 87 | } 88 | 89 | interface CompiledFilter { 90 | id: WebGLProgram, 91 | uniforms: Uniforms, 92 | duration: Expr, 93 | transparent: string | null, 94 | paramsPanel: Tag 95 | } 96 | 97 | interface Snapshot { 98 | [name: string]: { 99 | uniform: WebGLUniformLocation | null, 100 | value: number | null 101 | } 102 | } 103 | 104 | // TODO(#54): pre-load all of the filters and just switch between them without loading/unloading them constantly 105 | function loadFilterProgram(gl: WebGLRenderingContext, filter: Filter, vertexAttribs: VertexAttribs): CompiledFilter { 106 | let vertexShader = compileShaderSource(gl, filter.vertex, gl.VERTEX_SHADER); 107 | let fragmentShader = compileShaderSource(gl, filter.fragment, gl.FRAGMENT_SHADER); 108 | let id = linkShaderProgram(gl, [vertexShader, fragmentShader], vertexAttribs); 109 | gl.deleteShader(vertexShader); 110 | gl.deleteShader(fragmentShader); 111 | gl.useProgram(id); 112 | 113 | let uniforms: Uniforms = { 114 | "resolution": gl.getUniformLocation(id, 'resolution'), 115 | "time": gl.getUniformLocation(id, 'time'), 116 | "emoteSize": gl.getUniformLocation(id, 'emoteSize'), 117 | }; 118 | 119 | // TODO(#55): there no "reset to default" button in the params panel of a filter 120 | let paramsPanel = div().att$("class", "widget-element"); 121 | let paramsInputs: {[name: string]: Tag} = {}; 122 | 123 | for (let paramName in filter.params) { 124 | if (paramName in uniforms) { 125 | throw new Error(`Redefinition of existing uniform parameter ${paramName}`); 126 | } 127 | 128 | switch (filter.params[paramName].type) { 129 | case "float": { 130 | const valuePreview = span(filter.params[paramName].init.toString()); 131 | const valueInput = input("range"); 132 | 133 | if (filter.params[paramName].min !== undefined) { 134 | valueInput.att$("min", filter.params[paramName].min); 135 | } 136 | 137 | if (filter.params[paramName].max !== undefined) { 138 | valueInput.att$("max", filter.params[paramName].max); 139 | } 140 | 141 | if (filter.params[paramName].step !== undefined) { 142 | valueInput.att$("step", filter.params[paramName].step); 143 | } 144 | 145 | if (filter.params[paramName].init !== undefined) { 146 | valueInput.att$("value", filter.params[paramName].init); 147 | } 148 | 149 | paramsInputs[paramName] = valueInput; 150 | 151 | valueInput.oninput = function () { 152 | valuePreview.innerText = this.value; 153 | paramsPanel.dispatchEvent(new CustomEvent("paramsChanged")); 154 | }; 155 | 156 | const label: string = filter.params[paramName].label ?? paramName; 157 | 158 | paramsPanel.appendChild(div( 159 | span(`${label}: `), valuePreview, 160 | div(valueInput), 161 | )); 162 | } break; 163 | 164 | default: { 165 | throw new Error(`Filter parameters do not support type ${filter.params[paramName].type}`) 166 | } 167 | } 168 | 169 | uniforms[paramName] = gl.getUniformLocation(id, paramName); 170 | } 171 | 172 | 173 | paramsPanel.paramsSnapshot$ = function() { 174 | let snapshot: Snapshot = {}; 175 | for (let paramName in paramsInputs) { 176 | snapshot[paramName] = { 177 | "uniform": uniforms[paramName], 178 | "value": Number(paramsInputs[paramName].value) 179 | }; 180 | } 181 | return snapshot; 182 | }; 183 | 184 | return { 185 | "id": id, 186 | "uniforms": uniforms, 187 | "duration": compile_expr(filter.duration), 188 | "transparent": filter.transparent, 189 | "paramsPanel": paramsPanel, 190 | }; 191 | } 192 | 193 | function ImageSelector() { 194 | const imageInput = input("file"); 195 | const imagePreview = img("img/tsodinClown.png") 196 | .att$("class", "widget-element") 197 | .att$("width", CANVAS_WIDTH); 198 | const root = div( 199 | div(imageInput).att$("class", "widget-element"), 200 | imagePreview 201 | ).att$("class", "widget"); 202 | 203 | root.selectedImage$ = function() { 204 | return imagePreview; 205 | }; 206 | 207 | root.selectedFileName$ = function() { 208 | function removeFileNameExt(fileName: string): string { 209 | if (fileName.includes('.')) { 210 | return fileName.split('.').slice(0, -1).join('.'); 211 | } else { 212 | return fileName; 213 | } 214 | } 215 | 216 | const file = imageInput.files[0]; 217 | return file ? removeFileNameExt(file.name) : 'result'; 218 | }; 219 | 220 | root.updateFiles$ = function(files: FileList) { 221 | imageInput.files = files; 222 | imageInput.onchange(); 223 | } 224 | 225 | imagePreview.addEventListener('load', function(this: HTMLImageElement) { 226 | root.dispatchEvent(new CustomEvent("imageSelected", { 227 | detail: { 228 | imageData: this 229 | } 230 | })); 231 | }); 232 | 233 | imagePreview.addEventListener('error', function(this: HTMLImageElement) { 234 | imageInput.value = ''; 235 | this.src = 'img/error.png'; 236 | }); 237 | 238 | imageInput.onchange = function() { 239 | imagePreview.src = URL.createObjectURL(this.files[0]); 240 | }; 241 | 242 | return root; 243 | } 244 | 245 | function FilterList() { 246 | const root = select(); 247 | 248 | // Populating the FilterList 249 | for (let name in filters) { 250 | root.add(new Option(name)); 251 | } 252 | 253 | root.selectedFilter$ = function() { 254 | return filters[root.selectedOptions[0].value]; 255 | }; 256 | 257 | root.onchange = function() { 258 | root.dispatchEvent(new CustomEvent('filterChanged', { 259 | detail: { 260 | filter: root.selectedFilter$() 261 | } 262 | })); 263 | }; 264 | 265 | root.addEventListener('wheel', function(e: WheelEvent) { 266 | e.preventDefault(); 267 | if (e.deltaY < 0) { 268 | root.selectedIndex = Math.max(root.selectedIndex - 1, 0); 269 | } 270 | if (e.deltaY > 0) { 271 | root.selectedIndex = Math.min(root.selectedIndex + 1, root.length - 1); 272 | } 273 | root.onchange(); 274 | }); 275 | 276 | return root; 277 | } 278 | 279 | function FilterSelector() { 280 | const filterList_ = FilterList(); 281 | const filterPreview = canvas() 282 | .att$("width", CANVAS_WIDTH) 283 | .att$("height", CANVAS_HEIGHT); 284 | const root = div( 285 | div("Filter: ", filterList_) 286 | .att$("class", "widget-element"), 287 | filterPreview.att$("class", "widget-element"), 288 | ).att$("class", "widget"); 289 | 290 | const gl = filterPreview.getContext("webgl", {antialias: false, alpha: false}); 291 | if (!gl) { 292 | throw new Error("Could not initialize WebGL context"); 293 | } 294 | 295 | // Initialize GL 296 | { 297 | gl.enable(gl.BLEND); 298 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 299 | 300 | // Mesh Position 301 | { 302 | let meshPositionBufferData = new Float32Array(TRIANGLE_PAIR * TRIANGLE_VERTICIES * VEC2_COUNT); 303 | for (let triangle = 0; triangle < TRIANGLE_PAIR; ++triangle) { 304 | for (let vertex = 0; vertex < TRIANGLE_VERTICIES; ++vertex) { 305 | const quad = triangle + vertex; 306 | const index = 307 | triangle * TRIANGLE_VERTICIES * VEC2_COUNT + 308 | vertex * VEC2_COUNT; 309 | meshPositionBufferData[index + VEC2_X] = (2 * (quad & 1) - 1); 310 | meshPositionBufferData[index + VEC2_Y] = (2 * ((quad >> 1) & 1) - 1); 311 | } 312 | } 313 | 314 | let meshPositionBuffer = gl.createBuffer(); 315 | gl.bindBuffer(gl.ARRAY_BUFFER, meshPositionBuffer); 316 | gl.bufferData(gl.ARRAY_BUFFER, meshPositionBufferData, gl.STATIC_DRAW); 317 | 318 | const meshPositionAttrib = vertexAttribs['meshPosition']; 319 | gl.vertexAttribPointer( 320 | meshPositionAttrib, 321 | VEC2_COUNT, 322 | gl.FLOAT, 323 | false, 324 | 0, 325 | 0); 326 | gl.enableVertexAttribArray(meshPositionAttrib); 327 | } 328 | } 329 | 330 | // TODO(#49): FilterSelector does not handle loadFilterProgram() failures 331 | 332 | let emoteImage: HTMLImageElement | undefined = undefined; 333 | let emoteTexture: WebGLTexture | undefined = undefined; 334 | let program: CompiledFilter | undefined = undefined; 335 | 336 | function syncParams() { 337 | if (program) { 338 | const snapshot = program.paramsPanel.paramsSnapshot$(); 339 | for (let paramName in snapshot) { 340 | gl.uniform1f(snapshot[paramName].uniform, snapshot[paramName].value); 341 | } 342 | } 343 | } 344 | 345 | program = loadFilterProgram(gl, filterList_.selectedFilter$(), vertexAttribs); 346 | program.paramsPanel.addEventListener('paramsChanged', syncParams); 347 | if (feature_params) { 348 | root.appendChild(program.paramsPanel); 349 | } 350 | syncParams(); 351 | 352 | root.updateImage$ = function(newEmoteImage: HTMLImageElement) { 353 | emoteImage = newEmoteImage; 354 | if (emoteTexture) { 355 | gl.deleteTexture(emoteTexture); 356 | } 357 | emoteTexture = createTextureFromImage(gl, emoteImage); 358 | }; 359 | 360 | filterList_.addEventListener('filterChanged', function(e: any) { 361 | if (program) { 362 | gl.deleteProgram(program.id); 363 | program.paramsPanel.removeEventListener('paramsChanged', syncParams); 364 | if (feature_params) { 365 | root.removeChild(program.paramsPanel); 366 | } 367 | } 368 | 369 | program = loadFilterProgram(gl, e.detail.filter, vertexAttribs); 370 | program.paramsPanel.addEventListener('paramsChanged', syncParams); 371 | if (feature_params) { 372 | root.appendChild(program.paramsPanel); 373 | } 374 | syncParams(); 375 | }); 376 | 377 | root.render$ = function (filename: string): any | undefined { 378 | if (program === undefined) { 379 | console.warn('Could not rendering anything because the filter was not selected'); 380 | return undefined; 381 | } 382 | 383 | if (emoteImage == undefined) { 384 | console.warn('Could not rendering anything because the image was not selected'); 385 | return undefined; 386 | } 387 | 388 | // TODO(#74): gif.js typing are absolutely broken 389 | const gif = new GIF({ 390 | workers: 5, 391 | quality: 10, 392 | width: CANVAS_WIDTH, 393 | height: CANVAS_HEIGHT, 394 | transparent: program.transparent, 395 | }); 396 | 397 | const context: UserContext = { 398 | "vars": { 399 | "Math.PI": Math.PI 400 | } 401 | }; 402 | if (context.vars !== undefined) { 403 | const snapshot = program.paramsPanel.paramsSnapshot$(); 404 | for (let paramName in snapshot) { 405 | context.vars[paramName] = snapshot[paramName].value; 406 | } 407 | } 408 | 409 | const fps = 30; 410 | const dt = 1.0 / fps; 411 | // TODO(#59): come up with a reasonable way to handle malicious durations 412 | const duration = Math.min(run_expr(program.duration, context), 60); 413 | 414 | const renderProgress = document.getElementById("render-progress"); 415 | if (renderProgress === null) { 416 | throw new Error('Could not find "render-progress"'); 417 | } 418 | const renderSpinner = document.getElementById("render-spinner"); 419 | if (renderSpinner === null) { 420 | throw new Error('Could not find "render-spinner"'); 421 | } 422 | const renderPreview = document.getElementById("render-preview") as HTMLImageElement; 423 | if (renderPreview === null) { 424 | throw new Error('Could not find "render-preview"'); 425 | } 426 | const renderDownload = document.getElementById("render-download") as HTMLAnchorElement; 427 | if (renderDownload === null) { 428 | throw new Error('Could not find "render-download"'); 429 | } 430 | 431 | renderPreview.style.display = "none"; 432 | renderSpinner.style.display = "block"; 433 | 434 | let t = 0.0; 435 | while (t <= duration) { 436 | gl.uniform1f(program.uniforms.time, t); 437 | gl.uniform2f(program.uniforms.resolution, CANVAS_WIDTH, CANVAS_HEIGHT); 438 | gl.uniform2f(program.uniforms.emoteSize, emoteImage.width, emoteImage.height); 439 | 440 | gl.clearColor(0.0, 1.0, 0.0, 1.0); 441 | gl.clear(gl.COLOR_BUFFER_BIT); 442 | gl.drawArrays(gl.TRIANGLES, 0, TRIANGLE_PAIR * TRIANGLE_VERTICIES); 443 | 444 | let pixels = new Uint8ClampedArray(4 * CANVAS_WIDTH * CANVAS_HEIGHT); 445 | gl.readPixels(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, gl.RGBA, gl.UNSIGNED_BYTE, pixels); 446 | // Flip the image vertically 447 | { 448 | const center = Math.floor(CANVAS_HEIGHT / 2); 449 | for (let y = 0; y < center; ++y) { 450 | const row = 4 * CANVAS_WIDTH; 451 | for (let x = 0; x < row; ++x) { 452 | const ai = y * 4 * CANVAS_WIDTH + x; 453 | const bi = (CANVAS_HEIGHT - y - 1) * 4 * CANVAS_WIDTH + x; 454 | const a = pixels[ai]; 455 | const b = pixels[bi]; 456 | pixels[ai] = b; 457 | pixels[bi] = a; 458 | } 459 | } 460 | } 461 | 462 | gif.addFrame(new ImageData(pixels, CANVAS_WIDTH, CANVAS_HEIGHT), { 463 | delay: dt * 1000, 464 | dispose: 2, 465 | }); 466 | 467 | renderProgress.style.width = `${(t / duration) * 50}%`; 468 | 469 | t += dt; 470 | } 471 | 472 | gif.on('finished', (blob) => { 473 | renderPreview.src = URL.createObjectURL(blob); 474 | renderPreview.style.display = "block"; 475 | renderDownload.href = renderPreview.src; 476 | renderDownload.download = filename; 477 | renderDownload.style.display = "block"; 478 | renderSpinner.style.display = "none"; 479 | 480 | }); 481 | 482 | gif.on('progress', (p) => { 483 | renderProgress.style.width = `${50 + p * 50}%`; 484 | }); 485 | 486 | gif.render(); 487 | 488 | return gif; 489 | }; 490 | 491 | // Rendering Loop 492 | { 493 | const step = function(timestamp: number) { 494 | gl.clearColor(0.0, 1.0, 0.0, 1.0); 495 | gl.clear(gl.COLOR_BUFFER_BIT); 496 | 497 | if (program && emoteImage) { 498 | gl.uniform1f(program.uniforms.time, timestamp * 0.001); 499 | gl.uniform2f(program.uniforms.resolution, filterPreview.width, filterPreview.height); 500 | gl.uniform2f(program.uniforms.emoteSize, emoteImage.width, emoteImage.height); 501 | 502 | gl.drawArrays(gl.TRIANGLES, 0, TRIANGLE_PAIR * TRIANGLE_VERTICIES); 503 | } 504 | 505 | window.requestAnimationFrame(step); 506 | } 507 | 508 | window.requestAnimationFrame(step); 509 | } 510 | 511 | return root; 512 | } 513 | 514 | window.onload = () => { 515 | if ("serviceWorker" in navigator) { 516 | navigator.serviceWorker.register('serviceworker.js').then( 517 | (registration) => { 518 | console.log("Registered a Service Worker ", registration); 519 | }, 520 | (error) => { 521 | console.error("Could not register a Service Worker ", error); 522 | }, 523 | ); 524 | } else { 525 | console.error("Service Workers are not supported in this browser."); 526 | } 527 | 528 | feature_params = new URLSearchParams(document.location.search).has("feature-params"); 529 | 530 | const filterSelectorEntry = document.getElementById('filter-selector-entry'); 531 | if (filterSelectorEntry === null) { 532 | throw new Error('Could not find "filter-selector-entry"'); 533 | } 534 | const imageSelectorEntry = document.getElementById('image-selector-entry'); 535 | if (imageSelectorEntry === null) { 536 | throw new Error('Could not find "image-selector-entry"'); 537 | } 538 | 539 | const imageSelector = ImageSelector(); 540 | const filterSelector = FilterSelector(); 541 | imageSelector.addEventListener('imageSelected', function(e: CustomEvent) { 542 | filterSelector.updateImage$(e.detail.imageData); 543 | }); 544 | filterSelectorEntry.appendChild(filterSelector); 545 | imageSelectorEntry.appendChild(imageSelector); 546 | 547 | // drag file from anywhere 548 | document.ondrop = function(event: DragEvent) { 549 | event.preventDefault(); 550 | imageSelector.updateFiles$(event.dataTransfer?.files); 551 | } 552 | 553 | document.ondragover = function(event) { 554 | event.preventDefault(); 555 | } 556 | 557 | // TODO(#50): extract "renderer" as a separate grecha.js component 558 | // Similar to imageSelector and filterSelector 559 | let gif: GIF | undefined = undefined; 560 | const renderButton = document.getElementById("render"); 561 | if (renderButton === null) { 562 | throw new Error('Could not find "render"'); 563 | } 564 | renderButton.onclick = function() { 565 | if (gif && gif.running) { 566 | gif.abort(); 567 | } 568 | const fileName = imageSelector.selectedFileName$(); 569 | gif = filterSelector.render$(`${fileName}.gif`); 570 | }; 571 | } 572 | // TODO(#75): run typescript compiler on CI 573 | --------------------------------------------------------------------------------