├── 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 |
29 |
about
30 |
NFTs: No Fucking Thanks
31 |
32 |
made by maple - inspired by christine love's interstellar selfie station
33 |
34 |
if the app is blank, make sure you have cameras connected and browser camera permissions enabled!
35 |
36 |
webgbcam uses gif.js
37 | you can check the source code on github!
38 |
39 |
as seen on:
40 |
45 |
46 |
47 |
if you like the stuff i do, check out my website and please donate to me on ko-fi!
48 |
49 |
ps: you look great today!
50 |
51 |
52 |
53 |
54 |
55 |
gif preview
56 |

57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |

65 |

66 |

67 |

68 |

69 |

70 |
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 | }
--------------------------------------------------------------------------------