├── .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 |
115 |
116 |
119 |
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 |
--------------------------------------------------------------------------------