├── icon.png ├── ui ├── bg.png ├── loading.gif ├── ui-main.png ├── mac-frame.png ├── ui-capture.png ├── ui-hidden.png ├── ui-record.png ├── ui-timer.png └── ui-settings.png ├── .github └── FUNDING.yml ├── .gitignore ├── manifest.webmanifest ├── gifjs ├── LICENSE ├── gif.js └── gif.worker.js ├── sw.js ├── LICENSE ├── style.css ├── index.html ├── README.md └── app.js /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/icon.png -------------------------------------------------------------------------------- /ui/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/ui/bg.png -------------------------------------------------------------------------------- /ui/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/ui/loading.gif -------------------------------------------------------------------------------- /ui/ui-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/ui/ui-main.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Lana-chan 2 | patreon: maple_syrup 3 | ko_fi: squirrel 4 | -------------------------------------------------------------------------------- /ui/mac-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/ui/mac-frame.png -------------------------------------------------------------------------------- /ui/ui-capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/ui/ui-capture.png -------------------------------------------------------------------------------- /ui/ui-hidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/ui/ui-hidden.png -------------------------------------------------------------------------------- /ui/ui-record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/ui/ui-record.png -------------------------------------------------------------------------------- /ui/ui-timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/ui/ui-timer.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.aseprite 2 | todo.txt 3 | key.pem 4 | server.pem 5 | simple-https-server.py -------------------------------------------------------------------------------- /ui/ui-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lana-chan/webgbcam/HEAD/ui/ui-settings.png -------------------------------------------------------------------------------- /manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#99ccff", 3 | "description": "A web-based camera filter that emulates the look of the Game Boy Camera.", 4 | "display": "standalone", 5 | "icons": [ 6 | { 7 | "src": "icon.png", 8 | "sizes": "256x256", 9 | "type": "image/png" 10 | } 11 | ], 12 | "name": "webgbcam", 13 | "short_name": "webgbcam", 14 | "start_url": "index.html" 15 | } -------------------------------------------------------------------------------- /gifjs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2018 Johan Nordberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /sw.js: -------------------------------------------------------------------------------- 1 | const cacheName = 'webgbcam-v4.3b' 2 | 3 | // Install a service worker 4 | self.addEventListener("install", (event) => { 5 | // Perform install steps 6 | caches.open(cacheName).then(function(cache) { 7 | return cache.addAll([ 8 | '/', 9 | '/index.html', 10 | '/style.css', 11 | '/app.js', 12 | '/ui/bg.png', 13 | '/ui/mac-frame.png', 14 | '/ui/ui-capture.png', 15 | '/ui/ui-settings.png', 16 | '/ui/ui-main.png', 17 | '/ui/ui-hidden.png', 18 | '/ui/ui-timer.png', 19 | '/ui/ui-record.png', 20 | '/ui/loading.gif', 21 | '/gifjs/gif.js', 22 | '/gifjs/gif.worker.js' 23 | ]); 24 | }); 25 | }); 26 | 27 | // Cache lookup and fetch the request 28 | self.addEventListener("fetch", (event) => { 29 | event.respondWith( 30 | caches.match(event.request).then(function (response) { 31 | // Cache hit - return response 32 | if (response) { 33 | return response; 34 | } 35 | return fetch(event.request).then(function (response) { 36 | if (!response || response.status !== 200 || response.type !== "basic") { 37 | return response; 38 | } 39 | 40 | //Clone the response before putting into cache so that response to browser and response to cache happens in two difference streams 41 | var responseForCache = response.clone(); 42 | caches.open(cacheName).then(function (cache) { 43 | cache.put(event.request, responseForCache); 44 | }); 45 | return response; 46 | }); 47 | }) 48 | ); 49 | }); 50 | 51 | // Update a service worker 52 | self.addEventListener("activate", (event) => { 53 | event.waitUntil( 54 | caches.keys().then(function(keyList) { 55 | return Promise.all(keyList.map(function(key) { 56 | if (key != cacheName) { 57 | return caches.delete(key); 58 | } 59 | })); 60 | }) 61 | ).then(self.clients.claim()); 62 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2022 maple@maple.pet 2 | 3 | The following license is modified from the Anti-Fascist MIT License and the 4 | Anti-Capitalist Software License. 5 | 6 | ANTI-FASCIST LICENSE: 7 | 8 | The following conditions must be met by any person obtaining a copy of this 9 | software: 10 | 11 | - You MAY NOT be a fascist. 12 | - You MUST not financially support fascists. 13 | - You MUST not publicly voice support for fascists. 14 | 15 | "Fascist" can be understood as any entity which supports radical authoritarian 16 | nationalism. For example: Donald Trump is a fascist; if you donated to his 17 | campaign then all rights provided by this license are not granted to you. 18 | 19 | ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4): 20 | 21 | This is anti-capitalist software, released for free use by individuals and 22 | organizations that do not operate by capitalist principles. 23 | 24 | Permission is hereby granted, free of charge, to any person or organization 25 | (the "User") obtaining a copy of this software and associated documentation 26 | files (the "Software"), to use, copy, modify, merge, distribute, and/or sell 27 | copies of the Software, subject to the following conditions: 28 | 29 | 1. The above copyright notice and this permission notice shall be included 30 | in all copies or modified versions of the Software. 31 | 32 | 2. The User is one of the following: 33 | a. An individual person, laboring for themselves 34 | b. A non-profit organization 35 | c. An educational institution 36 | d. An organization that seeks shared profit for all of its members, and 37 | allows non-members to set the cost of their labor 38 | 39 | 3. If the User is an organization with owners, then all owners are workers 40 | and all workers are owners with equal equity and/or equal vote. 41 | 42 | 4. If the User is an organization, then the User is not law enforcement or 43 | military, or working for or under either. 44 | 45 | 5. The User must not be involved in Non-Fungible Tokens or any other form of 46 | cryptocurrency minting or exchange. 47 | 48 | The above copyright notice and this permission notice shall be included in all 49 | copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY 52 | KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 53 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 54 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 55 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 56 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html, body{ 2 | margin: 0; 3 | padding: 0; 4 | height: 100%; 5 | width: 100%; 6 | touch-action: manipulation; 7 | } 8 | 9 | body { 10 | background: url("ui/bg.png"); 11 | text-align: center; 12 | font: 12px sans-serif; 13 | } 14 | 15 | .centered { 16 | text-align: center !important; 17 | } 18 | 19 | .maple-window { 20 | margin: 5px; 21 | vertical-align: top; 22 | display: inline-block; 23 | border-width: 26px 12px 20px 12px; 24 | border-style: solid; 25 | border-image: url("ui/mac-frame.png") 30 40 22 22 fill repeat; 26 | border-image-width: 30px 40px 22px 22px; 27 | color: #000; 28 | position: relative; 29 | text-align: left; 30 | font-size: 12px; 31 | font-family: geneva, sans-serif; 32 | min-width: 200px; 33 | box-sizing: border-box; 34 | } 35 | 36 | .maple-window a { 37 | color: #9999cc; 38 | text-shadow: none; 39 | } 40 | .maple-window a:hover { 41 | color: #ccccff; 42 | } 43 | 44 | .maple-window-title { 45 | position: absolute; 46 | top: -23px; 47 | text-align: center; 48 | width: 100%; 49 | left: 0; 50 | } 51 | 52 | .maple-window-title > span { 53 | background: #ccc; 54 | padding: 1px 5px 1px 5px; 55 | font-size: 12px; 56 | font-weight: bold; 57 | font-family: chicago, sans-serif; 58 | /*vertical-align: middle;*/ 59 | margin-right: 20px; 60 | white-space: nowrap; 61 | } 62 | 63 | /*#camera { 64 | position: fixed; 65 | left: 50%; 66 | top: 50%; 67 | transform: translate(-50%, -50%); 68 | }*/ 69 | 70 | #app-view { 71 | height: 100%; 72 | width: 100%; 73 | /*image-rendering: -moz-crisp-edges; 74 | image-rendering: -webkit-optimize-contrast; 75 | image-rendering: -o-crisp-edges; 76 | image-rendering: crisp-edges;*/ 77 | } 78 | 79 | #camera-stream, #camera-output, #camera-view, .hidden { 80 | display: none; 81 | } 82 | 83 | .button { 84 | width: 200px; 85 | background-color: black; 86 | color: white; 87 | font-size: 16px; 88 | border-radius: 30px; 89 | border: none; 90 | padding: 15px 20px; 91 | text-align: center; 92 | box-shadow: 0 5px 10px 0 rgba(0,0,0,0.2); 93 | /*position: fixed; 94 | bottom: 30px; 95 | left: calc(50% - 100px);*/ 96 | } 97 | .right { 98 | float: right; 99 | } 100 | 101 | .modal { 102 | position: absolute; 103 | top: 0; 104 | left: 0; 105 | max-width: 80%; 106 | max-height: 80%; 107 | transform: translate(calc(50vw - 50%),calc(50vh - 50%)); 108 | } 109 | 110 | #gif-img { 111 | object-fit: scale-down; 112 | max-width:100%; 113 | display: block; 114 | margin: auto; 115 | } 116 | #gif-buttons { 117 | margin: .5em; 118 | } 119 | 120 | .blink { 121 | color: #f00; 122 | text-align: center; 123 | animation: blink-animation 1s steps(5, start) 4; 124 | -webkit-animation: blink-animation 1s steps(5, start) 4; 125 | } 126 | @keyframes blink-animation { 127 | to { 128 | visibility: hidden; 129 | } 130 | } 131 | @-webkit-keyframes blink-animation { 132 | to { 133 | visibility: hidden; 134 | } 135 | } 136 | 137 | ul { 138 | margin-top: -1em; 139 | padding: 0; 140 | list-style: none; 141 | } 142 | 143 | #main-app-window { 144 | width: 98%; 145 | top: 48%; 146 | transform: translateY(-50%); 147 | } 148 | 149 | #camera { 150 | display: inline-block; 151 | width: 100%; 152 | } 153 | 154 | @media (orientation:landscape) { 155 | #main-app-window { 156 | width: unset; 157 | height: 98%; 158 | top: unset; 159 | transform: unset; 160 | } 161 | 162 | #camera { 163 | width: unset; 164 | height: calc(100% - 2rem); 165 | } 166 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | webgbcam 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
webgbcam v4.3
19 |
20 | 21 | 22 | 23 | 24 |

25 | 26 |
27 | 28 | 53 | 54 | 62 | 63 | 71 | 72 | 73 | 74 | 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webgbcam 2 | 3 | A simple Game Boy Camera-style filter made in HTML5 and JavaScript 4 | 5 | [Play with it here!](https://maple.pet/webgbcam/) 6 | 7 | ## Disclaimer 8 | 9 | As of this commit, this is simply a direct copy of the files currently in my webserver. 10 | Eventually, I will clean things up and make improvements to the code, but since there 11 | has been interest in looking at the code, I'm mirroring the files here. 12 | 13 | ## A quick explanation of how it all works 14 | 15 | The concept of [Bayer dithering](https://en.wikipedia.org/wiki/Ordered_dithering) was 16 | hard for me to grasp at first, but after a [few different projects](https://github.com/Lana-chan/maples-retro-extravaganza) 17 | getting acquainted with it, I've found an easy way to apply it, which can be used in 18 | a procedural setting like JS Canvas filters, or shaders. 19 | 20 | Basically, you start with an array of pixels, then grayscale them and optionally apply 21 | simple arithmetics to apply gamma and contrast adjustments. Then, you offset those by 22 | the value in the Bayer matrix corresponding to that pixel, giving it a patterned look. 23 | Finally, you divide and quantize the values until all pixels each have only one of four 24 | possible values. This will give you a dithered pixel art look. After this, my code applies 25 | a palette swap for those 4 values back to RGB space. 26 | 27 | ## Acknowledgements 28 | 29 | Thanks to [Christine Love](https://twitter.com/christinelove) for making the Interstellar 30 | Selfie Station back in 2014. It helped me a lot with my dysphoria and was the inspiration 31 | to learning how Bayer dithering works in order to remake her camera app once it was no 32 | longer available in app stores. 33 | 34 | Thanks to [Joel Yliluoma's arbitrary-palette positional dithering algorithm](https://bisqwit.iki.fi/story/howto/dither/jy/) 35 | page, which was the first analysis of ordered dithering that I found comprehensible and 36 | used to implement the filter in different applications. 37 | 38 | Thanks to [lospec.com](https://lospec.com/palette-list) for making a list of palettes available, 39 | many of which were used in this project. 40 | 41 | Thanks to [gbdev.io](https://gbdev.io/pandocs/Gameboy_Camera.html) for information on 42 | the Game Boy Camera hardware, used for accurate filtering. 43 | 44 | ## License 45 | 46 | ``` 47 | Copyright (c) 2021 maple@maple.pet 48 | 49 | The following license is modified from the Anti-Fascist MIT License and the 50 | Anti-Capitalist Software License. 51 | 52 | ANTI-FASCIST LICENSE: 53 | 54 | The following conditions must be met by any person obtaining a copy of this 55 | software: 56 | 57 | - You MAY NOT be a fascist. 58 | - You MUST not financially support fascists. 59 | - You MUST not publicly voice support for fascists. 60 | 61 | "Fascist" can be understood as any entity which supports radical authoritarian 62 | nationalism. For example: Donald Trump is a fascist; if you donated to his 63 | campaign then all rights provided by this license are not granted to you. 64 | 65 | ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4): 66 | 67 | This is anti-capitalist software, released for free use by individuals and 68 | organizations that do not operate by capitalist principles. 69 | 70 | Permission is hereby granted, free of charge, to any person or organization 71 | (the "User") obtaining a copy of this software and associated documentation 72 | files (the "Software"), to use, copy, modify, merge, distribute, and/or sell 73 | copies of the Software, subject to the following conditions: 74 | 75 | 1. The above copyright notice and this permission notice shall be included 76 | in all copies or modified versions of the Software. 77 | 78 | 2. The User is one of the following: 79 | a. An individual person, laboring for themselves 80 | b. A non-profit organization 81 | c. An educational institution 82 | d. An organization that seeks shared profit for all of its members, and 83 | allows non-members to set the cost of their labor 84 | 85 | 3. If the User is an organization with owners, then all owners are workers 86 | and all workers are owners with equal equity and/or equal vote. 87 | 88 | 4. If the User is an organization, then the User is not law enforcement or 89 | military, or working for or under either. 90 | 91 | 5. The User must not be involved in Non-Fungible Tokens or any other form of 92 | cryptocurrency minting or exchange. 93 | 94 | The above copyright notice and this permission notice shall be included in all 95 | copies or substantial portions of the Software. 96 | 97 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY 98 | KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 99 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 100 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 101 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 102 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 103 | ``` 104 | 105 | The license does not obligate you do so, but if you build something upon the code in this repository, I'd love to hear about it and I ask that you credit me in it. 106 | 107 | [gif.js](https://github.com/jnordberg/gif.js) by Johan Nordberg: 108 | 109 | ``` 110 | The MIT License (MIT) 111 | 112 | Copyright (c) 2013-2018 Johan Nordberg 113 | 114 | Permission is hereby granted, free of charge, to any person obtaining a copy 115 | of this software and associated documentation files (the "Software"), to deal 116 | in the Software without restriction, including without limitation the rights 117 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 118 | copies of the Software, and to permit persons to whom the Software is 119 | furnished to do so, subject to the following conditions: 120 | 121 | The above copyright notice and this permission notice shall be included in 122 | all copies or substantial portions of the Software. 123 | 124 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 125 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 126 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 127 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 128 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 129 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 130 | THE SOFTWARE. 131 | ``` 132 | -------------------------------------------------------------------------------- /gifjs/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 e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&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.setFill=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,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 | -------------------------------------------------------------------------------- /gifjs/gif.worker.js: -------------------------------------------------------------------------------- 1 | // gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js 2 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=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=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 Math.min(Math.max(num, a), b); 297 | 298 | // bounding boxes for each button in the app 299 | var buttons = { 300 | bottomLeft: { 301 | x:1, 302 | y:113, 303 | width:30, 304 | height:30 305 | }, 306 | bottomRight: { 307 | x:129, 308 | y:113, 309 | width:30, 310 | height:30 311 | }, 312 | topLeft: { 313 | x:1, 314 | y:1, 315 | width:30, 316 | height:30 317 | }, 318 | contrastLeft: { 319 | x:10, 320 | y:13, 321 | width:15, 322 | height:13 323 | }, 324 | contrastRight: { 325 | x:25, 326 | y:13, 327 | width:15, 328 | height:13 329 | }, 330 | brightnessLeft: { 331 | x:65, 332 | y:13, 333 | width:15, 334 | height:13 335 | }, 336 | brightnessRight: { 337 | x:80, 338 | y:13, 339 | width:15, 340 | height:13 341 | }, 342 | paletteLeft: { 343 | x:120, 344 | y:13, 345 | width:15, 346 | height:13 347 | }, 348 | paletteRight: { 349 | x:135, 350 | y:13, 351 | width:15, 352 | height:13 353 | }, 354 | sharpnessLeft: { 355 | x:120, 356 | y:126, 357 | width:15, 358 | height:13 359 | }, 360 | sharpnessRight: { 361 | x:135, 362 | y:126, 363 | width:15, 364 | height:13 365 | }, 366 | screenHotspot: { 367 | x:31, 368 | y:31, 369 | width:98, 370 | height:82 371 | }, 372 | hideUI: { 373 | x:145, 374 | y:0, 375 | width:15, 376 | height:15 377 | }, 378 | timer: { 379 | x:52, 380 | y:131, 381 | width:13, 382 | height:13 383 | }, 384 | record: { 385 | x:95, 386 | y:131, 387 | width:16, 388 | height:13 389 | }, 390 | bppSwitch: { 391 | x:45, 392 | y:133, 393 | width:50, 394 | height:9 395 | } 396 | }; 397 | 398 | var screens = { 399 | uiMain: { 400 | elem: document.querySelector("#ui-main"), 401 | buttons: [ 402 | { 403 | bounding: buttons.bottomLeft, 404 | action: captureImage 405 | }, 406 | { 407 | bounding: buttons.screenHotspot, 408 | action: captureImage 409 | }, 410 | { 411 | bounding: buttons.bottomRight, 412 | action: switchCameras 413 | }, 414 | { 415 | bounding: buttons.topLeft, 416 | action: ()=> { 417 | currentUI = screens.uiSettings; 418 | } 419 | }, 420 | { 421 | bounding: buttons.hideUI, 422 | action: ()=> { 423 | currentUI = screens.uiHidden; 424 | } 425 | }, 426 | { 427 | bounding: buttons.timer, 428 | action: ()=> { 429 | // change UI to timer and trigger 3s delay to capture 430 | currentUI = screens.uiTimer; 431 | setTimeout(captureImage, 3000); 432 | } 433 | }, 434 | { 435 | bounding: buttons.record, 436 | action: gifStart 437 | } 438 | ] 439 | }, 440 | uiCapture: { 441 | elem: document.querySelector("#ui-capture"), 442 | buttons: [ 443 | { 444 | bounding: buttons.bottomLeft, 445 | action: ()=> { 446 | // return 447 | cameraStream.play(); 448 | currentUI = screens.uiMain; 449 | } 450 | }, 451 | { 452 | bounding: buttons.bottomRight, 453 | action: savePicture 454 | }, 455 | { 456 | bounding: buttons.topLeft, 457 | action: ()=> { 458 | // go to settings 459 | currentUI = screens.uiSettings; 460 | } 461 | } 462 | ] 463 | }, 464 | uiSettings: { 465 | elem: document.querySelector("#ui-settings"), 466 | buttons: [ 467 | { 468 | bounding: buttons.bottomLeft, 469 | action: ()=> { 470 | // return 471 | if(cameraStream.paused == true) { 472 | // we're in capture 473 | currentUI = screens.uiCapture; 474 | } else { 475 | currentUI = screens.uiMain; 476 | } 477 | } 478 | }, 479 | { 480 | bounding: buttons.contrastLeft, 481 | action: ()=> { 482 | if(cameraVars.contrast > 0) cameraVars.contrast--; 483 | savePrefs(); 484 | } 485 | }, 486 | { 487 | bounding: buttons.contrastRight, 488 | action: ()=> { 489 | if(cameraVars.contrast < 6) cameraVars.contrast++; 490 | savePrefs(); 491 | } 492 | }, 493 | { 494 | bounding: buttons.brightnessLeft, 495 | action: ()=> { 496 | if(cameraVars.gamma > 0) cameraVars.gamma--; 497 | savePrefs(); 498 | } 499 | }, 500 | { 501 | bounding: buttons.brightnessRight, 502 | action: ()=> { 503 | if(cameraVars.gamma < 6) cameraVars.gamma++; 504 | savePrefs(); 505 | } 506 | }, 507 | { 508 | bounding: buttons.sharpnessLeft, 509 | action: ()=> { 510 | if(cameraVars.sharpness > 0) cameraVars.sharpness--; 511 | savePrefs(); 512 | } 513 | }, 514 | { 515 | bounding: buttons.sharpnessRight, 516 | action: ()=> { 517 | if(cameraVars.sharpness < 6) cameraVars.sharpness++; 518 | savePrefs(); 519 | } 520 | }, 521 | { 522 | bounding: buttons.paletteLeft, 523 | action: ()=> { 524 | currentPalette--; 525 | if(currentPalette < 0) currentPalette = palettes.length-1; 526 | savePrefs(); 527 | } 528 | }, 529 | { 530 | bounding: buttons.paletteRight, 531 | action: ()=> { 532 | currentPalette++; 533 | if(currentPalette >= palettes.length) currentPalette = 0; 534 | savePrefs(); 535 | } 536 | }, 537 | { 538 | bounding: buttons.bppSwitch, 539 | action: ()=> { 540 | updateBpp(cameraVars.bppSwitch == 2 ? 1 : 2); 541 | } 542 | } 543 | ] 544 | }, 545 | uiHidden: { 546 | elem: document.querySelector("#ui-hidden"), 547 | buttons: [ 548 | { 549 | bounding: buttons.hideUI, 550 | action: ()=> { 551 | // go back to main 552 | currentUI = screens.uiMain; 553 | } 554 | } 555 | ] 556 | }, 557 | uiTimer: { 558 | elem: document.querySelector("#ui-timer"), 559 | buttons: [] 560 | }, 561 | uiRecord: { 562 | elem: document.querySelector("#ui-record"), 563 | buttons: [] 564 | } 565 | }; 566 | 567 | // global settings for gbcamera 568 | var renderWidth = 320, 569 | renderHeight = 288, 570 | currentPalette = 0, 571 | currentUI = screens.uiMain; 572 | 573 | var cameraVars = { 574 | width: 128, 575 | height: 112, 576 | dither: 0.6, 577 | contrast: 3, 578 | gamma: 3, 579 | sharpness: 3, 580 | xOffset: 0, 581 | yOffset: 0, 582 | xScale: 1, 583 | yScale: 1, 584 | bppSwitch: 2 // 2bpp = gameboy, 1bpp = atkinson, doubleres 585 | }; 586 | 587 | // function to check if phone is portrait oriented 588 | function screenIsPortrait() { 589 | try { 590 | let orientation = (screen.orientation || {}).type || screen.mozOrientation || screen.msOrientation; 591 | if(orientation != undefined) { 592 | if(orientation.includes('portrait')) return true; 593 | } else if(window.orientation != undefined) { 594 | if(window.orientation == 0) return true; 595 | } 596 | return false; 597 | } catch(e) { 598 | return false; 599 | } 600 | } 601 | 602 | //Function to get the mouse position 603 | function getMousePos(canvas, event) { 604 | let rect = canvas.getBoundingClientRect(); 605 | let mousePos = { 606 | x: (event.clientX - rect.left) / (rect.right - rect.left) * renderWidth / 2, 607 | y: (event.clientY - rect.top) / (rect.bottom - rect.top) * renderHeight / 2 608 | }; 609 | return mousePos; 610 | } 611 | //Function to check whether a point is inside a rectangle 612 | function isInside(pos, rect){ 613 | return pos.x > rect.x && pos.x < (rect.x+rect.width) && pos.y < (rect.y+rect.height) && pos.y > rect.y; 614 | } 615 | 616 | function switchCameras() { 617 | if(amountOfCameras > 1) { 618 | if (currentFacingMode === 'environment') currentFacingMode = 'user'; 619 | else currentFacingMode = 'environment'; 620 | initCameraStream(); 621 | } 622 | } 623 | 624 | function download(filename, blob) { 625 | var link = document.createElement('a'); 626 | link.href = URL.createObjectURL(blob); 627 | link.download = filename; 628 | link.style.display = 'none'; 629 | document.body.appendChild(link); 630 | link.click(); 631 | document.body.removeChild(link); 632 | } 633 | 634 | function getFileDate() { 635 | let now = new Date(); 636 | // i love javascript 637 | let dateString = now.getDate() + "-" + (now.getMonth()+1) + "-"+ now.getFullYear() + " " + now.getHours() + " " + now.getMinutes() + " " + now.getSeconds(); 638 | return dateString; 639 | } 640 | 641 | function savePicture() { 642 | let ctx = cameraOutput.getContext("2d"); 643 | ctx.drawImage(cameraView, 0,0, cameraOutput.width, cameraOutput.height); 644 | Filters.filterImage(Filters.paletteSwap, cameraOutput, [palettes[currentPalette]]) 645 | cameraOutput.toBlob((blob) => { 646 | download("webgbcam " + getFileDate() + ".png", blob); 647 | }, 'image/png'); 648 | } 649 | 650 | function loadPrefs() { 651 | let localContrast = parseInt(localStorage.getItem("cameraContrast")); 652 | let localGamma = parseInt(localStorage.getItem("cameraGamma")); 653 | let localPalette = parseInt(localStorage.getItem("cameraPalette")); 654 | let localSharpness = parseInt(localStorage.getItem("cameraSharpness")); 655 | let localBpp = parseInt(localStorage.getItem("cameraBpp")); 656 | cameraVars.contrast = (localContrast ? localContrast : 3); 657 | cameraVars.gamma = (localGamma ? localGamma : 3); 658 | cameraVars.sharpness = (localSharpness ? localSharpness : 3); 659 | updateBpp(localBpp ? localBpp : 2); 660 | outputScale = (cameraVars.bppSwitch == 2 ? 6 : 3); 661 | currentPalette = (localPalette ? localPalette : 0); 662 | } 663 | 664 | function savePrefs() { 665 | localStorage.setItem("cameraContrast", cameraVars.contrast); 666 | localStorage.setItem("cameraGamma", cameraVars.gamma); 667 | localStorage.setItem("cameraSharpness", cameraVars.sharpness); 668 | localStorage.setItem("cameraBpp", cameraVars.bppSwitch); 669 | localStorage.setItem("cameraPalette", currentPalette); 670 | } 671 | 672 | function updateBpp(bpp = 2) { 673 | if (bpp == 1) { 674 | // to 1bpp 675 | cameraVars.bppSwitch = 1; 676 | cameraVars.width = 256; 677 | cameraVars.height = 224; 678 | outputScale = 3; 679 | } else { 680 | // to 2bpp 681 | cameraVars.bppSwitch = 2; 682 | cameraVars.width = 128; 683 | cameraVars.height = 112; 684 | outputScale = 6; 685 | } 686 | cameraView.width = cameraVars.width; 687 | cameraView.height = cameraVars.height; 688 | initCameraDrawing(false); 689 | } 690 | 691 | function applyLevels(value, brightness, contrast, gamma) { 692 | let newValue = value / 255.0; 693 | newValue = (newValue - 0.5) * contrast + 0.5; 694 | //newValue = newValue + brightness; 695 | return Math.pow(clampNumber(newValue, 0, 1), gamma) * 255; 696 | } 697 | 698 | var Filters = {}; 699 | Filters.getPixels = function(c) { 700 | return c.getContext('2d').getImageData(0,0,c.width,c.height); 701 | }; 702 | 703 | Filters.filterImage = function(filter, canvas, var_args) { 704 | let args = [this.getPixels(canvas)]; 705 | for (let i=0; i.5, err=(pix-col)/8; 795 | m.forEach(x => e[x]+=err); 796 | let c = col ? 255 : 0; 797 | d[i] = c; 798 | d[i+1] = c; 799 | d[i+2] = c; 800 | } 801 | return pixels; 802 | } 803 | 804 | // takes grayscale and paints it with palette 805 | Filters.paletteSwap = function(pixels, palette) { 806 | let d = pixels.data; 807 | 808 | for (let i = 0; i < d.length; i += 4) { 809 | let c = clampNumber(Math.floor(d[i] / 64), 0, 3); 810 | 811 | let r,g,b; 812 | [r, g, b] = palette[c]; 813 | 814 | d[i] = r; 815 | d[i+1] = g; 816 | d[i+2] = b; 817 | } 818 | 819 | return pixels; 820 | } 821 | 822 | // this function counts the amount of video inputs 823 | // it replaces DetectRTC that was previously implemented. 824 | function deviceCount() { 825 | return new Promise(function (resolve) { 826 | var videoInCount = 0; 827 | 828 | navigator.mediaDevices 829 | .enumerateDevices() 830 | .then(function (devices) { 831 | devices.forEach(function (device) { 832 | if (device.kind === 'video') { 833 | device.kind = 'videoinput'; 834 | } 835 | 836 | if (device.kind === 'videoinput') { 837 | videoInCount++; 838 | //console.log('videocam: ' + device.label); 839 | } 840 | }); 841 | 842 | resolve(videoInCount); 843 | }) 844 | .catch(function (err) { 845 | console.log(err.name + ': ' + err.message); 846 | resolve(0); 847 | }); 848 | }); 849 | } 850 | 851 | document.addEventListener('DOMContentLoaded', function (event) { 852 | // check if mediaDevices is supported 853 | if ( 854 | navigator.mediaDevices && 855 | navigator.mediaDevices.getUserMedia && 856 | navigator.mediaDevices.enumerateDevices 857 | ) { 858 | // first we call getUserMedia to trigger permissions 859 | // we need this before deviceCount, otherwise Safari doesn't return all the cameras 860 | // we need to have the number in order to display the switch front/back button 861 | navigator.mediaDevices 862 | .getUserMedia({ 863 | audio: false, 864 | video: true, 865 | }) 866 | .then(function (stream) { 867 | stream.getTracks().forEach(function (track) { 868 | track.stop(); 869 | }); 870 | 871 | deviceCount().then(function (deviceCount) { 872 | amountOfCameras = deviceCount; 873 | 874 | // init the UI and the camera stream 875 | initCameraUI(); 876 | initCameraStream(); 877 | }); 878 | }) 879 | .catch(function (error) { 880 | //https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia 881 | if (error.name === 'NotAllowedError') { 882 | alert('camera permission denied, please refresh and allow camera capture!'); 883 | } else if (error.name === 'NotFoundError') { 884 | alert('no cameras found! make sure your webcam is plugged in and enabled!'); 885 | } else { 886 | alert('unspecified camera error! make sure camera permissions are enabled!'); 887 | } 888 | 889 | console.error('getUserMedia() error: ', error); 890 | }); 891 | } else { 892 | alert( 893 | 'it seems your browser does not support camera capture! :(', 894 | ); 895 | } 896 | loadPrefs(); 897 | }); 898 | 899 | function restartCamera() { 900 | clearTimeout(resizeTimeout); 901 | resizeTimeout = setTimeout(function() { 902 | initAppScaling(); 903 | initCameraDrawing(); 904 | }, 300); 905 | } 906 | 907 | //window.onorientationchange = restartCamera; 908 | //window.onresize = restartCamera; 909 | 910 | function captureImage() { 911 | cameraStream.pause(); 912 | currentUI = screens.uiCapture; 913 | } 914 | 915 | function initCameraUI() { 916 | initAppScaling(); 917 | 918 | const queryString = window.location.search; 919 | const urlParams = new URLSearchParams(queryString); 920 | if (urlParams.has('hideui')) { 921 | currentUI = screens.uiHidden; 922 | } 923 | 924 | // handle canvas app clicks 925 | appView.addEventListener('click', function(e) { 926 | var mousePos = getMousePos(appView, e); 927 | 928 | currentUI.buttons.forEach((button) => { 929 | if (isInside(mousePos, button.bounding)) { 930 | button.action(); 931 | } 932 | }); 933 | 934 | }, false); 935 | } 936 | 937 | function initAppScaling(scale = 2) { 938 | appScale = scale; 939 | 940 | // canvas sizes 941 | cameraView.width = cameraVars.width; 942 | cameraView.height = cameraVars.height; 943 | appView.width = renderWidth * appScale; 944 | appView.height = renderHeight * appScale; 945 | 946 | let ctx = appView.getContext("2d"); 947 | ctx.imageSmoothingEnabled = false; 948 | ctx.scale(appScale,appScale); 949 | ctx = cameraView.getContext("2d"); 950 | ctx.imageSmoothingEnabled = false; 951 | } 952 | 953 | // https://github.com/webrtc/samples/blob/gh-pages/src/content/devices/input-output/js/main.js 954 | function initCameraStream() { 955 | // stop any active streams in the window 956 | if (window.stream) { 957 | window.stream.getTracks().forEach(function (track) { 958 | //console.log(track); 959 | track.stop(); 960 | }); 961 | } 962 | 963 | var constraints = { 964 | audio: false, 965 | video: { 966 | width: { ideal: 640 }, 967 | height: { ideal: 480 }, 968 | facingMode: currentFacingMode, 969 | }, 970 | }; 971 | 972 | function handleSuccess(stream) { 973 | if (stream) { 974 | window.stream = stream; // make stream available to browser console 975 | cameraStream.srcObject = stream; 976 | 977 | let track = window.stream.getVideoTracks()[0]; 978 | cameraStream.width = track.getSettings().width; 979 | cameraStream.height = track.getSettings().height; 980 | 981 | setTimeout(initCameraDrawing, 500); 982 | } 983 | } 984 | 985 | function handleError(error) { 986 | console.error('getUserMedia() error: ', error); 987 | } 988 | 989 | navigator.mediaDevices 990 | .getUserMedia(constraints) 991 | .then(handleSuccess) 992 | .catch(handleError); 993 | } 994 | 995 | function initCameraDrawing(start = true) { 996 | // if cameraStream has vertical or horizontal resolution of 0 then it's not initialized, we retry until the browser decides to properly work 997 | if (cameraStream.videoHeight == 0) setTimeout(restartCamera, 500); 998 | 999 | const track = window.stream.getVideoTracks()[0]; 1000 | let settings = track.getSettings(); 1001 | //let str = JSON.stringify(settings, null, 4); 1002 | //console.log('settings ' + str); 1003 | 1004 | // calculate scale and offset to render camera stream to camera view canvas 1005 | if(cameraStream.videoWidth >= cameraStream.videoHeight) { 1006 | // horizontal 1007 | cameraVars.yScale = cameraStream.videoHeight; 1008 | cameraVars.xScale = Math.floor((cameraStream.videoHeight / cameraVars.height) * cameraVars.width); 1009 | cameraVars.yOffset = 0; 1010 | cameraVars.xOffset = Math.floor((cameraStream.videoWidth - cameraVars.xScale) / 2); 1011 | } else { 1012 | //vertical 1013 | cameraVars.xScale = cameraStream.videoWidth; 1014 | cameraVars.yScale = Math.floor((cameraStream.videoWidth / cameraVars.width) * cameraVars.height); 1015 | cameraVars.xOffset = 0; 1016 | cameraVars.yOffset = Math.floor((cameraStream.videoHeight - cameraVars.yScale) / 2); 1017 | } 1018 | 1019 | // canvas starts flipped for user facing camera 1020 | if(settings.facingMode != "environment") { // not environment = front-facing phone cam or pc webcam, flip 1021 | cameraView.getContext('2d').setTransform(-1, 0, 0, 1, cameraVars.width, 0); 1022 | } else { 1023 | cameraView.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); 1024 | } 1025 | //console.log(cameraVars); 1026 | 1027 | cameraOutput.width = cameraVars.width * outputScale; 1028 | cameraOutput.height = cameraVars.height * outputScale; 1029 | let ctx = cameraOutput.getContext("2d"); 1030 | ctx.imageSmoothingEnabled = false; 1031 | 1032 | if (start) { 1033 | cameraStream.play(); 1034 | clearInterval(frameDrawing) 1035 | frameDrawing = setInterval(drawFrame, 100); 1036 | } 1037 | } 1038 | 1039 | function showGifModal() { 1040 | gifPreview.classList.remove("hidden"); 1041 | } 1042 | 1043 | function loadGifModal(blob) { 1044 | gifImg.src = URL.createObjectURL(blob); 1045 | gifBlob = blob; 1046 | gifButtons.classList.remove("hidden"); 1047 | } 1048 | 1049 | function downloadGif() { 1050 | download("webgbcam " + getFileDate() + ".gif", gifBlob); 1051 | resetGifModal(); 1052 | } 1053 | 1054 | function resetGifModal() { 1055 | gifBlob = null; 1056 | gifImg.src = "loading.gif"; 1057 | gifPreview.classList.add("hidden"); 1058 | gifButtons.classList.add("hidden"); 1059 | } 1060 | 1061 | function gifStart() { 1062 | gifEncoder = new GIF({ 1063 | workers: 2, 1064 | workerScript: 'gifjs/gif.worker.js', 1065 | quality: 10, 1066 | repeat: 0, 1067 | width: cameraOutput.width, 1068 | height: cameraOutput.height 1069 | }); 1070 | gifEncoder.on('finished', function(blob) { 1071 | loadGifModal(blob); 1072 | //download("webgbcam " + getFileDate() + ".gif", URL.createObjectURL(blob)); 1073 | }); 1074 | gifFrames = gifLength; 1075 | currentUI = screens.uiRecord; 1076 | gifRecording = true; 1077 | } 1078 | 1079 | function gifEnd() { 1080 | gifRecording = false; 1081 | currentUI = screens.uiMain; 1082 | gifEncoder.render(); 1083 | showGifModal(); 1084 | } 1085 | 1086 | function gifFrame() { 1087 | let ctx = cameraOutput.getContext("2d"); 1088 | Filters.filterImage(Filters.paletteSwap, cameraView, [palettes[currentPalette]]) 1089 | ctx.drawImage(cameraView, 0,0, cameraOutput.width, cameraOutput.height); 1090 | gifEncoder.addFrame(ctx, {delay: 100, copy: true}); 1091 | if(--gifFrames == 0) gifEnd(); 1092 | } 1093 | 1094 | function scaledFillRect(ctx, x, y, width, height) { 1095 | const scale = 2; 1096 | ctx.fillRect(x * scale, y * scale, width * scale, height * scale); 1097 | } 1098 | 1099 | function drawFrame() { 1100 | let camctx = cameraView.getContext('2d'); 1101 | camctx.drawImage(cameraStream, cameraVars.xOffset, cameraVars.yOffset, cameraVars.xScale, cameraVars.yScale, 0, 0, cameraVars.width, cameraVars.height); 1102 | 1103 | Filters.filterImage(Filters.grayscale, cameraView, []); 1104 | Filters.filterImage(Filters.sharpen, cameraView, [sliderSharpness[cameraVars.sharpness]]); 1105 | 1106 | if (cameraVars.bppSwitch == 2) { 1107 | Filters.filterImage(Filters.gbcamera, cameraView, [cameraVars.dither]); 1108 | } else { 1109 | Filters.filterImage(Filters.atkinson, cameraView, []); 1110 | } 1111 | 1112 | let ctx = appView.getContext("2d"); 1113 | ctx.drawImage(cameraView, 32, 32, 256, 224); 1114 | ctx.drawImage(currentUI.elem, 0, 0, 160, 144, 0, 0, 320, 288); 1115 | 1116 | if (currentUI === screens.uiSettings) { 1117 | // update settings values 1118 | ctx.fillStyle = "rgb(192,192,192)"; 1119 | for(let i = 1; i <= cameraVars.contrast; i++) { 1120 | scaledFillRect(ctx, 42, 22 - (i*3), 4, 2); 1121 | } 1122 | for(let i = 1; i <= cameraVars.gamma; i++) { 1123 | scaledFillRect(ctx, 97, 22 - (i*3), 4, 2); 1124 | } 1125 | for(let i = 1; i <= cameraVars.sharpness; i++) { 1126 | scaledFillRect(ctx, 152, 135 - (i*3), 4, 2); 1127 | } 1128 | 1129 | ctx.fillStyle = "rgb(130,130,130)"; 1130 | if (cameraVars.bppSwitch == 2) { 1131 | scaledFillRect(ctx, 70, 135, 4, 4); 1132 | } else { 1133 | scaledFillRect(ctx, 65, 135, 4, 4); 1134 | } 1135 | } else if (currentUI === screens.uiRecord) { 1136 | // update record length 1137 | ctx.fillStyle = "rgb(64,64,64)"; 1138 | scaledFillRect(ctx, 25, 134, 110 - (gifFrames / gifLength * 110), 6); 1139 | } 1140 | 1141 | try { 1142 | Filters.filterImage(Filters.paletteSwap, appView, [palettes[currentPalette]]) 1143 | } catch(e) { 1144 | 1145 | } 1146 | if (gifRecording) gifFrame(); 1147 | } 1148 | 1149 | function toggleAbout() { 1150 | const elemAbout = document.getElementById("about"); 1151 | 1152 | if (elemAbout.classList.contains("hidden")) { 1153 | elemAbout.classList.remove("hidden"); 1154 | } else { 1155 | elemAbout.classList.add("hidden"); 1156 | } 1157 | } --------------------------------------------------------------------------------