URI:
       Initial commit - webgbcam - [fork] gameboy webcam
  HTML git clone git://src.adamsgaard.dk/webgbcam
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit f45e1873f323851786a10031b83a01ac17222877
   DIR parent d122c4bb6e26506eca3f8033a9120c0affca12a0
  HTML Author: BuildTools <unconfigured@null.spigotmc.org>
       Date:   Thu, 22 Oct 2020 19:14:17 -0300
       
       Initial commit
       
       Diffstat:
         M README.md                           |      46 ++++++++++++++++++++++++++++++-
         A app.js                              |     664 +++++++++++++++++++++++++++++++
         A index.html                          |      41 +++++++++++++++++++++++++++++++
         A style.css                           |      59 +++++++++++++++++++++++++++++++
         A ui-capture.png                      |       0 
         A ui-main.png                         |       0 
         A ui-settings.png                     |       0 
       
       7 files changed, 809 insertions(+), 1 deletion(-)
       ---
   DIR diff --git a/README.md b/README.md
       @@ -1,2 +1,46 @@
        # webgbcam
       -Game Boy Camera-style filter made in HTML5 and JavaScript
       +
       +A simple Game Boy Camera-style filter made in HTML5 and JavaScript
       +
       +[Play with it here!](https://maple.pet/webgbcam/)
       +
       +## Disclaimer
       +
       +As of this commit, this is simply a direct copy of the files currently in my webserver.
       +Eventually, I will clean things up and make improvements to the code, but since there
       +has been interest in looking at the code, I'm mirroring the files here.
       +
       +## A quick explanation of how it all works
       +
       +The concept of [Bayer dithering](https://en.wikipedia.org/wiki/Ordered_dithering) was
       +hard for me to grasp at first, but after a [few different projects](https://github.com/Lana-chan/maples-retro-extravaganza)
       +getting acquainted with it, I've found an easy way to apply it, which can be used in
       +a procedural setting like JS Canvas filters, or shaders.
       +
       +Basically, you start with an array of pixels, then grayscale them and optionally apply
       +simple arithmetics to apply gamma and contrast adjustments. Then, you offset those by
       +the value in the Bayer matrix corresponding to that pixel, giving it a patterned look.
       +Finally, you divide and quantize the values until all pixels each have only one of four
       +possible values. This will give you a dithered pixel art look. After this, my code applies
       +a palette swap for those 4 values back to RGB space.
       +
       +## Acknowledgements
       +
       +Thanks to [Christine Love](https://twitter.com/christinelove) for making the Interstellar
       +Selfie Station back in 2014. It helped me a lot with my dysphoria and was the inspiration
       +to learning how Bayer dithering works in order to remake her camera app once it was no
       +longer available in app stores.
       +
       +## License
       +
       +```
       +/*
       + * ------------------------------------------------------------
       + * "THE BEERWARE LICENSE" (Revision 42):
       + * maple "mavica" syrup <maple@maple.pet> wrote this code.
       + * As long as you retain this notice, you can do whatever you
       + * want with this stuff. If we meet someday, and you think this
       + * stuff is worth it, you can buy me a beer in return.
       + * ------------------------------------------------------------
       + */
       +```
   DIR diff --git a/app.js b/app.js
       @@ -0,0 +1,663 @@
       +/*
       + * ------------------------------------------------------------
       + * "THE BEERWARE LICENSE" (Revision 42):
       + * maple "mavica" syrup <maple@maple.pet> wrote this code.
       + * As long as you retain this notice, you can do whatever you
       + * want with this stuff. If we meet someday, and you think this
       + * stuff is worth it, you can buy me a beer in return.
       + * ------------------------------------------------------------
       + */
       +
       +const cameraStream = document.querySelector("#camera-stream"),
       +                        cameraView = document.querySelector("#camera-view"),
       +                        cameraOutput = document.querySelector("#camera-output"),
       +                        cameraDiv = document.querySelector("#camera"),
       +                        appView = document.querySelector("#app-view"),
       +                        /*buttonSwitch = document.querySelector("#camera-switch"),
       +                        buttonShutter = document.querySelector("#camera-shutter"),*/
       +                        uiMain = document.querySelector("#ui-main"),
       +                        uiCapture = document.querySelector("#ui-capture"),
       +                        uiSettings = document.querySelector("#ui-settings");
       +var amountOfCameras = 0;
       +var currentFacingMode = 'user';
       +var reportedFacingMode;
       +var appScale;
       +var frameDrawing;
       +
       +// global settings for gbcamera
       +var cameraWidth = 128,
       +                cameraHeight = 112,
       +                cameraDither = 0.6,
       +                //cameraBrightness = 0.0,
       +                cameraContrast = 3,
       +                cameraGamma = 3,
       +                renderWidth = 160,
       +                renderHeight = 144,
       +                currentPalette = 0,
       +                currentUI = uiMain;
       +
       +const sliderGamma = [
       +        2.5,
       +        2,
       +        1.5,
       +        1,
       +        0.8,
       +        0.6,
       +        0.4
       +];
       +
       +const sliderContrast = [
       +        0.5,
       +        0.7,
       +        1.0,
       +        1.5,
       +        1.7,
       +        1.9,
       +        2.0
       +];
       +
       +// 8 x 8 Bayer Matrix
       +const bayer8 = [
       +        [0,48,12,60,3,51,15,63],
       +        [32,16,44,28,35,19,47,31],
       +        [8,56,4,52,11,59,7,55],
       +        [40,24,36,20,43,27,39,23],
       +        [2,50,14,62,1,49,13,61],
       +        [34,18,46,30,33,17,45,29],
       +        [10,58,6,54,9,57,5,53],
       +        [42,26,38,22,41,25,37,21]
       +];
       +
       +// 4-color GB palette must be dark to light
       +const palettes = [
       +  // AYY4
       +  [
       +    [0, 48, 59],
       +    [255, 119, 119],
       +    [255, 206, 150],
       +    [241, 242, 218]
       +  ],
       +  // Barbie: The Slasher Movie
       +  [
       +    [0, 0, 0],
       +    [110, 31, 177],
       +    [204, 51, 133],
       +    [248, 251, 243]
       +  ],
       +  // CRTGB
       +  [
       +    [6, 6, 1],
       +    [11, 62, 8],
       +    [72, 154, 13],
       +    [218, 242, 34]
       +  ],
       +  // Amber CRTGB
       +  [
       +    [13, 4, 5],
       +    [94, 18, 16],
       +    [211, 86, 0],
       +    [254, 208, 24]
       +  ],
       +  // Kirby (SGB)
       +  [
       +    [44, 44, 150],
       +    [119, 51, 231],
       +    [231, 134, 134],
       +    [247, 190, 247]
       +  ],
       +  // CherryMelon
       +  [
       +    [1, 40, 36],
       +    [38, 89, 53],
       +    [255, 77, 109],
       +    [252, 222, 234]
       +  ],
       +  // Pumpkin GB
       +  [
       +    [20, 43, 35],
       +    [25, 105, 44],
       +    [244, 110, 22],
       +    [247, 219, 126]
       +  ],
       +  // Purpledawn
       +  [
       +    [0, 27, 46],
       +    [45, 117, 126],
       +    [154, 123, 188],
       +    [238, 253, 237]
       +  ],
       +  // Royal4
       +  [
       +    [82, 18, 150],
       +    [138, 31, 172],
       +    [212, 134, 74],
       +    [235, 219, 94]
       +  ],
       +  // Grand Dad 4
       +  [
       +    [76, 28, 45],
       +    [210, 60, 78],
       +    [95, 177, 245],
       +    [234, 245, 250]
       +  ],
       +  // Mural GB
       +  [
       +    [10, 22, 78],
       +    [162, 81, 48],
       +    [206, 173, 107],
       +    [250, 253, 255]
       +  ],
       +  // Ocean GB
       +  [
       +    [28, 21, 48],
       +    [42, 48, 139],
       +    [54, 125, 1216],
       +    [141, 226, 246]
       +        ],
       +        // purple and yellow from ISS
       +        [
       +                [66, 66, 66],
       +                [123, 123, 206],
       +                [255, 107, 255],
       +                [255, 214, 0]
       +        ],
       +        // subdued gb colors
       +        [
       +                [108, 108, 78],
       +                [142, 139, 97],
       +                [195, 196, 165],
       +                [227, 230, 201]
       +        ],
       +  // Kadabur4
       +  [
       +    [0, 0, 0],
       +    [87, 87, 87],
       +    [219, 0, 12],
       +    [255, 255, 255]
       +  ],
       +  // ISS VB
       +  [
       +    [2, 0, 0],
       +    [65, 0, 0],
       +    [127, 0, 0],
       +    [255, 0, 0]
       +  ],
       +  // ISS Strawberry
       +  [
       +    [176, 16, 48],
       +    [255, 96, 176],
       +    [255, 184, 232],
       +    [255, 255, 255]
       +  ],
       +  // Metroid II (SGB)
       +  [
       +    [44, 23, 0],
       +    [4, 126, 96],
       +    [182, 37, 88],
       +    [174, 223, 30]
       +  ],
       +  // Micro 86
       +  [
       +    [38, 0, 14],
       +    [255, 0, 0],
       +    [255, 123, 48],
       +    [255, 217, 178]
       +  ],
       +  // Vivid 2Bit Scream
       +  [
       +    [86, 29, 23],
       +    [92, 79, 163],
       +    [116, 175, 52],
       +    [202, 245, 50]
       +  ]
       +];
       +
       +const clampNumber = (num, a, b) => Math.min(Math.max(num, a), b);
       +
       +//Function to get the mouse position
       +function getMousePos(canvas, event) {
       +        var rect = canvas.getBoundingClientRect();
       +        return {
       +                        x: (event.clientX - rect.left) / appScale,
       +                        y: (event.clientY - rect.top) / appScale
       +        };
       +}
       +//Function to check whether a point is inside a rectangle
       +function isInside(pos, rect){
       +        return pos.x > rect.x && pos.x < rect.x+rect.width && pos.y < rect.y+rect.height && pos.y > rect.y
       +}
       +
       +function switchCameras() {
       +        if(amountOfCameras > 1) {
       +                if (currentFacingMode === 'environment') currentFacingMode = 'user';
       +                else currentFacingMode = 'environment';
       +                initCameraStream();
       +        }
       +}
       +
       +function download(filename, content) {
       +  var element = document.createElement('a');
       +  element.setAttribute('href', content);
       +  element.setAttribute('download', filename);
       +
       +  element.style.display = 'none';
       +  document.body.appendChild(element);
       +
       +  element.click();
       +
       +  document.body.removeChild(element);
       +}
       +
       +function savePicture() {
       +        let scale = 5;
       +
       +        let now = new Date();
       +        // i love javascript
       +        let dateString = now.getDate() + "-" + (now.getMonth()+1) + "-"+ now.getFullYear() + " " + now.getHours() + " " + now.getMinutes() + " " + now.getSeconds();
       +
       +        cameraOutput.width = cameraWidth * scale;
       +        cameraOutput.height = cameraHeight * scale;
       +        let ctx = cameraOutput.getContext("2d");
       +        ctx.imageSmoothingEnabled = false;
       +        ctx.drawImage(cameraView, 0,0, cameraOutput.width, cameraOutput.height);
       +        Filters.filterImage(Filters.paletteSwap, cameraOutput, [palettes[currentPalette]])
       +        var dataURL = cameraOutput.toDataURL('image/png');
       +        download("webcamgb " + dateString + ".png", dataURL);
       +}
       +
       +// bounding boxes for each button in the app
       +var buttons = {
       +        bottomLeft: {
       +                x:1,
       +                y:113,
       +                width:30,
       +                height:30
       +        },
       +        bottomRight: {
       +                x:129,
       +                y:113,
       +                width:30,
       +                height:30
       +        },
       +        topLeft: {
       +                x:1,
       +                y:1,
       +                width:30,
       +                height:30
       +        },
       +        contrastLeft: {
       +                x:10,
       +                y:13,
       +                width:15,
       +                height:13
       +        },
       +        contrastRight: {
       +                x:25,
       +                y:13,
       +                width:15,
       +                height:13
       +        },
       +        brightnessLeft: {
       +                x:65,
       +                y:13,
       +                width:15,
       +                height:13
       +        },
       +        brightnessRight: {
       +                x:80,
       +                y:13,
       +                width:15,
       +                height:13
       +        },
       +        paletteLeft: {
       +                x:120,
       +                y:13,
       +                width:15,
       +                height:13
       +        },
       +        paletteRight: {
       +                x:135,
       +                y:13,
       +                width:15,
       +                height:13
       +        },
       +};
       +
       +function applyLevels(value, brightness, contrast, gamma) {
       +        let newValue = value / 255.0;
       +        newValue = (newValue - 0.5) * contrast + 0.5;
       +        //newValue = newValue + brightness;
       +        return Math.pow(clampNumber(newValue, 0, 1), gamma) * 255;
       +}
       +
       +Filters = {};
       +Filters.getPixels = function(c) {
       +        return c.getContext('2d').getImageData(0,0,c.width,c.height);
       +};
       +
       +Filters.filterImage = function(filter, canvas, var_args) {
       +        let args = [this.getPixels(canvas)];
       +        for (let i=0; i<var_args.length; i++) {
       +                args.push(var_args[i]);
       +        }
       +        let idata = filter.apply(null, args);
       +        canvas.getContext("2d").putImageData(idata, 0, 0);
       +};
       +
       +Filters.gbcamera = function(pixels, ditherFactor) {
       +        let d = pixels.data;
       +
       +        for(let y = 0; y < pixels.height; y++) {
       +                for(let x = 0; x < pixels.width; x++) {
       +                        let n = (x + y*pixels.width);
       +                        let i = n * 4;
       +
       +                        let bayer = bayer8[(y)%8][(x)%8];
       +
       +                        let r = d[i];
       +                        let g = d[i+1];
       +                        let b = d[i+2];
       +
       +                        // grayscale
       +                        let c = r*0.3 + g*0.59 + b*0.11;
       +
       +                        // apply levels
       +                        c = clampNumber(applyLevels(c, 0, sliderContrast[cameraContrast], sliderGamma[cameraGamma]), 0, 255);
       +
       +                        // apply bayer
       +                        c = clampNumber(c + ((bayer - 32) * ditherFactor), 0, 255);
       +
       +                        // quantize to four places which will determine palette color
       +                        c = clampNumber(Math.round(c / 64), 0, 3) * 64;
       +
       +                        d[i] = c;
       +                        d[i+1] = c;
       +                        d[i+2] = c;
       +                }
       +        }
       +        
       +        return pixels;
       +}
       +
       +// takes grayscale and paints it with palette
       +Filters.paletteSwap = function(pixels, palette) {
       +        let d = pixels.data;
       +
       +        for (let i = 0; i < d.length; i += 4) {
       +                c = clampNumber(Math.floor(d[i] / 64), 0, 3);
       +                
       +                [r, g, b] = palette[c];
       +
       +                d[i] = r;
       +                d[i+1] = g;
       +                d[i+2] = b;
       +        }
       +
       +        return pixels;
       +}
       +
       +// this function counts the amount of video inputs
       +// it replaces DetectRTC that was previously implemented.
       +function deviceCount() {
       +        return new Promise(function (resolve) {
       +                var videoInCount = 0;
       +
       +                navigator.mediaDevices
       +                        .enumerateDevices()
       +                        .then(function (devices) {
       +                                devices.forEach(function (device) {
       +                                        if (device.kind === 'video') {
       +                                                device.kind = 'videoinput';
       +                                        }
       +
       +                                        if (device.kind === 'videoinput') {
       +                                                videoInCount++;
       +                                                //console.log('videocam: ' + device.label);
       +                                        }
       +                                });
       +
       +                                resolve(videoInCount);
       +                        })
       +                        .catch(function (err) {
       +                                console.log(err.name + ': ' + err.message);
       +                                resolve(0);
       +                        });
       +        });
       +}
       +
       +document.addEventListener('DOMContentLoaded', function (event) {
       +        // check if mediaDevices is supported
       +        if (
       +                navigator.mediaDevices &&
       +                navigator.mediaDevices.getUserMedia &&
       +                navigator.mediaDevices.enumerateDevices
       +        ) {
       +                // first we call getUserMedia to trigger permissions
       +                // we need this before deviceCount, otherwise Safari doesn't return all the cameras
       +                // we need to have the number in order to display the switch front/back button
       +                navigator.mediaDevices
       +                        .getUserMedia({
       +                                audio: false,
       +                                video: true,
       +                        })
       +                        .then(function (stream) {
       +                                stream.getTracks().forEach(function (track) {
       +                                        track.stop();
       +                                });
       +
       +                                deviceCount().then(function (deviceCount) {
       +                                        amountOfCameras = deviceCount;
       +
       +                                        // init the UI and the camera stream
       +                                        initCameraUI();
       +                                        initCameraStream();
       +                                });
       +                        })
       +                        .catch(function (error) {
       +                                //https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
       +                                if (error === 'PermissionDeniedError') {
       +                                        alert('Permission denied. Please refresh and give permission.');
       +                                }
       +
       +                                console.error('getUserMedia() error: ', error);
       +                        });
       +        } else {
       +                alert(
       +                        'Mobile camera is not supported by browser, or there is no camera detected/connected',
       +                );
       +        }
       +});
       +
       +function initCameraUI() {
       +        // figure out max integer render scale for window
       +        if(window.innerWidth >= window.innerHeight) {
       +                // horizontal
       +                appScale = Math.floor(window.innerHeight / renderHeight);
       +        } else {
       +                // vertical
       +                appScale = Math.floor(window.innerWidth / renderWidth);
       +        }
       +        cameraDiv.style.width = appScale * renderWidth + "px";
       +        cameraDiv.style.height = appScale * renderHeight + "px";
       +
       +        // canvas sizes
       +        cameraView.width = cameraWidth;
       +        cameraView.height = cameraHeight;
       +        appView.width = renderWidth;
       +        appView.height = renderHeight;
       +
       +        // https://developer.mozilla.org/nl/docs/Web/HTML/Element/button
       +        // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role
       +
       +        // -- switch camera part
       +        /*if (amountOfCameras > 1) {
       +                buttonSwitch.style.display = 'block';
       +
       +                buttonSwitch.addEventListener('click', 
       +        }*/
       +
       +        // save picture to disk
       +        /*buttonShutter.addEventListener('click', function() {
       +
       +        });*/
       +
       +        // handle canvas app clicks
       +        appView.addEventListener('click', function(e) {
       +    var mousePos = getMousePos(appView, e);
       +
       +                // buttons in main screen
       +                if(currentUI === uiMain) {
       +                        if(isInside(mousePos, buttons.bottomLeft)) {
       +                                // shutter
       +                                cameraStream.pause();
       +                                currentUI = uiCapture;
       +                        } else if(isInside(mousePos, buttons.bottomRight)) {
       +                                // switch camera
       +                                switchCameras();
       +                        } else if(isInside(mousePos, buttons.topLeft)) {
       +                                // go to settings
       +                                currentUI = uiSettings;
       +                        } 
       +                } else if(currentUI === uiCapture) {
       +                        if(isInside(mousePos, buttons.bottomLeft)) {
       +                                // return
       +                                cameraStream.play();
       +                                currentUI = uiMain;
       +                        } else if(isInside(mousePos, buttons.bottomRight)) {
       +                                // save picture
       +                                savePicture();
       +                        } else if(isInside(mousePos, buttons.topLeft)) {
       +                                // go to settings
       +                                currentUI = uiSettings;
       +                        } 
       +                } else if(currentUI === uiSettings) {
       +                        if(isInside(mousePos, buttons.bottomLeft)) {
       +                                // return
       +                                if(cameraStream.paused == true) {
       +                                        // we're in capture
       +                                        currentUI = uiCapture;
       +                                } else {
       +                                        currentUI = uiMain;
       +                                }
       +                        } else if(isInside(mousePos, buttons.contrastLeft)) {
       +                                if(cameraContrast > 0) cameraContrast--;
       +                        } else if(isInside(mousePos, buttons.contrastRight)) {
       +                                if(cameraContrast < 6) cameraContrast++;
       +                        } else if(isInside(mousePos, buttons.brightnessLeft)) {
       +                                if(cameraGamma > 0) cameraGamma--;
       +                        } else if(isInside(mousePos, buttons.brightnessRight)) {
       +                                if(cameraGamma < 6) cameraGamma++;
       +                        } else if(isInside(mousePos, buttons.paletteLeft)) {
       +                                currentPalette--;
       +                                if(currentPalette < 0) currentPalette = palettes.length-1;
       +                        } else if(isInside(mousePos, buttons.paletteRight)) {
       +                                currentPalette++;
       +                                if(currentPalette >= palettes.length) currentPalette = 0;
       +                        }
       +                }
       +    
       +        }, false);
       +}
       +
       +// https://github.com/webrtc/samples/blob/gh-pages/src/content/devices/input-output/js/main.js
       +function initCameraStream() {
       +        // stop any active streams in the window
       +        if (window.stream) {
       +                window.stream.getTracks().forEach(function (track) {
       +                        //console.log(track);
       +                        track.stop();
       +                });
       +        }
       +
       +        var constraints = {
       +                audio: false,
       +                video: {
       +                        width: { ideal: 640 },
       +                        height: { ideal: 480 },
       +                        //width: { min: 1024, ideal: window.innerWidth, max: 1920 },
       +                        //height: { min: 776, ideal: window.innerHeight, max: 1080 },
       +                        facingMode: currentFacingMode,
       +                },
       +        };
       +
       +        navigator.mediaDevices
       +                .getUserMedia(constraints)
       +                .then(handleSuccess)
       +                .catch(handleError);
       +
       +        function handleSuccess(stream) {
       +                window.stream = stream; // make stream available to browser console
       +                cameraStream.srcObject = stream;
       +
       +                /*if (constraints.video.facingMode) {
       +                        if (constraints.video.facingMode === 'environment') {
       +                                buttonSwitch.setAttribute('aria-pressed', true);
       +                        } else {
       +                                buttonSwitch.setAttribute('aria-pressed', false);
       +                        }
       +                }*/
       +
       +                const track = window.stream.getVideoTracks()[0];
       +                const settings = track.getSettings();
       +                str = JSON.stringify(settings, null, 4);
       +                console.log('settings ' + str);
       +                reportedFacingMode = settings.facingMode;
       +                
       +                // canvas starts flipped for user facing camera
       +                cameraView.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
       +                if(reportedFacingMode != 'environment') cameraView.getContext('2d').scale(-1,1);
       +
       +                clearInterval(frameDrawing)
       +                frameDrawing = setInterval(drawFrame, 100);
       +        }
       +
       +        function handleError(error) {
       +                console.error('getUserMedia() error: ', error);
       +        }
       +}
       +
       +function drawFrame() {
       +        let xOffset, yOffset, xScale, yScale;
       +
       +        // calculate scale and offset to render camera stream to camera view canvas
       +        if(cameraStream.videoWidth >= cameraStream.videoHeight) {
       +                // horizontal
       +                yScale = cameraHeight;
       +                xScale = (cameraHeight / cameraStream.videoHeight) * cameraStream.videoWidth;
       +                yOffset = 0;
       +                xOffset = -((xScale - cameraWidth) / 2);
       +        } else {
       +                //vertical
       +                xScale = cameraWidth;
       +                yScale = (cameraWidth / cameraStream.videoWidth) * cameraStream.videoHeight;
       +                xOffset = 0;
       +                yOffset = -((yScale - cameraHeight) / 2);
       +        }
       +
       +        let camctx = cameraView.getContext('2d');
       +
       +        if(reportedFacingMode != 'environment') {
       +                xOffset *= -1;
       +                xScale *= -1;
       +        }
       +        camctx.drawImage(cameraStream, xOffset, yOffset, xScale, yScale);
       +        
       +        Filters.filterImage(Filters.gbcamera, cameraView, [cameraDither]);
       +        
       +        let ctx = appView.getContext("2d");
       +        ctx.drawImage(cameraView, 16, 16);
       +        ctx.drawImage(currentUI, 0, 0);
       +
       +        if(currentUI === uiSettings) {
       +                // update settings values
       +                ctx.fillStyle = "rgb(192,192,192)"
       +                for(let i = 1; i <= cameraContrast; i++) {
       +                        ctx.fillRect(42, 22 - (i*3), 4, 2);
       +                }
       +                for(let i = 1; i <= cameraGamma; i++) {
       +                        ctx.fillRect(97, 22 - (i*3), 4, 2);
       +                }
       +        }
       +
       +        Filters.filterImage(Filters.paletteSwap, appView, [palettes[currentPalette]])
       +}
       +\ No newline at end of file
   DIR diff --git a/index.html b/index.html
       @@ -0,0 +1,40 @@
       +<!doctype html>
       +<html lang=”en”>
       +<head>
       +        <meta charset="utf-8">
       +        <meta http-equiv="x-ua-compatible" content="ie=edge">
       +        <meta name="viewport" content="width=device-width, initial-scale=1">
       +        <title>gremlin girl camera</title>
       +        <link rel="stylesheet" href="style.css">
       +        <link rel="stylesheet" href="../css/jul.css">
       +</head>
       +<body>
       +
       +        <div id="camera" class="maple-window">
       +                <canvas id="app-view"></canvas>
       +                <canvas id="camera-view"></canvas>
       +                <canvas id="camera-output"></canvas>
       +                <video id="camera-stream" autoplay playsinline></video>
       +        </div>
       +
       +        <div class="maple-window centered">
       +                <p>made by <a href="https://twitter.com/maplesbian">@maplesbian</a> - inspired by christine love's interstellar selfie station</p>
       +
       +                <p>if the app above is blank, make sure you have cameras connected and browser camera permissions enabled!</p>
       +
       +                <p>this app is still very buggy on iOS and Safari, sorry for the inconvenience :(</p>
       +
       +                <p>if you like the stuff i do, check out <a href="https://maple.pet/">my website</a> or <a href="https://ko-fi.com/squirrel">donate to me on ko-fi</a>!</p>
       +
       +                <p>ps: you look great today!</p>
       +        </div>
       +        
       +        <div class="hidden">
       +                <img src="ui-main.png" id="ui-main" />
       +                <img src="ui-settings.png" id="ui-settings" />
       +                <img src="ui-capture.png" id="ui-capture" />
       +        </div>
       +
       +        <script src="app.js"></script>
       +</body>
       +</html>
       +\ No newline at end of file
   DIR diff --git a/style.css b/style.css
       @@ -0,0 +1,58 @@
       +html, body{
       +  margin: 0;
       +  padding: 0;
       +  height: 100%;
       +  width: 100%;
       +}
       +
       +body {
       +  background: url("../bg.png");
       +  text-align: center;
       +  font: 12px sans-serif;
       +}
       +
       +.centered {
       +  text-align: center !important;
       +}
       +
       +.maple-window {
       +  margin: 5px;
       +  vertical-align: top;
       +}
       +
       +/*#camera {
       +        position: fixed;
       +        left: 50%;
       +        top: 50%;
       +        transform: translate(-50%, -50%);
       +}*/
       +
       +#app-view {
       +        height: 100%;
       +        width: 100%;
       +        image-rendering: optimizeSpeed;             /* Older versions of FF          */
       +        image-rendering: -moz-crisp-edges;          /* FF 6.0+                       */
       +        image-rendering: -webkit-optimize-contrast; /* Safari                        */
       +        image-rendering: -o-crisp-edges;            /* OS X & Windows Opera (12.02+) */
       +        image-rendering: pixelated;                 /* Awesome future-browsers       */
       +        -ms-interpolation-mode: nearest-neighbor;   /* IE                            */
       +}
       +
       +#camera-stream, #camera-output, #camera-view, .hidden {
       +        display: none;
       +}
       +
       +.button {
       +        width: 200px;
       +        background-color: black;
       +        color: white;
       +        font-size: 16px;
       +        border-radius: 30px;
       +        border: none;
       +        padding: 15px 20px;
       +        text-align: center;
       +        box-shadow: 0 5px 10px 0 rgba(0,0,0,0.2);
       +        /*position: fixed;
       +        bottom: 30px;
       +        left: calc(50% - 100px);*/
       +}
       +\ No newline at end of file
   DIR diff --git a/ui-capture.png b/ui-capture.png
       Binary files differ.
   DIR diff --git a/ui-main.png b/ui-main.png
       Binary files differ.
   DIR diff --git a/ui-settings.png b/ui-settings.png
       Binary files differ.