URI: 
       app.js - webgbcam - [fork] gameboy webcam
  HTML git clone git://src.adamsgaard.dk/webgbcam
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
       app.js (29102B)
       ---
            1 /*
            2 * Copyright (c) 2021-2022 maple@maple.pet
            3 * 
            4 * This software is licensed through the Anti-Fascist and the Anti-Capitalist
            5 * licenses. See LICENSE for more details.
            6 * 
            7 * The above copyright notice, this permission notice and any supplementary 
            8 * license documents shall be included in all copies or substantial portions
            9 * of the Software.
           10 */
           11 
           12 // 4-color GB palette must be dark to light
           13 const palettes = [
           14         // AYY4 - https://lospec.com/palette-list/ayy4
           15         [
           16                 [0, 48, 59],
           17                 [255, 119, 119],
           18                 [255, 206, 150],
           19                 [241, 242, 218]
           20         ],
           21         /* this one was removed in favour of spacehaze which is very similar
           22         // Barbie: The Slasher Movie - https://lospec.com/palette-list/barbie-the-slasher-movie
           23         [
           24                 [0, 0, 0],
           25                 [110, 31, 177],
           26                 [204, 51, 133],
           27                 [248, 251, 243]
           28         ],*/
           29         // SpaceHaze - https://lospec.com/palette-list/spacehaze
           30         [
           31                 [11, 6, 48],
           32                 [107, 31, 177],
           33                 [204, 52, 149],
           34                 [248, 227, 196]
           35         ],
           36         // CRTGB - https://lospec.com/palette-list/crtgb
           37         [
           38                 [6, 6, 1],
           39                 [11, 62, 8],
           40                 [72, 154, 13],
           41                 [218, 242, 34]
           42         ],
           43         // Amber CRTGB - https://lospec.com/palette-list/amber-crtgb
           44         [
           45                 [13, 4, 5],
           46                 [94, 18, 16],
           47                 [211, 86, 0],
           48                 [254, 208, 24]
           49         ],
           50         // Kirby (SGB) - https://lospec.com/palette-list/kirby-sgb
           51         [
           52                 [44, 44, 150],
           53                 [119, 51, 231],
           54                 [231, 134, 134],
           55                 [247, 190, 247]
           56         ],
           57         // CherryMelon - https://lospec.com/palette-list/cherrymelon
           58         [
           59                 [1, 40, 36],
           60                 [38, 89, 53],
           61                 [255, 77, 109],
           62                 [252, 222, 234]
           63         ],
           64         // Pumpkin GB - https://lospec.com/palette-list/pumpkin-gb
           65         [
           66                 [20, 43, 35],
           67                 [25, 105, 44],
           68                 [244, 110, 22],
           69                 [247, 219, 126]
           70         ],
           71         // Purpledawn - https://lospec.com/palette-list/purpledawn
           72         [
           73                 [0, 27, 46],
           74                 [45, 117, 126],
           75                 [154, 123, 188],
           76                 [238, 253, 237]
           77         ],
           78         // Royal4 - https://lospec.com/palette-list/royal4
           79         [
           80                 [82, 18, 150],
           81                 [138, 31, 172],
           82                 [212, 134, 74],
           83                 [235, 219, 94]
           84         ],
           85         // Grand Dad 4 - https://lospec.com/palette-list/grand-dad-4
           86         [
           87                 [76, 28, 45],
           88                 [210, 60, 78],
           89                 [95, 177, 245],
           90                 [234, 245, 250]
           91         ],
           92         // Mural GB - https://lospec.com/palette-list/mural-gb
           93         [
           94                 [10, 22, 78],
           95                 [162, 81, 48],
           96                 [206, 173, 107],
           97                 [250, 253, 255]
           98         ],
           99         // Ocean GB - https://lospec.com/palette-list/ocean-gb
          100         [
          101                 [28, 21, 48],
          102                 [42, 48, 139],
          103                 [54, 125, 1216],
          104                 [141, 226, 246]
          105         ],
          106         // Alleyway - ISS
          107         [
          108                 [66, 66, 66],
          109                 [123, 123, 206],
          110                 [255, 107, 255],
          111                 [255, 214, 0]
          112         ],
          113         // Pocket - ISS
          114         [
          115                 [108, 108, 78],
          116                 [142, 139, 97],
          117                 [195, 196, 165],
          118                 [227, 230, 201]
          119         ],
          120         // Kadabura4 - https://lospec.com/palette-list/kadabura4
          121         [
          122                 [0, 0, 0],
          123                 [87, 87, 87],
          124                 [219, 0, 12],
          125                 [255, 255, 255]
          126         ],
          127         // Virtual - ISS
          128         [
          129                 [2, 0, 0],
          130                 [65, 0, 0],
          131                 [127, 0, 0],
          132                 [255, 0, 0]
          133         ],
          134         // Love! Love! - ISS
          135         [
          136                 [176, 16, 48],
          137                 [255, 96, 176],
          138                 [255, 184, 232],
          139                 [255, 255, 255]
          140         ],
          141         // Metroid II (SGB) - https://lospec.com/palette-list/metroid-ii-sgb
          142         [
          143                 [44, 23, 0],
          144                 [4, 126, 96],
          145                 [182, 37, 88],
          146                 [174, 223, 30]
          147         ],
          148         // Micro 86 - https://lospec.com/palette-list/micro-86
          149         [
          150                 [38, 0, 14],
          151                 [255, 0, 0],
          152                 [255, 123, 48],
          153                 [255, 217, 178]
          154         ],
          155         // Vivid 2Bit Scream - https://lospec.com/palette-list/vivid-2bit-scream
          156         [
          157                 [86, 29, 23],
          158                 [92, 79, 163],
          159                 [116, 175, 52],
          160                 [202, 245, 50]
          161         ],
          162         // Pastel GBC/SGB - submitted by synth___ruiner
          163         [ 
          164                 [4,2,4],
          165                 [156,146,244],
          166                 [236,138,140],
          167                 [252,250,172]
          168         ],
          169         // trans flag - by mavica
          170         [
          171                 [32, 32, 32],
          172                 [91, 207, 250],
          173                 [245, 171, 185],
          174                 [255, 255, 255]
          175         ],
          176         // grayscale - by mavica
          177         [
          178                 [40, 40, 40],
          179                 [104, 104, 104],
          180                 [168, 168, 168],
          181                 [252, 252, 252]
          182         ],
          183         // Scold 2 bit - https://lospec.com/palette-list/scold-2-bit
          184         [
          185                 [16, 28, 86],
          186                 [206, 0, 148],
          187                 [15, 183, 0],
          188                 [211,211,211]
          189         ],
          190         // strawberry parfait - by mavica
          191         [
          192                 [31, 19, 0],
          193                 [216, 32, 46],
          194                 [247, 80, 215],
          195                 [255, 231, 204]
          196         ],
          197         // bric-a-brac - by mavica
          198         [
          199                 [12, 39, 56],
          200                 [237, 79, 54],
          201                 [248, 150, 23],
          202                 [184, 211, 218]
          203         ],
          204         // poolors 1 - mavica
          205         [
          206                 [35, 37, 7],
          207                 [50, 8, 105],
          208                 [144, 88, 97],
          209                 [233, 137, 120]
          210         ],
          211         // poolors 2 - mavica
          212         [
          213                 [64, 150, 138],
          214                 [169, 179, 121],
          215                 [206, 185, 181],
          216                 [183, 250, 241]
          217         ],
          218         // poolors 3 - mavica
          219         [
          220                 [81, 17, 107],
          221                 [130, 87, 147],
          222                 [152, 243, 175],
          223                 [254, 218, 57]
          224         ],
          225         // poolors 4 - mavica
          226         [
          227                 [19, 6, 18],
          228                 [72, 71, 173],
          229                 [8, 152, 149],
          230                 [152, 202, 145]
          231         ],
          232 ];
          233 
          234 const cameraStream = document.querySelector("#camera-stream"),
          235                         cameraView = document.querySelector("#camera-view"),
          236                         cameraOutput = document.querySelector("#camera-output"),
          237                         cameraDiv = document.querySelector("#camera"),
          238                         appView = document.querySelector("#app-view"),
          239                         gifPreview = document.querySelector("#gif-preview"),
          240                         gifImg = document.querySelector("#gif-img"),
          241                         gifButtons = document.querySelector("#gif-buttons");
          242 var amountOfCameras = 0;
          243 var currentFacingMode = 'user';
          244 var appScale;
          245 var frameDrawing;
          246 var resizeTimeout;
          247 const gifLength = 50;
          248 var outputScale = 6;
          249 const minCameraZoom = 1;
          250 const maxCameraZoom = 4;
          251 var pinchStartDistance = null;
          252 var pinchStartZoom = minCameraZoom;
          253 var gifRecording,
          254                 gifEncoder,
          255                 gifFrames,
          256                 gifBlob;
          257 
          258 const sliderGamma = [
          259         2.5,
          260         2,
          261         1.5,
          262         1,
          263         0.8,
          264         0.6,
          265         0.4
          266 ];
          267 
          268 const sliderContrast = [
          269         0.6,
          270         0.9,
          271         1.2,
          272         1.5,
          273         1.8,
          274         2.1,
          275         2.4
          276 ];
          277 
          278 const sliderSharpness = [
          279         0,
          280         0.25,
          281         0.50,
          282         0.75,
          283         1.00,
          284         1.25,
          285         2.00
          286 ];
          287 
          288 // 8 x 8 Bayer Matrix
          289 const bayer8 = [
          290         [0,48,12,60,3,51,15,63],
          291         [32,16,44,28,35,19,47,31],
          292         [8,56,4,52,11,59,7,55],
          293         [40,24,36,20,43,27,39,23],
          294         [2,50,14,62,1,49,13,61],
          295         [34,18,46,30,33,17,45,29],
          296         [10,58,6,54,9,57,5,53],
          297         [42,26,38,22,41,25,37,21]
          298 ];
          299 
          300 const clampNumber = (num, a, b) => Math.min(Math.max(num, a), b);
          301 
          302 // bounding boxes for each button in the app
          303 var buttons = {
          304         bottomLeft: {
          305                 x:1,
          306                 y:113,
          307                 width:30,
          308                 height:30
          309         },
          310         bottomRight: {
          311                 x:129,
          312                 y:113,
          313                 width:30,
          314                 height:30
          315         },
          316         topLeft: {
          317                 x:1,
          318                 y:1,
          319                 width:30,
          320                 height:30
          321         },
          322         contrastLeft: {
          323                 x:10,
          324                 y:13,
          325                 width:15,
          326                 height:13
          327         },
          328         contrastRight: {
          329                 x:25,
          330                 y:13,
          331                 width:15,
          332                 height:13
          333         },
          334         brightnessLeft: {
          335                 x:65,
          336                 y:13,
          337                 width:15,
          338                 height:13
          339         },
          340         brightnessRight: {
          341                 x:80,
          342                 y:13,
          343                 width:15,
          344                 height:13
          345         },
          346         paletteLeft: {
          347                 x:120,
          348                 y:13,
          349                 width:15,
          350                 height:13
          351         },
          352         paletteRight: {
          353                 x:135,
          354                 y:13,
          355                 width:15,
          356                 height:13
          357         },
          358         sharpnessLeft: {
          359                 x:120,
          360                 y:126,
          361                 width:15,
          362                 height:13
          363         },
          364         sharpnessRight: {
          365                 x:135,
          366                 y:126,
          367                 width:15,
          368                 height:13
          369         },
          370         screenHotspot: {
          371                 x:31,
          372                 y:31,
          373                 width:98,
          374                 height:82
          375         },
          376         hideUI: {
          377                 x:145,
          378                 y:0,
          379                 width:15,
          380                 height:15
          381         },
          382         timer: {
          383                 x:52,
          384                 y:131,
          385                 width:13,
          386                 height:13
          387         },
          388         record: {
          389                 x:95,
          390                 y:131,
          391                 width:16,
          392                 height:13
          393         },
          394         bppSwitch: {
          395                 x:45,
          396                 y:133,
          397                 width:50,
          398                 height:9
          399         }
          400 };
          401 
          402 var screens = {
          403         uiMain: {
          404                 elem: document.querySelector("#ui-main"),
          405                 buttons: [
          406                         {
          407                                 bounding: buttons.bottomLeft,
          408                                 action: captureImage
          409                         },
          410                         {
          411                                 bounding: buttons.screenHotspot,
          412                                 action: captureImage
          413                         },
          414                         {
          415                                 bounding: buttons.bottomRight,
          416                                 action: switchCameras
          417                         },
          418                         {
          419                                 bounding: buttons.topLeft,
          420                                 action: ()=> {
          421                                         currentUI = screens.uiSettings;
          422                                 }
          423                         },
          424                         {
          425                                 bounding: buttons.hideUI,
          426                                 action: ()=> {
          427                                         currentUI = screens.uiHidden;
          428                                 }
          429                         },
          430                         {
          431                                 bounding: buttons.timer,
          432                                 action: ()=> {
          433                                         // change UI to timer and trigger 3s delay to capture
          434                                         currentUI = screens.uiTimer;
          435                                         setTimeout(captureImage, 3000);
          436                                 }
          437                         },
          438                         {
          439                                 bounding: buttons.record,
          440                                 action: gifStart
          441                         }
          442                 ]
          443         },
          444         uiCapture: {
          445                 elem: document.querySelector("#ui-capture"),
          446                 buttons: [
          447                         {
          448                                 bounding: buttons.bottomLeft,
          449                                 action: ()=> {
          450                                         // return
          451                                         cameraStream.play();
          452                                         currentUI = screens.uiMain;
          453                                 }
          454                         },
          455                         {
          456                                 bounding: buttons.bottomRight,
          457                                 action: savePicture
          458                         },
          459                         {
          460                                 bounding: buttons.topLeft,
          461                                 action: ()=> {
          462                                         // go to settings
          463                                         currentUI = screens.uiSettings;
          464                                 }
          465                         }
          466                 ]
          467         },
          468         uiSettings: {
          469                 elem: document.querySelector("#ui-settings"),
          470                 buttons: [
          471                         {
          472                                 bounding: buttons.bottomLeft,
          473                                 action: ()=> {
          474                                         // return
          475                                         if(cameraStream.paused == true) {
          476                                                 // we're in capture
          477                                                 currentUI = screens.uiCapture;
          478                                         } else {
          479                                                 currentUI = screens.uiMain;
          480                                         }
          481                                 }
          482                         },
          483                         {
          484                                 bounding: buttons.contrastLeft,
          485                                 action: ()=> {
          486                                         if(cameraVars.contrast > 0) cameraVars.contrast--;
          487                                         savePrefs();
          488                                 }
          489                         },
          490                         {
          491                                 bounding: buttons.contrastRight,
          492                                 action: ()=> {
          493                                         if(cameraVars.contrast < 6) cameraVars.contrast++;
          494                                         savePrefs();
          495                                 }
          496                         },
          497                         {
          498                                 bounding: buttons.brightnessLeft,
          499                                 action: ()=> {
          500                                         if(cameraVars.gamma > 0) cameraVars.gamma--;
          501                                         savePrefs();
          502                                 }
          503                         },
          504                         {
          505                                 bounding: buttons.brightnessRight,
          506                                 action: ()=> {
          507                                         if(cameraVars.gamma < 6) cameraVars.gamma++;
          508                                         savePrefs();
          509                                 }
          510                         },
          511                         {
          512                                 bounding: buttons.sharpnessLeft,
          513                                 action: ()=> {
          514                                         if(cameraVars.sharpness > 0) cameraVars.sharpness--;
          515                                         savePrefs();
          516                                 }
          517                         },
          518                         {
          519                                 bounding: buttons.sharpnessRight,
          520                                 action: ()=> {
          521                                         if(cameraVars.sharpness < 6) cameraVars.sharpness++;
          522                                         savePrefs();
          523                                 }
          524                         },
          525                         {
          526                                 bounding: buttons.paletteLeft,
          527                                 action: ()=> {
          528                                         currentPalette--;
          529                                         if(currentPalette < 0) currentPalette = palettes.length-1;
          530                                         savePrefs(); 
          531                                 }
          532                         },
          533                         {
          534                                 bounding: buttons.paletteRight,
          535                                 action: ()=> {
          536                                         currentPalette++;
          537                                         if(currentPalette >= palettes.length) currentPalette = 0;
          538                                         savePrefs();
          539                                 }
          540                         },
          541                         {
          542                                 bounding: buttons.bppSwitch,
          543                                 action: ()=> {
          544                                         updateBpp(cameraVars.bppSwitch == 2 ? 1 : 2);
          545                                 }
          546                         }
          547                 ]
          548         },
          549         uiHidden: {
          550                 elem: document.querySelector("#ui-hidden"),
          551                 buttons: [
          552                         {
          553                                 bounding: buttons.hideUI,
          554                                 action: ()=> {
          555                                         // go back to main
          556                                         currentUI = screens.uiMain;
          557                                 }
          558                         }
          559                 ]
          560         },
          561         uiTimer: {
          562                 elem: document.querySelector("#ui-timer"),
          563                 buttons: []
          564         },
          565         uiRecord: {
          566                 elem: document.querySelector("#ui-record"),
          567                 buttons: []
          568         }
          569 };
          570 
          571 // global settings for gbcamera
          572 var renderWidth = 320,
          573                 renderHeight = 288,
          574                 currentPalette = 0,
          575                 currentUI = screens.uiMain;
          576 
          577 var cameraVars = {
          578         width: 128,
          579         height: 112,
          580         dither: 0.6,
          581         contrast: 3,
          582         gamma: 3,
          583         sharpness: 3,
          584         zoom: minCameraZoom,
          585         trackZoomSupported: false,
          586         xOffset: 0,
          587         yOffset: 0,
          588         xScale: 1,
          589         yScale: 1,
          590         bppSwitch: 2 // 2bpp = gameboy, 1bpp = atkinson, doubleres
          591 };
          592 
          593 // function to check if phone is portrait oriented
          594 function screenIsPortrait() {
          595         try {
          596                 let orientation = (screen.orientation || {}).type || screen.mozOrientation || screen.msOrientation;
          597                 if(orientation != undefined) {
          598                         if(orientation.includes('portrait')) return true;
          599                 } else if(window.orientation != undefined) {
          600                         if(window.orientation == 0) return true;
          601                 }
          602                 return false;
          603         } catch(e) {
          604                 return false;
          605         }
          606 }
          607 
          608 //Function to get the mouse position
          609 function getMousePos(canvas, event) {
          610         let rect = canvas.getBoundingClientRect();
          611         let mousePos = {
          612                         x: (event.clientX - rect.left) / (rect.right - rect.left) * renderWidth / 2,
          613                         y: (event.clientY - rect.top) / (rect.bottom - rect.top) * renderHeight / 2
          614         };
          615         return mousePos; 
          616 }
          617 
          618 function getTouchDistance(touchA, touchB) {
          619         let deltaX = touchA.clientX - touchB.clientX;
          620         let deltaY = touchA.clientY - touchB.clientY;
          621         return Math.sqrt((deltaX * deltaX) + (deltaY * deltaY));
          622 }
          623 //Function to check whether a point is inside a rectangle
          624 function isInside(pos, rect){
          625         return pos.x > rect.x && pos.x < (rect.x+rect.width) && pos.y < (rect.y+rect.height) && pos.y > rect.y;
          626 }
          627 
          628 function applyTrackZoom() {
          629         if (!window.stream || !window.stream.getVideoTracks) return;
          630         let track = window.stream.getVideoTracks()[0];
          631         if (!track || !track.getCapabilities || !track.applyConstraints) return;
          632         let capabilities = track.getCapabilities();
          633         if (!capabilities || !capabilities.zoom) {
          634                 cameraVars.trackZoomSupported = false;
          635                 return;
          636         }
          637         cameraVars.trackZoomSupported = true;
          638         let zoom = clampNumber(cameraVars.zoom, capabilities.zoom.min, capabilities.zoom.max);
          639         if (capabilities.zoom.step && capabilities.zoom.step > 0) {
          640                 let steps = Math.round((zoom - capabilities.zoom.min) / capabilities.zoom.step);
          641                 zoom = capabilities.zoom.min + (steps * capabilities.zoom.step);
          642         }
          643         cameraVars.zoom = zoom;
          644         track.applyConstraints({
          645                 advanced: [{ zoom: zoom }]
          646         }).catch((error) => {
          647                 console.warn('applyConstraints() zoom failed: ', error);
          648         });
          649 }
          650 
          651 function setCameraZoom(zoom, save = true) {
          652         let parsedZoom = Number(zoom);
          653         if (Number.isNaN(parsedZoom)) return;
          654         cameraVars.zoom = clampNumber(parsedZoom, minCameraZoom, maxCameraZoom);
          655         if (save) {
          656                 localStorage.setItem("cameraZoom", cameraVars.zoom);
          657         }
          658         if (window.stream) {
          659                 applyTrackZoom();
          660                 initCameraDrawing(false);
          661         }
          662 }
          663 
          664 function switchCameras() {
          665         if(amountOfCameras > 1) {
          666                 if (currentFacingMode === 'environment') currentFacingMode = 'user';
          667                 else currentFacingMode = 'environment';
          668                 initCameraStream();
          669         }
          670 }
          671 
          672 function download(filename, blob) {
          673         var link = document.createElement('a');
          674         link.href = URL.createObjectURL(blob);
          675         link.download = filename;
          676         link.style.display = 'none';
          677         document.body.appendChild(link);
          678         link.click();
          679         document.body.removeChild(link);
          680 }
          681 
          682 function getFileDate() {
          683         let now = new Date();
          684         // i love javascript
          685         let dateString = now.getDate() + "-" + (now.getMonth()+1) + "-"+ now.getFullYear() + " " + now.getHours() + " " + now.getMinutes() + " " + now.getSeconds();
          686         return dateString;
          687 }
          688 
          689 function savePicture() {
          690         let ctx = cameraOutput.getContext("2d");
          691         ctx.drawImage(cameraView, 0,0, cameraOutput.width, cameraOutput.height);
          692         Filters.filterImage(Filters.paletteSwap, cameraOutput, [palettes[currentPalette]])
          693         cameraOutput.toBlob((blob) => {
          694                 download("webgbcam " + getFileDate() + ".png", blob);
          695         }, 'image/png');
          696 }
          697 
          698 function loadPrefs() {
          699         let localContrast = parseInt(localStorage.getItem("cameraContrast"));
          700         let localGamma = parseInt(localStorage.getItem("cameraGamma"));
          701         let localPalette = parseInt(localStorage.getItem("cameraPalette"));
          702         let localSharpness = parseInt(localStorage.getItem("cameraSharpness"));
          703         let localBpp = parseInt(localStorage.getItem("cameraBpp"));
          704         let localZoom = parseFloat(localStorage.getItem("cameraZoom"));
          705         cameraVars.contrast = (localContrast ? localContrast : 3);
          706         cameraVars.gamma = (localGamma ? localGamma : 3);
          707         cameraVars.sharpness = (localSharpness ? localSharpness : 3);
          708         cameraVars.zoom = (Number.isNaN(localZoom) ? minCameraZoom : clampNumber(localZoom, minCameraZoom, maxCameraZoom));
          709         updateBpp(localBpp ? localBpp : 2);
          710         outputScale = (cameraVars.bppSwitch == 2 ? 6 : 3);
          711         currentPalette = (localPalette ? localPalette : 0);
          712 }
          713 
          714 function savePrefs() {
          715         localStorage.setItem("cameraContrast", cameraVars.contrast);
          716         localStorage.setItem("cameraGamma", cameraVars.gamma);
          717         localStorage.setItem("cameraSharpness", cameraVars.sharpness);
          718         localStorage.setItem("cameraBpp", cameraVars.bppSwitch);
          719         localStorage.setItem("cameraPalette", currentPalette);
          720         localStorage.setItem("cameraZoom", cameraVars.zoom);
          721 }
          722 
          723 function updateBpp(bpp = 2) {
          724         if (bpp == 1) {
          725                 // to 1bpp
          726                 cameraVars.bppSwitch = 1;
          727                 cameraVars.width = 256;
          728                 cameraVars.height = 224;
          729                 outputScale = 3;
          730         } else {
          731                 // to 2bpp
          732                 cameraVars.bppSwitch = 2;
          733                 cameraVars.width = 128;
          734                 cameraVars.height = 112;
          735                 outputScale = 6;
          736         }
          737         cameraView.width = cameraVars.width;
          738         cameraView.height = cameraVars.height;
          739         initCameraDrawing(false);
          740 }
          741 
          742 function applyLevels(value, brightness, contrast, gamma) {
          743         let newValue = value / 255.0;
          744         newValue = (newValue - 0.5) * contrast + 0.5;
          745         //newValue = newValue + brightness;
          746         return Math.pow(clampNumber(newValue, 0, 1), gamma) * 255;
          747 }
          748 
          749 var Filters = {};
          750 Filters.getPixels = function(c) {
          751         return c.getContext('2d').getImageData(0,0,c.width,c.height);
          752 };
          753 
          754 Filters.filterImage = function(filter, canvas, var_args) {
          755         let args = [this.getPixels(canvas)];
          756         for (let i=0; i<var_args.length; i++) {
          757                 args.push(var_args[i]);
          758         }
          759         let idata = filter.apply(null, args);
          760         canvas.getContext("2d").putImageData(idata, 0, 0);
          761 };
          762 
          763 function xyToIndex(x, y, width) {
          764         return (x + y*width)*4;
          765 }
          766 
          767 Filters.grayscale = function(pixels) {
          768         let d = pixels.data;
          769 
          770         for (let i = 0; i < d.length; i += 4) {
          771                 let r = d[i];
          772                 let g = d[i+1];
          773                 let b = d[i+2];
          774 
          775                 // grayscale
          776                 let c = r*0.3 + g*0.59 + b*0.11;
          777 
          778                 // apply levels
          779                 c = clampNumber(applyLevels(c, 0, sliderContrast[cameraVars.contrast], sliderGamma[cameraVars.gamma]), 0, 255);
          780 
          781                 d[i] = c;
          782                 d[i+1] = c;
          783                 d[i+2] = c;
          784         }
          785 
          786         return pixels;
          787 }
          788 
          789 Filters.sharpen = function(pixels, sharpness) {
          790         if (sharpness == 0) return pixels;
          791         let d = pixels.data;
          792         let temp_buf = [];
          793 
          794         for(i = 0; i < pixels.width; i++) for(j = 0; j < pixels.height; j++)
          795         {
          796                         let ms = d[xyToIndex(i, Math.min(j+1,pixels.height-1), pixels.width)]-128;
          797                         let mn = d[xyToIndex(i, Math.max(0,j-1), pixels.width)]-128;
          798                         let mw = d[xyToIndex(Math.max(0,i-1), j, pixels.width)]-128;
          799                         let me = d[xyToIndex(Math.min(i+1,pixels.width-1), j, pixels.width)]-128;
          800                         let px = d[xyToIndex(i, j, pixels.width)]-128;
          801 
          802                         temp_buf[xyToIndex(i, j, pixels.width)] = clampNumber(px+((4*px-mw-me-mn-ms)*sharpness), -128, 127)+128;
          803         }
          804         for (let i = 0; i < d.length; i += 4) {
          805                 d[i] = temp_buf[i];
          806                 d[i+1] = temp_buf[i];
          807                 d[i+2] = temp_buf[i];
          808         }
          809 
          810         return pixels;
          811 }
          812 
          813 Filters.gbcamera = function(pixels, ditherFactor) {
          814         let d = pixels.data;
          815 
          816         for(let y = 0; y < pixels.height; y++) {
          817                 for(let x = 0; x < pixels.width; x++) {
          818                         let i = xyToIndex(x, y, pixels.width);
          819 
          820                         let bayer = bayer8[(y)%8][(x)%8];
          821                         let c = d[i];
          822 
          823                         // apply bayer
          824                         c = clampNumber(c + ((bayer - 32) * ditherFactor), 0, 255);
          825 
          826                         // quantize to four places which will determine palette color
          827                         c = clampNumber(Math.round(c / 64), 0, 3) * 64;
          828 
          829                         d[i] = c;
          830                         d[i+1] = c;
          831                         d[i+2] = c;
          832                 }
          833         }
          834         
          835         return pixels;
          836 }
          837 
          838 // thanks to https://beyondloom.com/blog/dither.html
          839 Filters.atkinson = function(pixels) {
          840         let w = pixels.width;
          841         let d = pixels.data;
          842         const e=Array(2*w).fill(0), m=[0,1,w-2,w-1,w,2*w-1];
          843         for (i = 0; i < d.length; i=i+4) {
          844                 let x = d[i]/256.0;
          845                 const pix=x+(e.push(0),e.shift()), col=pix>.5, err=(pix-col)/8;
          846                 m.forEach(x => e[x]+=err);
          847                 let c = col ? 255 : 0;
          848                 d[i] = c;
          849                 d[i+1] = c;
          850                 d[i+2] = c;
          851         }
          852         return pixels;
          853 }
          854 
          855 // takes grayscale and paints it with palette
          856 Filters.paletteSwap = function(pixels, palette) {
          857         let d = pixels.data;
          858 
          859         for (let i = 0; i < d.length; i += 4) {
          860                 let c = clampNumber(Math.floor(d[i] / 64), 0, 3);
          861                 
          862                 let r,g,b;
          863                 [r, g, b] = palette[c];
          864 
          865                 d[i] = r;
          866                 d[i+1] = g;
          867                 d[i+2] = b;
          868         }
          869 
          870         return pixels;
          871 }
          872 
          873 // this function counts the amount of video inputs
          874 // it replaces DetectRTC that was previously implemented.
          875 function deviceCount() {
          876         return new Promise(function (resolve) {
          877                 var videoInCount = 0;
          878 
          879                 navigator.mediaDevices
          880                         .enumerateDevices()
          881                         .then(function (devices) {
          882                                 devices.forEach(function (device) {
          883                                         if (device.kind === 'video') {
          884                                                 device.kind = 'videoinput';
          885                                         }
          886 
          887                                         if (device.kind === 'videoinput') {
          888                                                 videoInCount++;
          889                                                 //console.log('videocam: ' + device.label);
          890                                         }
          891                                 });
          892 
          893                                 resolve(videoInCount);
          894                         })
          895                         .catch(function (err) {
          896                                 console.log(err.name + ': ' + err.message);
          897                                 resolve(0);
          898                         });
          899         });
          900 }
          901 
          902 document.addEventListener('DOMContentLoaded', function (event) {
          903         // check if mediaDevices is supported
          904         if (
          905                 navigator.mediaDevices &&
          906                 navigator.mediaDevices.getUserMedia &&
          907                 navigator.mediaDevices.enumerateDevices
          908         ) {
          909                 // first we call getUserMedia to trigger permissions
          910                 // we need this before deviceCount, otherwise Safari doesn't return all the cameras
          911                 // we need to have the number in order to display the switch front/back button
          912                 navigator.mediaDevices
          913                         .getUserMedia({
          914                                 audio: false,
          915                                 video: true,
          916                         })
          917                         .then(function (stream) {
          918                                 stream.getTracks().forEach(function (track) {
          919                                         track.stop();
          920                                 });
          921 
          922                                 deviceCount().then(function (deviceCount) {
          923                                         amountOfCameras = deviceCount;
          924 
          925                                         // init the UI and the camera stream
          926                                         initCameraUI();
          927                                         initCameraStream();
          928                                 });
          929                         })
          930                         .catch(function (error) {
          931                                 //https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
          932                                 if (error.name === 'NotAllowedError') {
          933                                         alert('camera permission denied, please refresh and allow camera capture!');
          934                                 } else if (error.name === 'NotFoundError') {
          935                                         alert('no cameras found! make sure your webcam is plugged in and enabled!');
          936                                 } else {
          937                                         alert('unspecified camera error! make sure camera permissions are enabled!');
          938                                 }
          939 
          940                                 console.error('getUserMedia() error: ', error);
          941                         });
          942         } else {
          943                 alert(
          944                         'it seems your browser does not support camera capture! :(',
          945                 );
          946         }
          947         loadPrefs();
          948 });
          949 
          950 function restartCamera() {
          951         clearTimeout(resizeTimeout);
          952         resizeTimeout = setTimeout(function() {
          953                 initAppScaling();
          954                 initCameraDrawing();
          955         }, 300);
          956 }
          957 
          958 //window.onorientationchange = restartCamera;
          959 //window.onresize = restartCamera;
          960 
          961 function captureImage() {
          962         cameraStream.pause();
          963         currentUI = screens.uiCapture;
          964 }
          965 
          966 function initCameraUI() {
          967         initAppScaling();
          968 
          969         const queryString = window.location.search;
          970         const urlParams = new URLSearchParams(queryString);
          971         if (urlParams.has('hideui')) {
          972                 currentUI = screens.uiHidden;
          973         }
          974         if (urlParams.has('zoom')) {
          975                 setCameraZoom(parseFloat(urlParams.get('zoom')), false);
          976         }
          977 
          978         // handle canvas app clicks
          979         appView.addEventListener('click', function(e) {
          980                 var mousePos = getMousePos(appView, e);
          981 
          982                 currentUI.buttons.forEach((button) => {
          983                         if (isInside(mousePos, button.bounding)) {
          984                                 button.action();
          985                         }
          986                 });
          987                 
          988         }, false);
          989 
          990         appView.addEventListener('touchstart', function(e) {
          991                 if (e.touches.length === 2) {
          992                         pinchStartDistance = getTouchDistance(e.touches[0], e.touches[1]);
          993                         pinchStartZoom = cameraVars.zoom;
          994                 }
          995         }, { passive: false });
          996 
          997         appView.addEventListener('touchmove', function(e) {
          998                 if (e.touches.length !== 2 || pinchStartDistance === null) return;
          999                 e.preventDefault();
         1000                 let currentDistance = getTouchDistance(e.touches[0], e.touches[1]);
         1001                 let zoomFromPinch = pinchStartZoom * (currentDistance / pinchStartDistance);
         1002                 if (Math.abs(zoomFromPinch - cameraVars.zoom) >= 0.03) {
         1003                         setCameraZoom(zoomFromPinch);
         1004                 }
         1005         }, { passive: false });
         1006 
         1007         appView.addEventListener('touchend', function(e) {
         1008                 if (e.touches.length < 2) {
         1009                         pinchStartDistance = null;
         1010                 }
         1011         }, false);
         1012 }
         1013 
         1014 function initAppScaling(scale = 2) {
         1015         appScale = scale;
         1016 
         1017         // canvas sizes
         1018         cameraView.width = cameraVars.width;
         1019         cameraView.height = cameraVars.height;
         1020         appView.width = renderWidth * appScale;
         1021         appView.height = renderHeight * appScale;
         1022         
         1023         let ctx = appView.getContext("2d");
         1024         ctx.imageSmoothingEnabled = false;
         1025         ctx.scale(appScale,appScale);
         1026         ctx = cameraView.getContext("2d");
         1027         ctx.imageSmoothingEnabled = false;
         1028 }
         1029 
         1030 // https://github.com/webrtc/samples/blob/gh-pages/src/content/devices/input-output/js/main.js
         1031 function initCameraStream() {
         1032         // stop any active streams in the window
         1033         if (window.stream) {
         1034                 window.stream.getTracks().forEach(function (track) {
         1035                         //console.log(track);
         1036                         track.stop();
         1037                 });
         1038         }
         1039 
         1040         var constraints = {
         1041                 audio: false,
         1042                 video: {
         1043                         width: { ideal: 640 },
         1044                         height: { ideal: 480 },
         1045                         facingMode: currentFacingMode,
         1046                 },
         1047         };
         1048         
         1049         function handleSuccess(stream) {
         1050                 if (stream) {
         1051                         window.stream = stream; // make stream available to browser console
         1052                         cameraStream.srcObject = stream;
         1053         
         1054                         let track = window.stream.getVideoTracks()[0];
         1055                         cameraStream.width = track.getSettings().width;
         1056                         cameraStream.height = track.getSettings().height;
         1057                         applyTrackZoom();
         1058                         
         1059                         setTimeout(initCameraDrawing, 500);
         1060                 }
         1061         }
         1062         
         1063         function handleError(error) {
         1064                 console.error('getUserMedia() error: ', error);
         1065         }
         1066         
         1067         navigator.mediaDevices
         1068                 .getUserMedia(constraints)
         1069                 .then(handleSuccess)
         1070                 .catch(handleError);
         1071 }
         1072 
         1073 function initCameraDrawing(start = true) {
         1074         // if cameraStream has vertical or horizontal resolution of 0 then it's not initialized, we retry until the browser decides to properly work
         1075         if (cameraStream.videoHeight == 0) setTimeout(restartCamera, 500);
         1076 
         1077         const track = window.stream.getVideoTracks()[0];
         1078         let settings = track.getSettings();
         1079         //let str = JSON.stringify(settings, null, 4);
         1080         //console.log('settings ' + str);
         1081 
         1082         // calculate scale and offset to render camera stream to camera view canvas
         1083         if(cameraStream.videoWidth >= cameraStream.videoHeight) {
         1084                 // horizontal
         1085                 cameraVars.yScale = cameraStream.videoHeight;
         1086                 cameraVars.xScale = Math.floor((cameraStream.videoHeight / cameraVars.height) * cameraVars.width);
         1087                 cameraVars.yOffset = 0;
         1088                 cameraVars.xOffset = Math.floor((cameraStream.videoWidth - cameraVars.xScale) / 2);
         1089         } else {
         1090                 //vertical
         1091                 cameraVars.xScale = cameraStream.videoWidth;
         1092                 cameraVars.yScale = Math.floor((cameraStream.videoWidth / cameraVars.width) * cameraVars.height);
         1093                 cameraVars.xOffset = 0;
         1094                 cameraVars.yOffset = Math.floor((cameraStream.videoHeight - cameraVars.yScale) / 2);
         1095         }
         1096 
         1097         // If track zoom is unavailable, apply digital center-crop zoom.
         1098         if (!cameraVars.trackZoomSupported && cameraVars.zoom > minCameraZoom) {
         1099                 let zoomedXScale = Math.max(1, Math.floor(cameraVars.xScale / cameraVars.zoom));
         1100                 let zoomedYScale = Math.max(1, Math.floor(cameraVars.yScale / cameraVars.zoom));
         1101                 cameraVars.xOffset += Math.floor((cameraVars.xScale - zoomedXScale) / 2);
         1102                 cameraVars.yOffset += Math.floor((cameraVars.yScale - zoomedYScale) / 2);
         1103                 cameraVars.xScale = zoomedXScale;
         1104                 cameraVars.yScale = zoomedYScale;
         1105         }
         1106         
         1107         // canvas starts flipped for user facing camera
         1108         if(settings.facingMode != "environment") { // not environment = front-facing phone cam or pc webcam, flip
         1109                 cameraView.getContext('2d').setTransform(-1, 0, 0, 1, cameraVars.width, 0);
         1110         } else {
         1111                 cameraView.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
         1112         }
         1113         //console.log(cameraVars);
         1114         
         1115         cameraOutput.width = cameraVars.width * outputScale;
         1116         cameraOutput.height = cameraVars.height * outputScale;
         1117         let ctx = cameraOutput.getContext("2d");
         1118         ctx.imageSmoothingEnabled = false;
         1119 
         1120         if (start) {
         1121                 cameraStream.play();
         1122                 clearInterval(frameDrawing)
         1123                 frameDrawing = setInterval(drawFrame, 100);
         1124         }
         1125 }
         1126 
         1127 function showGifModal() {
         1128         gifPreview.classList.remove("hidden");
         1129 }
         1130 
         1131 function loadGifModal(blob) {
         1132         gifImg.src = URL.createObjectURL(blob);
         1133         gifBlob = blob;
         1134         gifButtons.classList.remove("hidden");
         1135 }
         1136 
         1137 function downloadGif() {
         1138         download("webgbcam " + getFileDate() + ".gif", gifBlob);
         1139         resetGifModal();
         1140 }
         1141 
         1142 function resetGifModal() {
         1143         gifBlob = null;
         1144         gifImg.src = "loading.gif";
         1145         gifPreview.classList.add("hidden");
         1146         gifButtons.classList.add("hidden");
         1147 }
         1148 
         1149 function gifStart() {
         1150         gifEncoder = new GIF({
         1151                 workers: 2,
         1152                 workerScript: 'gifjs/gif.worker.js',
         1153                 quality: 10,
         1154                 repeat: 0,
         1155                 width: cameraOutput.width,
         1156                 height: cameraOutput.height
         1157         });
         1158         gifEncoder.on('finished', function(blob) {
         1159                 loadGifModal(blob);
         1160                 //download("webgbcam " + getFileDate() + ".gif", URL.createObjectURL(blob));
         1161         });
         1162         gifFrames = gifLength;
         1163         currentUI = screens.uiRecord;
         1164         gifRecording = true;
         1165 }
         1166 
         1167 function gifEnd() {
         1168         gifRecording = false;
         1169         currentUI = screens.uiMain;
         1170         gifEncoder.render();
         1171         showGifModal();
         1172 }
         1173 
         1174 function gifFrame() {
         1175         let ctx = cameraOutput.getContext("2d");
         1176         Filters.filterImage(Filters.paletteSwap, cameraView, [palettes[currentPalette]])
         1177         ctx.drawImage(cameraView, 0,0, cameraOutput.width, cameraOutput.height);
         1178         gifEncoder.addFrame(ctx, {delay: 100, copy: true});
         1179         if(--gifFrames == 0) gifEnd();
         1180 }
         1181 
         1182 function scaledFillRect(ctx, x, y, width, height) {
         1183         const scale = 2;
         1184         ctx.fillRect(x * scale, y * scale, width * scale, height * scale);
         1185 }
         1186 
         1187 function drawFrame() {
         1188         let camctx = cameraView.getContext('2d');
         1189         camctx.drawImage(cameraStream, cameraVars.xOffset, cameraVars.yOffset, cameraVars.xScale, cameraVars.yScale, 0, 0, cameraVars.width, cameraVars.height);
         1190         
         1191         Filters.filterImage(Filters.grayscale, cameraView, []);
         1192         Filters.filterImage(Filters.sharpen, cameraView, [sliderSharpness[cameraVars.sharpness]]);
         1193         
         1194         if (cameraVars.bppSwitch == 2) {
         1195                 Filters.filterImage(Filters.gbcamera, cameraView, [cameraVars.dither]);
         1196         } else {
         1197                 Filters.filterImage(Filters.atkinson, cameraView, []);
         1198         }
         1199         
         1200         let ctx = appView.getContext("2d");
         1201         ctx.drawImage(cameraView, 32, 32, 256, 224);
         1202         ctx.drawImage(currentUI.elem, 0, 0, 160, 144, 0, 0, 320, 288);
         1203 
         1204         if (currentUI === screens.uiSettings) {
         1205                 // update settings values        
         1206                 ctx.fillStyle = "rgb(192,192,192)";
         1207                 for(let i = 1; i <= cameraVars.contrast; i++) {
         1208                         scaledFillRect(ctx, 42, 22 - (i*3), 4, 2);
         1209                 }
         1210                 for(let i = 1; i <= cameraVars.gamma; i++) {
         1211                         scaledFillRect(ctx, 97, 22 - (i*3), 4, 2);
         1212                 }
         1213                 for(let i = 1; i <= cameraVars.sharpness; i++) {
         1214                         scaledFillRect(ctx, 152, 135 - (i*3), 4, 2);
         1215                 }
         1216                 
         1217                 ctx.fillStyle = "rgb(130,130,130)";
         1218                 if (cameraVars.bppSwitch == 2) {
         1219                         scaledFillRect(ctx, 70, 135, 4, 4);
         1220                 } else {
         1221                         scaledFillRect(ctx, 65, 135, 4, 4);
         1222                 }
         1223         } else if (currentUI === screens.uiRecord) {
         1224                 // update record length
         1225                 ctx.fillStyle = "rgb(64,64,64)";
         1226                 scaledFillRect(ctx, 25, 134, 110 - (gifFrames / gifLength * 110), 6);
         1227         }
         1228 
         1229         try {
         1230                 Filters.filterImage(Filters.paletteSwap, appView, [palettes[currentPalette]])
         1231         } catch(e) {
         1232 
         1233         }
         1234         if (gifRecording) gifFrame();
         1235 }
         1236 
         1237 function toggleAbout() {
         1238         const elemAbout = document.getElementById("about");
         1239 
         1240         if (elemAbout.classList.contains("hidden")) {
         1241                 elemAbout.classList.remove("hidden");
         1242         } else {
         1243                 elemAbout.classList.add("hidden");
         1244         }
         1245 }