├── .gitignore ├── README.md ├── bin └── build.js ├── package-lock.json ├── package.json ├── public ├── index.html ├── json │ └── en.json └── static │ └── jeeliz │ ├── NNC.json │ └── jeelizFaceFilter.js └── src ├── app ├── components │ ├── App.js │ ├── common │ │ ├── Cursor.js │ │ ├── Dialog.js │ │ ├── Voice.js │ │ └── Webcam.js │ ├── translate │ │ └── Translate.js │ ├── ui │ │ └── Icons.js │ └── views │ │ └── Home.js ├── helpers │ └── helpers.js ├── routes.js └── stores │ ├── AppStore.js │ ├── CursorStore.js │ └── index.js ├── css └── screen.css ├── index.js └── scss ├── modules ├── _btn.scss ├── _cursor.scss ├── _dialog.scss ├── _page.scss ├── _panel.scss └── _webcam.scss ├── screen.scss └── utils ├── _diagnostic.scss ├── _fonts.scss ├── _global.scss ├── _helpers.scss ├── _mixins.scss ├── _reset.scss ├── _type.scss └── _variables.scss /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | package-lock.json 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FaceVoice 2 | 3 | An app where you control the cursor by turning your face and click by saying the word “click”. 4 | 5 | This was originally intended to be a bad UI that was [posted to /r/badUIbattles](https://www.reddit.com/r/badUIbattles/comments/e1npf6/an_app_where_you_control_the_cursor_by_turning/) but some people in the comments pointed out that it could be a useful interface for people with certain disabilities. As such, I decided to share the code so people could rip it apart and make it into something good. 6 | 7 | Just as a note, I wrote this very quickly using a starter kit that I already had. The code isn’t great and there are definitely areas where improvements could be made, but my goal was just to get a working prototype together so I could post it and get some sweet, sweet karma. 8 | 9 | The technologies used are Create React App, [Jeeliz Face Filter](https://github.com/jeeliz/jeelizFaceFilter) for tracking the user’s face, and the [Speech Recognition API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API) for listening for the user to say the word “click”. 10 | 11 | This has only been tested in Chrome and Chrome may actually be the only browser that supports Speech Recognition right now. 12 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | // Based on Automattic’s create-react-app config override 2 | // https://github.com/Automattic/wp-api-console/commit/6838226240143595146c91d96e6f654bb14b6192#diff-78d6e474ae315d0bea76cd46368acfed 3 | 4 | // Load the git-revision package 5 | const gitRevision = require('git-revision'); 6 | 7 | // Add the git commit hash as an env variable 8 | const shortHash = gitRevision('short'); 9 | process.env.REACT_APP_REVISION = shortHash; 10 | 11 | // Run the build. 12 | require( 'react-scripts/scripts/build' ); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facevoice", 3 | "description": "Hopefully the most annoying UI ever built", 4 | "homepage": ".", 5 | "version": "0.0.1", 6 | "private": true, 7 | "devDependencies": { 8 | "babel-eslint": "9.0.0", 9 | "concurrently": "3.0.0", 10 | "eslint": "5.12.0", 11 | "git-revision": "^0.0.2", 12 | "node-sass": "^4.5.0", 13 | "node-sass-glob-importer": "^5.3.2", 14 | "autoprefixer": "^6.7.2", 15 | "postcss": "^5.2.12", 16 | "postcss-cli": "^2.6.0", 17 | "nodemon": "^1.11.0" 18 | }, 19 | "dependencies": { 20 | "axios": "^0.19.0", 21 | "date-fns": "^2.0.1", 22 | "date-fns-tz": "^1.0.7", 23 | "history": "^4.9.0", 24 | "mobx": "^5.13.0", 25 | "mobx-react": "~5.4.4", 26 | "react": "^16.9.0", 27 | "react-dom": "^16.9.0", 28 | "react-router": "^5.0.1", 29 | "react-router-dom": "^5.0.1", 30 | "react-scripts": "2.1.5" 31 | }, 32 | "scripts": { 33 | "start": "concurrently --names \"webpack, node-sass\" --prefix name \"npm run scripts\" \"npm run watch-styles\" || true", 34 | "build": "node bin/build.js", 35 | "eject": "react-scripts eject", 36 | "scripts": "react-scripts start", 37 | "styles": "node-sass --importer node_modules/node-sass-glob-importer/dist/cli.js --output-style 'compressed' ./src/scss/ -o ./src/css/ && ./node_modules/postcss-cli/bin/postcss -u autoprefixer ./src/css/*.css -d ./src/css/", 38 | "watch-styles": "nodemon -e scss -x 'npm run styles' || true", 39 | "deploy": "ns ./build --cmd 'list ./content -s'" 40 | }, 41 | "eslintConfig": { 42 | "extends": "./node_modules/react-scripts/.eslintrc" 43 | }, 44 | "browserslist": [ 45 | ">0.2%", 46 | "last 2 versions", 47 | "Firefox ESR", 48 | "not ie <= 11", 49 | "not ie_mob <= 10", 50 | "not op_mini all" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 😐 FaceVoice 🎤 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/json/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "Home": { 4 | "heading": "Hello!!!", 5 | "body": "Welcome to this amazing app. Please click the button.", 6 | "button": "Click me!!!", 7 | "clicked": { 8 | "heading": "You did it!", 9 | "body": "Great work! You did a really good job clicking that button. Please click the dismiss button to close this dialog.", 10 | "button": "Dismiss" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /public/static/jeeliz/jeelizFaceFilter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Jeeliz Face Filter - https://github.com/jeeliz/jeelizFaceFilter 3 | * 4 | * Copyright 2018 Jeeliz ( https://jeeliz.com ) 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | var JEEFACEFILTERAPI=(function(){function pa(a,b,d){return a*(1-d)+b*d}function ra(a,b){var d=new XMLHttpRequest;d.open("GET",a,!0);d.withCredentials=!1;d.onreadystatechange=function(){4===d.readyState&&200===d.status&&b(d.responseText)};d.send()}function ua(a,b,d){return Math.min(Math.max((d-a)/(b-a),0),1)} 20 | function va(a){switch(a){case "relu":return"gl_FragColor=max(vec4(0.,0.,0.,0.),gl_FragColor);";case "elu":return"gl_FragColor=mix(exp(-abs(gl_FragColor))-vec4(1.,1.,1.,1.),gl_FragColor,step(0.,gl_FragColor));";case "elu01":return"gl_FragColor=mix(0.1*exp(-abs(gl_FragColor))-vec4(0.1,0.1,0.1,0.1),gl_FragColor,step(0.,gl_FragColor));";case "arctan":return"gl_FragColor=atan(3.14159265359*texture2D(u0,vUV))/3.14159265359;";case "copy":return"";default:return!1}} 21 | function wa(a,b){var d=b%8;return a[(b-d)/8]>>7-d&1} 22 | function xa(a){var b=JSON.parse(a);a=b.ne;var d=b.nf,e=b.n,g="undefined"===typeof btoa?Buffer.from(b.data,"base64").toString("latin1"):atob(b.data),f=g.length,m;b=new Uint8Array(f);for(m=0;m=x;--r)A+=t*wa(b,r),t*=2;r=A;x=b;t=k+1+a;A=f;var C=0,D=A.length;for(k=t;ka.ba.length){var t=Uint16Array;var A=c.UNSIGNED_SHORT; 52 | var C=2}else t=Uint32Array,A=c.UNSIGNED_INT,C=4;c.bufferData(c.ELEMENT_ARRAY_BUFFER,a.ba instanceof t?a.ba:new t(a.ba),c.STATIC_DRAW);g=b}var D={ac:function(a){e!==b&&(c.bindBuffer(c.ARRAY_BUFFER,h),e=b);a&&ya.Ya()},Zb:function(){g!==b&&(c.bindBuffer(c.ELEMENT_ARRAY_BUFFER,m),g=b)},bind:function(a){D.ac(a);D.Zb()},nd:function(){c.drawElements(c.TRIANGLES,f,A,0)},od:function(a,b){c.drawElements(c.TRIANGLES,a,A,b*C)},remove:function(){c.deleteBuffer(h);a.ba&&c.deleteBuffer(m);D=null}};return D},fa:function(){-1!== 53 | e&&(c.bindBuffer(c.ARRAY_BUFFER,a),e=-1);-1!==g&&(c.bindBuffer(c.ELEMENT_ARRAY_BUFFER,b),g=-1)},g:function(a,b){a&&M.fa();b&&ya.na();c.drawElements(c.TRIANGLES,3,c.UNSIGNED_SHORT,0)},rc:function(){c.deleteBuffer(a);c.deleteBuffer(b)}};return m}(),w=function(){var a,b,d,e=!1,g={v:-2,pc:1};return{l:function(){if(!e){a=c.createFramebuffer();var f=z.o();b=f&&c.DRAW_FRAMEBUFFER?c.DRAW_FRAMEBUFFER:c.FRAMEBUFFER;d=f&&c.READ_FRAMEBUFFER?c.READ_FRAMEBUFFER:c.FRAMEBUFFER;e=!0}},yd:function(){return b},Pa:function(){return d}, 54 | $:function(){return c.FRAMEBUFFER},Ad:function(){return g},qd:function(){return a},a:function(d){void 0===d.sb&&(d.sb=!1);var e=d.oa?d.oa:!1,f=d.width,r=void 0!==d.height?d.height:d.width,k=a,p=!1,x=!1,t=0;e&&(f=f?f:e.w(),r=r?r:e.L());var A={Kb:function(){x||(k=c.createFramebuffer(),x=!0,t=g.pc++)},Sb:function(){A.Kb();A.j();p=c.createRenderbuffer();c.bindRenderbuffer(c.RENDERBUFFER,p);c.renderbufferStorage(c.RENDERBUFFER,c.DEPTH_COMPONENT16,f,r);c.framebufferRenderbuffer(b,c.DEPTH_ATTACHMENT,c.RENDERBUFFER, 55 | p);c.clearDepth(1)},bind:function(a,d){t!==g.v&&(c.bindFramebuffer(b,k),g.v=t);e&&e.j();d&&c.viewport(0,0,f,r);a&&c.clear(c.COLOR_BUFFER_BIT|c.DEPTH_BUFFER_BIT)},fd:function(){t!==g.v&&(c.bindFramebuffer(b,k),g.v=t)},clear:function(){c.clear(c.COLOR_BUFFER_BIT|c.DEPTH_BUFFER_BIT)},jd:function(){c.clear(c.COLOR_BUFFER_BIT)},kd:function(){c.clear(c.DEPTH_BUFFER_BIT)},Vc:function(){c.viewport(0,0,f,r)},j:function(){t!==g.v&&(c.bindFramebuffer(b,k),g.v=t)},rtt:function(a){e=a;g.v!==t&&(c.bindFramebuffer(c.FRAMEBUFFER, 56 | k),g.v=t);a.j()},J:function(){c.bindFramebuffer(b,null);g.v=-1},resize:function(a,b){f=a;r=b;p&&(c.bindRenderbuffer(c.RENDERBUFFER,p),c.renderbufferStorage(c.RENDERBUFFER,c.DEPTH_COMPONENT16,f,r))},remove:function(){c.bindFramebuffer(b,k);c.framebufferTexture2D(b,c.COLOR_ATTACHMENT0,c.TEXTURE_2D,null,0);p&&c.framebufferRenderbuffer(b,c.DEPTH_ATTACHMENT,c.RENDERBUFFER,null);c.bindFramebuffer(b,null);c.deleteFramebuffer(k);p&&c.deleteRenderbuffer(p);A=null}};d.sb&&A.Sb();return A},J:function(){c.bindFramebuffer(b, 57 | null);g.v=-1},ad:function(){c.bindFramebuffer(b,null);c.clear(c.COLOR_BUFFER_BIT|c.DEPTH_BUFFER_BIT);c.viewport(0,0,z.w(),z.L());g.v=-1},reset:function(){g.v=-2},S:function(){0!==g.v&&(c.bindFramebuffer(b,a),g.v=0)},clear:function(){c.viewport(0,0,z.w(),z.L());c.clear(c.COLOR_BUFFER_BIT)}}}(),V=function(){function a(a){c.bindTexture(c.TEXTURE_2D,a)}function b(a){ja[0]=a;a=sa[0];var b=a>>16&32768,d=a>>12&2047,L=a>>23&255;return 103>L?b:142L?(d|=2048,b|(d>>114- 58 | L)+(d>>113-L&1)):b=(b|L-112<<10|d>>1)+(d&1)}function d(a){var d=new Uint16Array(a.length);a.forEach(function(a,L){d[L]=b(a)});return d}function e(){if(null!==W.Qa)return W.Qa;var a=f(d([1,1,1,1]));return null===a?!0:W.Qa=a}function g(){if(null!==W.Ra)return W.Ra;var a=f(new Uint8Array([255,255,255,255]));return null===a?!0:W.Ra=a}function f(a){if(!ya.Ta()||!C)return null;a=Q.a({isFloat:!1,H:!0,array:a,width:1});w.J();c.viewport(0,0,1,1);c.clearColor(0,0,0,0);c.clear(c.COLOR_BUFFER_BIT);ya.set("s0"); 59 | a.eb(0);M.g(!1,!0);var b=new Uint8Array(4);c.readPixels(0,0,1,1,c.RGBA,c.UNSIGNED_BYTE,b);b=.9b;++b)a[b]=2*Math.random()-1;p={random:Q.a({isFloat:!0,isPot:!0,array:a,width:64}),Ob:Q.a({isFloat:!1,isPot:!0,width:1,array:new Uint8Array([0,0,0,0])})}}C=!0}},yc:function(){Q.bd()},Dd:function(){return p.Ob},bd:function(){x[1]=z.va()},Pc:function(){A=t=[c.RGBA,c.RGBA,c.RGBA,c.RGBA]},Od:function(a,b){l.set("s1"); 61 | w.J();var d=a.w(),L=a.L();c.viewport(0,0,d,L);a.b(0);M.g(!1,!1);c.readPixels(0,0,d,L,c.RGBA,c.UNSIGNED_BYTE,b)},qc:function(b,d,e){c.activeTexture(c.TEXTURE0);m=0;var L=c.createTexture();a(L);var f=z.o()&&c.RGBA32F?c.RGBA32F:c.FLOAT;d=d instanceof Float32Array?d:new Float32Array(d);var g=Math.log(d.length)/Math.log(2);g!==Math.floor(g)&&(c.texParameteri(c.TEXTURE_2D,c.TEXTURE_WRAP_S,c.CLAMP_TO_EDGE),c.texParameteri(c.TEXTURE_2D,c.TEXTURE_WRAP_T,c.CLAMP_TO_EDGE));c.texParameteri(c.TEXTURE_2D,c.TEXTURE_MAG_FILTER, 62 | c.NEAREST);c.texParameteri(c.TEXTURE_2D,c.TEXTURE_MIN_FILTER,c.NEAREST);c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,e);c.texImage2D(c.TEXTURE_2D,0,c.RGBA,b.w(),b.L(),0,c.RGBA,f,d);a(null);c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,!1);w.S();l.set("s0");b.A();c.clearColor(0,0,0,0);c.clear(c.COLOR_BUFFER_BIT);a(L);M.g(!0,!1);c.deleteTexture(L)},a:function(b){function f(){a(N);ba&&c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,ba);b.isPot?(c.texParameteri(c.TEXTURE_2D,c.TEXTURE_WRAP_S,b.wb?c.MIRRORED_REPEAT:c.REPEAT),c.texParameteri(c.TEXTURE_2D, 63 | c.TEXTURE_WRAP_T,b.U?c.MIRRORED_REPEAT:c.REPEAT)):(c.texParameteri(c.TEXTURE_2D,c.TEXTURE_WRAP_S,c.CLAMP_TO_EDGE),c.texParameteri(c.TEXTURE_2D,c.TEXTURE_WRAP_T,c.CLAMP_TO_EDGE));b.wa&&"undefined"!==typeof JESETTINGS&&c.texParameterf(c.TEXTURE_2D,JEContext.xd().TEXTURE_MAX_ANISOTROPY_EXT,JESETTINGS.dd);c.texParameteri(c.TEXTURE_2D,c.TEXTURE_MAG_FILTER,b.isLinear?c.LINEAR:c.NEAREST);b.isLinear?c.texParameteri(c.TEXTURE_2D,c.TEXTURE_MIN_FILTER,b.isMipmap&&!ka?c.NEAREST_MIPMAP_LINEAR:c.LINEAR):c.texParameteri(c.TEXTURE_2D, 64 | c.TEXTURE_MIN_FILTER,b.isMipmap&&!ka?c.NEAREST_MIPMAP_NEAREST:c.NEAREST);S=t[b.la-1];P=A[b.la-1];U=x[p];if(z.o()){var d=c.RGBA32F;S===c.RGBA&&U===c.FLOAT&&d&&(P=d);S===c.RGB&&U===c.FLOAT&&d&&(P=d,S=c.RGBA)}if(b.H&&!b.isFloat||b.isFloat&&b.isMipmap&&za.Bc())(d=c.RGBA16F)&&(P=d),U=z.va();b.zb&&"undefined"!==typeof c.texStorage2D&&(Z=b.zb);b.xb&&4===b.la&&(S=JEContext.Bd());if(b.D)c.texImage2D(c.TEXTURE_2D,0,P,S,U,b.D);else if(b.url)c.texImage2D(c.TEXTURE_2D,0,P,S,U,u);else if(H){try{c.texImage2D(c.TEXTURE_2D, 65 | 0,P,q,v,0,S,U,H),c.getError()!==c.NO_ERROR&&(c.texImage2D(c.TEXTURE_2D,0,P,q,v,0,S,U,null),c.getError()!==c.NO_ERROR&&c.texImage2D(c.TEXTURE_2D,0,c.RGBA,q,v,0,c.RGBA,c.UNSIGNED_BYTE,null))}catch(cb){c.texImage2D(c.TEXTURE_2D,0,P,q,v,0,S,U,null)}b.isKeepArray||(H=null)}else c.texImage2D(c.TEXTURE_2D,0,P,q,v,0,S,U,null);if(b.isMipmap)if(!ka&&J)J.Oa(),qa=!0;else if(ka){d=Math.log(Math.min(q,v))/Math.log(2);var e;la=Array(1+d);la[0]=N;for(e=1;e<=d;++e){var f=Math.pow(2,e);var g=q/f;f=v/f;var O=c.createTexture(); 66 | a(O);c.texParameteri(c.TEXTURE_2D,c.TEXTURE_MIN_FILTER,c.NEAREST);c.texParameteri(c.TEXTURE_2D,c.TEXTURE_MAG_FILTER,c.NEAREST);c.texImage2D(c.TEXTURE_2D,0,P,g,f,0,S,U,null);a(null);la[e]=O}qa=!0}a(null);k[m]=-1;ba&&c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,!1);n=!0;R&&J&&(R(J),R=!1)}"undefined"===typeof b.isFloat&&(b.isFloat=!1);"undefined"===typeof b.H&&(b.H=!1);"undefined"===typeof b.isPot&&(b.isPot=!0);"undefined"===typeof b.isLinear&&(b.isLinear=!1);"undefined"===typeof b.isMipmap&&(b.isMipmap=!1); 67 | "undefined"===typeof b.Ga&&(b.Ga=!1);void 0===b.wa&&(b.wa=!1);void 0===b.U&&(b.U=!1);void 0===b.wb&&(b.wb=!1);void 0===b.xb&&(b.xb=!1);void 0===b.la&&(b.la=4);void 0===b.ub&&(b.ub=!1);"undefined"===typeof b.isFlipY&&(b.isFlipY=b.url||b.array?!0:!1);"undefined"===typeof b.isKeepArray&&(b.isKeepArray=!1);b.data&&(b.array="string"===typeof b.data?xa(b.data):b.isFloat?new Float32Array(b.data):new Uint8Array(b.data),b.isFlipY=!1);var p=0,L=b.D?!0:!1,C=null,X=null,ca=!1,W=null;b.isFloat&&(b.H=!0);b.H&& 68 | (p=1);b.ub||z.o()||!b.isFloat||!na||z.gb()||(b.isFloat=!1);b.isFloat&&(p=2);b.wa&&da&&!JEContext.Hd()&&(b.wa=!1);var N=c.createTexture(),R=b.Ga,u=null,H=!1,q=0,v=0,n=!1,B=r++,ma=!1,E,Y,ja,ea,P,S,U,ba=b.isFlipY,ka=b.H&&b.isMipmap&&"undefined"!==typeof za&&!za.dc()?!0:!1,la,Z=-1,qa=!1;"undefined"!==typeof b.width&&b.width&&(q=b.width,v="undefined"!==typeof b.height&&b.height?b.height:q);var J={get:function(){return N},w:function(){return q},L:function(){return v},Ed:function(){return b.url},Id:function(){return b.isFloat}, 69 | Kd:function(){return b.H},Ld:function(){return b.isLinear},Oa:function(){c.generateMipmap(c.TEXTURE_2D)},fb:function(b,d){ka?(b||(b=J.ob()),J.Ea(d),a(la[b]),k[d]=-1):J.b(d)},ob:function(){-1===Z&&(Z=Math.log(q)/Math.log(2));return Z},mb:function(b){if(ka){b||(b=J.ob());l.set("s11");J.Ea(0);var d,e=q,f=v;for(d=1;d<=b;++d)e/=2,f/=2,l.P("u7",.25/e,.25/f),c.viewport(0,0,e,f),a(la[d-1]),c.framebufferTexture2D(w.$(),c.COLOR_ATTACHMENT0,c.TEXTURE_2D,la[d],0),M.g(!1,1===d);k[0]=-1}else J.Oa()},Ea:function(a){a!== 70 | m&&(c.activeTexture(h[a]),m=a)},b:function(b){if(!n)return!1;J.Ea(b);if(k[b]===B)return!1;a(N);k[b]=B;return!0},eb:function(b){c.activeTexture(h[b]);m=b;a(N);k[b]=B},j:function(){c.framebufferTexture2D(w.$(),c.COLOR_ATTACHMENT0,c.TEXTURE_2D,N,0)},A:function(){c.viewport(0,0,q,v);c.framebufferTexture2D(w.$(),c.COLOR_ATTACHMENT0,c.TEXTURE_2D,N,0)},ae:function(){c.framebufferTexture2D(w.$(),c.COLOR_ATTACHMENT0,c.TEXTURE_2D,null,0)},resize:function(a,b){q=a;v=b;f()},clone:function(a){a=Q.a({width:q,height:v, 71 | H:b.H,isFloat:b.isFloat,isLinear:b.isLinear,U:b.U,isFlipY:a?!ba:ba,isPot:b.isPot});ya.set("s0");w.S();a.j();c.viewport(0,0,q,v);J.b(0);M.g(!0,!0);return a},Vc:function(){c.viewport(0,0,q,v)},remove:function(){c.deleteTexture(N);J=null},refresh:function(){J.eb(0);ba&&c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,!0);L?c.texImage2D(c.TEXTURE_2D,0,P,S,c.UNSIGNED_BYTE,b.D):c.texImage2D(c.TEXTURE_2D,0,P,q,v,0,S,U,H);ba&&c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,!1)},hb:function(){var a=q*v*4;Y=[new Uint8Array(a),new Uint8Array(a), 72 | new Uint8Array(a),new Uint8Array(a)];E=[new Float32Array(Y[0].buffer),new Float32Array(Y[1].buffer),new Float32Array(Y[2].buffer),new Float32Array(Y[3].buffer)];ja=new Uint8Array(4*a);ea=new Float32Array(ja.buffer);ma=!0},Jb:function(){ma||J.hb();c.readPixels(0,0,q,4*v,c.RGBA,c.UNSIGNED_BYTE,ja);var a,b=q*v,d=2*b,e=3*b;for(a=0;aa;++a)c.viewport(0,v*a,q,v),l.Mb("u8", 73 | aa[a]),M.g(!1,0===a)},be:function(b){var d=U===x[0]&&!g();a(N);ba&&c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,ba);d?(ca||(C=document.createElement("canvas"),C.width=q,C.height=v,X=C.getContext("2d"),W=X.createImageData(q,v),ca=!0),W.data.set(b),X.putImageData(W,0,0),c.texImage2D(c.TEXTURE_2D,0,P,S,U,C)):c.texImage2D(c.TEXTURE_2D,0,P,q,v,0,S,U,b);k[m]=B;ba&&c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,!1)},ce:function(b,d){a(N);c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,d);c.texImage2D(c.TEXTURE_2D,0,P,S,U,b);k[m]=B;d&& 74 | c.pixelStorei(c.UNPACK_FLIP_Y_WEBGL,!1)},Qd:function(a,d){var e=q*v,f=4*e;a=b.H?a?"RGBE":"JSON":"RGBA";d&&(a=d);d=z.o()&&!1;switch(a){case "RGBE":var O="s39";break;case "JSON":O=d?"s0":"s12";break;case "RGBA":case "RGBAARRAY":O="s6"}ma||("RGBA"===a||"RGBE"===a||"RGBAARRAY"===a?(Y=new Uint8Array(f),ma=!0):"JSON"!==a||d||J.hb());w.J();l.set(O);J.b(0);if("RGBA"===a||"RGBE"===a||"RGBAARRAY"===a){c.viewport(0,0,q,v);M.g(!0,!0);c.readPixels(0,0,q,v,c.RGBA,c.UNSIGNED_BYTE,Y);if("RGBAARRAY"===a)return{data:Y}; 75 | D||(G=document.createElement("canvas"),K=G.getContext("2d"),D=!0);G.width=q;G.height=v;F=K.createImageData(q,v);F.data.set(Y);K.putImageData(F,0,0);var g=G.toDataURL("image/png")}else if("JSON"===a)if(d)g=new Float32Array(e),c.viewport(0,0,q,v),M.g(!0,!0),c.readPixels(0,0,q,v,c.RGBA,c.FLOAT,g);else{for(g=0;4>g;++g)c.viewport(0,v*g,q,v),l.Mb("u8",aa[g]),M.g(!g,!g);J.Jb();g=Array(e);for(O=0;Ofa&&(fa+=b);0>T&&(T+=b);fa>=b&&(fa-=b);T>=b&&(T-=b);var ia=fa/b;var ha=T/b;L=1-L-1/g;ia+=aa;ha+=aa;Q+=da;L+=da;var X=h*e+k,ca=r*e+p;ca=d*e-ca-1;X=ca*d*e+X;C[4*X]=Q;C[4*X+1]=L;C[4*X+2]=ia;C[4*X+3]=ha;ia=G[T*b+fa]++;ha=ia%f;fa=fa*f+ha;T=T*f+(ia-ha)/f;T=b*f-1-T;T=T*b*f+fa;D[4*T]=Q;D[4*T+1]=L;D[4*T+ 106 | 2]=sa;D[4*T+3]=W;++x>=g&&(x=0,++t);++A}}var ta=V.a(a.weights);V.a({width:g,isFloat:!0,array:new Float32Array(D),isPot:!0});D=null;var N=V.a({width:g,isFloat:!0,array:new Float32Array(C),isPot:!0});C=null;return{W:!0,ja:function(){return f},F:function(){l.set("s23");ta.b(1);N.b(2);M.g(!1,!1)}}}},Pa={a:function(a){var b=a.kernelsNumber,d=a.toSparsity,e=d*a.toLayerSize/a.fromLayerSize,g=V.a(a.weights);return{W:!0,ja:function(){return e},Cd:function(){return d},wc:function(){return g},F:function(){l.set("s26"); 107 | l.u("u24",b);l.u("u25",d);l.u("u18",a.toLayerSize);l.u("u26",a.fromLayerSize);g.b(1);M.g(!1,!1)}}}},Na={a:function(a,b){var d=a.fromLayerSize,e=a.toLayerSize,g=a.toSparsity,f=a.stride?a.stride:1,m=g*e/d,h=e16/9+.1||d(function(a){a.video.width.ideal=h;a.video.height.ideal=k;return a})}}d(function(a){return Wa(a)})}a.video.width&&a.video.height&&(a.video.width.ideal&&a.video.height.ideal&&d(function(a){delete a.video.width.ideal;delete a.video.height.ideal;return a}),d(function(a){delete a.video.width;delete a.video.height;return a}));a.video.facingMode&&(d(function(a){delete a.video.facingMode;return a}),a.video.width&&a.video.height&&d(function(a){Wa(a);delete a.video.facingMode; 115 | return a}));e.push({audio:a.audio,video:!0});return e}function Ya(a){try{var b=window.matchMedia("(orientation: portrait)").matches?!0:!1}catch(e){b=window.innerHeight>window.innerWidth}if(b&&a&&a.video){b=a.video.width;var d=a.video.height;b&&d&&b.ideal&&d.ideal&&b.ideal>d.ideal&&(a.video.height=b,a.video.width=d)}} 116 | function Za(a){a.volume=0;Ra(a,"muted");if(Ta()){if(1===a.volume){var b=function(){a.volume=0;window.removeEventListener("mousemove",b,!1);window.removeEventListener("touchstart",b,!1)};window.addEventListener("mousemove",b,!1);window.addEventListener("touchstart",b,!1)}setTimeout(function(){a.volume=0;Ra(a,"muted")},5)}} 117 | function $a(a,b,d,e){function g(a){f||(f=!0,d(a))}var f=!1;navigator.mediaDevices.getUserMedia(e).then(function(d){window.sid_stream=d;function e(){setTimeout(function(){if(a.currentTime){var e=a.videoWidth,h=a.videoHeight;if(0===e||0===h)g("VIDEO_NULLSIZE");else{e&&(a.style.width=e.toString()+"px");h&&(a.style.height=h.toString()+"px");e={ec:null,Wc:null,Ec:null};try{var m=d.getVideoTracks()[0];m&&(e.Ec=m,e.ec=m.getCapabilities(),e.Wc=m.getSettings())}catch(x){}Ta()||Sa()?a.parentNode&&null!==a.parentNode?(f||b(a,d, 118 | e),setTimeout(function(){a.play()},100)):(document.body.appendChild(a),Za(a),f||b(a,d,e),setTimeout(function(){a.style.transform="scale(0.0001,0.0001)";a.style.position="fixed";a.style.bottom="0px";a.style.right="0px";Za(a);setTimeout(function(){a.play()},100)},80)):f||b(a,d,e)}}else g("VIDEO_NOTSTARTED")},700)}"undefined"!==typeof a.srcObject?a.srcObject=d:(a.src=window.URL.createObjectURL(d),a.videoStream=d);Za(a);a.addEventListener("loadeddata",function(){var b=a.play();Za(a);"undefined"===typeof b? 119 | e():b.then(function(){e()}).catch(function(){g("VIDEO_PLAYPROMISEREJECTED")})},!1)}).catch(function(a){g(a)})} 120 | function ab(a,b,d){var e=Ua()?document.createElement("video"):!1;e?Ua()?(d&&d.video&&(Sa()&&Ya(d),d.video.width&&d.video.width.ideal&&(e.style.width=d.video.width.ideal+"px"),d.video.height&&d.video.height.ideal&&(e.style.height=d.video.height.ideal+"px")),Ra(e,"autoplay"),Ra(e,"playsinline"),d&&d.audio?e.volume=0:Ra(e,"muted"),$a(e,a,function(){function g(d){if(0===d.length)b("INVALID_FALLBACKCONSTRAINS");else{var f=d.shift();$a(e,a,function(){g(d)},f)}}var f=Xa(d);g(f)},d)):b&&b("MEDIASTREAMAPI_NOTFOUND"): 121 | b&&b("VIDEO_NOTPROVIDED")}function bb(a){if(!navigator.mediaDevices||!navigator.mediaDevices.enumerateDevices)return a(!1,"NOTSUPPORTED"),!1;navigator.mediaDevices.enumerateDevices().then(function(b){(b=b.filter(function(a){return a.kind&&-1!==a.kind.toLowerCase().indexOf("video")&&a.label&&a.deviceId}))&&b.length&&0a&&(n.Aa=n.element.currentTime);1E3*aI.G)y.K.splice(0,y.K.length-I.G);else for(;y.K.lengthb&&(b=Z[d].detected,a=0);for(b=0;b=y.i&&(d=0)}y.yb=d}for(a=0;au.Y[1]?(a=u.qa[1],1u.Ua}function L(a,b,d,e){return d>a?Math.max(0,a+b/2-(d-e/2)):Math.max(0,d+e/2-(a-b/2))}function fa(){return la.some(function(a,b){if(b===y.T)return!1;b=la[y.T];if(b.ya>a.ya||3>a.ya||L(b.x/2,b.M,a.x/2,a.M)u.Ab*b.M*d})}function T(){var O=y.T;ba.Sc(1);1!==y.i&&(c.viewport(0,0,3,y.i),l.set("s0"),l.Lb("u1",1),M.g(!1,!1),l.Lb("u1",0));c.viewport(0,O,1,1);l.set("s50"); 133 | B.Z&&l.u("u37",Z[O].rz);1!==y.i&&l.u("u36",y.Va);if(1m&&(a+=h,d=f,a>t&&(a=p,b+=r,b>A&&(b=x)));e=a+.8*(Math.random()-.5)*h;g=b+.8*(Math.random()-.5)*r;Aa=d+.8*(Math.random()-.5)*k}function ia(){n.oa=V.a({D:n.element,isPot:!1,isFloat:!1,isFlipY:!0})}function ha(){l.I("s48",[{type:"1i",name:"u1",value:0},{type:"mat2",name:"u33",value:n.C}])} 134 | function X(){n.B[0]=.5;n.B[1]=.5;var a=n.O[1]/n.O[0],b=Ba.L()/Ba.w();90===Math.abs(H.rotate)&&(a=1/a);a>b?n.B[1]*=b/a:n.B[0]*=a/b;l.I("s50",[{name:"u45",type:"1f",value:b}]);n.C[0]=0;n.C[1]=0;n.C[2]=0;n.C[3]=0;switch(H.rotate){case 0:n.C[0]=n.B[0];n.C[3]=n.B[1];break;case 180:n.C[0]=-n.B[0];n.C[3]=-n.B[1];break;case 90:n.C[1]=n.B[0];n.C[2]=-n.B[1];break;case -90:n.C[1]=-n.B[0],n.C[2]=n.B[1]}}function ca(a,b){if(v===q.error)return!1;var d=a.videoHeight;n.O[0]=a.videoWidth;n.O[1]=d;n.element=a;b&&b(); 135 | return!0}function ta(a,b,d){a&&a();a={video:{facingMode:{ideal:H.facingMode},width:{min:H.minWidth,max:H.maxWidth,ideal:H.idealWidth},height:{min:H.minHeight,max:H.maxHeight,ideal:H.idealHeight}},audio:!1};H.deviceId&&(a.deviceId=H.deviceId);ab(function(a){b&&b(a);d(a)},function(){N("WEBCAM_UNAVAILABLE")},a)}function N(a){v!==q.error&&(v=q.error,B.ga_&&B.ga_(a))}function R(a,b){for(var d in a)"undefined"!==typeof b[d]&&(a[d]=b[d]);b===E&&E.nDetectsPerLoop&&(I.G=E.nDetectsPerLoop,I.Cb=E.nDetectsPerLoop)} 136 | var u={save:"NNC.json",cb:0,Xb:25,Fa:.2,Y:[45,55],ed:1/3.5,qa:[2,6],Lc:{minScale:.15,maxScale:.6,borderWidth:.2,borderHeight:.2,nStepsX:6,nStepsY:5,nStepsScale:3,nDetectsPerLoop:-1},Za:[.092,.092,.3],$c:50,Ab:.12,Ua:.6,Fc:8,Rb:.75,Qb:1,Yc:{translationFactorRange:[.0015,.005],rotationFactorRange:[.003,.02],qualityFactorRange:[.9,.98],alphaRange:[.05,1]},Zc:[.65,1,.262],Ub:.2,Wb:2,Vb:.1,Gc:8,za:1,oc:[ua.bind(null,.3,.75)],cd:20},H={facingMode:"user",idealWidth:800, 137 | idealHeight:600,minWidth:480,maxWidth:1280,minHeight:480,maxHeight:1280,rotate:0},q={Cc:-1,error:-2,qb:0,play:1,pause:2},v=q.qb,n={Sa:!1,element:!1,oa:!1,pa:!1,O:[0,0],B:[.5,.5],C:[.5,0,0,.5],Aa:0},B={ga_:!1,ta:!1,ab:"./",N:!1,ra:u.cb,Hb:u.cb,xa:!1,Z:!1},ma,E=Object.create(u.Lc),Y=Object.create(u.Yc);var Aa=d=g=e=b=a=m=f=A=t=x=p=k=r=h=0;var ea,P,S,U,ba,ka,la,Z,qa=!1,J=!1,oa=u.Zc,y={i:1,T:0,K:[0],vb:!1,yb:0,Va:0},I={ia:0,timestamp:0,Db:0,Eb:0,G:u.qa[0],Cb:u.qa[0],Fb:0,ca:0,ld:1},Ca=[],Da=[];return{init:function(a){function b(){v!== 138 | q.error&&2===++e&&(X(),ia(),ha(),B.ga_&&(B.ga_(!1,{GL:c,canvasElement:B.N,videoTexture:n.pa.get(),maxFacesDetected:y.i}),K()),D())}if(v!==q.qb)return a.callbackReady&&a.callbackReady("ALREADY_INITIALIZED"),!1;v=q.Cc;a.callbackReady&&(B.ga_=a.callbackReady);a.callbackTrack&&(B.ta=a.callbackTrack);"undefined"!==typeof a.animateDelay&&(B.ra=a.animateDelay);"undefined"!==typeof a.NNCpath&&(B.ab=a.NNCpath);"undefined"!==typeof a.maxFacesDetected&&(y.i=Math.max(1,a.maxFacesDetected));"undefined"!==typeof a.followZRot&& 139 | (B.Z=a.followZRot?!0:!1);if(y.i>u.Fc)return N("MAXFACES_TOOHIGH"),!1;if(!a.canvasId&&!a.canvas)return N("NO_CANVASID"),!1;B.N=a.canvas?a.canvas:document.getElementById(a.canvasId);if(!B.N)return N("INVALID_CANVASID"),!1;ea=B.N.width;P=B.N.height;if(!ea||!P)return N("INVALID_CANVASDIMENSIONS"),!1;for(var d=0;du42&&b>i+u43;j?a.r=2.:a.r>u41?a.r=0.:a.r>1.9?a.r+=1.:0.,a.r*=u44;if(a.r<.9)a=vec4(1.,u39);else{a.r*=step(1.9,a.r);float k=dot(e,texture2D(u38,vec2(.875,.875))),l=dot(e,texture2D(u38,vec2(.125,.625))),m=dot(e,texture2D(u38,vec2(.375,.625))),c=cos(u37),d=sin(u37);vec2 f=mat2(c,d*u45,-d/u45,c)*vec2(k,l);a.gba+=vec3(f,m)*u40*a.a;}gl_FragColor=a;}", 142 | X:"attribute vec2 a0;void main(){gl_Position=vec4(a0,0.,1.);}",f:"u38 u34 u39 u41 u40 u44 u37 u45 u42 u43 u36".split(" ")},{id:"s51",name:"_",X:"attribute vec2 a0;void main(){gl_Position=vec4(a0,0.,1.);}",c:"uniform sampler2D u38;const vec4 e=vec4(.25,.25,.25,.25);const vec3 g=vec3(.5,.5,.5);void main(){float a=dot(e,texture2D(u38,vec2(.125,.875))),b=dot(e,texture2D(u38,vec2(.375,.875))),c=dot(e,texture2D(u38,vec2(.625,.875))),d=dot(e,texture2D(u38,vec2(.625,.625)));vec3 f=vec3(a,b,c)*.5+g;gl_FragColor=vec4(f,d);}", 143 | f:["u38"]},{id:"s52",name:"_",X:"attribute vec2 a0;void main(){gl_Position=vec4(a0,0.,1.);}",c:"uniform sampler2D u38;const vec4 e=vec4(.25,.25,.25,.25);void main(){float a=dot(e,texture2D(u38,vec2(.25,.25)));gl_FragColor=vec4(a,0.,0.,0.);}",f:["u38"]},{id:"s47",name:"_",c:"uniform sampler2D u34;uniform vec2 u46;uniform float u47;varying vec2 vv0;void main(){float g=step(.5,mod(gl_FragCoord.y+1.5,2.)),c=step(.33,vv0.x);vec4 a=texture2D(u34,vv0+u46);a.a=mix(a.a*u47,a.a,c);vec4 d=floor(255.*a),f=255.*(255.*a-d),b=mix(d,f,g)/255.;b.x=mix(step(a.x,1.5),b.x,c),gl_FragColor=b;}", 144 | f:["u34","u47","u46"]}]);aa();na();da();b()});return!0},toggle_pause:function(a){if(-1!==[q.play,q.pause].indexOf(v))return a?v!==q.play?a=!1:(qa&&(clearTimeout(qa),qa=!1),J&&(window.cancelAnimationFrame(J),J=!1),v=q.pause,a=!0):a=D(),a},toggle_slow:function(a){-1!==[q.play,q.pause].indexOf(v)&&v===q.play&&(a&&!B.xa?(B.Hb=B.ra,E.nDetectsPerLoop=1,this.Oc(100),B.xa=!0):!a&&B.xa&&(E.nDetectsPerLoop=-1,this.Oc(B.Hb),B.xa=!1))},set_animateDelay:function(a){B.ra=a},resize:function(){var a=B.N.width,b= 145 | B.N.height;if(a===ea&&b===P)return!1;ea=a;P=b;na();da();X();ha();return!0},set_inputTexture:function(a,b,d){n.O[0]=b;n.O[1]=d;n.Sa=!0;X();K();ha();l.set("s48");n.pa.A();c.activeTexture(c.TEXTURE0);c.bindTexture(c.TEXTURE_2D,a);M.g(!0,!0)},reset_inputTexture:function(){n.O[0]=n.element.videoWidth;n.O[1]=n.element.videoHeight;n.Sa=!1;X();ha()},get_videoDevices:function(a){return bb(a)},set_scanSettings:function(a){R(E,a);na();da()},set_stabilizationSettings:function(a){R(Y,a)},update_videoElement:function(a, 146 | b){ca(a,function(){ia();X();b&&b()})}}}(); 147 | ;return JEEFACEFILTERAPI;})(); 148 | -------------------------------------------------------------------------------- /src/app/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 4 | 5 | // Routes 6 | import { routes } from '../routes'; 7 | 8 | // Import common components 9 | import Cursor from './common/Cursor'; 10 | import Dialog from './common/Dialog'; 11 | import Voice from './common/Voice'; 12 | import Webcam from './common/Webcam'; 13 | 14 | // Import views 15 | import Home from './views/Home'; 16 | 17 | class App extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | 21 | // Set the initial state 22 | this.state = { 23 | render: true 24 | }; 25 | 26 | // Load translation 27 | const { AppStore } = props.store; 28 | 29 | if(!AppStore.translation) { 30 | AppStore.getTranslation(); 31 | } 32 | } 33 | 34 | render() { 35 | const { render } = this.state; 36 | const { AppStore } = this.props.store; 37 | 38 | if(render) { 39 | return ( 40 | 41 |
42 | ( 43 | 44 | ( 45 | 49 | )}/> 50 | 51 | )} /> 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {AppStore.dialog && 60 | 61 | } 62 |
63 |
64 | ) 65 | } 66 | 67 | return null; 68 | } 69 | } 70 | 71 | export default observer(App); 72 | -------------------------------------------------------------------------------- /src/app/components/common/Cursor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | // Icons 5 | import * as icons from '../ui/Icons'; 6 | 7 | const Cursor = observer(class Cursor extends React.Component { 8 | constructor() { 9 | super(); 10 | 11 | // Set the initial state 12 | this.state = { 13 | x: window.innerWidth / 2, 14 | y: window.innerHeight / 2 15 | }; 16 | } 17 | 18 | componentDidMount() { 19 | const speed = 10; 20 | const { direction, position } = this.props.store.CursorStore; 21 | 22 | // document.addEventListener('mousemove', (e) => this.handleMouse(e)); 23 | 24 | this.moveInterval = setInterval(() => { 25 | if(direction.x) { 26 | let newX = this.state.x += direction.x * speed; 27 | 28 | if(newX < 0) { 29 | newX = 0; 30 | } else if(newX > window.innerWidth - 20) { 31 | newX = window.innerWidth - 20; 32 | } 33 | 34 | this.setState({ 35 | x: newX 36 | }); 37 | } 38 | 39 | if(direction.y) { 40 | let newY = this.state.y += direction.y * speed; 41 | 42 | if(newY < 0) { 43 | newY = 0; 44 | } else if(newY > window.innerHeight - 32) { 45 | newY = window.innerHeight - 32; 46 | } 47 | 48 | this.setState({ 49 | y: newY 50 | }); 51 | } 52 | 53 | position.x = this.state.x; 54 | position.y = this.state.y; 55 | }, 100); 56 | } 57 | 58 | handleMouse(e) { 59 | this.setState({ 60 | x: e.pageX, 61 | y: e.pageY 62 | }) 63 | } 64 | 65 | render() { 66 | const { direction } = this.props.store.CursorStore; 67 | const { x, y } = this.state; 68 | 69 | return ( 70 | 74 | 75 | {direction.y} 76 | 77 | ) 78 | } 79 | }); 80 | 81 | export default Cursor; -------------------------------------------------------------------------------- /src/app/components/common/Dialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Renders a pop-up dialog box 3 | */ 4 | 5 | import React from 'react'; 6 | import { observer } from 'mobx-react'; 7 | 8 | const Dialog = observer(class Dialog extends React.Component { 9 | closeDialog(e) { 10 | e.preventDefault(); 11 | this.props.store.AppStore.closeDialog() 12 | } 13 | 14 | render() { 15 | const { dialogContent } = this.props.store.AppStore; 16 | 17 | return ( 18 | 37 | ) 38 | } 39 | }) 40 | 41 | export default observer(Dialog); 42 | -------------------------------------------------------------------------------- /src/app/components/common/Voice.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Voice = class Voice extends React.Component { 4 | constructor() { 5 | super(); 6 | 7 | // Set the initial state 8 | this.state = { 9 | isSupported: false 10 | }; 11 | } 12 | 13 | componentDidMount() { 14 | window.SpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition; 15 | 16 | if(window.SpeechRecognition) { 17 | this.setState({ 18 | isSupported: true 19 | }); 20 | 21 | this.initVoice(); 22 | } 23 | } 24 | 25 | initVoice() { 26 | this.voice = new window.SpeechRecognition(); 27 | this.voice.lang = 'en-US'; 28 | this.voice.continuous = true; 29 | 30 | this.voice.onresult = (e) => { 31 | let transcript = e.results[e.results.length - 1][0].transcript; 32 | 33 | // For some reason it looks likce the Speech Recognition 34 | // API adds a space to the beginning of results after 35 | // the first one, so let’s remove that 36 | if(transcript[0] === ' ') { 37 | transcript = transcript.replace(' ', ''); 38 | } 39 | 40 | if(transcript.toLowerCase() === 'click') { 41 | console.log('Clicking…'); 42 | 43 | this.handleClick(); 44 | } else { 45 | console.log(`User may have said ${transcript}`); 46 | } 47 | } 48 | 49 | // Not sure why, but this.voice kept stopping 50 | // after a random amount of time, so rather 51 | // than trying to figure it out, let’s just 52 | // stop it on our own and restart it every 53 | // 10 seconds or something 54 | setInterval(() => { 55 | this.voice.stop(); 56 | 57 | setTimeout(() => { 58 | try { 59 | this.voice.start(); 60 | } catch(e) { 61 | console.error(e); 62 | } 63 | }, 100); 64 | }, 10000); 65 | 66 | this.voice.start(); 67 | } 68 | 69 | handleClick() { 70 | const { position } = this.props.store.CursorStore; 71 | const element = document.elementFromPoint(position.x, position.y); 72 | 73 | console.log(element, element.click); 74 | 75 | if(element && element.click) { 76 | element.click(); 77 | } 78 | } 79 | 80 | render() { 81 | return null; 82 | } 83 | } 84 | 85 | export default Voice; -------------------------------------------------------------------------------- /src/app/components/common/Webcam.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Helpers 4 | import { testGetUserMedia } from '../../helpers/helpers'; 5 | 6 | const Webcam = class Webcam extends React.Component { 7 | constructor() { 8 | super(); 9 | 10 | // Set the initial state 11 | this.state = { 12 | isSupported: false 13 | }; 14 | 15 | // Create refs 16 | this.preview = React.createRef(); 17 | } 18 | 19 | componentDidMount() { 20 | const isSupported = testGetUserMedia(); 21 | 22 | if(isSupported) { 23 | this.setState({ 24 | isSupported 25 | }); 26 | 27 | this.initWebcam(); 28 | this.startFaceTrack(); 29 | } 30 | } 31 | 32 | componentWillUnmount() { 33 | this.killWebcam(); 34 | } 35 | 36 | initWebcam() { 37 | const constraints = { 38 | audio: false, 39 | video: { 40 | facingMode: 'user', 41 | height: 180, 42 | width: 320 43 | } 44 | } 45 | 46 | navigator.mediaDevices.getUserMedia(constraints) 47 | .then(media => { 48 | this.stream = media; 49 | 50 | // Stream the media to the video element 51 | const videoEl = this.preview.current; 52 | videoEl.srcObject = media; 53 | 54 | videoEl.onloadedmetadata = (e) => { 55 | videoEl.play(); 56 | } 57 | }); 58 | } 59 | 60 | startFaceTrack() { 61 | const faceTrack = window.JEEFACEFILTERAPI; 62 | 63 | faceTrack.init({ 64 | canvasId: '_webcamData', 65 | NNCpath: '/static/jeeliz/NNC.json', 66 | callbackReady: (error) => { 67 | if(error) { 68 | console.error(error); 69 | return; 70 | } 71 | }, 72 | callbackTrack: (detectState) => { 73 | if(detectState.detected >= .8) { 74 | // Pass the x and y rotation values to a function 75 | // to handle the updating of the cursor 76 | this.handleMovement(detectState.rx, detectState.ry); 77 | } 78 | } 79 | }); 80 | } 81 | 82 | handleMovement(rx, ry) { 83 | const { CursorStore } = this.props.store; 84 | 85 | if(ry >= .15) { 86 | // Left 87 | CursorStore.direction.x = -1; 88 | } else if(ry <= -.15) { 89 | // Right 90 | CursorStore.direction.x = 1; 91 | } else { 92 | CursorStore.direction.x = null; 93 | } 94 | 95 | if(rx >= .25) { 96 | // Down 97 | CursorStore.direction.y = 1; 98 | } else if(rx <= -.15) { 99 | // Up 100 | CursorStore.direction.y = -1; 101 | } else { 102 | CursorStore.direction.y = null; 103 | } 104 | } 105 | 106 | render() { 107 | return ( 108 |
109 |
120 | ) 121 | } 122 | } 123 | 124 | export default Webcam; -------------------------------------------------------------------------------- /src/app/components/translate/Translate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the translation for a given component 3 | */ 4 | 5 | import React from 'react'; 6 | import { observer } from 'mobx-react'; 7 | 8 | export default function translate(key) { 9 | return Component => { 10 | class TranslationComponent extends React.Component { 11 | componentDidMount() { 12 | // Get the current component’s translation 13 | const { AppStore } = this.props.store; 14 | 15 | if(!AppStore.translationLoaded) { 16 | AppStore.getTranslation(); 17 | } 18 | } 19 | 20 | render() { 21 | const { AppStore } = this.props.store; 22 | 23 | if(AppStore.translation) { 24 | // Return the translation for the component 25 | let translation = AppStore.translation[key]; 26 | 27 | return ( 28 | 32 | ) 33 | } else { 34 | return ( 35 | Translation not found 36 | ); 37 | } 38 | } 39 | } 40 | 41 | return observer(TranslationComponent); 42 | } 43 | } -------------------------------------------------------------------------------- /src/app/components/ui/Icons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function cursor() { 4 | return ( 5 | 6 | ) 7 | } -------------------------------------------------------------------------------- /src/app/components/views/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | // Translation 5 | import translate from '../translate/Translate'; 6 | 7 | const Home = observer(class Home extends React.Component { 8 | render() { 9 | const { store, translation } = this.props; 10 | const { AppStore } = store; 11 | 12 | return ( 13 |
14 |
15 |

{translation.heading}

16 | 17 |

{translation.body}

18 | 19 | 26 |
27 |
28 | ) 29 | } 30 | }) 31 | 32 | export default translate('Home')(Home); 33 | -------------------------------------------------------------------------------- /src/app/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if getUserMedia is supported 3 | * @param {object} callback 4 | */ 5 | export function testGetUserMedia(constraints = { audio: true, video: true }) { 6 | let supported = true; 7 | 8 | if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 9 | async function testMedia() { 10 | try { 11 | const stream = await navigator.mediaDevices.getUserMedia(constraints); 12 | 13 | if(stream.getTracks().length === 0) { 14 | console.error('Got media stream but no tracks exist!'); 15 | supported = false; 16 | } 17 | 18 | stream.getTracks().forEach(track => { 19 | track.stop(); 20 | }) 21 | } catch(error) { 22 | console.error(error); 23 | supported = false; 24 | } 25 | } 26 | 27 | testMedia(); 28 | } else { 29 | supported = false; 30 | } 31 | 32 | return supported; 33 | } -------------------------------------------------------------------------------- /src/app/routes.js: -------------------------------------------------------------------------------- 1 | export let routes = { 2 | index: '/' 3 | }; -------------------------------------------------------------------------------- /src/app/stores/AppStore.js: -------------------------------------------------------------------------------- 1 | import { action, observable } from 'mobx'; 2 | // import { api } from '../config'; 3 | 4 | // Axios (used for loading static translation file) 5 | import axios from 'axios'; 6 | 7 | let obx = observable({ 8 | /*------------------------------------------- 9 | Loading 10 | -------------------------------------------*/ 11 | loadingCalls: [], 12 | loading: false, 13 | loadingClass: '', 14 | 15 | /** 16 | * startLoading - Sets loading state to true and adds the provided request to the array of current requests 17 | * @param {string} request - The name of the request being loaded 18 | */ 19 | startLoading: action(function(request) { 20 | obx.loading = true; 21 | obx.loadingCalls.push(request) 22 | }), 23 | 24 | /** 25 | * finishLoading - Removes the provided request from the array of requests and sets the loading state to false if the request array is empty 26 | * @param {string} request - The name of the request being loaded 27 | */ 28 | finishLoading: action(function(request) { 29 | const requestIndex = obx.loadingCalls.indexOf(request); 30 | 31 | if(requestIndex >= 0) { 32 | obx.loadingCalls.splice(requestIndex, 1); 33 | } else { 34 | obx.loadingCalls.length = 0; 35 | } 36 | 37 | if(obx.loadingCalls.length === 0 || typeof request === 'undefined') { 38 | obx.loadingClass = 'leave'; 39 | obx.loading = false; 40 | 41 | // Wait for loader to fade out before removing 42 | setTimeout(function(){ 43 | obx.loadingClass = ''; 44 | obx.loadingMessage = null; 45 | }, 250); 46 | } 47 | }), 48 | 49 | /*------------------------------------------- 50 | Error handling 51 | -------------------------------------------*/ 52 | /** 53 | * throwError - Determines whether to show a specific or general error message 54 | * @param {object} error 55 | */ 56 | throwError: action(function(error){ 57 | obx.finishLoading(); 58 | 59 | console.error(error); 60 | 61 | if(typeof error.response !== 'undefined') { 62 | // API errors will return an error object with a response 63 | // which means we have a message for them 64 | obx.showErrorMsg(error.response.data); 65 | } else { 66 | // If there’s no response with the error, it’s a server 67 | // connection error (i.e. axios can’t reach the URL) 68 | obx.showErrorMsg(); 69 | } 70 | }), 71 | 72 | /** 73 | * showErrorMsg - Shows a pop-up dialog with the appropriate error message for a provided error 74 | * @param {object} data - The error data 75 | * @param {string} data.code - The error code 76 | */ 77 | showErrorMsg: action(function(data){ 78 | obx.dialog = true; 79 | obx.dialogContent.heading = obx.translation.Errors.heading; 80 | obx.dialogContent.button = obx.translation.Errors.button; 81 | 82 | if(!data) { 83 | // Server error (API call returned no data) 84 | obx.dialogContent.body = obx.translation.Errors[1301].message; 85 | } else { 86 | // API call returned data 87 | if(typeof obx.translation.Errors[data.code] !== 'undefined') { 88 | // Error translation exists 89 | obx.dialogContent.body = obx.translation.Errors[data.code].message; 90 | } else { 91 | // Error translation does not exist 92 | console.warn('Error code not present in translation file. Falling back to API response message'); 93 | obx.dialogContent.body = data.message; 94 | } 95 | } 96 | 97 | // Redirect expired session 98 | if(data && data.code === '5002') { 99 | window.location.href = '/#/session-expired'; 100 | } 101 | }), 102 | 103 | /*------------------------------------------- 104 | Translation 105 | -------------------------------------------*/ 106 | locale: 'en', 107 | translation: null, 108 | translationLoaded: false, 109 | 110 | /** 111 | * getTranslation - Loads the appropriate translation file 112 | * @param {string} locale 113 | */ 114 | getTranslation: action(function(locale){ 115 | obx.translationLoaded = true; 116 | 117 | const timestamp = (new Date()).getTime(); 118 | 119 | obx.startLoading('getTranslation'); 120 | 121 | axios.get(`/json/${obx.locale}.json?t=${timestamp}`) 122 | .then(response => { 123 | obx.translation = response.data.content; 124 | obx.finishLoading('getTranslation'); 125 | }) 126 | .catch(error => { 127 | obx.throwError(error); 128 | }); 129 | }), 130 | 131 | /*------------------------------------------- 132 | Dialog box 133 | -------------------------------------------*/ 134 | dialog: false, 135 | dialogContent: {}, 136 | dialogCallback: null, 137 | 138 | /** 139 | * showDialog - Shows a pop-up dialog box with the provided content 140 | * @param {object} content 141 | */ 142 | showDialog: action(function(content){ 143 | if(content) { 144 | obx.dialogContent = content; 145 | } 146 | 147 | obx.dialog = true; 148 | 149 | // Focus the dialog 150 | const dialogTitle = document.querySelector('.dialog__title'); 151 | 152 | if(dialogTitle) { 153 | dialogTitle.focus(); 154 | } 155 | }), 156 | 157 | /** 158 | * closeDialog - Closes the pop-up dialog box 159 | * @param {object} content 160 | */ 161 | closeDialog: action(function(){ 162 | obx.dialog = false; 163 | obx.dialogContent = {}; 164 | 165 | // Focus the main content element 166 | const main = document.querySelector('#main-content'); 167 | 168 | if(main) { 169 | main.focus(); 170 | } 171 | 172 | // Dialog callback 173 | if(obx.dialogCallback) { 174 | obx.dialogCallback(); 175 | obx.dialogCallback = null; 176 | } 177 | }) 178 | }); 179 | 180 | export default obx; 181 | -------------------------------------------------------------------------------- /src/app/stores/CursorStore.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | let obx = observable({ 4 | direction: { 5 | x: null, 6 | y: null 7 | }, 8 | position: { 9 | x: 0, 10 | y: 0 11 | } 12 | }); 13 | 14 | export default obx; -------------------------------------------------------------------------------- /src/app/stores/index.js: -------------------------------------------------------------------------------- 1 | // Import stores 2 | import AppStore from './AppStore'; 3 | import CursorStore from './CursorStore'; 4 | 5 | // Combine stores 6 | const store = { 7 | AppStore, 8 | CursorStore 9 | }; 10 | 11 | export default store; 12 | -------------------------------------------------------------------------------- /src/css/screen.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Roboto:300,300italic,400,400italic,500,700,700italic");html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-family:inherit;font-size:100%;font-style:inherit;font-weight:inherit;margin:0;outline:0;padding:0;vertical-align:baseline}body{background:#fff;color:#333;line-height:1}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}*,*:before,*:after{box-sizing:border-box}h1,h2,h3,h4,h5,h6{font-weight:normal}a,a:hover{color:inherit;text-decoration:none}a:focus,:focus{outline:none}ol{list-style:decimal;margin:0 0 0 2em}ol ol{list-style:upper-alpha}ol ol ol{list-style:upper-roman}ol ol ol ol{list-style:lower-alpha}ol ol ol ol ol{list-style:lower-roman}ul{list-style:disc;margin:0 0 0 2em}ul ul{list-style:circle}ul ul ul{list-style:square}input,textarea,button{font-family:inherit;font-size:inherit}textarea{resize:none}input[type="checkbox"]{vertical-align:bottom;*vertical-align:baseline}button{cursor:pointer}*[disabled]{cursor:not-allowed}input[type="radio"]{vertical-align:text-bottom}input{_vertical-align:text-bottom}textarea{display:block}table{border-collapse:separate;border-spacing:0}caption,th,td{font-weight:normal;text-align:left}blockquote:before,blockquote:after,q:before,q:after{content:""}blockquote,q{quotes:"" ""}@-webkit-keyframes 'loader-spin'{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes 'loader-spin'{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.btn,.btn--outline,.btn--link,.btn--reset{background:none;border:none;display:inline-block;margin:0;padding:0}.dialog,a,.btn--link,a .icon,.btn--link .icon,.btn,.btn--outline,.btn:before,.btn--outline:before,.btn--ghost{-webkit-transition:all .25s ease;transition:all .25s ease}.meta{border:0 !important;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px);height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dialog{bottom:0;left:0;padding:15px;position:fixed;right:0;text-align:center;top:0;z-index:3000}.dialog:before{content:'';display:inline-block;height:100%;margin-right:-10px;vertical-align:middle;width:10px}.dialog__content{background:#fff;border-radius:3px;box-shadow:0 4px 15px 0 rgba(0,0,0,0.1);display:inline-block;vertical-align:middle}html{font-size:16px}body{background:#fff;color:#76838f;cursor:none;font-family:"Roboto",sans-serif}.content{position:relative}.wrap{margin:0 auto;max-width:78.75rem;padding:0 15px}@media only screen and (min-width: 48em){.wrap{padding:0 30px}}a,.btn--link{color:#0074cc;text-decoration:underline}a:hover,.btn--link:hover{color:#003a66;text-decoration:underline}a:hover .icon,.btn--link:hover .icon{fill:#003a66}a:focus:not([class*='btn']):not(.nav__item):not(.tab),.btn--link:focus:not([class*='btn']):not(.nav__item):not(.tab){color:#003a66;text-decoration:underline}h1{font-size:1.5rem}h2,.panel__title{font-size:1.25rem}h3{font-size:1.1875rem}h4{font-size:1.125rem}h5,.dialog__title{font-size:1rem}h6{font-size:1rem}body,p,ol,ul,dl,.preposition,.btn,.btn--outline,.dialog__content p{font-size:.875rem}@media only screen and (min-width: 48em){h1{font-size:2rem}h2,.panel__title{font-size:1.75rem}h3{font-size:1.5rem}h4{font-size:1.25rem}h5,.dialog__title{font-size:1.125rem}h6{font-size:1rem}body,p,ol,ul,dl,.preposition,.btn,.btn--outline,.dialog__content p{font-size:.875rem}}h1{line-height:1.4}h2{line-height:1.6785}h3{line-height:1.666666667}h4{line-height:1.7}h5{line-height:1.666666667}h6{line-height:1.6875}p,ol,ul,dl{line-height:1.6875;margin-bottom:1.6875em}em{font-style:italic}strong{font-weight:700}abbr{text-decoration:none}code{background:#e4eaec;font-family:monospace}.preposition{display:inline-block;margin:0 .625rem}.preposition--vertical{margin:.75rem 0 .625rem}.no-results{display:block;line-height:1.2;margin:1em 0;text-align:center}.no-results:last-child{margin-bottom:0}.required:after{content:' *';color:#0074cc}.btn,.btn--outline{background:#0074cc;border:none;border-radius:3px;color:#fff;cursor:pointer;display:inline-block;flex-shrink:0;line-height:1.5;margin:0;overflow:hidden;padding:8px 15px 6px;position:relative;text-align:center;text-decoration:none;vertical-align:middle;white-space:nowrap;z-index:1}.btn:before,.btn--outline:before{background:#0091ff;content:'';height:100%;left:0;opacity:0;position:absolute;top:0;-webkit-transform:scaleX(0);transform:scaleX(0);width:100%;z-index:-1}.btn .icon,.btn--outline .icon{height:14px;margin-right:6px;top:1px;width:14px}.btn:hover,.btn--outline:hover{color:#fff;text-decoration:none}.btn:hover:before,.btn--outline:hover:before{opacity:1;-webkit-transform:none;transform:none}.btn:hover[type="button"][value],.btn--outline:hover[type="button"][value]{background:#0091ff}.btn:focus:not(:active):not(.btn--clicked),.btn--outline:focus:not(:active):not(.btn--clicked){outline:2px dotted #000}.btn:disabled,.btn--outline:disabled{background:#e4eaec;color:#526069}.btn:disabled:before,.btn:disabled:after,.btn--outline:disabled:before,.btn--outline:disabled:after{display:none}.btn--outline{background:#fff;border:1px solid #0074cc;color:#0074cc;padding:7px 14px 5px}.btn--outline:hover{border-color:#33a7ff;color:#fff}.btn--outline:disabled{background:#fff;border-color:#c6d3d7;color:#76838f}.btn--outline:disabled:hover{color:#76838f}.btn--negative{background:#e9595b;color:#fff}.btn--negative:before{background:#ef8687}.btn--negative:hover[type="button"][value]{background:#ef8687}.btn--negative.btn--outline{background:#fff;border-color:#e9595b;color:#e9595b}.btn--negative.btn--outline:before{background:#e9595b}.btn--negative.btn--outline:hover{color:#fff}.btn--positive{background:#46be8a;color:#fff}.btn--positive:before{background:#6ccba2}.btn--positive:hover[type="button"][value]{background:#6ccba2}.btn--positive.btn--outline{background:#fff;border-color:#46be8a;color:#46be8a}.btn--positive.btn--outline:before{background:#46be8a}.btn--positive.btn--outline:hover{color:#fff}.btn--full{width:100%}.btn--ghost{background:none;color:#76838f;height:1.875rem;padding:0 10px;min-width:1.875rem}.btn--ghost:before{display:none}.btn--ghost .icon{height:.75rem;margin-right:0;top:1px;width:.75rem}.btn--ghost .icon:only-child{margin-right:6px}.btn--ghost:hover{background:rgba(0,0,0,0.04);color:#0074cc}.btn--ghost:hover.btn--negative{color:#e9595b}.cursor{color:#000;-webkit-filter:drop-shadow(0 0 2px #fff);filter:drop-shadow(0 0 2px #fff);height:32px;left:0;pointer-events:none;position:fixed;top:0;width:20px;z-index:3001}.cursor svg{height:100%;width:100%}.dialog__content{max-height:100%;max-width:100%;overflow:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:none;padding:20px;text-align:left;width:22.5rem}.dialog__content::-webkit-scrollbar{width:0}.dialog__content label{display:block;margin-bottom:5px}.dialog__content p{line-height:1.5;margin-bottom:1em}.dialog__title{background:#0074cc;color:#fff;display:block;font-weight:400;margin:-20px -20px 1.25rem;padding:19px 21px 17px}.dialog__action{margin-top:1.5625rem;text-align:right}.dialog__action *+*{margin-left:.625rem}.page{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background:rgba(255,255,255,0.875);min-height:100vh;padding:15px;width:100vw}@media only screen and (min-width: 48em){.page{padding:30px}}.panel{background:#fff;border:1px solid #e4eaec;border-radius:3px;box-shadow:0 1px 1px 0 rgba(0,0,0,0.05);padding:15px;position:relative}@media only screen and (min-width: 48em){.panel{padding:30px}}.panel p:last-child,.panel ol:last-child,.panel ul:last-child{margin-bottom:0}.panel--md{max-width:41.375rem;width:100%}.panel--sm{max-width:30.125rem;width:100%}.panel--solo{margin:15px auto;text-align:center}@media only screen and (min-width: 48em){.panel--solo{margin:30px auto}}.panel--form{text-align:left}.panel__title{margin-bottom:.75rem;text-align:center}.panel__title--alt{text-align:left}.panel__action{background:#f3f7f9;border-top:1px solid #e4eaec;border-radius:0 0 3px 3px;display:-webkit-box;display:flex;margin:15px -15px -15px;padding:15px}.panel__action>.btn{margin-left:.625rem}.panel__action>.btn:first-child{margin-left:auto}@media only screen and (min-width: 48em){.panel__action{margin:30px -30px -30px}}.webcam{background:#000;height:100vh;left:0;position:fixed;top:0;-webkit-transform:scaleX(-1);transform:scaleX(-1);width:100vw;z-index:-1} 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Import React 2 | import React from 'react'; 3 | 4 | // Import dependencies 5 | import { render } from 'react-dom'; 6 | 7 | // Import styles 8 | import './css/screen.css'; 9 | 10 | // Import components 11 | import App from './app/components/App'; 12 | 13 | // Import stores 14 | import store from './app/stores/'; 15 | 16 | // Main app component 17 | const app = ( 18 | 19 | ); 20 | 21 | // Root DOM element 22 | const rootEl = document.querySelector('#app'); 23 | 24 | render(app, rootEl); 25 | -------------------------------------------------------------------------------- /src/scss/modules/_btn.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Button styles 3 | -------------------------------------------*/ 4 | // Default button 5 | .btn, %btn { 6 | @extend %btn-reset; 7 | @extend %transition; 8 | background: $accent-1; 9 | border: none; 10 | border-radius: $border-radius-all; 11 | color: $white; 12 | cursor: pointer; 13 | display: inline-block; 14 | flex-shrink: 0; 15 | @extend %sm; 16 | line-height: 1.5; 17 | margin: 0; 18 | overflow: hidden; 19 | padding: 8px 15px 6px; 20 | position: relative; 21 | text-align: center; 22 | text-decoration: none; 23 | vertical-align: middle; 24 | white-space: nowrap; 25 | z-index: 1; 26 | 27 | &:before { 28 | @extend %transition; 29 | background: lighten($accent-1, 10); 30 | content: ''; 31 | height: 100%; 32 | left: 0; 33 | opacity: 0; 34 | position: absolute; 35 | top: 0; 36 | transform: scaleX(0); 37 | width: 100%; 38 | z-index: -1; 39 | } 40 | 41 | .icon { 42 | height: 14px; 43 | margin-right: 6px; 44 | top: 1px; 45 | width: 14px; 46 | } 47 | 48 | &:hover { 49 | color: $white; 50 | text-decoration: none; 51 | 52 | &:before { 53 | opacity: 1; 54 | transform: none; 55 | } 56 | 57 | &[type="button"][value] { 58 | background: lighten($accent-1, 10); 59 | } 60 | } 61 | 62 | &:focus:not(:active) { 63 | &:not(.btn--clicked) { 64 | outline: 2px dotted $black; 65 | } 66 | } 67 | 68 | &:disabled { 69 | background: $gray-light; 70 | color: $text-dark; 71 | 72 | &:before, 73 | &:after { 74 | display: none; 75 | } 76 | } 77 | } 78 | 79 | // Outline buttons 80 | %btn--outline, 81 | .btn--outline { 82 | @extend %btn; 83 | background: $white; 84 | border: 1px solid $accent-1; 85 | color: $accent-1; 86 | padding: 7px 14px 5px; 87 | 88 | &:hover { 89 | border-color: lighten($accent-1, 20); 90 | color: $white; 91 | } 92 | 93 | &:disabled { 94 | background: $white; 95 | border-color: darken($border-color, 10); 96 | color: $text; 97 | 98 | &:hover { 99 | color: $text; 100 | } 101 | } 102 | } 103 | 104 | // Color variations 105 | .btn--negative, 106 | %btn--negative { 107 | background: $minus-color; 108 | color: $white; 109 | 110 | &:before { 111 | background: lighten($minus-color, 10); 112 | } 113 | 114 | &:hover { 115 | &[type="button"][value] { 116 | background: lighten($minus-color, 10); 117 | } 118 | } 119 | 120 | &.btn--outline { 121 | background: $white; 122 | border-color: $minus-color; 123 | color: $minus-color; 124 | 125 | &:before { 126 | background: $minus-color; 127 | } 128 | 129 | &:hover { 130 | color: $white; 131 | } 132 | } 133 | } 134 | 135 | .btn--positive, 136 | %btn--positive { 137 | background: $plus-color; 138 | color: $white; 139 | 140 | &:before { 141 | background: lighten($plus-color, 10); 142 | } 143 | 144 | &:hover { 145 | &[type="button"][value] { 146 | background: lighten($plus-color, 10); 147 | } 148 | } 149 | 150 | &.btn--outline { 151 | background: $white; 152 | border-color: $plus-color; 153 | color: $plus-color; 154 | 155 | &:before { 156 | background: $plus-color; 157 | } 158 | 159 | &:hover { 160 | color: $white; 161 | } 162 | } 163 | } 164 | 165 | // Full-width buttons 166 | .btn--full { 167 | width: 100%; 168 | } 169 | 170 | // Link style buttons 171 | .btn--link, %btn--link { 172 | @extend %btn-reset; 173 | @extend %link; 174 | } 175 | 176 | // Ghost button 177 | .btn--ghost { 178 | @extend %transition; 179 | background: none; 180 | color: $text; 181 | height: rem(30); 182 | padding: 0 10px; 183 | min-width: rem(30); 184 | 185 | &:before { 186 | display: none; 187 | } 188 | 189 | .icon { 190 | height: rem(12); 191 | margin-right: 0; 192 | top: 1px; 193 | width: rem(12); 194 | 195 | &:only-child { 196 | margin-right: 6px; 197 | } 198 | } 199 | 200 | &:hover { 201 | background: transparentize($black, .96); 202 | color: $accent-1; 203 | 204 | &.btn--negative { 205 | color: $minus-color; 206 | } 207 | } 208 | } 209 | 210 | // Button reset 211 | .btn--reset { 212 | @extend %btn-reset; 213 | } 214 | -------------------------------------------------------------------------------- /src/scss/modules/_cursor.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Cursor styles 3 | -------------------------------------------*/ 4 | .cursor { 5 | color: #000; 6 | filter: drop-shadow(0 0 2px #fff); 7 | height: 32px; 8 | left: 0; 9 | pointer-events: none; 10 | position: fixed; 11 | top: 0; 12 | width: 20px; 13 | z-index: $top + 1; 14 | 15 | svg { 16 | height: 100%; 17 | width: 100%; 18 | } 19 | } -------------------------------------------------------------------------------- /src/scss/modules/_dialog.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Dialog styles 3 | -------------------------------------------*/ 4 | .dialog { 5 | @extend %overlay-container; 6 | } 7 | 8 | .dialog__content { 9 | @extend %overlay-msg; 10 | max-height: 100%; 11 | max-width: 100%; 12 | overflow: auto; 13 | -webkit-overflow-scrolling: touch; 14 | -ms-overflow-style: none; 15 | padding: 20px; 16 | text-align: left; 17 | width: rem(360); 18 | 19 | &::-webkit-scrollbar { 20 | width: 0; 21 | } 22 | 23 | label { 24 | display: block; 25 | margin-bottom: 5px; 26 | } 27 | 28 | p { 29 | @extend %sm; 30 | line-height: 1.5; 31 | margin-bottom: 1em; 32 | } 33 | } 34 | 35 | .dialog__title { 36 | background: $blue; 37 | // border-radius: $border-radius-top; 38 | color: $white; 39 | display: block; 40 | @extend %h5; 41 | font-weight: 400; 42 | margin: -20px -20px rem(20); 43 | padding: 19px 21px 17px; 44 | } 45 | 46 | .dialog__action { 47 | margin-top: rem(25); 48 | text-align: right; 49 | 50 | * + * { 51 | margin-left: rem(10); 52 | } 53 | } -------------------------------------------------------------------------------- /src/scss/modules/_page.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Page styles 3 | -------------------------------------------*/ 4 | .page { 5 | backdrop-filter: blur(10px); 6 | background: transparentize($white, .125); 7 | min-height: 100vh; 8 | padding: $page-padding; 9 | width: 100vw; 10 | 11 | @include min-up($tablet) { 12 | padding: $page-padding-lg; 13 | } 14 | } -------------------------------------------------------------------------------- /src/scss/modules/_panel.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Panel styles 3 | -------------------------------------------*/ 4 | .panel, %panel { 5 | background: $white; 6 | border: 1px solid $border-color; 7 | border-radius: $border-radius-all; 8 | box-shadow: $box-shadow-small; 9 | padding: $page-padding; 10 | position: relative; 11 | 12 | @include min-up($tablet) { 13 | padding: $page-padding-lg; 14 | } 15 | 16 | p, ol, ul { 17 | &:last-child { 18 | margin-bottom: 0; 19 | } 20 | } 21 | } 22 | 23 | .panel--md { 24 | max-width: rem(662); 25 | width: 100%; 26 | } 27 | 28 | .panel--sm { 29 | max-width: rem(482); 30 | width: 100%; 31 | } 32 | 33 | .panel--solo { 34 | margin: $page-padding auto; 35 | text-align: center; 36 | 37 | @include min-up($tablet) { 38 | margin: $page-padding-lg auto; 39 | } 40 | } 41 | 42 | .panel--form { 43 | text-align: left; 44 | } 45 | 46 | .panel__title { 47 | @extend %h2; 48 | margin-bottom: rem(12); 49 | text-align: center; 50 | } 51 | 52 | .panel__title--alt { 53 | text-align: left; 54 | } 55 | 56 | .panel__action { 57 | background: $gray-extra-light; 58 | border-top: 1px solid $border-color; 59 | border-radius: $border-radius-bottom; 60 | display: flex; 61 | margin: $page-padding (-$page-padding) (-$page-padding); 62 | padding: $page-padding; 63 | 64 | > .btn { 65 | margin-left: rem(10); 66 | 67 | &:first-child { 68 | margin-left: auto; 69 | } 70 | } 71 | 72 | @include min-up($tablet) { 73 | margin: $page-padding-lg (-$page-padding-lg) (-$page-padding-lg); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/scss/modules/_webcam.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Webcam styles 3 | -------------------------------------------*/ 4 | .webcam { 5 | background: $black; 6 | height: 100vh; 7 | left: 0; 8 | position: fixed; 9 | top: 0; 10 | transform: scaleX(-1); 11 | width: 100vw; 12 | z-index: -1; 13 | } -------------------------------------------------------------------------------- /src/scss/screen.scss: -------------------------------------------------------------------------------- 1 | @import 'utils/reset'; 2 | @import 'utils/mixins'; 3 | @import 'utils/variables'; 4 | @import 'utils/helpers'; 5 | @import 'utils/fonts'; 6 | @import 'utils/global'; 7 | @import 'utils/type'; 8 | 9 | // Modules 10 | @import 'modules/**/_*.scss'; -------------------------------------------------------------------------------- /src/scss/utils/_diagnostic.scss: -------------------------------------------------------------------------------- 1 | div:empty, span:empty, 2 | li:empty, p:empty, 3 | td:empty, th:empty {padding: 0.5em; background: yellow;} 4 | 5 | *[style], font, center {outline: 5px solid red;} 6 | *[class=""], *[id=""] {outline: 5px dotted red;} 7 | 8 | img[alt=""] {border: 3px dotted red;} 9 | img:not([alt]) {border: 5px solid red;} 10 | img[title=""] {outline: 3px dotted fuchsia;} 11 | img:not([title]) {outline: 5px solid fuchsia;} 12 | 13 | table:not([summary]) {outline: 5px solid red;} 14 | table[summary=""] {outline: 3px dotted red;} 15 | th {border: 2px solid red;} 16 | th[scope="col"], th[scope="row"] {border: none;} 17 | 18 | a[href]:not([title]) {border: 5px solid red;} 19 | a[title=""] {outline: 3px dotted red;} 20 | a[href="#"] {background: lime;} 21 | a[href=""] {background: fuchsia;} -------------------------------------------------------------------------------- /src/scss/utils/_fonts.scss: -------------------------------------------------------------------------------- 1 | // Google fonts imports 2 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,300italic,400,400italic,500,700,700italic'); -------------------------------------------------------------------------------- /src/scss/utils/_global.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Global styles 3 | -------------------------------------------*/ 4 | html { 5 | font-size: 16px; 6 | } 7 | 8 | body { 9 | @extend %antialias; 10 | background: $white; 11 | color: $text; 12 | cursor: none; 13 | font-family: $sans-serif; 14 | @extend %sm; 15 | } 16 | 17 | // Container elements 18 | .content { 19 | position: relative; 20 | } 21 | 22 | .wrap, { 23 | margin: 0 auto; 24 | max-width: $page-width; 25 | padding: 0 $page-padding; 26 | 27 | @include min-up($tablet) { 28 | padding: 0 $page-padding-lg; 29 | } 30 | } 31 | 32 | // Links 33 | a, %link { 34 | @extend %transition; 35 | color: $accent-1; 36 | text-decoration: underline; 37 | 38 | .icon { 39 | @extend %transition; 40 | } 41 | 42 | &:hover { 43 | color: darken($accent-1, 20); 44 | text-decoration: underline; 45 | 46 | .icon { 47 | fill: darken($accent-1, 20); 48 | } 49 | } 50 | 51 | &:focus { 52 | &:not([class*='btn']):not(.nav__item):not(.tab) { 53 | color: darken($accent-1, 20); 54 | text-decoration: underline; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/scss/utils/_helpers.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Helper classes 3 | -------------------------------------------*/ 4 | // List resets 5 | %list-reset { 6 | list-style: none; 7 | margin: 0; 8 | } 9 | 10 | %inline-list { 11 | @extend %list-reset; 12 | 13 | > li { 14 | display: inline-block; 15 | } 16 | } 17 | 18 | // Button resets 19 | %btn-reset { 20 | background: none; 21 | border: none; 22 | display: inline-block; 23 | margin: 0; 24 | padding: 0; 25 | } 26 | 27 | // Centering 28 | %center-horz { 29 | left: 50%; 30 | transform: translateX(-50%); 31 | } 32 | 33 | %center-vert { 34 | top: 50%; 35 | transform: translateY(-50%); 36 | } 37 | 38 | %center-both { 39 | left: 50%; 40 | top: 50%; 41 | transform: translateX(-50%) translateY(-50%); 42 | } 43 | 44 | %center-vert-alt { 45 | &:before { 46 | content: ''; 47 | display: inline-block; 48 | height: 100%; 49 | margin-right: -10px; 50 | vertical-align: middle; 51 | width: 10px; 52 | } 53 | } 54 | 55 | // Cover 56 | %cover { 57 | height: 100%; 58 | left: 0; 59 | position: absolute; 60 | top: 0; 61 | width: 100%; 62 | } 63 | 64 | // Transitions 65 | %transition { 66 | transition: all .25s ease; 67 | } 68 | 69 | // Meta text 70 | .meta, %meta { 71 | border: 0!important; 72 | clip: rect(1px 1px 1px 1px); 73 | clip: rect(1px, 1px, 1px, 1px); 74 | height: 1px!important; 75 | overflow: hidden!important; 76 | padding: 0!important; 77 | position: absolute!important; 78 | width: 1px!important; 79 | } 80 | 81 | %unmeta { 82 | clip: auto; 83 | height: auto!important; 84 | overflow: visible!important; 85 | position: static!important; 86 | width: auto!important; 87 | } 88 | 89 | // Font smoothing 90 | %antialias { 91 | -webkit-font-smoothing: antialiased; 92 | -moz-osx-font-smoothing: grayscale; 93 | } 94 | 95 | %subpixel { 96 | -webkit-font-smoothing: subpixel-antialiased; 97 | -moz-osx-font-smoothing: auto; 98 | } 99 | 100 | %auto-antialias { 101 | -webkit-font-smoothing: auto; 102 | -moz-osx-font-smoothing: auto; 103 | } 104 | 105 | // Dropdown arrow 106 | %down-arrow { 107 | &:after { 108 | border: 4px solid $text; 109 | border-bottom-width: 0; 110 | border-right-color: transparent; 111 | border-left-color: transparent; 112 | content: ''; 113 | display: inline-block; 114 | height: 0; 115 | margin-left: 4px; 116 | vertical-align: middle; 117 | width: 0; 118 | } 119 | } 120 | 121 | // Overlays 122 | %overlay { 123 | &:before { 124 | background: transparentize($black, .9); 125 | content: ''; 126 | height: 100%; 127 | left: 0; 128 | opacity: 1; 129 | position: fixed; 130 | top: 0; 131 | visibility: visible; 132 | width: 100%; 133 | z-index: $middle; 134 | } 135 | } 136 | 137 | %overlay-container { 138 | bottom: 0; 139 | left: 0; 140 | padding: $page-padding; 141 | position: fixed; 142 | right: 0; 143 | text-align: center; 144 | top: 0; 145 | @extend %transition; 146 | z-index: $top; 147 | 148 | &:before { 149 | content: ''; 150 | display: inline-block; 151 | height: 100%; 152 | margin-right: -10px; 153 | vertical-align: middle; 154 | width: 10px; 155 | } 156 | } 157 | 158 | %overlay-msg { 159 | background: $white; 160 | border-radius: $border-radius-all; 161 | box-shadow: $box-shadow-large; 162 | display: inline-block; 163 | vertical-align: middle; 164 | } 165 | 166 | %overlay-banner { 167 | &:before { 168 | background: linear-gradient(to bottom, transparentize($black, .75), transparentize(black, .25) 75%); 169 | bottom: 0; 170 | content: ''; 171 | left: 0; 172 | position: absolute; 173 | right: 0; 174 | top: 0; 175 | } 176 | } 177 | 178 | // Checkboxes/radios 179 | %checker { 180 | display: inline-block; 181 | line-height: 1.6875; 182 | } 183 | 184 | %checker__input { 185 | opacity: 0; 186 | height: 0; 187 | position: absolute; 188 | width: 0; 189 | } 190 | 191 | %checker__lbl { 192 | @extend %sm; 193 | display: inline-block; 194 | padding-left: rem(30); 195 | position: relative; 196 | user-select: none; 197 | 198 | &:before, 199 | &:after { 200 | content: ''; 201 | height: rem(20); 202 | left: 0; 203 | margin-top: rem(-1); 204 | position: absolute; 205 | top: rem(2); 206 | width: rem(20); 207 | } 208 | 209 | &:before { 210 | background: $white; 211 | border: 1px solid darken($border-color, 10); 212 | } 213 | 214 | &:after { 215 | @extend %transition; 216 | } 217 | 218 | input[disabled] + & { 219 | color: $text-light; 220 | 221 | &:before { 222 | background: $gray-extra-light; 223 | border-color: $border-color; 224 | cursor: not-allowed; 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/scss/utils/_mixins.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Global mixins 3 | -------------------------------------------*/ 4 | // The micro clearfix http://nicolasgallagher.com/micro-clearfix-hack/ 5 | @mixin clearfix() { 6 | *zoom:1; 7 | &:before, 8 | &:after { 9 | content:""; 10 | display:table; 11 | } 12 | &:after { 13 | clear:both; 14 | } 15 | } 16 | 17 | // Media queries 18 | @mixin max-down($max) { 19 | @media only screen and (max-width: $max) { @content; } 20 | } 21 | 22 | @mixin min-up($min) { 23 | @media only screen and (min-width: $min) { @content; } 24 | } 25 | 26 | @mixin min-max($min, $max) { 27 | @media only screen and (min-width: $min) and (max-width: $max) { @content; } 28 | } 29 | 30 | @mixin height-up($min) { 31 | @media only screen and (min-height: $min) { @content; } 32 | } 33 | 34 | // px to em 35 | $em-base: 16; 36 | 37 | @function em($pxval, $base: $em-base) { 38 | @if not unitless($pxval) { 39 | $pxval: strip-units($pxval); 40 | } 41 | @if not unitless($base) { 42 | $base: strip-units($base); 43 | } 44 | @return ($pxval / $base) * 1em; 45 | } 46 | 47 | // px to rem 48 | @function rem($pxval) { 49 | @if not unitless($pxval) { 50 | $pxval: strip-units($pxval); 51 | } 52 | 53 | $base: $em-base; 54 | @if not unitless($base) { 55 | $base: strip-units($base); 56 | } 57 | @return ($pxval / $base) * 1rem; 58 | } 59 | 60 | // Table to list 61 | @mixin table-to-list { 62 | width: 100%; 63 | 64 | thead { 65 | display: none; 66 | } 67 | tr, td, th { 68 | display: block; 69 | } 70 | td, th { 71 | &[data-col] { 72 | &:before { 73 | content: attr(data-col) ': '; 74 | } 75 | } 76 | } 77 | } 78 | 79 | // Grid 80 | @mixin grid($total-width, $gutter-width, $row-count, $element) { 81 | #{$element} { 82 | $g: $gutter-width/$total-width; 83 | float: left; 84 | margin-bottom: $g * 100%; 85 | width: ((1 - ($g * ($row-count - 1))) / $row-count) * 100%; 86 | 87 | &:nth-child(#{$row-count}n) { 88 | margin-right: 0; 89 | } 90 | } 91 | #{$element}:not(:nth-child(#{$row-count}n)) { 92 | $g: $gutter-width/$total-width; 93 | margin-right: $g * 100%; 94 | } 95 | } 96 | 97 | /* USAGE: 98 | 99 | ul { 100 | background: darkblue; 101 | list-style: none; 102 | margin: 0 auto; 103 | max-width: 960px; 104 | overflow: hidden; 105 | 106 | @include grid(960, 20, 3, li); 107 | 108 | li { 109 | background: darkred; 110 | height: 100px; 111 | text-indent: -999em; 112 | 113 | &:nth-child(2n+2) { 114 | -webkit-animation: silly2 5s ease-in-out 0s infinite alternate; 115 | } 116 | &:nth-child(3n) { 117 | -webkit-animation: silly 5s ease-in-out 0s infinite alternate; 118 | } 119 | } 120 | 121 | @media screen and (min-width: 30em) { 122 | @include grid(960, 20, 4, li); 123 | } 124 | @media screen and (min-width: 40em) { 125 | @include grid(960, 20, 6, li); 126 | } 127 | 128 | } 129 | 130 | */ 131 | 132 | // Loading graphic 133 | @mixin loader($color) { 134 | animation: loader-spin 1s infinite linear; 135 | display: block; 136 | height: 30px; 137 | width: 30px; 138 | 139 | &:before { 140 | background: $color; 141 | border-radius: 50%; 142 | box-shadow: 7px 3px 0 0 transparentize($color, .12), 143 | 10px 10px 0 0 transparentize($color, .24), 144 | 7px 17px 0 0 transparentize($color, .36), 145 | 0px 20px 0 0 transparentize($color, .48), 146 | -7px 17px 0 0 transparentize($color, .60), 147 | -10px 10px 0 0 transparentize($color, .72), 148 | -7px 3px 0 0 transparentize($color, .84); 149 | content: ''; 150 | height: 5px; 151 | left: 50%; 152 | position: absolute; 153 | top: 2px; 154 | transform: translateX(-50%); 155 | width: 5px; 156 | } 157 | } 158 | 159 | @keyframes 'loader-spin' { 160 | from { 161 | transform: rotate(0deg); 162 | } to { 163 | transform: rotate(360deg); 164 | } 165 | } 166 | 167 | /// Mixin to place items on a circle 168 | /// @author Hugo Giraudel 169 | /// @author Ana Tudor 170 | /// @param {Integer} $item-count - Number of items on the circle 171 | /// @param {Length} $circle-size - Large circle size 172 | /// @param {Length} $item-size - Single item size 173 | @mixin on-circle($item-count, $circle-size, $item-size) { 174 | position: relative; 175 | width: $circle-size; 176 | height: $circle-size; 177 | padding: 0; 178 | border-radius: 50%; 179 | list-style: none; 180 | 181 | > * { 182 | display: block; 183 | position: absolute; 184 | top: 50%; 185 | left: 50%; 186 | width: $item-size; 187 | height: $item-size; 188 | margin: -($item-size / 2); 189 | 190 | $angle: (360 / $item-count); 191 | $rot: 0; 192 | 193 | @for $i from 1 through $item-count { 194 | &:nth-of-type(#{$i}) { 195 | transform: 196 | rotate($rot * 1deg) 197 | translate($circle-size / 2) 198 | rotate($rot * -1deg); 199 | } 200 | 201 | $rot: $rot + $angle; 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /src/scss/utils/_reset.scss: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | border: 0; 15 | font-family: inherit; 16 | font-size: 100%; 17 | font-style: inherit; 18 | font-weight: inherit; 19 | margin: 0; 20 | outline: 0; 21 | padding: 0; 22 | vertical-align: baseline; 23 | } 24 | 25 | // default background, color, and line height 26 | body { 27 | background: #fff; 28 | color: #333; 29 | line-height: 1; 30 | } 31 | 32 | // set html5 elements to block for older browsers 33 | article, aside, details, figcaption, figure, 34 | footer, header, hgroup, menu, nav, section { 35 | display: block; 36 | } 37 | 38 | // set all box sizing to border 39 | *, 40 | *:before, 41 | *:after { 42 | -moz-box-sizing: border-box; 43 | -webkit-box-sizing: border-box; 44 | box-sizing: border-box; 45 | } 46 | 47 | // reset heading weight 48 | h1, h2, h3, h4, h5, h6 { 49 | font-weight: normal; 50 | } 51 | 52 | // default link styles 53 | // :focus adds outline for keyboard users 54 | // :active removes outline for users who click and then don't follow the link 55 | a, a:hover { 56 | color: inherit; 57 | text-decoration: none; 58 | } 59 | a:focus, :focus { 60 | outline: none; 61 | } 62 | 63 | // list styles (five deep for ol three for ul) 64 | ol { 65 | list-style: decimal; margin: 0 0 0 2em; 66 | } 67 | ol ol { 68 | list-style: upper-alpha; 69 | } 70 | ol ol ol { 71 | list-style: upper-roman; 72 | } 73 | ol ol ol ol { 74 | list-style: lower-alpha; 75 | } 76 | ol ol ol ol ol { 77 | list-style: lower-roman; 78 | } 79 | 80 | ul { 81 | list-style: disc; margin: 0 0 0 2em; 82 | } 83 | ul ul { 84 | list-style: circle; 85 | } 86 | ul ul ul { 87 | list-style: square; 88 | } 89 | 90 | // set input textarea and button font-family to match that of the body 91 | input, textarea, button { 92 | font-family: inherit; 93 | font-size: inherit; 94 | } 95 | textarea { 96 | resize: none; 97 | } 98 | 99 | // vertical alignment of checkboxes (a different value is served to IE 7) 100 | input[type="checkbox"] { 101 | vertical-align: bottom; 102 | *vertical-align: baseline; 103 | } 104 | 105 | // cursors 106 | button { 107 | cursor: pointer; 108 | } 109 | 110 | *[disabled] { 111 | cursor: not-allowed; 112 | } 113 | 114 | // vertical alignment of radio buttons 115 | input[type="radio"] { 116 | vertical-align: text-bottom; 117 | } 118 | 119 | 120 | // vertical alignment of input fields for IE 6 121 | input { 122 | _vertical-align: text-bottom; 123 | } 124 | 125 | 126 | // set textarea to block */ 127 | textarea { 128 | display: block; 129 | } 130 | 131 | // tables still need 'cellspacing="0"' in the markup 132 | table { 133 | border-collapse: separate; 134 | border-spacing: 0; 135 | } 136 | caption, th, td { 137 | font-weight: normal; 138 | text-align: left; 139 | } 140 | 141 | // Remove quote marks 142 | blockquote:before, 143 | blockquote:after, 144 | q:before, 145 | q:after { 146 | content: ""; 147 | } 148 | 149 | blockquote, q { 150 | quotes: "" ""; 151 | } 152 | -------------------------------------------------------------------------------- /src/scss/utils/_type.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Font sizes, line-heights, and margins 3 | -------------------------------------------*/ 4 | %xl {font-size: rem(32)} 5 | %h1 {font-size: rem(24)} 6 | %h2 {font-size: rem(20)} 7 | %h3 {font-size: rem(19)} 8 | %h4 {font-size: rem(18)} 9 | %h5 {font-size: rem(16)} 10 | %h6 {font-size: rem(16)} 11 | %sm {font-size: rem(14)} 12 | %xs {font-size: rem(12)} 13 | 14 | @include min-up($tablet) { 15 | %xl {font-size: rem(48)} 16 | %h1 {font-size: rem(32)} 17 | %h2 {font-size: rem(28)} 18 | %h3 {font-size: rem(24)} 19 | %h4 {font-size: rem(20)} 20 | %h5 {font-size: rem(18)} 21 | %h6 {font-size: rem(16)} 22 | %sm {font-size: rem(14)} 23 | %xs {font-size: rem(12)} 24 | } 25 | 26 | h1 { 27 | @extend %h1; 28 | line-height: 1.4; 29 | } 30 | 31 | h2 { 32 | @extend %h2; 33 | line-height: 1.6785; 34 | } 35 | 36 | h3 { 37 | @extend %h3; 38 | line-height: 1.666666667; 39 | } 40 | 41 | h4 { 42 | @extend %h4; 43 | line-height: 1.7; 44 | } 45 | 46 | h5 { 47 | @extend %h5; 48 | line-height: 1.666666667; 49 | } 50 | 51 | h6 { 52 | @extend %h6; 53 | line-height: 1.6875; 54 | } 55 | 56 | p, ol, ul, dl { 57 | @extend %sm; 58 | line-height: 1.6875; 59 | margin-bottom: 1.6875em; 60 | } 61 | 62 | // Font styles/weights 63 | em { 64 | font-style: italic; 65 | } 66 | 67 | strong { 68 | font-weight: 700; 69 | } 70 | 71 | abbr { 72 | text-decoration: none; 73 | } 74 | 75 | code { 76 | background: $gray-light; 77 | font-family: monospace; 78 | } 79 | 80 | .preposition { 81 | @extend %sm; 82 | display: inline-block; 83 | margin: 0 rem(10); 84 | } 85 | 86 | .preposition--vertical { 87 | margin: rem(12) 0 rem(10); 88 | } 89 | 90 | .no-results { 91 | display: block; 92 | line-height: 1.2; 93 | margin: 1em 0; 94 | text-align: center; 95 | 96 | &:last-child { 97 | margin-bottom: 0; 98 | } 99 | } 100 | 101 | .required { 102 | &:after { 103 | content: ' *'; 104 | color: $accent-1; 105 | } 106 | } -------------------------------------------------------------------------------- /src/scss/utils/_variables.scss: -------------------------------------------------------------------------------- 1 | /*------------------------------------------- 2 | Colors 3 | -------------------------------------------*/ 4 | // Brand colors 5 | $blue: #0074cc; 6 | $blue-alt: #3a91e4; 7 | $blue-dark: #5166d6; 8 | $green: #46be8a; 9 | $green-alt: #5cd29d; 10 | $orange: #f2a652; 11 | $pink: #f44c87; 12 | $purple: #bba7e4; 13 | $purple-alt: darken($purple, 10); 14 | $red: #e9595b; 15 | $red-alt: lighten($red, 5); 16 | $teal: #47b8c6; 17 | $yellow: #f9cd48; 18 | $lime: $green; 19 | 20 | // Grays 21 | $black: #000; 22 | $gray: #76838f; 23 | $gray-dark: #526069; 24 | $gray-extra-dark: #37474f; 25 | $gray-light: #e4eaec; 26 | $gray-extra-light: #f3f7f9; 27 | $white: #fff; 28 | 29 | // Accents 30 | $accent-1: $blue; 31 | $accent-1-alt: $blue-alt; 32 | $accent-2: $purple; 33 | $accent-2-alt: $purple-alt; 34 | $accent-3: $green; 35 | $accent-3-alt: $green-alt; 36 | $accent-4: $red; 37 | $accent-4-alt: lighten($red, 20); 38 | $accent-5: $orange; 39 | $accent-5-alt: lighten($orange, 20); 40 | $accent-6: $pink; 41 | $accent-6-alt: lighten($pink, 20); 42 | $accent-7: $yellow; 43 | $accent-7-alt: lighten($yellow, 20); 44 | 45 | // UI colors 46 | $text: $gray; 47 | $text-dark: $gray-dark; 48 | $text-extra-dark: $gray-extra-dark; 49 | $text-light: lighten($text, 20); 50 | $border-color: $gray-light; 51 | $toggle-color: $purple; 52 | 53 | // Loader colors 54 | $loader-1: #3a91e4; 55 | $loader-2: #f9cd48; 56 | $loader-3: #46be8a; 57 | $loader-4: #f44c87; 58 | 59 | // Dialog colors 60 | $dialog-accent: #0074cc; 61 | 62 | // State colors 63 | $success: $green; 64 | $plus-color: $green; 65 | $plus-color-alt: $green-alt; 66 | $warn-color: $yellow; 67 | $error: $red; 68 | $minus-color: $red; 69 | $minus-color-alt: $red-alt; 70 | 71 | /*------------------------------------------- 72 | Fonts 73 | -------------------------------------------*/ 74 | $sans-serif: 'Roboto', sans-serif; 75 | 76 | /*------------------------------------------- 77 | Stacking 78 | -------------------------------------------*/ 79 | $bottom: 1000; 80 | $middle: 2000; 81 | $top: 3000; 82 | 83 | /*------------------------------------------- 84 | Breakpoints 85 | -------------------------------------------*/ 86 | // Min-up 87 | $mobile: em(320); 88 | $mobile-lg: em(460); 89 | $tablet: em(768); 90 | $desktop-sm: em(820); 91 | $desktop: em(1024); 92 | 93 | // Max-down 94 | $mobile-down: em(319); 95 | $mobile-lg-down: em(459); 96 | $tablet-down: em(767); 97 | $desktop-down: em(1023); 98 | 99 | /*------------------------------------------- 100 | Grid settings 101 | -------------------------------------------*/ 102 | $page-width: rem(1260); 103 | $page-padding: 15px; 104 | $page-padding-lg: 30px; 105 | 106 | @include min-up($tablet) { 107 | $padding-size: 30px; 108 | } 109 | 110 | /*------------------------------------------- 111 | Misc 112 | -------------------------------------------*/ 113 | $nav-width: rem(220); 114 | $mobile-header-height: rem(58); 115 | 116 | // Border radius 117 | $border-radius-amount: 3px; 118 | $border-radius-all: $border-radius-amount; 119 | $border-radius-top: $border-radius-amount $border-radius-amount 0 0; 120 | $border-radius-bottom: 0 0 $border-radius-amount $border-radius-amount; 121 | $border-radius-left: $border-radius-amount 0 0 $border-radius-amount; 122 | $border-radius-right: 0 $border-radius-amount $border-radius-amount 0; 123 | 124 | // Box shadows 125 | $box-shadow-small: 0 1px 1px 0 transparentize($black, .95); 126 | $box-shadow-normal: 0 3px 12px 0 transparentize($black, .95); 127 | $box-shadow-large: 0 4px 15px 0 transparentize($black, .9); 128 | $box-shadow-drawer: 0 0 20px 0 transparentize($black, .75); 129 | $box-shadow-inset-normal: inset 0 2px 3px 0 transparentize($black, .95); 130 | 131 | /*------------------------------------------- 132 | Text inputs 133 | -------------------------------------------*/ 134 | $text-input-list: ( 135 | 'input[type="color"]', 136 | 'input[type="date"]', 137 | 'input[type="datetime-local"]', 138 | 'input[type="datetime"]', 139 | 'input[type="email"]', 140 | 'input[type="month"]', 141 | 'input[type="number"]', 142 | 'input[type="password"]', 143 | 'input[type="search"]', 144 | 'input[type="tel"]', 145 | 'input[type="text"]', 146 | 'input[type="time"]', 147 | 'input[type="url"]', 148 | 'input[type="week"]', 149 | ); 150 | 151 | $all-text-inputs: (); 152 | 153 | @each $input in $text-input-list { 154 | $all-text-inputs: append($all-text-inputs, $input, comma); 155 | } 156 | --------------------------------------------------------------------------------