├── .eslintrc.json ├── .gitignore ├── .stylintrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.yaml ├── appengine_config.py ├── assets ├── .gitkeep ├── arrows │ ├── arrow-1.svg │ ├── arrow-2.svg │ ├── arrow-3.svg │ └── arrow-4.svg ├── back-arrow.svg ├── cam-icon.svg ├── camera-flip.svg ├── close.svg ├── edit-icon.svg ├── ew-resize.svg ├── ff-camera-icon.png ├── footer-intro.svg ├── footer.png ├── footer.svg ├── giphy.svg ├── intro.svg ├── leds.png ├── leds.svg ├── machine-handdrawn.png ├── machine-handdrawn.svg ├── madeby.svg ├── mic-icon.svg ├── outputs │ ├── back-icon.svg │ ├── delete-icon.svg │ ├── edit-icon.svg │ ├── play-icon.svg │ ├── record-icon.svg │ ├── recorder-icon.svg │ ├── sound │ │ ├── back-icons.svg │ │ ├── edit-icons.svg │ │ ├── sounds │ │ │ ├── applause.mp3 │ │ │ ├── bass.mp3 │ │ │ ├── birds.mp3 │ │ │ ├── cow.mp3 │ │ │ ├── drum_joke.mp3 │ │ │ ├── drum_roll.mp3 │ │ │ ├── drums_1.mp3 │ │ │ ├── drums_2.mp3 │ │ │ ├── fanfare.mp3 │ │ │ ├── flute_1.mp3 │ │ │ ├── flute_2.mp3 │ │ │ ├── flute_3.mp3 │ │ │ ├── guitar_1.mp3 │ │ │ ├── guitar_2.mp3 │ │ │ ├── harp.mp3 │ │ │ ├── jingle.mp3 │ │ │ ├── orchestra.mp3 │ │ │ ├── organ.mp3 │ │ │ ├── trombone.mp3 │ │ │ ├── trumpet_1.mp3 │ │ │ ├── trumpet_2.mp3 │ │ │ ├── trumpet_3.mp3 │ │ │ └── tuba.mp3 │ │ ├── speaker-icon.svg │ │ ├── speaker-icons.svg │ │ └── tts-play-icons.svg │ ├── speaker-icon.svg │ ├── stop-icon.svg │ └── words │ │ └── edit-icon.svg ├── play-icon.svg ├── select-arrow.svg ├── sharing-facebook.svg ├── sharing-twitter.svg ├── social-facebook.svg ├── social-twitter.svg ├── speaker-icon.svg ├── static │ ├── favicon │ │ ├── favicon-152.png │ │ ├── favicon-192.png │ │ └── favicon.ico │ └── share-image.jpg ├── teachable-handdrawn.png ├── teachable-handdrawn.svg ├── teachable-machine-splash-desktop.svg ├── teachable-machine-splash-mobile.svg ├── teachable-machine.svg ├── wires-left.png ├── wires-recorder.png ├── wizard │ ├── 1.gif │ ├── 2.gif │ ├── 3.gif │ ├── 4.gif │ ├── gif-backdrop.svg │ └── voice-over.mp3 ├── youtube-back-icon.svg ├── youtube-pins.svg └── youtube-search-icon.svg ├── html ├── fb.html └── index.html ├── main.py ├── package.json ├── requirements.txt ├── src ├── ai │ ├── WebcamClassifier.js │ ├── imagenet_util.js │ └── squeezenet.js ├── config.js ├── index.js ├── outputs │ ├── GIFOutput.js │ ├── SoundOutput.js │ ├── SpeechOutput.js │ ├── sound │ │ └── SoundSearch.js │ └── speech │ │ └── TextToSpeech.js └── ui │ ├── components │ ├── BrowserUtils.js │ ├── Button.js │ ├── CamInput.js │ ├── GifCanvas.js │ ├── HighlightArrow.js │ ├── RecordOpener.js │ └── Selector.js │ └── modules │ ├── InputSection.js │ ├── IntroSection.js │ ├── LearningClass.js │ ├── LearningSection.js │ ├── OutputSection.js │ ├── Recording.js │ ├── TrainingQuality.js │ ├── WiresLeft.js │ ├── WiresRight.js │ ├── Wizard.js │ ├── WizardGIFExample.js │ └── wizard │ ├── LaunchScreen.js │ ├── Storyboard.js │ └── Storyboard2.js ├── style ├── base.styl ├── buttons.styl ├── colors.styl ├── components │ ├── faq.styl │ ├── footer.styl │ ├── intro.styl │ ├── machine.styl │ ├── output__gif.styl │ ├── output__sound.styl │ ├── output__speech.styl │ ├── outputs.styl │ ├── recording.styl │ └── wizard.styl ├── config.styl ├── icons.styl ├── links.styl ├── main.styl └── typography.styl └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .sass-cache 3 | .DS_Store 4 | *.swp 5 | public/* 6 | .idea 7 | server.key 8 | server.cer 9 | *.pyc 10 | lib/* -------------------------------------------------------------------------------- /.stylintrc: -------------------------------------------------------------------------------- 1 | { 2 | "borderNone": true, 3 | "brackets": "always", 4 | "colons": "always", 5 | "colors": false, 6 | "commaSpace": true, 7 | "commentSpace": false, 8 | "cssLiteral": false, 9 | "depthLimit": false, 10 | "duplicates": false, 11 | "efficient": true, 12 | "emoji": false, 13 | "enforceBlockStyle": false, 14 | "enforceVarStyle": false, 15 | "extendPref": false, 16 | "globalDupe": false, 17 | "indentSpaces": 4, 18 | "leadingZero": true, 19 | "maxWarnings": 0, 20 | "maxWarningsKill": false, 21 | "mixed": false, 22 | "namingConvention": false, 23 | "namingConventionStrict": false, 24 | "parenSpace": false, 25 | "placeholders": true, 26 | "quotePref": false, 27 | "semicolons": "never", 28 | "sortOrder": false, 29 | "stackedProperties": true, 30 | "universal": true, 31 | "valid": false, 32 | "whitespace": true, 33 | "zeroUnits": true, 34 | "zIndexDuplicates": false, 35 | "zIndexNormalize": false 36 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Teachable Machine 2 | ## About 3 | Teachable Machine is an experiment that makes it easier for anyone to explore machine learning, live in the browser – no coding required. Learn more about the experiment and try it yourself on [g.co/teachablemachine](https://g.co/teachablemachine). 4 | 5 | The experiment is built using the [TensorFlow.js](https://js.tensorflow.org/) library. 6 | 7 | We have also released a boilerplate version of this project that can be used as a starting point for your own projects: [googlecreativelab/teachable-machine-boilerplate](https://github.com/googlecreativelab/teachable-machine-boilerplate) 8 | 9 | ## Development 10 | #### Install dependencies by running (similar to `npm install`) 11 | ``` 12 | yarn 13 | ``` 14 | 15 | #### Build project 16 | ``` 17 | yarn build 18 | ``` 19 | 20 | #### Start local server by running 21 | ``` 22 | yarn run watch 23 | ``` 24 | 25 | #### Code Styles 26 | - There’s a pre-commit hook set up that will prevent commits when there are errors 27 | - Run `yarn eslint` for es6 errors & warnings 28 | - Run `yarn stylint` for stylus errors & warnings 29 | 30 | #### To run https locally: 31 | https is required to get camera permissions to work when not working with `localhost` 32 | 33 | 1. Generate Keys 34 | ``` 35 | openssl genrsa -out server.key 2048 36 | openssl req -new -x509 -sha256 -key server.key -out server.cer -days 365 -subj /CN=YOUR_IP 37 | ``` 38 | 2. Use `yarn run watch-https` 39 | 3. Go to `https://YOUR_IP:3000`, then accept the insecure privacy notice, and proceed. 40 | 41 | ## Credit 42 | This is not an official Google product, but an experiment that was a collaborative effort by friends from [Støj](http://stoj.io/), [Use All Five](https://useallfive.com/) and Creative Lab and [PAIR](https://ai.google/pair/) teams at Google. 43 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | runtime: python27 16 | api_version: 1 17 | threadsafe: true 18 | 19 | handlers: 20 | - url: / 21 | static_files: public/index.html 22 | upload: public/index.html 23 | secure: always 24 | 25 | - url: /fb 26 | static_files: public/fb.html 27 | upload: public/fb.html 28 | secure: always 29 | 30 | - url: /share-video 31 | script: main.app 32 | 33 | - url: /style.css 34 | static_files: public/style.css 35 | upload: public/style.css 36 | secure: always 37 | 38 | - url: /bundle.js 39 | static_files: public/bundle.js 40 | upload: public/bundle.js 41 | secure: always 42 | 43 | # image files 44 | - url: /(.*\.(bmp|gif|ico|jpeg|jpg|png|svg|mp3)) 45 | static_files: public/\1 46 | upload: public/(.*\.(bmp|gif|ico|jpeg|jpg|png|svg|mp3)) 47 | secure: always 48 | 49 | env_variables: 50 | FB_CLIENT_SECRET: '' 51 | 52 | skip_files: 53 | - ^.idea/.* 54 | - ^.git/.* 55 | - ^node_modules/.* 56 | - ^(.*/)?.*\.DS_Store$ 57 | - ^src/.* 58 | - ^style/.* 59 | - ^(.*/)?.*\.swp$ -------------------------------------------------------------------------------- /appengine_config.py: -------------------------------------------------------------------------------- 1 | from google.appengine.ext import vendor 2 | vendor.add('lib') -------------------------------------------------------------------------------- /assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/.gitkeep -------------------------------------------------------------------------------- /assets/back-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/cam-icon.svg: -------------------------------------------------------------------------------- 1 | camera-icon -------------------------------------------------------------------------------- /assets/camera-flip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/edit-icon.svg: -------------------------------------------------------------------------------- 1 | edit-icon -------------------------------------------------------------------------------- /assets/ew-resize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/ff-camera-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/ff-camera-icon.png -------------------------------------------------------------------------------- /assets/footer-intro.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/footer.png -------------------------------------------------------------------------------- /assets/footer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/giphy.svg: -------------------------------------------------------------------------------- 1 | giphySearch powered by -------------------------------------------------------------------------------- /assets/leds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/leds.png -------------------------------------------------------------------------------- /assets/machine-handdrawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/machine-handdrawn.png -------------------------------------------------------------------------------- /assets/machine-handdrawn.svg: -------------------------------------------------------------------------------- 1 | machine-handdrawn_1 -------------------------------------------------------------------------------- /assets/mic-icon.svg: -------------------------------------------------------------------------------- 1 | mic-icon -------------------------------------------------------------------------------- /assets/outputs/back-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/outputs/delete-icon.svg: -------------------------------------------------------------------------------- 1 | delete-icon -------------------------------------------------------------------------------- /assets/outputs/edit-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/outputs/play-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/outputs/record-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/outputs/recorder-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /assets/outputs/sound/back-icons.svg: -------------------------------------------------------------------------------- 1 | back-icons_1 -------------------------------------------------------------------------------- /assets/outputs/sound/edit-icons.svg: -------------------------------------------------------------------------------- 1 | edit-icons -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/applause.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/applause.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/bass.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/bass.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/birds.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/birds.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/cow.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/cow.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/drum_joke.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/drum_joke.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/drum_roll.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/drum_roll.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/drums_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/drums_1.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/drums_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/drums_2.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/fanfare.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/fanfare.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/flute_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/flute_1.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/flute_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/flute_2.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/flute_3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/flute_3.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/guitar_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/guitar_1.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/guitar_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/guitar_2.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/harp.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/harp.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/jingle.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/jingle.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/orchestra.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/orchestra.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/organ.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/organ.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/trombone.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/trombone.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/trumpet_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/trumpet_1.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/trumpet_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/trumpet_2.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/trumpet_3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/trumpet_3.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/sounds/tuba.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/outputs/sound/sounds/tuba.mp3 -------------------------------------------------------------------------------- /assets/outputs/sound/speaker-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/outputs/sound/speaker-icons.svg: -------------------------------------------------------------------------------- 1 | speaker-icons_1 -------------------------------------------------------------------------------- /assets/outputs/sound/tts-play-icons.svg: -------------------------------------------------------------------------------- 1 | tts-play-icons -------------------------------------------------------------------------------- /assets/outputs/speaker-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/outputs/stop-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/outputs/words/edit-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/play-icon.svg: -------------------------------------------------------------------------------- 1 | play-icon -------------------------------------------------------------------------------- /assets/select-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | select-arrow 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/sharing-facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fill 4 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /assets/sharing-twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fill 10 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/social-facebook.svg: -------------------------------------------------------------------------------- 1 | social-facebook -------------------------------------------------------------------------------- /assets/social-twitter.svg: -------------------------------------------------------------------------------- 1 | social-twitter_1 -------------------------------------------------------------------------------- /assets/speaker-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | _Compound_Path_ 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/static/favicon/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/static/favicon/favicon-152.png -------------------------------------------------------------------------------- /assets/static/favicon/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/static/favicon/favicon-192.png -------------------------------------------------------------------------------- /assets/static/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/static/favicon/favicon.ico -------------------------------------------------------------------------------- /assets/static/share-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/static/share-image.jpg -------------------------------------------------------------------------------- /assets/teachable-handdrawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/teachable-handdrawn.png -------------------------------------------------------------------------------- /assets/teachable-handdrawn.svg: -------------------------------------------------------------------------------- 1 | teachable-handdrawn_1 -------------------------------------------------------------------------------- /assets/wires-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/wires-left.png -------------------------------------------------------------------------------- /assets/wires-recorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/wires-recorder.png -------------------------------------------------------------------------------- /assets/wizard/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/wizard/1.gif -------------------------------------------------------------------------------- /assets/wizard/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/wizard/2.gif -------------------------------------------------------------------------------- /assets/wizard/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/wizard/3.gif -------------------------------------------------------------------------------- /assets/wizard/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/wizard/4.gif -------------------------------------------------------------------------------- /assets/wizard/gif-backdrop.svg: -------------------------------------------------------------------------------- 1 | Asset 1 -------------------------------------------------------------------------------- /assets/wizard/voice-over.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/teachable-machine-v1/5ed2c7757baa8f7443cea4303377a02ff5304fa7/assets/wizard/voice-over.mp3 -------------------------------------------------------------------------------- /assets/youtube-back-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | youtube-back-icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/youtube-pins.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | youtube-pins 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/youtube-search-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | youtube-search-icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /html/fb.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Share 6 | 7 | 8 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import webapp2, json, logging, urllib, requests, os 17 | import requests_toolbelt.adapters.appengine 18 | from google.appengine.api import urlfetch 19 | # from poster.encode import multipart_encode, MultipartParam 20 | 21 | class ShareVideo(webapp2.RequestHandler): 22 | def post(self): 23 | self.response.headers['Content-Type'] = 'text/plain' 24 | self.response.headers['Access-Control-Allow-Origin'] = '*' 25 | code = self.request.get('code') 26 | clientSecret = os.environ['FB_CLIENT_SECRET'] 27 | url = 'https://graph.facebook.com/oauth/access_token?client_id=1442525379177526&redirect_uri=https://teachablemachine.withgoogle.com/fb&code=' + code + '&client_secret=' + clientSecret 28 | try: 29 | result = urlfetch.fetch(url, validate_certificate=True) 30 | if result.status_code == 200: 31 | data = json.loads(result.content) 32 | accessToken = data['access_token'] 33 | try: 34 | requests_toolbelt.adapters.appengine.monkeypatch() 35 | video = self.request.POST.get('video') 36 | url = 'https://graph-video.facebook.com/v2.10/me/videos' 37 | query = { 38 | 'access_token': accessToken, 39 | 'description': 'Train your own neural net and explore machine learning with #teachablemachine: http://g.co/teachablemachine' 40 | } 41 | files = { 42 | 'source': (video.filename, video.file, video.type) 43 | } 44 | response = requests.post(url, data=query, files=files) 45 | self.response.write(response.content) 46 | 47 | except urlfetch.Error: 48 | logging.exception('Caught exception fetching url') 49 | else: 50 | self.response.status_int = result.status_code 51 | except urlfetch.Error: 52 | print '{"error": "error"}' 53 | 54 | 55 | app = webapp2.WSGIApplication([ 56 | ('/share-video', ShareVideo), 57 | ], debug=True) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teachablemachine", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "browserify": { 7 | "transform": [ 8 | [ 9 | "babelify", 10 | { 11 | "presets": [ 12 | "es2015" 13 | ] 14 | } 15 | ] 16 | ] 17 | }, 18 | "scripts": { 19 | "build": "npm run html-copy && npm run style-build && npm run assets-copy && npm run browserify", 20 | "browserify": "browserify src/index.js -o public/bundle.js", 21 | "assets-copy": "cpx \"assets/**/*\" \"public/assets/\" --clean", 22 | "assets-watch": "npm run assets-copy -- --watch", 23 | "html-copy": "cpx \"html/**/*.html\" \"public/\" --clean", 24 | "html-watch": "npm run html-copy -- --watch", 25 | "style-watch": "stylus -m -w -u nib style/main.styl -o public/style.css", 26 | "style-build": "stylus -c -u nib style/main.styl -o public/style.css", 27 | "eslint": "eslint src", 28 | "stylint": "stylint style", 29 | "budo": "budo src/index.js:bundle.js --port=3000 --live -v --dir=public -o", 30 | "budo-https": "budo src/index.js:bundle.js --ssl --cert=server.cer --key=server.key --port=3000 --live -v --dir=public -o", 31 | "watch": "npm run style-build && concurrently \"npm run assets-watch\" \"npm run html-watch\" \"npm run budo\" \"npm run style-watch\"", 32 | "watch-https": "npm run style-build && concurrently \"npm run assets-watch\" \"npm run html-watch\" \"npm run budo-https\" \"npm run style-watch\"" 33 | }, 34 | "pre-commit": [ 35 | "eslint", 36 | "stylint" 37 | ], 38 | "dependencies": { 39 | "@tensorflow/tfjs": "0.11.7", 40 | "@tensorflow-models/mobilenet": "0.1.1", 41 | "@tensorflow-models/knn-classifier": "0.1.0", 42 | "babel-preset-es2015": "^6.14.0", 43 | "babelify": "^7.3.0", 44 | "browserify": "^14.4.0", 45 | "budo": "^10.0.3", 46 | "concurrently": "^3.1.0", 47 | "cpx": "^1.5.0", 48 | "gifler": "git://github.com/themadcreator/gifler.git#v0.3.0", 49 | "gsap": "^1.20.2", 50 | "ncp": "^2.0.0", 51 | "nib": "^1.1.2", 52 | "openssl": "^1.1.0", 53 | "stylus": "^0.54.5" 54 | }, 55 | "devDependencies": { 56 | "eslint": "^4.3.0", 57 | "pre-commit": "^1.2.2", 58 | "stylint": "^1.5.9" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.18.4 2 | poster==0.8.1 3 | requests-toolbelt -------------------------------------------------------------------------------- /src/ai/imagenet_util.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable */ 16 | 17 | import {webgl_util, ENV} from 'deeplearn'; 18 | 19 | /** 20 | * Unpacks an RGB packed image texture into a 2D physical, 3D logical texture 21 | * with the conventional ndarray format and performs the standard imagenet image 22 | * preprocessing. 23 | */ 24 | export function getUnpackAndPreprocessInputShader(gpgpu, inputShapeRC, useFloatTextures) { 25 | let setOutputSnippet; 26 | 27 | if (useFloatTextures) { 28 | setOutputSnippet = ` 29 | void setOutput(float decodedValue) { 30 | gl_FragColor = vec4(decodedValue, 0, 0, 0); 31 | } 32 | `; 33 | } else { 34 | setOutputSnippet = ` 35 | const vec4 floatPowers = vec4( 36 | 1.0, 37 | 255.0, 38 | 255.0 * 255.0, 39 | 255.0 * 255.0 * 255.0 40 | ); 41 | 42 | const float maxValue = 20000.0; 43 | const float minValue = -maxValue; 44 | const float range = (maxValue - minValue) / 255.0; 45 | 46 | const vec2 recipRange = vec2(1.0/range); 47 | const vec2 recipRange255 = vec2(1.0/(maxValue - minValue)); 48 | 49 | void setOutput(float decodedValue) { 50 | float a = dot(vec2(decodedValue, -minValue), recipRange); 51 | float b = fract(a) * 255.0; 52 | float c = fract(b) * 255.0; 53 | float d = fract(c) * 255.0; 54 | gl_FragColor = floor(vec4(a, b, c, d)) / 255.0; 55 | } 56 | `; 57 | } 58 | 59 | const fragmentShaderSource = ` 60 | precision highp float; 61 | uniform sampler2D source; 62 | varying vec2 resultUV; 63 | 64 | const vec2 inputShapeCR = vec2(${inputShapeRC[1]}.0, ${inputShapeRC[0]}.0); 65 | 66 | const vec2 halfCR = vec2(0.5, 0.5); 67 | 68 | ${setOutputSnippet} 69 | 70 | void main() { 71 | vec2 outputCR = floor(gl_FragCoord.xy); 72 | 73 | vec2 sourceCR = vec2(floor(outputCR[0] / 3.0), outputCR[1]); 74 | vec2 sourceUV = (sourceCR + halfCR) / inputShapeCR; 75 | 76 | vec4 sourceValue = texture2D(source, sourceUV) * 255.0; 77 | 78 | float channelValue = 0.0; 79 | int channel = int(mod(outputCR[0], 3.0)); 80 | 81 | if (channel == 0) { 82 | channelValue = sourceValue.r - 103.939; 83 | } else if (channel == 1) { 84 | channelValue = sourceValue.g - 116.779; 85 | } else if (channel == 2) { 86 | channelValue = sourceValue.b - 123.68; 87 | } 88 | 89 | setOutput(channelValue); 90 | }`; 91 | 92 | return gpgpu.createProgram(fragmentShaderSource); 93 | } 94 | 95 | export function preprocessInput( 96 | gpgpu, preprocessInputShader, sourceTex, resultTex, shapeRowCol) { 97 | gpgpu.setOutputMatrixTexture(resultTex, shapeRowCol[0], shapeRowCol[1]); 98 | gpgpu.setProgram(preprocessInputShader); 99 | const samplerLocation = webgl_util.getProgramUniformLocationOrThrow( 100 | gpgpu.gl, preprocessInputShader, 'source'); 101 | gpgpu.setInputMatrixTexture(sourceTex, samplerLocation, 0); 102 | gpgpu.executeProgram(); 103 | } -------------------------------------------------------------------------------- /src/ai/squeezenet.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* eslint-disable */ 16 | 17 | import {CheckpointLoader, NDArray} from 'deeplearn'; 18 | import * as imagenet_util from './imagenet_util'; 19 | 20 | const IMAGE_SIZE = 227; 21 | const GOOGLE_CLOUD_STORAGE_DIR = 22 | 'https://storage.googleapis.com/teachable-machine-squeezenet/'; 23 | 24 | class SqueezeNet { 25 | 26 | constructor(gpgpu, math, useFloatTextures) { 27 | this.math = math; 28 | this.gpgpu = gpgpu; 29 | this.preprocessInputShader = 30 | imagenet_util.getUnpackAndPreprocessInputShader( 31 | gpgpu, [IMAGE_SIZE, IMAGE_SIZE], useFloatTextures); 32 | } 33 | 34 | /** 35 | * Loads necessary variables for SqueezeNet. Resolves the promise when the 36 | * variables have all been loaded. 37 | */ 38 | loadVariables() { 39 | return new Promise((resolve, reject) => { 40 | const checkpointLoader = 41 | new CheckpointLoader(GOOGLE_CLOUD_STORAGE_DIR + 'squeezenet1_1/'); 42 | checkpointLoader.getAllVariables().then(variables => { 43 | this.variables = variables; 44 | resolve(); 45 | }); 46 | }); 47 | } 48 | 49 | /** 50 | * Preprocess an RGB color texture before inferring through squeezenet. 51 | * @param rgbTexture The RGB color texture to process into an Array3D. 52 | * @param imageDimensions The 2D dimensions of the image. 53 | */ 54 | preprocessColorTextureToArray3D(rgbTexture, imageDimensions) { 55 | const preprocessResultShapeRC = 56 | [imageDimensions[0], imageDimensions[0] * 3]; 57 | 58 | const preprocessResultTexture = 59 | this.math.getTextureManager().acquireTexture(preprocessResultShapeRC); 60 | 61 | imagenet_util.preprocessInput( 62 | this.gpgpu, this.preprocessInputShader, rgbTexture, 63 | preprocessResultTexture, preprocessResultShapeRC); 64 | return NDArray.make([imageDimensions[0], imageDimensions[0], 3], { 65 | texture: preprocessResultTexture, 66 | textureShapeRC: preprocessResultShapeRC 67 | }); 68 | } 69 | 70 | /** 71 | * Infer through SqueezeNet, assumes variables have been loaded. This does 72 | * standard ImageNet pre-processing before inferring through the model. This 73 | * method returns named activations as well as pre-softmax logits. The user 74 | * needs to clean up namedActivations after inferring. 75 | * 76 | * @param preprocessedInput preprocessed input Array. 77 | * @return Named activations and the pre-softmax logits. 78 | */ 79 | infer(preprocessedInput) { 80 | const namedActivations = {}; 81 | 82 | const avgpool10 = this.math.scope((keep) => { 83 | const conv1 = this.math.conv2d( 84 | preprocessedInput, this.variables['conv1_W:0'], 85 | this.variables['conv1_b:0'], 2, 0); 86 | const conv1relu = keep(this.math.relu(conv1)); 87 | 88 | namedActivations['conv_1'] = conv1relu; 89 | 90 | const pool1 = keep(this.math.maxPool(conv1relu, 3, 2, 0)); 91 | namedActivations['maxpool_1'] = pool1; 92 | 93 | const fire2 = keep(this.fireModule(pool1, 2)); 94 | namedActivations['fire2'] = fire2; 95 | 96 | const fire3 = keep(this.fireModule(fire2, 3)); 97 | namedActivations['fire3'] = fire3; 98 | 99 | // Because we don't have uneven padding yet, manually pad the ndarray on 100 | // the right. 101 | const fire3Reshape2d = 102 | fire3.as2D(fire3.shape[0], fire3.shape[1] * fire3.shape[2]); 103 | const fire3Sliced2d = this.math.slice2D( 104 | fire3Reshape2d, [0, 0], 105 | [fire3.shape[0] - 1, (fire3.shape[1] - 1) * fire3.shape[2]]); 106 | const fire3Sliced = fire3Sliced2d.as3D( 107 | fire3.shape[0] - 1, fire3.shape[1] - 1, fire3.shape[2]); 108 | const pool2 = keep(this.math.maxPool(fire3Sliced, 3, 2, 0)); 109 | namedActivations['maxpool_2'] = pool2; 110 | 111 | const fire4 = keep(this.fireModule(pool2, 4)); 112 | namedActivations['fire4'] = fire4; 113 | 114 | const fire5 = keep(this.fireModule(fire4, 5)); 115 | namedActivations['fire5'] = fire5; 116 | 117 | const pool3 = keep(this.math.maxPool(fire5, 3, 2, 0)); 118 | namedActivations['maxpool_3'] = pool3; 119 | 120 | const fire6 = keep(this.fireModule(pool3, 6)); 121 | namedActivations['fire6'] = fire6; 122 | 123 | const fire7 = keep(this.fireModule(fire6, 7)); 124 | namedActivations['fire7'] = fire7; 125 | 126 | const fire8 = keep(this.fireModule(fire7, 8)); 127 | namedActivations['fire8'] = fire8; 128 | 129 | const fire9 = keep(this.fireModule(fire8, 9)); 130 | namedActivations['fire9'] = fire9; 131 | 132 | const conv10 = keep(this.math.conv2d( 133 | fire9, this.variables['conv10_W:0'], 134 | this.variables['conv10_b:0'], 1, 0)); 135 | namedActivations['conv10'] = conv10; 136 | 137 | return this.math.avgPool(conv10, conv10.shape[0], 1, 0).as1D(); 138 | }); 139 | 140 | return {namedActivations, logits: avgpool10}; 141 | } 142 | 143 | fireModule(input, fireId) { 144 | const y1 = this.math.conv2d( 145 | input, this.variables['fire' + fireId + '/squeeze1x1_W:0'], 146 | this.variables['fire' + fireId + '/squeeze1x1_b:0'], 1, 0); 147 | const y2 = this.math.relu(y1); 148 | const left1 = this.math.conv2d( 149 | y2, this.variables['fire' + fireId + '/expand1x1_W:0'], 150 | this.variables['fire' + fireId + '/expand1x1_b:0'], 1, 0); 151 | const left2 = this.math.relu(left1); 152 | 153 | const right1 = this.math.conv2d( 154 | y2, this.variables['fire' + fireId + '/expand3x3_W:0'], 155 | this.variables['fire' + fireId + '/expand3x3_b:0'], 1, 1); 156 | const right2 = this.math.relu(right1); 157 | 158 | return this.math.concat3D(left2, right2, 2); 159 | } 160 | } 161 | 162 | export default SqueezeNet; -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | let AudioContext = window.AudioContext || window.webkitAudioContext; 15 | let GLOBALS = { 16 | button: { 17 | padding: 0, 18 | frontHeight: 40, 19 | states: { 20 | normal: { 21 | x: 8, 22 | y: 8 23 | }, 24 | pressed: { 25 | x: 4, 26 | y: 4 27 | } 28 | } 29 | }, 30 | classNames: [ 31 | 'green', 32 | 'purple', 33 | 'orange', 34 | 'red' 35 | ], 36 | colors: { 37 | 'green': '#2baa5e', 38 | 'purple': '#c95ac5', 39 | 'orange': '#dd4d31', 40 | 'red': '#e8453c' 41 | }, 42 | rgbaColors: { 43 | 'green': 'rgba(43, 170, 94, 0.25)', 44 | 'purple': 'rgba(201, 90, 197, 0.25)', 45 | 'orange': 'rgba(221, 77, 49, 0.25)', 46 | 'red': 'rgba(232, 69, 60, 0.25)' 47 | }, 48 | classId: null, 49 | predicting: false, 50 | micThreshold: 25, 51 | classesTrained: { 52 | 'green': false, 53 | 'purple': false, 54 | 'orange': false 55 | }, 56 | numClasses: 3, 57 | audioContext: new AudioContext(), 58 | isBackFacingCam: false 59 | }; 60 | 61 | export default GLOBALS; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import TweenMax from 'gsap'; 16 | 17 | import GLOBALS from './config.js'; 18 | import Button from './ui/components/Button.js'; 19 | import IntroSection from './ui/modules/IntroSection.js'; 20 | import InputSection from './ui/modules/InputSection.js'; 21 | import LearningSection from './ui/modules/LearningSection.js'; 22 | import OutputSection from './ui/modules/OutputSection.js'; 23 | import Wizard from './ui/modules/Wizard.js'; 24 | import Recording from './ui/modules/Recording'; 25 | import RecordOpener from './ui/components/RecordOpener.js'; 26 | import LaunchScreen from './ui/modules/wizard/LaunchScreen.js'; 27 | import BrowserUtils from './ui/components/BrowserUtils'; 28 | 29 | function init() { 30 | 31 | // Shim for forEach for IE/Edge 32 | if (typeof NodeList.prototype.forEach !== 'function') { 33 | NodeList.prototype.forEach = Array.prototype.forEach; 34 | } 35 | 36 | GLOBALS.browserUtils = new BrowserUtils(); 37 | GLOBALS.launchScreen = new LaunchScreen(); 38 | 39 | GLOBALS.learningSection = new LearningSection(document.querySelector('#learning-section')); 40 | GLOBALS.inputSection = new InputSection(document.querySelector('#input-section')); 41 | GLOBALS.outputSection = new OutputSection(document.querySelector('#output-section')); 42 | GLOBALS.recordOpener = new RecordOpener(document.querySelector('#record-open-section')); 43 | 44 | GLOBALS.inputSection.ready(); 45 | GLOBALS.learningSection.ready(); 46 | GLOBALS.wizard = new Wizard(); 47 | GLOBALS.recordSection = new Recording(document.querySelector('#recording')); 48 | if (localStorage.getItem('isBackFacingCam') && localStorage.getItem('isBackFacingCam') === 'true') { 49 | GLOBALS.isBackFacingCam = true; 50 | } 51 | 52 | // Camera status messages per browser 53 | if (GLOBALS.browserUtils.isChrome && !GLOBALS.browserUtils.isEdge) { 54 | document.querySelector('.input__media__activate').innerHTML = 'To teach your machine, you need to click up here to turn on your camera and then refresh the page. you need to refresh the page and allow camera access.

'; 55 | 56 | if (!GLOBALS.browserUtils.isCompatable) { 57 | document.querySelector('.wizard__browser-warning').innerHTML = 'Something went wrong and we could not load the site, please try restarting your browser.'; 58 | } 59 | }else if (GLOBALS.browserUtils.isSafari) { 60 | document.querySelector('.input__media__activate').innerHTML = 'To teach your machine, you need to turn on your camera. To do this click "Safari" in the menu bar, navigate to "Settings for This Website", in the "Camera" drop down menu choose "Allow" and then refresh the page.'; 61 | }else if (GLOBALS.browserUtils.isFirefox) { 62 | document.querySelector('.input__media__activate').innerHTML = 'To teach your machine, you need to turn on your camera. To do this you need to click this icon to grant access and refresh the page.'; 63 | } 64 | } 65 | 66 | window.addEventListener('load', init); 67 | 68 | export default GLOBALS; -------------------------------------------------------------------------------- /src/outputs/sound/SoundSearch.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class SoundSearch { 16 | constructor(options) { 17 | this.playCallback = options.playCallback; 18 | this.selectCallback = options.selectCallback; 19 | this.assets = options.assets; 20 | 21 | this.element = document.createElement('div'); 22 | this.element.classList.add('output__sound-search'); 23 | this.searchBar = document.createElement('div'); 24 | this.searchBar.classList.add('output__sound-search-bar'); 25 | this.element.appendChild(this.searchBar); 26 | this.searchInput = document.createElement('input'); 27 | this.searchInput.classList.add('output__sound-search-input'); 28 | this.searchBar.appendChild(this.searchInput); 29 | this.backButton = document.createElement('div'); 30 | this.backButton.addEventListener('click', this.hide.bind(this)); 31 | this.backButton.classList.add('output__sound-back'); 32 | 33 | let loader = ((el) => { 34 | let ajax = new XMLHttpRequest(); 35 | ajax.open('GET', 'assets/outputs/back-icon.svg', true); 36 | ajax.onload = (event) => { 37 | el.innerHTML = ajax.responseText; 38 | }; 39 | ajax.send(); 40 | })(this.backButton); 41 | 42 | this.searchBar.appendChild(this.backButton); 43 | 44 | this.searchResults = document.createElement('div'); 45 | this.searchResults.classList.add('output__sound-search-results'); 46 | 47 | this.allResults = []; 48 | for (let index = 0; index < this.assets.length; index += 1) { 49 | let item = document.createElement('div'); 50 | item.classList.add('output__sound-search-result'); 51 | 52 | let icon = document.createElement('div'); 53 | icon.classList.add('output__sound-search-result-icon'); 54 | 55 | let label = document.createElement('input'); 56 | label.setAttribute('readonly', 'readonly'); 57 | label.classList.add('output__sound-search-result-input'); 58 | 59 | // icon.classList.add('output__sound-search-result-play'); 60 | let loader = ((el) => { 61 | let ajax = new XMLHttpRequest(); 62 | ajax.open('GET', 'assets/outputs/play-icon.svg', true); 63 | ajax.onload = (event) => { 64 | el.innerHTML = ajax.responseText; 65 | }; 66 | ajax.send(); 67 | })(icon); 68 | 69 | item.value = this.assets[index]; 70 | label.value = item.value; 71 | icon.addEventListener('click', this.playCallback); 72 | this.allResults.push(item); 73 | 74 | item.appendChild(icon); 75 | item.appendChild(label); 76 | 77 | item.addEventListener('click', this.selectCallback); 78 | 79 | this.searchResults.appendChild(item); 80 | } 81 | this.element.appendChild(this.searchResults); 82 | this.searchInput.addEventListener('keyup', this.filterResults.bind(this)); 83 | } 84 | 85 | hide() { 86 | this.element.style.display = 'none'; 87 | this.searchResults.className = 'output__sound-search-results'; 88 | this.searchInput.className = 'output__sound-search-input'; 89 | this.backButton.className = 'output__sound-back'; 90 | 91 | this.allResults.forEach((item) => { 92 | let icon = item.children[0]; 93 | icon.className = 'output__sound-search-result-icon'; 94 | }); 95 | this.visible = false; 96 | } 97 | 98 | show(classId) { 99 | this.element.style.display = 'block'; 100 | this.searchResults.classList.add(`output__sound-search-results--${classId}`); 101 | this.searchInput.classList.add(`output__sound-search-input--${classId}`); 102 | this.backButton.classList.add(`output__sound-back--${classId}`); 103 | 104 | this.allResults.forEach((item) => { 105 | let icon = item.children[0]; 106 | icon.classList.add(`output__sound-search-result-icon--${classId}`); 107 | }); 108 | this.searchInput.focus(); 109 | this.visible = true; 110 | } 111 | 112 | filterResults() { 113 | let phrase = this.searchInput.value; 114 | let showAll = false; 115 | 116 | if (phrase.length === 0) { 117 | showAll = true; 118 | } 119 | 120 | this.allResults.forEach((item) => { 121 | if (showAll) { 122 | item.style.display = 'block'; 123 | }else if (item.value.toLowerCase().indexOf(phrase.toLowerCase()) > -1) { 124 | item.style.display = 'block'; 125 | }else { 126 | item.style.display = 'none'; 127 | } 128 | }); 129 | } 130 | } 131 | 132 | export default SoundSearch; -------------------------------------------------------------------------------- /src/outputs/speech/TextToSpeech.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class TextToSpeech { 16 | constructor() { 17 | this.voices = []; 18 | this.voice = null; 19 | this.message = null; 20 | if (typeof speechSynthesis === 'object' && typeof speechSynthesis.onvoiceschanged === 'function') { 21 | speechSynthesis.onvoiceschanged = this.setVoice; 22 | } 23 | 24 | } 25 | 26 | setVoice() { 27 | this.voices = window.speechSynthesis.getVoices(); 28 | this.voice = this.voices.filter(function(voice) { 29 | return voice.name === 'Google US English Female'; 30 | })[0]; 31 | } 32 | 33 | stop() { 34 | window.speechSynthesis.cancel(); 35 | } 36 | 37 | say(text, callback) { 38 | this.message = new SpeechSynthesisUtterance(); 39 | this.message.text = text; 40 | this.message.voice = this.voice; 41 | this.message.rate = 0.9; 42 | this.message.lang = 'en-US'; 43 | this.message.addEventListener('end', callback); 44 | window.speechSynthesis.speak(this.message); 45 | } 46 | } 47 | 48 | export default TextToSpeech; -------------------------------------------------------------------------------- /src/ui/components/BrowserUtils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class BrowserUtils { 16 | 17 | constructor() { 18 | /* eslint-disable */ 19 | this.isAndroid = navigator.userAgent.toLowerCase().indexOf('android') > -1; 20 | this.isSafari = navigator.userAgent.indexOf('Safari') > -1; 21 | this.isFirefox = navigator.userAgent.indexOf('Firefox') > -1; 22 | this.isIE = navigator.userAgent.indexOf('MSIE') > -1; 23 | this.isEdge = navigator.userAgent.indexOf('Edge') > -1; 24 | this.isMobile = false; 25 | this.isChrome = /chrome/i.test(navigator.userAgent); 26 | this.isCompatible; 27 | 28 | if (this.isChrome) { 29 | this.isSafari = false; 30 | } 31 | 32 | if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 33 | || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) { 34 | this.isMobile = true; 35 | } 36 | 37 | if (this.isMobile) { 38 | let androidVersion = this.getAndroidVersion(); 39 | if (androidVersion) { 40 | this.isSafari = false; 41 | } 42 | if (androidVersion && androidVersion >= 7) { 43 | this.isCompatible = true; 44 | } 45 | 46 | if ( 47 | navigator.mediaDevices && 48 | navigator.mediaDevices.getUserMedia && 49 | this.isSafari 50 | ) { 51 | this.isCompatible = true; 52 | } 53 | } else { 54 | if ( 55 | navigator.mediaDevices && 56 | navigator.mediaDevices.getUserMedia && 57 | !this.isEdge && 58 | !this.isIE 59 | ) { 60 | this.isCompatible = true; 61 | } 62 | } 63 | if (this.isCompatible) { 64 | this.isCompatible = this.webglSupport(); 65 | } 66 | } 67 | 68 | webglSupport() { 69 | var canvas; 70 | var ctx; 71 | var exts; 72 | var support = false; 73 | 74 | try { 75 | canvas = document.createElement('canvas'); 76 | ctx = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 77 | exts = ctx.getSupportedExtensions(); 78 | } catch (e) { 79 | return support; 80 | } 81 | 82 | if (ctx !== undefined) { 83 | support = true; 84 | } 85 | 86 | for (var i = -1, len = exts.length; ++i < len; ){ 87 | support = true; 88 | } 89 | 90 | canvas = undefined; 91 | console.log(support); 92 | return support; 93 | }; 94 | 95 | getAndroidVersion() { 96 | let ua = (navigator.userAgent).toLowerCase(); 97 | let match = ua.match(/android\s([0-9\.]*)/); 98 | return match ? parseInt(match[1], 10) : false; 99 | } 100 | /* eslint-enable */ 101 | } 102 | 103 | export default BrowserUtils; -------------------------------------------------------------------------------- /src/ui/components/Button.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import TweenMax from 'gsap'; 16 | import GLOBALS from './../../config.js'; 17 | 18 | class Button { 19 | 20 | constructor(element) { 21 | this.initialElement = element.innerHTML; 22 | 23 | this.element = element; 24 | 25 | element.addEventListener('mousedown', this.mousedown.bind(this)); 26 | element.addEventListener('mouseup', this.mouseup.bind(this)); 27 | element.addEventListener('touchstart', this.mousedown.bind(this)); 28 | element.addEventListener('touchend', this.mouseup.bind(this)); 29 | element.addEventListener('click', this.click.bind(this)); 30 | 31 | window.addEventListener('resize', () => { 32 | clearTimeout(this.sizeTimeout); 33 | this.sizeTimeout = setTimeout(() => { 34 | this.size(); 35 | }, 300); 36 | }); 37 | this.size(); 38 | this.selected = false; 39 | } 40 | 41 | click(event) { 42 | event.preventDefault(); 43 | } 44 | 45 | select() { 46 | this.element.classList.add('button__toggle--selected'); 47 | this.selected = true; 48 | this.down(); 49 | } 50 | 51 | deselect() { 52 | this.element.classList.remove('button__toggle--selected'); 53 | this.selected = false; 54 | this.up(); 55 | } 56 | 57 | mousedown(event) { 58 | event.preventDefault(); 59 | this.down(); 60 | } 61 | 62 | mouseup(event) { 63 | if (this.selected) { 64 | return; 65 | } 66 | event.preventDefault(); 67 | this.up(); 68 | } 69 | 70 | down() { 71 | TweenMax.to(this.content, 0.12, { 72 | x: -(this.depthX - this.depthXPressed), 73 | y: (this.depthY - this.depthYPressed) 74 | }); 75 | } 76 | 77 | up() { 78 | TweenMax.to(this.content, 0.12, { 79 | x: 0, 80 | y: 0 81 | }); 82 | } 83 | 84 | html() { 85 | return this.el; 86 | } 87 | 88 | setText(text) { 89 | this.label.children[0].innerHTML = text; 90 | } 91 | 92 | size() { 93 | let element = this.element; 94 | element.innerHTML = this.initialElement; 95 | element.style.height = 'auto'; 96 | element.style.width = 'auto'; 97 | let textWidth = element.offsetWidth; 98 | let textHeight = element.offsetHeight; 99 | let depthX = GLOBALS.button.states.normal.x; 100 | let depthY = GLOBALS.button.states.normal.y; 101 | 102 | let depthXPressed = GLOBALS.button.states.pressed.x; 103 | let depthYPressed = GLOBALS.button.states.pressed.y; 104 | 105 | 106 | if (element.classList.contains('button__toggle')) { 107 | textWidth += 3.5; 108 | this.isToggle = true; 109 | } 110 | 111 | if (element.classList.contains('button--large')) { 112 | textHeight += 20; 113 | textWidth += 20; 114 | } 115 | 116 | let frontWidth = textWidth + GLOBALS.button.padding; 117 | let frontHeight = textHeight < GLOBALS.button.frontHeight ? GLOBALS.button.frontHeight : textHeight; 118 | 119 | if (element.classList.contains('button--small')) { 120 | textWidth = 36; 121 | textHeight = 30; 122 | 123 | frontWidth = textWidth; 124 | frontHeight = textHeight; 125 | } 126 | 127 | frontWidth = textWidth - GLOBALS.button.states.normal.x; 128 | 129 | let buttonWidth = frontWidth + GLOBALS.button.states.normal.x; 130 | let buttonHeight = frontHeight + GLOBALS.button.states.normal.y; 131 | 132 | var colorClass = 'grey'; 133 | element.classList.forEach(function(className) { 134 | if (className.indexOf('button--color-') > -1) { 135 | let index = className.indexOf('button--color-') + 'button--color-'.length; 136 | colorClass = className.slice(index); 137 | } 138 | }); 139 | 140 | let buttonContent = element.children[0]; 141 | buttonContent.classList.remove('button__content'); 142 | let htmlContent = element.innerHTML; 143 | element.innerHTML = ''; 144 | 145 | element.style.width = buttonWidth + 'px'; 146 | element.style.height = buttonHeight + 'px'; 147 | 148 | 149 | let mask = document.createElement('div'); 150 | mask.classList.add('button__mask'); 151 | 152 | let content = document.createElement('div'); 153 | content.classList.add('button__inner'); 154 | 155 | 156 | let label = document.createElement('div'); 157 | label.classList.add('button__label'); 158 | label.innerHTML = htmlContent; 159 | label.style.width = frontWidth + 'px'; 160 | label.style.height = frontHeight + 'px'; 161 | // label.style.lineHeight = frontHeight + 'px'; 162 | label.style.left = depthX + 'px'; 163 | content.appendChild(label); 164 | label.style.top = '10px'; 165 | 166 | this.label = label; 167 | 168 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 169 | svg.setAttribute('width', buttonWidth); 170 | svg.setAttribute('height', buttonHeight); 171 | this.svg = svg; 172 | 173 | let frontFace = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 174 | frontFace.classList.add('front-face'); 175 | frontFace.classList.add('front-face--' + colorClass); 176 | frontFace.setAttribute('d', `M${depthX} 0 ${frontWidth + depthX} 0 ${frontWidth + depthX} ${frontHeight} ${depthX} ${frontHeight}z`); 177 | svg.appendChild(frontFace); 178 | this.frontFace = frontFace; 179 | 180 | let bottomFace = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 181 | bottomFace.classList.add('bottom-face'); 182 | bottomFace.classList.add('bottom-face--' + colorClass); 183 | bottomFace.setAttribute('d', `M${depthX} ${frontHeight} ${frontWidth + depthX} ${frontHeight} ${frontWidth} ${frontHeight + depthY} 0 ${frontHeight + depthY}z`); 184 | svg.appendChild(bottomFace); 185 | this.bottomFace = bottomFace; 186 | 187 | let leftFace = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 188 | leftFace.classList.add('left-face'); 189 | leftFace.classList.add('left-face--' + colorClass); 190 | leftFace.setAttribute('d', `M0 ${depthY} ${depthX} 0 ${depthX} ${frontHeight} 0 ${frontHeight + depthY}z`); 191 | svg.appendChild(leftFace); 192 | this.leftFace = leftFace; 193 | 194 | content.appendChild(svg); 195 | mask.appendChild(content); 196 | element.appendChild(mask); 197 | 198 | 199 | // Editable for reszing 200 | this.label = label; 201 | this.svg = svg; 202 | this.frontFace = frontFace; 203 | this.bottomFace = bottomFace; 204 | this.leftFace = leftFace; 205 | 206 | this.parentWidth = element.parentNode.offsetWidth; 207 | this.buttonWidth = buttonWidth; 208 | this.buttonHeight = buttonHeight; 209 | this.textWidth = textWidth; 210 | this.textHeight = textHeight; 211 | this.frontWidth = frontWidth; 212 | this.frontHeight = frontHeight; 213 | this.depthX = depthX; 214 | this.depthY = depthY; 215 | this.depthXPressed = depthXPressed; 216 | this.depthYPressed = depthYPressed; 217 | 218 | this.buttonWidth = buttonWidth; 219 | this.buttonHeight = buttonHeight; 220 | 221 | this.content = content; 222 | this.depthX = depthX; 223 | this.depthY = depthY; 224 | this.depthXPressed = depthXPressed; 225 | this.depthYPressed = depthYPressed; 226 | 227 | this.element = element; 228 | 229 | 230 | this.selected = false; 231 | 232 | } 233 | } 234 | 235 | 236 | export default Button; -------------------------------------------------------------------------------- /src/ui/components/CamInput.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class CamInput { 16 | constructor() { 17 | this.element = document.createElement('div'); 18 | this.element.classList.add('input__camera'); 19 | 20 | this.webcamClassifier = new WebcamClassifier(); 21 | this.element.appendChild(this.webcamClassifier.video); 22 | this.webcamClassifier.video.setAttribute('muted', 'true'); 23 | this.webcamClassifier.video.classList.add('input__camera-video'); 24 | this.webcamClassifier.video.addEventListener( 25 | 'loadeddata', this.videoLoaded.bind(this)); 26 | window.addEventListener('resize', this.size.bind(this)); 27 | GLOBALS.webcamClassifier = this.webcamClassifier; 28 | this.stop(); 29 | } 30 | 31 | videoLoaded() { 32 | this.videoWidth = this.webcamClassifier.video.videoWidth; 33 | this.videoHeight = this.webcamClassifier.video.videoHeight; 34 | this.videoRatio = this.videoWidth / this.videoHeight; 35 | this.size(); 36 | } 37 | 38 | start() { 39 | this.show(); 40 | if (this.started) { 41 | return; 42 | } 43 | this.started = true; 44 | this.webcamClassifier.ready(); 45 | } 46 | 47 | stop() { 48 | this.started = false; 49 | this.webcamClassifier.stopTimer(); 50 | this.hide(); 51 | } 52 | 53 | hide() { 54 | this.element.style.display = 'none'; 55 | } 56 | 57 | show() { 58 | this.element.style.display = 'block'; 59 | this.size(); 60 | } 61 | 62 | renderExamples(canvas) { 63 | // console.log('render', canvas); 64 | } 65 | 66 | resetClass(id) { 67 | this.webcamClassifier.deleteClassData(id); 68 | } 69 | 70 | size() { 71 | this.width = this.element.offsetWidth; 72 | this.height = this.width; 73 | this.webcamClassifier.video.width = 227; 74 | this.webcamClassifier.video.height = 227; 75 | 76 | /* 77 | if (this.videoRatio > 1) { 78 | this.webcamClassifier.video.width = this.height * this.videoRatio; 79 | this.webcamClassifier.video.height = this.height; 80 | }else { 81 | this.webcamClassifier.video.width = this.width; 82 | this.webcamClassifier.video.height = this.width / this.videoRatio; 83 | }*/ 84 | } 85 | } 86 | 87 | import GLOBALS from './../../config.js'; 88 | import WebcamClassifier from './../../ai/WebcamClassifier.js'; 89 | 90 | export default CamInput; -------------------------------------------------------------------------------- /src/ui/components/GifCanvas.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class GifCanvas { 16 | 17 | constructor() { 18 | this.element = document.createElement('div'); 19 | this.canvas = document.createElement('canvas'); 20 | this.context = this.canvas.getContext('2d'); 21 | this.element.style.display = 'none'; 22 | this.canvas.width = 340; 23 | this.canvas.height = 260; 24 | this.canvas.classList.add('gifler-canvas'); 25 | this.canvas.name = 'gif'; 26 | this.element.appendChild(this.canvas); 27 | this.gifs = {}; 28 | } 29 | 30 | setSource(source) { 31 | if (GLOBALS.isRecording) { 32 | // Downsize the recorder gif 33 | let smallerSource = source.replace('200w.gif', '200_d.gif'); 34 | if (smallerSource !== this.source) { 35 | this.clear(); 36 | this.source = smallerSource; 37 | this.getGifler(smallerSource); 38 | } 39 | } 40 | } 41 | 42 | getGifler(source) { 43 | return new Promise((resolve) => { 44 | if (!this.gifs[source]) { 45 | /* eslint-disable */ 46 | window.gifler(source) 47 | .frames('canvas.gifler-canvas', this.draw.bind(this)) 48 | .then((response) => { 49 | this.gifs[source] = response; 50 | this.gifs[source].start(); 51 | resolve(this.gifs[source]); 52 | }); 53 | /* eslint-enable */ 54 | } 55 | if (this.gifs[source]) { 56 | this.gifs[source].start(); 57 | resolve(this.gifs[source]); 58 | } 59 | }); 60 | } 61 | 62 | draw(ctx, frame) { 63 | if (GLOBALS.isRecording) { 64 | let hRatio = this.canvas.width / frame.width; 65 | let vRatio = this.canvas.height / frame.height; 66 | let ratio = Math.min(hRatio, vRatio); 67 | let centerShiftX = (this.canvas.width - frame.width * ratio) / 2; 68 | let centerShiftY = (this.canvas.height - frame.height * ratio) / 2; 69 | ctx.drawImage(frame.buffer, 0, 0, frame.width, frame.height, 70 | centerShiftX, centerShiftY, frame.width * ratio, frame.height * ratio); 71 | }else { 72 | this.clear(); 73 | } 74 | } 75 | 76 | clear() { 77 | this.source = null; 78 | Object.keys(this.gifs).forEach((key) => { 79 | let gif = this.gifs[key]; 80 | gif.reset(); 81 | gif.stop(); 82 | }); 83 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 84 | } 85 | 86 | getElement() { 87 | return this.element; 88 | } 89 | } 90 | import GLOBALS from '../../index'; 91 | export default GifCanvas; -------------------------------------------------------------------------------- /src/ui/components/HighlightArrow.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class HighlightArrow { 16 | constructor(type) { 17 | this.element = new Image(); 18 | this.element.classList.add('wizard__arrow'); 19 | this.element.width = 200; 20 | this.element.src = `assets/arrows/arrow-${type}.svg`; 21 | } 22 | 23 | hide() { 24 | this.element.style.display = 'none'; 25 | } 26 | 27 | show() { 28 | this.element.style.display = 'block'; 29 | } 30 | } 31 | 32 | export default HighlightArrow; -------------------------------------------------------------------------------- /src/ui/components/RecordOpener.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class RecordOpener { 16 | constructor(element) { 17 | this.element = element; 18 | this.openButton = new Button(document.querySelector('#open-recorder')); 19 | 20 | this.openButton.element.addEventListener('click', this.open.bind(this)); 21 | // GLOBALS.outputSection.onChangeHandler = () => { 22 | // this.enable(); 23 | // }; 24 | if (!GLOBALS.browserUtils.isChrome || GLOBALS.browserUtils.isMobile) { 25 | document.getElementById('record-open-section').style.display = 'none'; 26 | } 27 | } 28 | 29 | disable() { 30 | this.openButton.element.classList.add('disabled'); 31 | } 32 | 33 | enable() { 34 | this.openButton.element.classList.remove('disabled'); 35 | } 36 | 37 | open() { 38 | TweenLite.to(window, 0.3, {scrollTo: 1}); 39 | 40 | if ( 41 | GLOBALS.outputSection.currentOutput && 42 | GLOBALS.outputSection.currentOutput.element.querySelector('canvas') 43 | 44 | ) { 45 | setTimeout(() => { 46 | GLOBALS.recordSection.setCanvas(GLOBALS.outputSection.currentOutput.element.querySelector('canvas')); 47 | }, 500); 48 | } 49 | } 50 | } 51 | 52 | import GLOBALS from './../../config.js'; 53 | import Button from './../components/Button.js'; 54 | import TweenLite from 'gsap'; 55 | 56 | export default RecordOpener; -------------------------------------------------------------------------------- /src/ui/components/Selector.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class Selector { 16 | constructor(element) { 17 | this.element = element; 18 | let wrapper = element.parentNode; 19 | 20 | this.element.addEventListener('change', this.change.bind(this)); 21 | this.titleWrapper = document.createElement('div'); 22 | this.titleWrapper.classList.add('output__selector-title-wrapper'); 23 | this.title = document.createElement('span'); 24 | this.title.classList.add('output__selector-title'); 25 | this.titleWrapper.appendChild(this.title); 26 | 27 | wrapper.appendChild(this.titleWrapper); 28 | 29 | this.change(); 30 | } 31 | 32 | change() { 33 | let value = this.element.value; 34 | let text = this.element.options[this.element.selectedIndex].textContent; 35 | this.title.textContent = text; 36 | } 37 | } 38 | 39 | import GLOBALS from './../../config.js'; 40 | 41 | export default Selector; -------------------------------------------------------------------------------- /src/ui/modules/InputSection.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class InputSection { 16 | constructor(element) { 17 | this.element = element; 18 | // this.camInputToggle = new Button(element.querySelector('#cam-input-toggle')); 19 | // this.micInputToggle = new Button(element.querySelector('#mic-input-toggle')); 20 | // this.camInputToggle.deselect(); 21 | // this.micInputToggle.deselect(); 22 | // this.camInputToggle.element.addEventListener('click', this.selectCamInput.bind(this)); 23 | // this.micInputToggle.element.addEventListener('click', this.selectMicInput.bind(this)); 24 | // this.camInputToggle.element.addEventListener('touchend', this.selectCamInput.bind(this)); 25 | // this.micInputToggle.element.addEventListener('touchend', this.selectMicInput.bind(this)); 26 | this.mediaFlipButton = element.querySelector('.input__media__flip'); 27 | this.mediaFlipButton.addEventListener('click', this.flipCamera.bind(this)); 28 | 29 | this.inputContainer = element.querySelector('.input__media'); 30 | 31 | this.currentInput = null; 32 | // if (!GLOBALS.browserUtils.isMobile) { 33 | // this.createMicInput(); 34 | // this.createCamInput(); 35 | // this.selectCamInput(); 36 | // this.camInputToggle.select(); 37 | // } 38 | 39 | this.arrow = new HighlightArrow(1); 40 | 41 | if (GLOBALS.browserUtils.isMobile) { 42 | TweenMax.set(this.arrow.element, { 43 | rotation: -120, 44 | scaleX: -0.5, 45 | x: -20, 46 | y: -60 47 | }); 48 | }else { 49 | TweenMax.set(this.arrow.element, { 50 | rotation: -120, 51 | scaleX: -0.8, 52 | x: -20, 53 | y: -120 54 | }); 55 | } 56 | this.element.appendChild(this.arrow.element); 57 | 58 | this.gifs = []; 59 | 60 | let gif1 = new WizardGIFExample('assets/wizard/1.gif'); 61 | if (GLOBALS.browserUtils.isMobile) { 62 | TweenMax.set(gif1.element, { 63 | rotation: -5, 64 | scale: 0.65, 65 | x: 40, 66 | y: 275 67 | }); 68 | }else { 69 | TweenMax.set(gif1.element, { 70 | rotation: -5, 71 | scale: 0.65, 72 | x: 70, 73 | y: -25 74 | }); 75 | } 76 | this.element.appendChild(gif1.element); 77 | this.gifs.push(gif1); 78 | 79 | 80 | let gif2 = new WizardGIFExample('assets/wizard/2.gif'); 81 | if (GLOBALS.browserUtils.isMobile) { 82 | TweenMax.set(gif2.element, { 83 | rotation: -5, 84 | scale: 0.65, 85 | x: 40, 86 | y: 275 87 | }); 88 | }else { 89 | TweenMax.set(gif2.element, { 90 | rotation: -5, 91 | scale: 0.65, 92 | x: 70, 93 | y: -25 94 | }); 95 | } 96 | this.element.appendChild(gif2.element); 97 | this.gifs.push(gif2); 98 | 99 | let gif3 = new WizardGIFExample('assets/wizard/3.gif'); 100 | if (GLOBALS.browserUtils.isMobile) { 101 | TweenMax.set(gif3.element, { 102 | rotation: -5, 103 | scale: 0.65, 104 | x: 40, 105 | y: 275 106 | }); 107 | }else { 108 | TweenMax.set(gif3.element, { 109 | rotation: -5, 110 | scale: 0.65, 111 | x: 70, 112 | y: -25 113 | }); 114 | } 115 | this.element.appendChild(gif3.element); 116 | this.gifs.push(gif3); 117 | 118 | let gif4 = new WizardGIFExample('assets/wizard/4.gif'); 119 | if (GLOBALS.browserUtils.isMobile) { 120 | TweenMax.set(gif4.element, { 121 | rotation: -5, 122 | scale: 0.65, 123 | x: 40, 124 | y: 275 125 | }); 126 | }else { 127 | TweenMax.set(gif4.element, { 128 | rotation: -5, 129 | scale: 0.65, 130 | x: 70, 131 | y: -25 132 | }); 133 | } 134 | this.element.appendChild(gif4.element); 135 | this.gifs.push(gif4); 136 | 137 | } 138 | 139 | showGif(index) { 140 | this.gifs[index].show(); 141 | } 142 | 143 | hideGif(index) { 144 | this.gifs[index].hide(); 145 | } 146 | 147 | ready() { 148 | if (!GLOBALS.browserUtils.isMobile) { 149 | this.createCamInput(); 150 | this.selectCamInput(); 151 | } 152 | } 153 | 154 | highlight() { 155 | this.arrow.show(); 156 | TweenMax.from(this.arrow.element, 0.3, {opacity: 0}); 157 | } 158 | 159 | dehighlight() { 160 | TweenMax.killTweensOf(this.arrow.element); 161 | this.arrow.hide(); 162 | } 163 | 164 | enable(highlight) { 165 | this.element.classList.remove('section--disabled'); 166 | 167 | if (highlight) { 168 | this.highlight(); 169 | }else { 170 | this.dehighlight(); 171 | } 172 | } 173 | 174 | disable() { 175 | this.element.classList.add('section--disabled'); 176 | this.dehighlight(); 177 | } 178 | 179 | dim() { 180 | this.element.classList.add('dimmed'); 181 | } 182 | 183 | undim() { 184 | this.element.classList.remove('dimmed'); 185 | } 186 | 187 | createCamInput() { 188 | if (!this.camInput) { 189 | this.camInput = new CamInput(); 190 | this.inputContainer.appendChild(this.camInput.element); 191 | GLOBALS.camInput = this.camInput; 192 | // GLOBALS.camInput.start(); 193 | } 194 | } 195 | 196 | selectCamInput() { 197 | this.createCamInput(); 198 | this.currentInput = this.camInput; 199 | } 200 | 201 | 202 | resetClass(id) { 203 | this.camInput.resetClass(id); 204 | } 205 | 206 | flipCamera(event) { 207 | event.preventDefault(); 208 | if (!GLOBALS.browserUtils.isAndroid) { 209 | GLOBALS.isBackFacingCam = !GLOBALS.isBackFacingCam; 210 | GLOBALS.webcamClassifier.loaded = false; 211 | GLOBALS.webcamClassifier.ready(); 212 | } 213 | if (GLOBALS.browserUtils.isAndroid) { 214 | /*eslint-disable */ 215 | if (confirm('Switching camera will clear your trained classes and reload the page.')) { 216 | /* eslint-enable */ 217 | GLOBALS.isBackFacingCam = !GLOBALS.isBackFacingCam; 218 | localStorage.setItem('isBackFacingCam', GLOBALS.isBackFacingCam.toString()); 219 | location.reload(); 220 | } 221 | } 222 | } 223 | } 224 | 225 | import TweenMax from 'gsap'; 226 | import GLOBALS from './../../config.js'; 227 | import Button from './../components/Button.js'; 228 | import CamInput from './../components/CamInput.js'; 229 | import HighlightArrow from './../components/HighlightArrow.js'; 230 | import WizardGIFExample from './WizardGIFExample.js'; 231 | 232 | export default InputSection; -------------------------------------------------------------------------------- /src/ui/modules/IntroSection.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class IntroSection { 16 | constructor(element) { 17 | this.videoPlayButton = element.querySelector('#video-play-link'); 18 | this.videoPlayButton.addEventListener('click', this.openVideoPlayer.bind(this)); 19 | this.closeVideoLink = element.querySelector('#video-close-link'); 20 | this.closeVideoLink.addEventListener('click', this.closeVideoPlayer.bind(this)); 21 | this.videoContainer = element.querySelector('.intro__video'); 22 | TweenMax.set(this.videoContainer, { 23 | transformOrigin: 'top center', 24 | opacity: 0 25 | }); 26 | 27 | this.player = null; 28 | this.open = false; 29 | 30 | } 31 | 32 | startVideo() { 33 | this.player.playVideo(); 34 | } 35 | 36 | openVideoPlayer() { 37 | if (this.open) { 38 | return; 39 | } 40 | 41 | if (GLOBALS.YOUTUBE_API_READY) { 42 | if (this.player) { 43 | this.player.seekTo(0); 44 | }else { 45 | this.player = new window.YT.Player('intro-video-player', { 46 | height: '100%', 47 | width: '100%', 48 | videoId: 'a1Y73sPHKxw', 49 | playerVars: { 50 | autoplay: 0, 51 | fs: 1, 52 | loop: 0, 53 | controls: 1, 54 | modestbranding: 1, 55 | rel: 0, 56 | showinfo: 0 57 | } 58 | }); 59 | } 60 | } 61 | 62 | 63 | 64 | const videoRatio = 1.7777777778; 65 | this.open = true; 66 | this.videoContainer.style.height = 'auto'; 67 | let width = this.videoContainer.offsetWidth; 68 | let height = width / videoRatio; 69 | this.videoContainer.style.height = 0 + 'px'; 70 | let that = this; 71 | 72 | TweenMax.to(this.videoPlayButton, 0.5, { 73 | opacity: 0, 74 | ease: TweenMax.Expo.easeOut 75 | }); 76 | 77 | TweenMax.to(this.videoContainer, 1, { 78 | opacity: 1, 79 | height: height, 80 | transformOrigin: 'top center', 81 | ease: TweenMax.Expo.easeOut, 82 | onComplete: that.startVideo.bind(that) 83 | }); 84 | } 85 | 86 | closeVideoPlayer() { 87 | this.open = false; 88 | if (this.player) { 89 | this.player.pauseVideo(); 90 | } 91 | TweenMax.to(this.videoContainer, 0.75, { 92 | height: 0, 93 | opacity: 0, 94 | ease: TweenMax.Expo.easeOut 95 | }); 96 | 97 | TweenMax.to(this.videoPlayButton, 0.5, { 98 | opacity: 1, 99 | delay: 0.5, 100 | ease: TweenMax.Expo.easeOut 101 | }); 102 | } 103 | } 104 | 105 | import TweenMax from 'gsap'; 106 | import GLOBALS from './../../config.js'; 107 | 108 | export default IntroSection; -------------------------------------------------------------------------------- /src/ui/modules/LearningClass.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class LearningClass { 16 | constructor(options) { 17 | this.element = options.element; 18 | this.section = options.section; 19 | this.canvas = this.element.querySelector('canvas.examples__viewer'); 20 | this.canvas.width = 98; 21 | this.canvas.height = 98; 22 | this.context = this.canvas.getContext('2d'); 23 | 24 | this.id = this.element.getAttribute('id'); 25 | this.index = options.index; 26 | this.button = new Button(this.element.querySelector('a.button--record')); 27 | this.button.element.addEventListener('mousedown', this.buttonDown.bind(this)); 28 | 29 | this.button.element.addEventListener('touchstart', this.buttonDown.bind(this)); 30 | this.button.element.addEventListener('touchend', this.buttonUp.bind(this)); 31 | 32 | this.resetLink = this.element.querySelector('.link--reset'); 33 | // this.button.element.addEventListener('mouseup', this.buttonUp.bind(this)); 34 | this.exampleCounterElement = this.element.querySelector('.examples__counter'); 35 | this.exampleCounter = 0; 36 | 37 | this.percentage = 0; 38 | this.percentageElement = this.element.querySelector('.machine__value'); 39 | this.percentageGrey = this.element.querySelector('.machine__percentage--grey'); 40 | this.percentageWhite = this.element.querySelector('.machine__percentage--white'); 41 | this.color = options.color; 42 | this.rgbaColor = options.rgbaColor; 43 | 44 | 45 | 46 | this.arrow = new HighlightArrow(3); 47 | this.arrow.element.style.left = 100 + '%'; 48 | this.arrow.element.style.top = 100 + '%'; 49 | this.arrow.element.width = 60; 50 | TweenMax.set(this.arrow.element, { 51 | rotation: 90, 52 | scale: 1, 53 | x: 10, 54 | y: -75 55 | }); 56 | this.element.appendChild(this.arrow.element); 57 | 58 | this.arrowX = new HighlightArrow(2); 59 | this.arrowX.element.style.left = 0 + '%'; 60 | this.arrowX.element.style.top = 0 + '%'; 61 | this.arrowX.element.width = 60; 62 | TweenMax.set(this.arrowX.element, { 63 | rotation: -90, 64 | scaleX: -0.8, 65 | scaleY: 0.8, 66 | x: 37, 67 | y: -30 68 | }); 69 | this.element.appendChild(this.arrowX.element); 70 | 71 | this.resetLink.addEventListener('click', this.resetClass.bind(this)); 72 | this.size(); 73 | window.addEventListener('resize', this.size.bind(this)); 74 | } 75 | 76 | hide() { 77 | this.element.style.display = 'none'; 78 | } 79 | 80 | show() { 81 | this.element.style.display = 'flex'; 82 | } 83 | 84 | highlight() { 85 | this.arrow.show(); 86 | TweenMax.from(this.arrow.element, 0.3, { 87 | opacity: 0, 88 | x: 40 89 | }); 90 | } 91 | 92 | dehighlight() { 93 | TweenMax.killTweensOf(this.arrow.element); 94 | this.arrow.hide(); 95 | } 96 | 97 | highlightX() { 98 | this.arrowX.show(); 99 | TweenMax.from(this.arrowX.element, 0.3, {opacity: 0}); 100 | } 101 | 102 | dehighlightX() { 103 | TweenMax.killTweensOf(this.arrowX.element); 104 | this.arrowX.hide(); 105 | } 106 | 107 | clear() { 108 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 109 | this.setSamples(0); 110 | } 111 | 112 | resetClass(event) { 113 | event.preventDefault(); 114 | GLOBALS.inputSection.resetClass(this.index); 115 | this.clear(); 116 | } 117 | 118 | setSamples(length) { 119 | this.exampleCounter = length; 120 | let text = this.exampleCounter; 121 | 122 | let recommendedNumSamples = (GLOBALS.inputType === 'cam') ? 30 : 10; 123 | 124 | this.exampleCounterElement.textContent = text; 125 | 126 | if (this.exampleCounter >= recommendedNumSamples && GLOBALS.classesTrained[this.id] === false) { 127 | GLOBALS.classesTrained[this.id] = true; 128 | } 129 | } 130 | 131 | setConfidence(percentage) { 132 | if (!GLOBALS.clearing) { 133 | // this.percentage = percentage; 134 | // this.updatePercentage(); 135 | let that = this; 136 | GLOBALS.recordSection.setMeters(this.id, percentage); 137 | TweenMax.to(this, 0.5, { 138 | percentage: percentage, 139 | onUpdate: () => { 140 | that.updatePercentage(); 141 | } 142 | }); 143 | } 144 | } 145 | 146 | highlightConfidence() { 147 | this.percentageElement.style.background = this.color; 148 | } 149 | 150 | dehighlightConfidence() { 151 | this.percentageElement.style.background = '#cfd1d2'; 152 | } 153 | 154 | buttonDown() { 155 | let that = this; 156 | this.button.setText('Training'); 157 | this.section.startRecording(this.index); 158 | 159 | this.buttonUpEvent = this.buttonUp.bind(this); 160 | window.addEventListener('mouseup', this.buttonUpEvent); 161 | 162 | GLOBALS.recording = true; 163 | GLOBALS.classId = this.id; 164 | 165 | GLOBALS.outputSection.toggleSoundOutput(false); 166 | clearTimeout(this.buttonClickTimeout); 167 | this.buttonClickTimeout = setTimeout(() => { 168 | GLOBALS.webcamClassifier.buttonDown(this.id, this.canvas, this); 169 | }, 100); 170 | 171 | gtag('event', 'training', {'id': this.index}); 172 | } 173 | 174 | buttonUp() { 175 | this.button.setText(`Train
${this.id}`); 176 | this.section.stopRecording(); 177 | clearTimeout(this.buttonClickTimeout); 178 | this.button.up(); 179 | 180 | GLOBALS.classId = null; 181 | GLOBALS.recording = false; 182 | 183 | GLOBALS.outputSection.toggleSoundOutput(true); 184 | 185 | GLOBALS.webcamClassifier.buttonUp(this.id, this.canvas); 186 | 187 | if (this.exampleCounter > 0) { 188 | let event = new CustomEvent('class-trained', { 189 | detail: { 190 | id: this.id, 191 | numSamples: this.exampleCounter 192 | } 193 | }); 194 | window.dispatchEvent(event); 195 | } 196 | 197 | window.removeEventListener('mouseup', this.buttonUpEvent); 198 | } 199 | 200 | updatePercentage() { 201 | let rounded = Math.floor(this.percentage); 202 | this.percentageElement.style.width = this.percentage + '%'; 203 | this.percentageWhite.textContent = rounded + '%'; 204 | 205 | if (this.timer) { 206 | clearInterval(this.timer); 207 | } 208 | this.timer = setInterval(() => { 209 | this.setConfidence(0); 210 | }, 500); 211 | } 212 | 213 | size() { 214 | this.percentageElement.style.width = 100 + '%'; 215 | let width = this.percentageElement.offsetWidth; 216 | this.percentageWhite.style.width = width + 'px'; 217 | this.percentageElement.style.width = 0 + '%'; 218 | } 219 | 220 | start() { 221 | this.size(); 222 | } 223 | } 224 | 225 | import GLOBALS from './../../config.js'; 226 | import TweenMax from 'gsap'; 227 | import Button from './../components/Button.js'; 228 | import HighlightArrow from './../components/HighlightArrow.js'; 229 | 230 | export default LearningClass; -------------------------------------------------------------------------------- /src/ui/modules/LearningSection.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class LearningSection { 16 | constructor(element) { 17 | this.element = element; 18 | let learningClassesElements = element.querySelectorAll('.learning__class'); 19 | this.condenseElement = element.querySelector('#learning-condensed-button'); 20 | this.condenseElement.addEventListener('click', this.condenseSection.bind(this)); 21 | let learningClasses = []; 22 | let that = this; 23 | this.condensed = false; 24 | 25 | that.learningClasses = []; 26 | let classNames = GLOBALS.classNames; 27 | let colors = GLOBALS.colors; 28 | 29 | learningClassesElements.forEach(function(element, index) { 30 | let id = classNames[index]; 31 | let color = colors[id]; 32 | let rgbaColor = GLOBALS.rgbaColors[id]; 33 | 34 | let options = { 35 | index: index, 36 | element: element, 37 | section: that, 38 | color: color, 39 | rgbaColor: rgbaColor 40 | }; 41 | 42 | let learningClass = new LearningClass(options); 43 | learningClass.index = index; 44 | learningClasses.push(learningClass); 45 | that.learningClasses[index] = learningClass; 46 | // learningClass.start(); 47 | }); 48 | 49 | // this.trainingQuality = new TrainingQuality(element.querySelector('.quality')); 50 | 51 | this.wiresLeft = new WiresLeft(document.querySelector('.wires--left'), learningClassesElements); 52 | this.wiresRight = new WiresRight(document.querySelector('.wires--right'), learningClassesElements); 53 | this.highestIndex = null; 54 | this.currentIndex = null; 55 | 56 | this.arrow = new HighlightArrow(2); 57 | TweenMax.set(this.arrow.element, { 58 | rotation: 90, 59 | scale: 0.6, 60 | x: 120, 61 | y: -175 62 | }); 63 | this.element.appendChild(this.arrow.element); 64 | } 65 | 66 | condenseSection() { 67 | this.condensed ? this.element.classList.remove('condensed') : this.element.classList.add('condensed'); 68 | this.condensed ? this.condensed = false : this.condensed = true; 69 | } 70 | 71 | ready() { 72 | this.learningClasses.forEach((learningClass) => { 73 | learningClass.start(); 74 | }); 75 | } 76 | 77 | 78 | highlight() { 79 | this.arrow.show(); 80 | TweenMax.from(this.arrow.element, 0.3, {opacity: 0}); 81 | } 82 | 83 | dehighlight() { 84 | TweenMax.killTweensOf(this.arrow.element, 0.3, {opacity: 0}); 85 | this.arrow.hide(); 86 | } 87 | 88 | enable(highlight) { 89 | this.element.classList.remove('section--disabled'); 90 | this.wiresLeft.element.classList.remove('wires--disabled'); 91 | this.wiresRight.element.classList.remove('wires--disabled'); 92 | 93 | if (highlight) { 94 | this.highlight(); 95 | } 96 | } 97 | 98 | disable() { 99 | this.element.classList.add('section--disabled'); 100 | this.wiresLeft.element.classList.add('wires--disabled'); 101 | this.wiresRight.element.classList.add('wires--disabled'); 102 | } 103 | 104 | dim() { 105 | this.element.classList.add('dimmed'); 106 | this.wiresLeft.element.classList.add('dimmed'); 107 | this.wiresRight.element.classList.add('dimmed'); 108 | } 109 | 110 | undim() { 111 | this.element.classList.remove('dimmed'); 112 | this.wiresLeft.element.classList.remove('dimmed'); 113 | this.wiresRight.element.classList.remove('dimmed'); 114 | } 115 | 116 | highlightClass(index) { 117 | this.learningClasses[index].highlight(); 118 | } 119 | 120 | dehighlightClass(index) { 121 | this.learningClasses[index].dehighlight(); 122 | } 123 | 124 | highlightClassX(index) { 125 | this.learningClasses[index].highlightX(); 126 | } 127 | 128 | dehighlightClassX(index) { 129 | this.learningClasses[index].dehighlightX(); 130 | } 131 | 132 | enableClass(index, highlight) { 133 | this.learningClasses[index].element.classList.remove('learning__class--disabled'); 134 | 135 | if (highlight) { 136 | this.highlightClass(index); 137 | } 138 | } 139 | 140 | disableClass(index) { 141 | this.learningClasses[index].element.classList.add('learning__class--disabled'); 142 | } 143 | 144 | clearExamples() { 145 | this.learningClasses.forEach((learningClass) => { 146 | learningClass.clear(); 147 | learningClass.setConfidence(0); 148 | learningClass.dehighlightConfidence(); 149 | }); 150 | } 151 | 152 | startRecording(id) { 153 | this.wiresLeft.highlight(id); 154 | } 155 | 156 | stopRecording() { 157 | this.wiresLeft.dehighlight(); 158 | } 159 | 160 | ledOn(id) { 161 | this.wiresRight.dehighlight(); 162 | this.wiresRight.highlight(id); 163 | } 164 | 165 | getMaxIndex(array) { 166 | let max = array[0]; 167 | let maxIndex = 0; 168 | 169 | for (let index = 1; index < array.length; index += 1) { 170 | if (array[index] > max) { 171 | maxIndex = index; 172 | max = array[index]; 173 | } 174 | } 175 | 176 | return maxIndex; 177 | } 178 | 179 | setConfidences(confidences) { 180 | const confidencesArry = Object.values(confidences); 181 | let maxIndex = this.getMaxIndex(confidencesArry); 182 | let maxValue = confidencesArry[maxIndex]; 183 | // if (maxValue > 0.5 && this.currentIndex !== maxIndex) { 184 | if (maxValue > 0.5) { 185 | this.currentIndex = maxIndex; 186 | let id = GLOBALS.classNames[this.currentIndex]; 187 | this.ledOn(id); 188 | GLOBALS.outputSection.trigger(id); 189 | } 190 | 191 | for (let index = 0; index < 3; index += 1) { 192 | this.learningClasses[index].setConfidence(confidencesArry[index] * 100); 193 | if (index === maxIndex) { 194 | this.learningClasses[index].highlightConfidence(); 195 | }else { 196 | this.learningClasses[index].dehighlightConfidence(); 197 | } 198 | } 199 | } 200 | 201 | setQuality(quality) { 202 | // this.trainingQuality.setQuality(quality); 203 | } 204 | 205 | } 206 | 207 | import GLOBALS from './../../config.js'; 208 | import TweenMax from 'gsap'; 209 | import WiresLeft from './WiresLeft.js'; 210 | import WiresRight from './WiresRight.js'; 211 | import LearningClass from './LearningClass.js'; 212 | import TrainingQuality from './TrainingQuality.js'; 213 | import HighlightArrow from './../components/HighlightArrow.js'; 214 | 215 | 216 | export default LearningSection; -------------------------------------------------------------------------------- /src/ui/modules/OutputSection.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class OutputSection { 16 | constructor(element) { 17 | this.element = element; 18 | 19 | const outputs = { 20 | GIFOutput: new GIFOutput(), 21 | SoundOutput: new SoundOutput(document.querySelector('#SoundOutput')), 22 | SpeechOutput: new SpeechOutput() 23 | }; 24 | GLOBALS.soundOutput = outputs.SoundOutput; 25 | 26 | this.classNames = GLOBALS.classNames; 27 | GLOBALS.predicting = true; 28 | 29 | this.outputs = outputs; 30 | this.loadedOutputs = []; 31 | 32 | let outputLinks = element.querySelectorAll('.output_selector__option'); 33 | outputLinks.forEach((link) => { 34 | link.addEventListener('click', this.changeOutput.bind(this)); 35 | }); 36 | this.currentLink = element.querySelector('.output_selector__option--selected'); 37 | 38 | this.outputContainer = document.querySelector('#output-player'); 39 | this.currentOutput = null; 40 | this.currentLink.click(); 41 | 42 | this.arrow = new HighlightArrow(1); 43 | 44 | TweenMax.set(this.arrow.element, { 45 | rotation: -50, 46 | scale: -0.8, 47 | x: 140, 48 | y: -100 49 | }); 50 | this.element.appendChild(this.arrow.element); 51 | } 52 | 53 | enable() { 54 | this.element.classList.remove('section--disabled'); 55 | } 56 | 57 | highlight() { 58 | this.arrow.show(); 59 | TweenMax.from(this.arrow.element, 0.3, {opacity: 0}); 60 | } 61 | 62 | dehighlight() { 63 | TweenMax.killTweensOf(this.arrow.element); 64 | this.arrow.hide(); 65 | } 66 | 67 | disable() { 68 | this.element.classList.add('section--disabled'); 69 | } 70 | 71 | dim() { 72 | this.element.classList.add('dimmed'); 73 | } 74 | 75 | undim() { 76 | this.element.classList.remove('dimmed'); 77 | } 78 | 79 | changeOutput(event) { 80 | if (this.currentLink) { 81 | this.currentLink.classList.remove('output_selector__option--selected'); 82 | } 83 | 84 | this.currentLink = event.target; 85 | this.currentLink.classList.add('output_selector__option--selected'); 86 | let outputId = this.currentLink.id; 87 | 88 | if (this.currentOutput) { 89 | this.currentOutput.stop(); 90 | this.currentOutput = null; 91 | } 92 | 93 | if (this.outputs[outputId]) { 94 | this.currentOutput = this.outputs[outputId]; 95 | } 96 | 97 | if (this.currentOutput) { 98 | this.outputContainer.appendChild(this.currentOutput.element); 99 | this.currentOutput.start(); 100 | } 101 | 102 | gtag('event', 'select_output', {'id': outputId}); 103 | } 104 | 105 | toggleSoundOutput(play) { 106 | if (this.currentOutput.id === 'SoundOutput' && play) { 107 | GLOBALS.soundOutput.playCurrentSound(); 108 | }else if (this.currentOutput.id === 'SoundOutput' && !play) { 109 | GLOBALS.soundOutput.pauseCurrentSound(); 110 | } 111 | } 112 | 113 | startWizardMode() { 114 | this.broadcastEvents = true; 115 | } 116 | 117 | stopWizardMode() { 118 | this.broadcastEvents = false; 119 | } 120 | 121 | trigger(id) { 122 | let index = this.classNames.indexOf(id); 123 | this.currentOutput.trigger(index); 124 | 125 | if (this.broadcastEvents) { 126 | let event = new CustomEvent('class-triggered', {detail: {id: id}}); 127 | window.dispatchEvent(event); 128 | } 129 | } 130 | } 131 | 132 | import TweenMax from 'gsap'; 133 | import GLOBALS from './../../config.js'; 134 | 135 | import Selector from './../components/Selector.js'; 136 | import Button from './../components/Button.js'; 137 | import CamInput from './../components/CamInput.js'; 138 | import HighlightArrow from './../components/HighlightArrow.js'; 139 | 140 | import SpeechOutput from './../../outputs/SpeechOutput.js'; 141 | import GIFOutput from './../../outputs/GIFOutput.js'; 142 | import SoundOutput from './../../outputs/SoundOutput.js'; 143 | 144 | export default OutputSection; -------------------------------------------------------------------------------- /src/ui/modules/TrainingQuality.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class TrainingQuality { 16 | constructor(element) { 17 | this.element = element; 18 | 19 | this.percentage = 0; 20 | this.percentageElement = element.querySelector('.machine__value'); 21 | this.percentageGrey = element.querySelector('.machine__percentage--grey'); 22 | this.percentageWhite = element.querySelector('.machine__percentage--white'); 23 | this.label = element.querySelector('.quality__status'); 24 | this.percentageWhite.style.width = this.percentageGrey.offsetWidth + 'px'; 25 | 26 | this.updatePercentage(); 27 | } 28 | 29 | updatePercentage() { 30 | let rounded = Math.floor(this.percentage); 31 | this.percentageElement.style.width = this.percentage + '%'; 32 | this.percentageGrey.textContent = rounded + '%'; 33 | this.percentageWhite.textContent = rounded + '%'; 34 | } 35 | 36 | setQuality(quality) { 37 | // this.percentage = quality * 100; 38 | // this.updatePercentage(); 39 | let that = this; 40 | let percentage = quality * 100; 41 | 42 | TweenMax.to(that, 0.5, { 43 | percentage: percentage, 44 | onUpdate: () => { 45 | // console.log(that.percentage); 46 | that.updatePercentage(); 47 | } 48 | }); 49 | 50 | if (percentage < 65) { 51 | this.label.textContent = 'Your machine probably won’t work well'; 52 | }else if (percentage > 90) { 53 | this.label.textContent = 'Perfect!'; 54 | }else { 55 | this.label.textContent = 'Looking good'; 56 | } 57 | } 58 | 59 | start() { 60 | // let that = this; 61 | // TweenMax.to(that, (Math.random() * 4) + 0.5, { 62 | // percentage: Math.floor(Math.random() * 100), 63 | // onUpdate: function() { 64 | // that.updatePercentage(); 65 | // }, 66 | // repeat: -1, 67 | // yoyo: true 68 | // }); 69 | } 70 | } 71 | 72 | import TweenMax from 'gsap'; 73 | 74 | export default TrainingQuality; -------------------------------------------------------------------------------- /src/ui/modules/WiresLeft.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import GLOBALS from './../../config.js'; 16 | 17 | class WiresLeft { 18 | constructor(element, learningClasses) { 19 | this.element = element; 20 | this.learningClasses = learningClasses; 21 | this.offsetY = 0; 22 | this.canvas = document.createElement('canvas'); 23 | this.size(); 24 | this.element.appendChild(this.canvas); 25 | this.wireGeneral = this.element.querySelector('.st0'); 26 | this.wireGreen = this.element.querySelector('.wire-green'); 27 | this.wirePurple = this.element.querySelector('.wire-purple'); 28 | this.wireOrange = this.element.querySelector('.wire-orange'); 29 | this.context = this.canvas.getContext('2d'); 30 | this.vertical = true; 31 | window.addEventListener('resize', () => { 32 | if (window.innerWidth <= 900) { 33 | this.canvas.style.display = 'none'; 34 | }else { 35 | this.canvas.style.display = 'block'; 36 | } 37 | }); 38 | this.currentAnimator = null; 39 | this.renderOnce = true; 40 | this.render(); 41 | } 42 | 43 | render(once) { 44 | this.context.clearRect(0, 0, this.width, this.height); 45 | this.context.lineWidth = 3; 46 | 47 | for (let index = 0; index < 3; index += 1) { 48 | 49 | let startY = this.startY + (this.startSpace * index); 50 | let endY = this.endY + (this.endSpace * index); 51 | 52 | let start = { 53 | x: 0, 54 | y: this.startY + (this.startSpace * index) 55 | }; 56 | 57 | let end = { 58 | x: this.endX, 59 | y: this.endY + (this.endSpace * index) 60 | }; 61 | 62 | let cp1 = { 63 | x: 35, 64 | y: start.y 65 | }; 66 | 67 | let cp2 = { 68 | x: 10, 69 | y: end.y 70 | }; 71 | 72 | this.context.strokeStyle = '#cfd1d2'; 73 | 74 | if (this.animator[index].highlight) { 75 | this.context.strokeStyle = this.animator[index].color; 76 | } 77 | 78 | this.context.beginPath(); 79 | this.context.moveTo(start.x, start.y); 80 | this.context.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y); 81 | this.context.lineTo(start.x + 100, start.y); 82 | this.context.stroke(); 83 | } 84 | 85 | if (this.renderOnce) { 86 | this.renderOnce = false; 87 | }else { 88 | this.timer = requestAnimationFrame(this.render.bind(this)); 89 | } 90 | 91 | } 92 | 93 | camMode() { 94 | if (this.vert) { 95 | this.element.style.left = 50 + '%'; 96 | } 97 | this.offsetY = -2; 98 | this.startY = (this.height / 2) + this.offsetY; 99 | this.renderOnce = true; 100 | this.render(); 101 | } 102 | 103 | 104 | highlight(index) { 105 | this.currentAnimator = this.animator[index]; 106 | this.currentAnimator.highlight = true; 107 | this.start(); 108 | 109 | switch (index) { 110 | case 0: 111 | this.wireGreen.classList.add('animate'); 112 | break; 113 | case 1: 114 | this.wireOrange.classList.add('animate'); 115 | break; 116 | case 2: 117 | this.wirePurple.classList.add('animate'); 118 | break; 119 | default: 120 | } 121 | } 122 | 123 | dehighlight(index) { 124 | if (this.currentAnimator) { 125 | this.currentAnimator.highlight = false; 126 | this.currentAnimator = null; 127 | this.stop(); 128 | this.renderOnce = true; 129 | this.render(); 130 | } 131 | this.wireGreen.classList.remove('animate'); 132 | this.wirePurple.classList.remove('animate'); 133 | this.wireOrange.classList.remove('animate'); 134 | } 135 | 136 | start() { 137 | this.stop(); 138 | this.timer = requestAnimationFrame(this.render.bind(this)); 139 | } 140 | 141 | stop() { 142 | if (this.timer) { 143 | cancelAnimationFrame(this.timer); 144 | } 145 | } 146 | 147 | size() { 148 | const BREAKPOINT_DESKTOP = 900; 149 | if (window.innerWidth <= BREAKPOINT_DESKTOP) { 150 | this.canvas.style.display = 'none'; 151 | } 152 | 153 | this.width = this.element.offsetWidth; 154 | 155 | let firstLearningClass = this.learningClasses[0]; 156 | let lastLearningClass = this.learningClasses[2]; 157 | 158 | let classesHeight = lastLearningClass.offsetTop - firstLearningClass.offsetTop; 159 | 160 | this.height = 440; 161 | 162 | // remove offset on desktop 163 | this.element.setAttribute('style', ''); 164 | this.endSpace = classesHeight / 2; 165 | 166 | this.canvas.width = this.width; 167 | this.canvas.height = this.height; 168 | 169 | this.startSpace = 3; 170 | 171 | this.startX = 0; 172 | // this.startY = (this.height / 2); 173 | this.startY = (this.height / 2) + this.offsetY; 174 | this.endX = this.width; 175 | this.endY = 80; 176 | 177 | this.animator = {}; 178 | for (let index = 0; index < 3; index += 1) { 179 | let id = GLOBALS.classNames[index]; 180 | this.animator[index] = { 181 | highlight: false, 182 | percentage: 0, 183 | color: GLOBALS.colors[id], 184 | numParticles: 15 185 | }; 186 | } 187 | 188 | this.renderOnce = true; 189 | } 190 | 191 | } 192 | 193 | export default WiresLeft; -------------------------------------------------------------------------------- /src/ui/modules/WizardGIFExample.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class WizardGIFExample { 16 | constructor(url) { 17 | this.element = document.createElement('div'); 18 | this.element.classList.add('wizard__gif'); 19 | 20 | let mask = document.createElement('div'); 21 | mask.classList.add('wizard__gif-mask'); 22 | 23 | let image = new Image(); 24 | image.classList.add('wizard__gif-image'); 25 | image.src = url; 26 | mask.appendChild(image); 27 | 28 | this.element.appendChild(mask); 29 | } 30 | 31 | show() { 32 | this.element.style.display = 'block'; 33 | } 34 | 35 | hide() { 36 | this.element.style.display = 'none'; 37 | } 38 | } 39 | 40 | export default WizardGIFExample; -------------------------------------------------------------------------------- /src/ui/modules/wizard/LaunchScreen.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class LaunchScreen { 16 | constructor() { 17 | this.element = document.querySelector('.intro'); 18 | 19 | this.startButton = new Button(document.querySelector('#start-tutorial-button')); 20 | this.skipButton = document.querySelector('#skip-tutorial-button'); 21 | this.skipButtonMobile = document.querySelector('#skip-tutorial-button-mobile'); 22 | 23 | this.messageIsCompatible = document.querySelector('#is-compatible'); 24 | this.messageIsNotCompatible = document.querySelector('#is-not-compatible'); 25 | 26 | this.startButton.element.classList.add('button--disabled'); 27 | document.querySelector('.wizard__launch-skip-paragraph').style.display = 'none'; 28 | document.querySelector('.wizard__browser-warning').style.display = 'block'; 29 | 30 | let facebookButton = document.querySelector('.intro__share-link--facebook'); 31 | let twitterButton = document.querySelector('.intro__share-link--twitter'); 32 | 33 | let intro = document.querySelector('.intro__content-mobile'); 34 | /*eslint-disable */ 35 | let defaultPrevent = (event) => { 36 | event.preventDefault(); 37 | }; 38 | /* eslint-enable*/ 39 | intro.addEventListener('touchstart', defaultPrevent); 40 | intro.addEventListener('touchmove', defaultPrevent); 41 | 42 | 43 | let loader = ((el) => { 44 | let ajax = new XMLHttpRequest(); 45 | ajax.open('GET', 'assets/social-facebook.svg', true); 46 | ajax.onload = (event) => { 47 | el.innerHTML = ajax.responseText; 48 | }; 49 | ajax.send(); 50 | })(facebookButton); 51 | 52 | loader = ((el) => { 53 | let ajax = new XMLHttpRequest(); 54 | ajax.open('GET', 'assets/social-twitter.svg', true); 55 | ajax.onload = (event) => { 56 | el.innerHTML = ajax.responseText; 57 | }; 58 | ajax.send(); 59 | })(twitterButton); 60 | 61 | facebookButton.addEventListener('click', this.openFacebookPopup.bind(this)); 62 | twitterButton.addEventListener('click', this.openTwitterPopup.bind(this)); 63 | 64 | if (GLOBALS.browserUtils.isCompatible === true && GLOBALS.browserUtils.isMobile === false) { 65 | this.startButton.element.classList.remove('button--disabled'); 66 | document.querySelector('.wizard__launch-skip-paragraph').style.display = 'block'; 67 | document.querySelector('.wizard__browser-warning').style.display = 'none'; 68 | } 69 | 70 | if (GLOBALS.browserUtils.isMobile) { 71 | this.messageIsCompatible.style.display = 'block'; 72 | 73 | }else { 74 | this.messageIsCompatible.style.display = 'none'; 75 | } 76 | 77 | if (GLOBALS.browserUtils.isMobile && !GLOBALS.browserUtils.isCompatible) { 78 | this.messageIsCompatible.style.display = 'none'; 79 | this.messageIsNotCompatible.style.display = 'block'; 80 | } 81 | 82 | this.skipButton.addEventListener('click', this.skipClick.bind(this)); 83 | this.skipButtonMobile.addEventListener('touchend', this.skipClick.bind(this)); 84 | this.skipButtonMobile.addEventListener('click', this.skipClick.bind(this)); 85 | this.startButton.element.addEventListener('click', this.startClick.bind(this)); 86 | this.startButton.element.addEventListener('touchend', this.startClick.bind(this)); 87 | } 88 | 89 | openFacebookPopup(event) { 90 | event.preventDefault(); 91 | let url = event.currentTarget.getAttribute('href'); 92 | /* eslint-disable space-infix-ops */ 93 | window.open(url, 'fbShareWindow', 'height=450, width=550, top='+(window.innerHeight/2-275)+', left='+(window.innerWidth/2-225)+',toolbar=0, location=0, menubar=0, directories=0, scrollbars=0'); 94 | /* eslint-enable space-infix-ops */ 95 | } 96 | 97 | openTwitterPopup(event) { 98 | event.preventDefault(); 99 | let url = event.currentTarget.getAttribute('href'); 100 | /* eslint-disable space-infix-ops */ 101 | window.open(url, 'fbShareWindow', 'height=450, width=600, top='+(window.innerHeight/2-150)+', left='+(window.innerWidth/2-225)+', toolbar=0, location=0, menubar=0, directories=0, scrollbars=0'); 102 | /* eslint-enable space-infix-ops */ 103 | } 104 | 105 | skipClick(event) { 106 | event.preventDefault(); 107 | let intro = document.querySelector('.intro'); 108 | let offset = intro.offsetHeight; 109 | GLOBALS.wizard.skip(); 110 | gtag('event', 'wizard_skip'); 111 | 112 | if (GLOBALS.browserUtils.isMobile) { 113 | let msg = new SpeechSynthesisUtterance(); 114 | msg.text = ' '; 115 | window.speechSynthesis.speak(msg); 116 | 117 | GLOBALS.inputSection.createCamInput(); 118 | GLOBALS.camInput.start(); 119 | let event = new CustomEvent('mobileLaunch'); 120 | window.dispatchEvent(event); 121 | } 122 | TweenMax.to(intro, 0.5, { 123 | y: -offset, 124 | onComplete: () => { 125 | this.destroy(); 126 | if (!GLOBALS.browserUtils.isMobile) { 127 | GLOBALS.wizard.startCamera(); 128 | } 129 | } 130 | }); 131 | } 132 | 133 | destroy() { 134 | document.body.classList.remove('no-scroll'); 135 | this.element.style.display = 'none'; 136 | 137 | } 138 | 139 | startClick() { 140 | let intro = document.querySelector('.intro'); 141 | let offset = intro.offsetHeight; 142 | if (GLOBALS.browserUtils.isMobile || GLOBALS.browserUtils.isSafari) { 143 | GLOBALS.inputSection.createCamInput(); 144 | GLOBALS.camInput.start(); 145 | GLOBALS.wizard.touchPlay(); 146 | let event = new CustomEvent('mobileLaunch'); 147 | window.dispatchEvent(event); 148 | } 149 | 150 | TweenMax.to(intro, 0.5, { 151 | y: -offset, 152 | onComplete: () => { 153 | this.destroy(); 154 | GLOBALS.wizard.start(); 155 | } 156 | }); 157 | } 158 | } 159 | 160 | import TweenMax from 'gsap'; 161 | import ScrollToPlugin from 'gsap/ScrollToPlugin'; 162 | import GLOBALS from './../../../config.js'; 163 | import Button from './../../components/Button.js'; 164 | 165 | export default LaunchScreen; -------------------------------------------------------------------------------- /src/ui/modules/wizard/Storyboard.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class Storyboard { 16 | constructor() { 17 | this.list = [ 18 | { 19 | text: 'This is an experiment called Teachable Machine. It lets you explore how machine learning works.', 20 | start: 0, 21 | stop: 5.8 22 | }, 23 | { 24 | text: 'Right now, your machine hasn’t been taught anything yet. You can start by teaching it to recognize you doing silly things.', 25 | start: 6.3, 26 | stop: 12.5 27 | }, 28 | { 29 | text: 'First, you’ll have to turn on your camera.', 30 | start: 13.5, 31 | stop: 15.4 32 | }, 33 | { 34 | text: 'Go ahead and click allow.', 35 | start: 16.4, 36 | stop: 18.2 37 | }, 38 | { 39 | text: 'Great.', 40 | start: 21.3, 41 | stop: 22.3 42 | }, 43 | { 44 | text: 'Over here is your input. You should see yourself.', 45 | start: 22.8, 46 | stop: 26.2 47 | }, 48 | { 49 | text: 'And over Here is where you teach your machine.', 50 | start: 26.9, 51 | stop: 29.2 52 | }, 53 | { 54 | text: 'So machine learning is basically about teaching by example.', 55 | start: 30, 56 | stop: 33.6 57 | }, 58 | { 59 | text: 'First, let’s teach it what it looks like when you’re not doing anything. Just hold this grey button down for a couple seconds.', 60 | start: 34.3, 61 | stop: 40.5 62 | }, 63 | { 64 | text: 'Okay, that should do.', 65 | start: 42.6, 66 | stop: 44 67 | } 68 | ]; 69 | 70 | this.errors = [ 71 | { 72 | text: 'Error 1', 73 | start: 0, 74 | stop: 5.8 75 | } 76 | ]; 77 | 78 | this.list[401] = this.errors[0]; 79 | } 80 | 81 | 82 | } 83 | 84 | export default Storyboard; -------------------------------------------------------------------------------- /src/ui/modules/wizard/Storyboard2.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class Storyboard2 { 16 | constructor() { 17 | this.list = []; 18 | 19 | // 0.4 20 | // 13.504 21 | // 19.4 22 | // 36.504 23 | // 44.4 24 | // 51.4 25 | // 60 + 2.4 26 | // 60 + 15.4 27 | // 60 + 20.4 28 | // 60 + 29.504 29 | // 60 + 34.504 30 | // 60 + 46.4 31 | // 60 + 56.504 32 | // 60 + 60 + 7.4 33 | // 60 + 60 + 24.504 34 | // 60 + 60 + 30.4 35 | // 60 + 60 + 36.4 36 | // 60 + 60 + 42.504 37 | 38 | 39 | this.list.push({ 40 | start: 0, 41 | stop: 12, 42 | triggers: [ 43 | { 44 | start: 0, 45 | stop: 5, 46 | event: () => { 47 | console.log('setMessage', 'This is an experiment called Teachable Machine. It lets you explore how machine learning works.'); 48 | } 49 | }, 50 | { 51 | start: 5.5, 52 | stop: 12, 53 | event: () => { 54 | console.log('setMessage', 'Your machine hasn’t been taught anything yet. You can start by teaching it to recognize you doing silly things. '); 55 | } 56 | } 57 | ] 58 | }); 59 | 60 | this.list.push({ 61 | start: 13.504, 62 | stop: 19.4, 63 | triggers: [ 64 | { 65 | start: 13.504, 66 | stop: 19.4, 67 | event: () => { 68 | console.log('setMessage', 'First, you’ll have to turn on your camera. Go ahead and click allow.'); 69 | } 70 | }, 71 | { 72 | start: 15.5, 73 | stop: 12, 74 | event: () => { 75 | console.log('Ask for camera permission'); 76 | } 77 | } 78 | ] 79 | }); 80 | } 81 | } 82 | 83 | export default Storyboard2; -------------------------------------------------------------------------------- /style/base.styl: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box 3 | } 4 | 5 | .hidden-text { 6 | visibility: hidden 7 | position: absolute 8 | } 9 | 10 | 11 | body { 12 | margin: 0 13 | background: $color__white 14 | color: $color__text--dark 15 | //overflow-x: hidden 16 | user-select: none 17 | } 18 | 19 | body.no-scroll { 20 | width: 100% 21 | height: 100% 22 | overflow: hidden 23 | } 24 | 25 | -------------------------------------------------------------------------------- /style/buttons.styl: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block 3 | text-decoration: none 4 | text-align: center 5 | color: $color__white 6 | margin: 0 7 | @extend .font__size--large 8 | } 9 | 10 | .button--disabled { 11 | pointer-events: none 12 | -webkit-filter: grayscale(100%) 13 | opacity: 0.3 14 | } 15 | 16 | 17 | .button-set { 18 | height: 48px 19 | width: 100% 20 | text-align: left 21 | } 22 | 23 | .button__toggle { 24 | position: relative 25 | width: 50% 26 | } 27 | 28 | .button__toggle:first-of-type { 29 | z-index: 2 30 | } 31 | 32 | .button__toggle:last-of-type { 33 | margin-left: -12px 34 | } 35 | 36 | .button__content { 37 | padding-left: space(2) 38 | padding-right: space(2) 39 | } 40 | 41 | .button__content--large { 42 | @extend .font__size--xlarge 43 | } 44 | 45 | .button__content--small { 46 | width: 36px 47 | height: 36px 48 | @extend .font__size--small 49 | } 50 | 51 | .button__mask { 52 | position: relative 53 | overflow: hidden 54 | width: 100% 55 | height: 100% 56 | } 57 | 58 | .button__label { 59 | position: absolute 60 | 61 | br { 62 | display: none 63 | } 64 | } 65 | 66 | // Colors 67 | .front-face { 68 | transition: fill 0.1s ease-out 69 | fill: $color__grey--primary 70 | } 71 | 72 | .left-face { 73 | transition: fill 0.1s ease-out 74 | fill: $color__grey--shadow-left 75 | } 76 | 77 | .bottom-face { 78 | transition: fill 0.1s ease-out 79 | fill: $color__grey--shadow-bottom 80 | } 81 | 82 | 83 | .front-face--blue { 84 | fill: $color__blue--primary 85 | } 86 | 87 | .left-face--blue { 88 | fill: $color__blue--shadow-left 89 | } 90 | 91 | .bottom-face--blue { 92 | fill: $color__blue--shadow-bottom 93 | } 94 | 95 | .front-face--grey { 96 | fill: $color__grey--primary 97 | } 98 | 99 | .left-face--grey { 100 | fill: $color__grey--shadow-left 101 | } 102 | 103 | .bottom-face--grey { 104 | fill: $color__grey--shadow-bottom 105 | } 106 | 107 | .front-face--neutral { 108 | fill: $color__class-neutral--primary 109 | } 110 | 111 | .left-face--neutral { 112 | fill: $color__class-neutral--shadow-left 113 | } 114 | 115 | .bottom-face--neutral { 116 | fill: $color__class-neutral--shadow-bottom 117 | } 118 | 119 | .front-face--green { 120 | fill: $color__class-green--primary 121 | } 122 | 123 | .left-face--green { 124 | fill: $color__class-green--shadow-left 125 | } 126 | 127 | .bottom-face--green { 128 | fill: $color__class-green--shadow-bottom 129 | } 130 | 131 | .front-face--purple { 132 | fill: $color__class-purple--primary 133 | } 134 | 135 | .left-face--purple { 136 | fill: $color__class-purple--shadow-left 137 | } 138 | 139 | .bottom-face--purple { 140 | fill: $color__class-purple--shadow-bottom 141 | } 142 | 143 | .front-face--orange { 144 | fill: $color__class-orange--primary 145 | } 146 | 147 | .left-face--orange { 148 | fill: $color__class-orange--shadow-left 149 | } 150 | 151 | .bottom-face--orange { 152 | fill: $color__class-orange--shadow-bottom 153 | } 154 | 155 | .button__toggle--selected { 156 | .front-face { 157 | @extend .front-face--blue 158 | } 159 | 160 | .left-face { 161 | @extend .left-face--blue 162 | } 163 | 164 | .bottom-face { 165 | @extend .bottom-face--blue 166 | } 167 | } 168 | 169 | .front-face--facebook { 170 | fill: $color__facebook--primary 171 | } 172 | 173 | .left-face--facebook { 174 | fill: $color__facebook--shadow-left 175 | } 176 | 177 | .bottom-face--facebook { 178 | fill: $color__facebook--shadow-bottom 179 | } 180 | 181 | .front-face--twitter { 182 | fill: $color__twitter--primary 183 | } 184 | 185 | .left-face--twitter { 186 | fill: $color__twitter--shadow-left 187 | } 188 | 189 | .bottom-face--twitter { 190 | fill: $color__twitter--shadow-bottom 191 | } 192 | 193 | .recording-share__button .button__label, 194 | .recording-start__button .button__label { 195 | top: 8px !important 196 | } 197 | 198 | @media screen and (max-width: 900px) { 199 | .button__label { 200 | 201 | br { 202 | display: block 203 | } 204 | } 205 | .button__content--small { 206 | position: relative 207 | top: -6px 208 | } 209 | .button__color-title { 210 | position: relative 211 | top: -36px 212 | display: block 213 | pointer-events: none 214 | } 215 | } -------------------------------------------------------------------------------- /style/colors.styl: -------------------------------------------------------------------------------- 1 | $color__black = #000000 2 | $color__white = #ffffff 3 | $color__background = #EDEDEE 4 | $color__text--dark = #A4A6A8 5 | $color__text--light = #6D6E71 6 | $color__divider = #e4e5e6 7 | 8 | $color__blue--primary = #3E80F6 9 | $color__blue--shadow-left = #2874E2 10 | $color__blue--shadow-bottom = #2068D1 11 | 12 | $color__grey--primary = #CFD1D2 13 | $color__grey--shadow-left = #BABCBE 14 | $color__grey--shadow-bottom = #A5A7AA 15 | 16 | $color__class-neutral--primary = #A4A6A9 17 | $color__class-neutral--shadow-left = #909295 18 | $color__class-neutral--shadow-bottom = #808082 19 | 20 | $color__class-green--primary = #2baa5e 21 | $color__class-green--shadow-left = #249b54 22 | $color__class-green--shadow-bottom = #1a8741 23 | 24 | $color__class-purple--primary = #c95ac5 25 | $color__class-purple--shadow-left = #b74eb7 26 | $color__class-purple--shadow-bottom = #a53fa5 27 | 28 | $color__class-orange--primary = #dd4d31 29 | $color__class-orange--shadow-left = #cc402e 30 | $color__class-orange--shadow-bottom = #bf3025 31 | 32 | $color__facebook--primary = #0066a8 33 | $color__facebook--shadow-left = #0080c4 34 | $color__facebook--shadow-bottom = #004a75 35 | 36 | $color__twitter--primary = #00abe0 37 | $color__twitter--shadow-left = #89d9f7 38 | $color__twitter--shadow-bottom = #0080ab 39 | -------------------------------------------------------------------------------- /style/components/faq.styl: -------------------------------------------------------------------------------- 1 | .faq { 2 | max-width: 1060px 3 | margin: 0 auto 4 | color: $color__text--light 5 | position: relative 6 | user-select: initial 7 | @extend .font__size--normal 8 | } 9 | 10 | .faq__inner { 11 | width: 40% 12 | } 13 | 14 | .faq__question { 15 | margin-top: space(4) 16 | margin-bottom: 8px 17 | @extend .font__size--normal 18 | @extend .font__weight--bold 19 | } 20 | 21 | .faq__answer { 22 | margin-top: 0 23 | } 24 | 25 | .faq__video { 26 | width: 100% 27 | margin-top: space(2.5) 28 | } 29 | 30 | .faq-record-container { 31 | position: relative 32 | } 33 | 34 | .record { 35 | position: absolute 36 | right: 0 37 | top: 0 38 | width: 20% 39 | text-align: right 40 | } 41 | 42 | .record-text { 43 | color: $color__blue--primary 44 | width: 100% 45 | text-align: center 46 | margin: 0 auto 47 | } 48 | 49 | @media screen and (max-width: 1200px) { 50 | 51 | .faq-record-container { 52 | margin-left: space(2) 53 | margin-right: space(2) 54 | } 55 | 56 | .record { 57 | text-align: center 58 | width: 25% 59 | } 60 | } 61 | 62 | @media screen and (max-width: 900px) { 63 | .faq { 64 | margin: 0 65 | margin-bottom: space(8) 66 | padding: 0 15px 67 | } 68 | 69 | .faq__inner { 70 | width: 90% 71 | } 72 | 73 | .record { 74 | display: none 75 | } 76 | 77 | .faq-record-container { 78 | margin-left: 0 79 | margin-right: 0 80 | } 81 | } -------------------------------------------------------------------------------- /style/components/footer.styl: -------------------------------------------------------------------------------- 1 | .footer { 2 | max-width: 1060px 3 | margin-left: auto 4 | margin-right: auto 5 | margin-top: space(6) 6 | margin-bottom: space(6) 7 | 8 | .link { 9 | display: block 10 | max-width: 400px 11 | position: relative 12 | } 13 | 14 | .link--privacy { 15 | margin-top: space(1) 16 | display: inline-block 17 | color: rgba($color__black, 0.2) 18 | } 19 | } 20 | 21 | @media screen and (max-width: 900px) { 22 | .footer { 23 | padding: 0 15px 24 | 25 | .link { 26 | display: block 27 | max-width: 400px 28 | position: relative 29 | } 30 | } 31 | 32 | .footer__image { 33 | width: 100% 34 | } 35 | } -------------------------------------------------------------------------------- /style/components/intro.styl: -------------------------------------------------------------------------------- 1 | .intro { 2 | background: #e9e9e9 3 | position: fixed 4 | z-index: 101 5 | width: 100% 6 | height: 100% 7 | display: flex 8 | justify-content: center 9 | align-items: center 10 | } 11 | 12 | .intro__sharing { 13 | flex: none 14 | position: absolute 15 | top: space(2) 16 | right: space(2) 17 | } 18 | 19 | .intro__share-link { 20 | display: inline-block 21 | width: 30px 22 | height: 30px 23 | margin: 0 0 0 space(0.5) 24 | 25 | svg { 26 | margin: 15% 27 | width: 70% 28 | height: 70% 29 | fill: #808184 30 | } 31 | } 32 | 33 | .intro__share-link:hover svg { 34 | fill: $color__blue--primary 35 | } 36 | 37 | .intro__footer { 38 | align-items: flex-end 39 | bottom: 0px 40 | display: flex 41 | justify-content: space-between 42 | left: 0 43 | padding: space(2) 44 | position: absolute 45 | width: 100% 46 | } 47 | 48 | .intro__footer-link--left { 49 | width: 300px 50 | } 51 | 52 | .intro__footer-link--right { 53 | color: #929395 54 | font-size: 14px 55 | position: relative 56 | text-decoration: none 57 | top: -5px 58 | } 59 | 60 | .intro__footer-image { 61 | width: 100% 62 | } 63 | 64 | .intro__content-mobile { 65 | display: none 66 | } 67 | 68 | .intro__content-mobile-incompatible { 69 | display: none 70 | } 71 | 72 | .intro__inner { 73 | text-align: center 74 | padding-top: space(3) 75 | padding-bottom: space(3) 76 | max-width: 1060px 77 | z-index: 2 78 | width: 100vw 79 | @extend .font__size--normal 80 | } 81 | 82 | 83 | .intro__cta { 84 | margin-top: space(2) 85 | } 86 | 87 | .wizard__browser-warning { 88 | display: none 89 | } 90 | 91 | .intro__image { 92 | width: 100% 93 | max-width: 400px 94 | } 95 | 96 | .intro__title { 97 | margin-top: space(1) 98 | width: 100% 99 | } 100 | 101 | .intro__title-image { 102 | width: 100% 103 | height: 220px 104 | 105 | &.intro__title-image--desktop{ 106 | display: block 107 | } 108 | 109 | &.intro__title-image--mobile{ 110 | display: none 111 | } 112 | } 113 | 114 | .intro__title-image--teachable { 115 | margin-right: 25px 116 | } 117 | 118 | .intro__title-image--machine { 119 | 120 | } 121 | 122 | .intro__text { 123 | max-width: 420px 124 | margin-top: space(5) 125 | margin-bottom: 0 126 | margin-left: auto 127 | margin-right: auto 128 | white-space: nowrap 129 | } 130 | 131 | .intro__text-break { 132 | display: block 133 | } 134 | 135 | .intro__video { 136 | position: relative 137 | max-width: 800px 138 | height: 0 139 | margin-left: auto 140 | margin-right: auto 141 | margin-bottom: space(2) 142 | overflow: hidden 143 | } 144 | .intro__video-wrapper { 145 | position: relative 146 | padding-bottom: 56.25% /* 16:9 */ 147 | padding-top: 25px 148 | height: 0 149 | margin-bottom: space(2) 150 | } 151 | .intro__video-player { 152 | position: absolute 153 | top: 0 154 | left: 0 155 | width: 100% 156 | height: 100% 157 | background: $color__black 158 | } 159 | 160 | @media screen and (max-width: 900px) { 161 | 162 | .intro__inner { 163 | padding-top: 0 164 | } 165 | 166 | .intro__content-desktop { 167 | display: none 168 | } 169 | 170 | .intro__content-mobile { 171 | background: $color__white 172 | border-radius: 5px 173 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24) 174 | display: block 175 | margin: 0 auto 176 | padding: 25px 177 | position: relative 178 | text-align: left 179 | width: 90% 180 | } 181 | 182 | .intro__content-mobile-incompatible { 183 | display: none 184 | margin: 20px auto 0 185 | width: 80% 186 | 187 | a { 188 | text-decoration: none 189 | } 190 | } 191 | 192 | .intro__content-mobile-header { 193 | color: #58595b 194 | line-height: 1.4 195 | } 196 | 197 | .intro__content-mobile-body { 198 | color: #828386 199 | } 200 | 201 | .intro__content-mobile-ok { 202 | margin-bottom: 0 203 | text-align: right 204 | } 205 | 206 | .intro__content-mobile-ok-link { 207 | display: inline-block 208 | padding: 10px 20px 209 | margin-right: -10px 210 | text-decoration: none 211 | } 212 | 213 | .intro__text { 214 | font-size: 14px 215 | } 216 | 217 | .intro__button-social { 218 | 219 | & + & { 220 | margin-left: 10px 221 | } 222 | } 223 | 224 | .wizard__launch-skip { 225 | font-size: 14px 226 | } 227 | 228 | .intro__title { 229 | height: 70px 230 | margin-bottom: space(1) 231 | margin-top: 0 232 | } 233 | 234 | .intro__cta { 235 | margin-top: space(2) 236 | } 237 | 238 | .intro__footer-link--left { 239 | width: 50% 240 | } 241 | 242 | .intro__title-image { 243 | height: 70px 244 | margin: 0 auto 245 | max-width: 280px 246 | width: 60% 247 | 248 | &.intro__title-image--desktop { 249 | display: none 250 | } 251 | 252 | &.intro__title-image--mobile { 253 | display: block 254 | width: 100% 255 | } 256 | } 257 | 258 | .intro__title-image--teachable, 259 | .intro__title-image--machine { 260 | margin-right: 0 261 | max-width: 80% 262 | height: 30px 263 | } 264 | } 265 | 266 | @media screen and (max-width: 900px) and (orientation: landscape) { 267 | 268 | .intro__inner { 269 | margin-top: -20px 270 | padding-bottom: 10px 271 | } 272 | 273 | .intro__title { 274 | height: 50px 275 | } 276 | 277 | .intro__title-image { 278 | height: 50px 279 | } 280 | 281 | .intro__content-mobile { 282 | width: 80% 283 | } 284 | 285 | .intro__content-mobile-ok { 286 | margin-top: -10px 287 | } 288 | 289 | .intro__content-mobile-header { 290 | font-size: 14px 291 | } 292 | 293 | .intro__content-mobile-body { 294 | font-size: 12px 295 | } 296 | 297 | .intro__footer { 298 | padding: 5px 20px 299 | } 300 | 301 | .intro__footer-link--left { 302 | width: 25% 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /style/components/output__gif.styl: -------------------------------------------------------------------------------- 1 | .gif__viewer { 2 | position: relative 3 | width: 100% 4 | height: 100% 5 | background-color: $color__background 6 | background-position: center center 7 | background-size: cover 8 | background-repeat: no-repeat 9 | 10 | @extend .font__size--small 11 | } 12 | 13 | .gif__edit { 14 | position: relative 15 | width: 100% 16 | height: 100% 17 | } 18 | 19 | .gif__edit-bar { 20 | width: 100% 21 | height: 80px 22 | bottom: 0px 23 | position: absolute 24 | background: $color__white 25 | display: flex 26 | justify-content: center 27 | } 28 | 29 | .gif__thumb { 30 | position: relative 31 | width: 70px 32 | height: 70px 33 | cursor: pointer 34 | margin: space(1) space(1) 0 space(1) 35 | } 36 | 37 | .gif__thumb-image-wrapper { 38 | position: relative 39 | overflow: hidden 40 | width: 80% 41 | height: 80% 42 | background: $color__black 43 | top: 50% 44 | left: 50% 45 | transform: translate3d(-50%, -50%, 0) 46 | background-position: center center 47 | background-size: cover 48 | background-repeat: no-repeat 49 | } 50 | 51 | .gif__thumb-image-wrapper:after { 52 | content: " " 53 | position: absolute 54 | top: 0 55 | left: 0 56 | width: 100% 57 | height: 100% 58 | background-color: rgba($color__black, 0.6) 59 | background-image: url(assets/edit-icon.svg) 60 | background-repeat: no-repeat 61 | background-size: 40% 62 | background-position: center center 63 | display: none 64 | } 65 | 66 | .gif__thumb-border { 67 | position: absolute 68 | width: 100% 69 | height: 100% 70 | border-width: 4px 71 | border-style: solid 72 | opacity: 1 73 | } 74 | 75 | .gif__thumb:hover .gif__thumb-image-wrapper:after { 76 | display: block 77 | } 78 | 79 | .gif__thumb-border--green { 80 | border-color: rgba($color__class-green--primary, 0.2) 81 | } 82 | 83 | .gif__thumb-border--purple { 84 | border-color: rgba($color__class-purple--primary, 0.2) 85 | } 86 | 87 | .gif__thumb-border--orange { 88 | border-color: rgba($color__class-orange--primary, 0.2) 89 | } 90 | 91 | .gif__thumb:hover .gif__thumb-border--green, .gif__thumb-border--green-selected { 92 | border-color: $color__class-green--primary 93 | } 94 | 95 | .gif__thumb:hover .gif__thumb-border--purple, .gif__thumb-border--purple-selected { 96 | border-color: $color__class-purple--primary 97 | } 98 | 99 | .gif__thumb:hover .gif__thumb-border--orange, .gif__thumb-border--orange-selected { 100 | border-color: $color__class-orange--primary 101 | } 102 | 103 | 104 | .gif__edit-viewer { 105 | width: 100% 106 | position: absolute 107 | bottom: 80px 108 | top: 0px 109 | background-color: $color__background 110 | background-position: center center 111 | background-size: cover 112 | background-repeat: no-repeat 113 | } 114 | 115 | .gif__canvas { 116 | width: 100% 117 | height: 100% 118 | 119 | canvas { 120 | width: 340px 121 | height: 260px 122 | } 123 | } 124 | 125 | .gif__search { 126 | position: absolute 127 | top: 0 128 | left: 0 129 | width: 100% 130 | height: 100% 131 | } 132 | 133 | .gif__search-bar { 134 | position: absolute 135 | width: 100% 136 | height: 80px 137 | background-color: $color__white 138 | } 139 | 140 | .gif__search-input { 141 | width: 100% 142 | height: 70px 143 | border: 4px solid $color__grey--primary 144 | padding-left: 60px 145 | outline: none 146 | 147 | @extend .font__size--small 148 | } 149 | 150 | .gif__search-input--a { 151 | color: $color__class-a--primary 152 | } 153 | 154 | .gif__search-input--b { 155 | color: $color__class-b--primary 156 | } 157 | 158 | .gif__search-input--c { 159 | color: $color__class-c--primary 160 | } 161 | 162 | .gif__search-input--d { 163 | color: $color__class-d--primary 164 | } 165 | 166 | .gif__search-back-button { 167 | left: 0 168 | top: 0 169 | width: 60px 170 | height: 70px 171 | position: absolute 172 | background-color: transparent 173 | cursor: pointer 174 | } 175 | // @stylint off 176 | .gif__search-back-button:after { 177 | content: " " 178 | position: absolute 179 | left: 50% 180 | top: 50% 181 | transform: translate3d(-50%, -50%, 0) 182 | width: 30px 183 | height: 30px 184 | background-image: url(assets/back-arrow.svg) 185 | background-size: 30px 186 | background-position: 0 0 187 | background-repeat: no-repeat 188 | } 189 | 190 | .gif__search-back-button--a:after { 191 | background-position-y: 0px 192 | } 193 | 194 | .gif__search-back-button--b:after { 195 | background-position-y: -30px 196 | } 197 | 198 | .gif__search-back-button--c:after { 199 | background-position-y: -60px 200 | } 201 | 202 | .gif__search-back-button--d:after { 203 | background-position-y: -90px 204 | } 205 | 206 | .gif__search-sponsor { 207 | width: 100% 208 | height: 40px 209 | background-image: url(assets/giphy.svg) 210 | background-position: center center 211 | background-size: 220px 212 | background-repeat: no-repeat 213 | } 214 | 215 | .gif__search-scroll { 216 | position: absolute 217 | overflow: hidden 218 | overflow-y: scroll 219 | width: 100% 220 | top: 80px 221 | bottom: 0px 222 | background-color: rgba($color__black, 0.9) 223 | } 224 | 225 | 226 | .gif__search-results { 227 | display: flex 228 | justify-content: space-around 229 | } 230 | 231 | .gif__search-column { 232 | width: 50% 233 | } 234 | 235 | .gif__search-column:first-of-type { 236 | padding-left: space(1) 237 | padding-right: space(0.5) 238 | } 239 | 240 | .gif__search-column:last-of-type { 241 | padding-left: space(0.5) 242 | padding-right: space(1) 243 | } 244 | 245 | .gif__search-column img { 246 | width: 100% 247 | display: block 248 | margin-bottom: space(1) 249 | cursor: pointer 250 | } 251 | 252 | .gif__load-more { 253 | color: $color__white 254 | opacity: 0 255 | cursor: pointer 256 | padding-top: space(1) 257 | padding-bottom: space(1) 258 | } 259 | -------------------------------------------------------------------------------- /style/components/output__sound.styl: -------------------------------------------------------------------------------- 1 | .output__sound { 2 | position: absolute 3 | width: 100% 4 | height: 100% 5 | background: $color__white 6 | } 7 | 8 | .output__sound-on { 9 | position: absolute 10 | width: 100% 11 | height: 100% 12 | background: red 13 | z-index: 4 14 | } 15 | 16 | .output__sound-search { 17 | position: absolute 18 | width: 100% 19 | height: 100% 20 | background: $color__white 21 | display: none 22 | z-index: 3 23 | } 24 | 25 | .output__sound-search-bar { 26 | background: $color__white 27 | width: 100% 28 | padding-bottom: space(1) 29 | } 30 | 31 | .output__sound-back { 32 | position: absolute 33 | width: 45px 34 | height: 50px 35 | background-color: rgba(0,0,0,0) 36 | z-index: 2 37 | top: 0px 38 | cursor: pointer 39 | 40 | svg { 41 | position: absolute 42 | width: 20px 43 | top: 15px 44 | left: 10px 45 | } 46 | } 47 | 48 | 49 | .output__sound-back--green svg { 50 | fill: $color__class-green--primary 51 | } 52 | 53 | 54 | .output__sound-back--purple svg { 55 | fill: $color__class-purple--primary 56 | } 57 | 58 | 59 | .output__sound-back--orange svg { 60 | fill: $color__class-orange--primary 61 | } 62 | 63 | .output__sound-search-input { 64 | display: block 65 | width: 100% 66 | height: 50px 67 | border: 4px solid $color__background 68 | padding-left: 40px 69 | outline: none 70 | @extend .font__size--small 71 | } 72 | 73 | .output__sound-search-input--green { 74 | border-color: $color__class-green--primary 75 | color: $color__class-green--primary 76 | } 77 | 78 | .output__sound-search-input--purple { 79 | border-color: $color__class-purple--primary 80 | color: $color__class-purple--primary 81 | } 82 | 83 | .output__sound-search-input--orange { 84 | border-color: $color__class-ornage--primary 85 | color: $color__class-ornage--primary 86 | } 87 | 88 | 89 | .output__sound-search-results { 90 | position: absolute 91 | width: 100% 92 | top: 60px 93 | bottom: 0px 94 | overflow: scroll 95 | } 96 | 97 | .output__sound-search-results--green { 98 | input { 99 | color: $color__class-green--primary 100 | } 101 | 102 | div:hover { 103 | input { 104 | border-color: $color__class-green--primary 105 | } 106 | } 107 | 108 | } 109 | 110 | .output__sound-search-results--purple { 111 | input { 112 | color: $color__class-purple--primary 113 | } 114 | 115 | .output__sound-search-result-icon:after { 116 | background-position-y: -26px 117 | } 118 | 119 | div:hover { 120 | input { 121 | border-color: $color__class-purple--primary 122 | } 123 | } 124 | 125 | } 126 | 127 | .output__sound-search-results--orange { 128 | input { 129 | color: $color__class-orange--primary 130 | } 131 | 132 | .output__sound-search-result-icon:after { 133 | background-position-y: -52px 134 | } 135 | 136 | div:hover { 137 | input { 138 | border-color: $color__class-orange--primary 139 | } 140 | } 141 | 142 | } 143 | .output__sound-search-result { 144 | height: 50px 145 | margin-bottom: space(1) 146 | position: relative 147 | cursor: pointer 148 | 149 | @extend .font__size--small 150 | } 151 | 152 | .output__sound-search-result-icon { 153 | position absolute 154 | width: 50px 155 | height: 50px 156 | background: transparent 157 | 158 | svg { 159 | pointer-events: none 160 | width: 24px 161 | position: absolute 162 | top: 12px 163 | left: 12px 164 | } 165 | } 166 | 167 | 168 | .output__sound-search-result-icon--green svg { 169 | fill: $color__class-green--primary 170 | } 171 | 172 | 173 | .output__sound-search-result-icon--purple svg { 174 | fill: $color__class-purple--primary 175 | } 176 | 177 | .output__sound-search-result-icon--orange svg { 178 | fill: $color__class-orange--primary 179 | } 180 | 181 | 182 | 183 | .output__sound-search-result-input { 184 | pointer-events: none 185 | width: 100% 186 | height: 50px 187 | padding-left: 50px 188 | color: inherit 189 | border: 4px solid $color__background 190 | } 191 | 192 | .output__sound-class { 193 | text-align: left 194 | display: flex 195 | align-items: center 196 | height: 33% 197 | padding-left: space(1) 198 | } 199 | 200 | .output__sound-delete { 201 | position: absolute 202 | width: 40px 203 | height: 42px 204 | right: 4px 205 | cursor: pointer 206 | background: white 207 | background-image: url(assets/outputs/delete-icon.svg) 208 | background-size: 40% 209 | background-repeat: no-repeat 210 | background-position: center center 211 | } 212 | 213 | .output__sound-speaker { 214 | width: 52px 215 | height: 50px 216 | margin-right: space(0) 217 | padding-right: 4px 218 | // cursor: pointer 219 | 220 | svg { 221 | fill: $color__class-green--primary 222 | width: 100% 223 | height: 100% 224 | pointer-events: none 225 | 226 | .sound-on { 227 | display: none 228 | } 229 | } 230 | } 231 | 232 | .output__sound-speaker-class-icon { 233 | width: 70px 234 | height: 70px 235 | margin-left: space(1) 236 | margin-top: 5px 237 | 238 | } 239 | 240 | .output__sound-speaker--green svg { 241 | fill: $color__class-green--primary 242 | } 243 | 244 | 245 | .output__sound-speaker--purple svg { 246 | fill: $color__class-purple--primary 247 | } 248 | 249 | .output__sound-speaker--orange svg { 250 | fill: $color__class-orange--primary 251 | } 252 | 253 | 254 | .output__sound-speaker--active svg { 255 | 256 | .sound-on { 257 | display: block 258 | } 259 | } 260 | 261 | .output__sound-edit { 262 | position: absolute 263 | z-index: 2 264 | width: 16px 265 | height: 50px 266 | opacity: 0.2 267 | background-image url(assets/outputs/edit-icon.svg) 268 | background-position 0 center 269 | background-repeat: no-repeat 270 | margin-left: 51px 271 | left: space(2.5) 272 | pointer-events: none 273 | 274 | svg { 275 | fill: $color__class-green--primary 276 | width: 100% 277 | height: 100% 278 | pointer-events: none 279 | } 280 | } 281 | 282 | .output__sound-edit:after { 283 | content: "Play" 284 | position: absolute 285 | top: 0 286 | left: 25px 287 | line-height: 50px 288 | color: $color__black 289 | } 290 | 291 | 292 | .output__sound-edit--green svg { 293 | fill: $color__class-green--primary 294 | } 295 | 296 | 297 | .output__sound-edit--purple svg { 298 | fill: $color__class-purple--primary 299 | } 300 | 301 | .output__sound-edit--orange svg { 302 | fill: $color__class-orange--primary 303 | } 304 | 305 | 306 | 307 | .output__sound-input { 308 | width: calc(100% - 50px) 309 | padding-left: space(8) 310 | padding-right: 40px 311 | height: 50px 312 | border: 4px solid $color__background 313 | outline: none 314 | position relative 315 | cursor: pointer 316 | 317 | @extend .font__size--small 318 | } 319 | 320 | .output__sound-input--green { 321 | color: $color__class-green--primary 322 | } 323 | 324 | .output__sound-input--purple { 325 | color: $color__class-purple--primary 326 | } 327 | 328 | .output__sound-input--orange { 329 | color: $color__class-orange--primary 330 | } 331 | 332 | .output__sound-input--green:hover, .output__sound-input--green-selected { 333 | border-color: $color__class-green--primary 334 | } 335 | 336 | .output__sound-input--purple:hover, .output__sound-input--purple-selected { 337 | border-color: $color__class-purple--primary 338 | } 339 | 340 | .output__sound-input--orange:hover, .output__sound-input--orange-selected { 341 | border-color: $color__class-orange--primary 342 | } 343 | 344 | .output__sound-input--nothing { 345 | color: rgba($color__black, 0.2); 346 | } 347 | 348 | .output__sound-on { 349 | display: flex 350 | flex-wrap: wrap 351 | } 352 | 353 | .output__sound-speaker-class { 354 | position: relative 355 | flex: 1 1 1 356 | width: 50% 357 | height: 50% 358 | background: $color__white 359 | } 360 | 361 | .output__sound-speaker-class:nth-child(1) { 362 | border-right: 1px solid $color__background 363 | border-bottom: 1px solid $color__background 364 | } 365 | 366 | .output__sound-speaker-class:nth-child(2) { 367 | border-left: 1px solid $color__background 368 | border-bottom: 1px solid $color__background 369 | } 370 | 371 | .output__sound-speaker-class:nth-child(3) { 372 | border-right: 1px solid $color__background 373 | border-top: 1px solid $color__background 374 | } 375 | 376 | .output__sound-speaker-class:nth-child(4) { 377 | border-left: 1px solid $color__background 378 | border-top: 1px solid $color__background 379 | } 380 | 381 | .output__sound-speaker-class-icon { 382 | position: relative 383 | top: 50% 384 | left: 50% 385 | transform: translate3d(-50%, -50%, 0) 386 | } 387 | 388 | .output__sound { 389 | canvas { 390 | z-index: 100 391 | position: absolute 392 | } 393 | } -------------------------------------------------------------------------------- /style/components/output__speech.styl: -------------------------------------------------------------------------------- 1 | .output__container--speech { 2 | background: $color__white 3 | } 4 | 5 | .output__speech-class { 6 | @extend .output__sound-class 7 | } 8 | 9 | 10 | .output__speech-speaker { 11 | @extend .output__sound-speaker 12 | } 13 | 14 | .output__speech-speaker--green svg { 15 | fill: $color__class-green--primary 16 | } 17 | 18 | 19 | .output__speech-speaker--purple svg { 20 | fill: $color__class-purple--primary 21 | } 22 | 23 | .output__speech-speaker--orange svg { 24 | fill: $color__class-orange--primary 25 | } 26 | 27 | 28 | .output__speech-speaker--active svg { 29 | @extend .output__sound-speaker--active svg 30 | } 31 | 32 | .output__speech-edit { 33 | @extend .output__sound-edit 34 | } 35 | 36 | .output__speech-edit:after { 37 | content: "Say" 38 | position: absolute 39 | top: 0 40 | left: 25px 41 | line-height: 50px 42 | color: $color__black 43 | } 44 | 45 | .output__speech-edit--green svg { 46 | fill: $color__class-green--primary 47 | } 48 | 49 | 50 | .output__speech-edit--purple svg { 51 | fill: $color__class-purple--primary 52 | } 53 | 54 | .output__speech-edit--orange svg { 55 | fill: $color__class-orange--primary 56 | } 57 | 58 | .output__speech-delete { 59 | position: absolute 60 | width: 40px 61 | height: 42px 62 | right: 4px 63 | cursor: pointer 64 | background: white 65 | background-image: url(assets/outputs/delete-icon.svg) 66 | background-size: 40% 67 | background-repeat: no-repeat 68 | background-position: center center 69 | } 70 | 71 | 72 | .output__speech-input { 73 | width: calc(100% - 50px) 74 | padding-left: space(7) 75 | padding-right: 40px 76 | height: 50px 77 | border: 4px solid $color__background 78 | outline: none 79 | position relative 80 | 81 | @extend .font__size--small 82 | } 83 | 84 | .output__speech-input--green { 85 | color: $color__class-green--primary 86 | } 87 | 88 | .output__speech-input--purple { 89 | color: $color__class-purple--primary 90 | } 91 | 92 | .output__speech-input--orange { 93 | color: $color__class-orange--primary 94 | } 95 | 96 | 97 | .output__speech-input--green:hover, .output__speech-input--green-selected { 98 | border-color: $color__class-green--primary 99 | } 100 | 101 | .output__speech-input--purple:hover, .output__speech-input--purple-selected { 102 | border-color: $color__class-purple--primary 103 | } 104 | 105 | .output__speech-input--orange:hover, .output__speech-input--orange-selected { 106 | border-color: $color__class-orange--primary 107 | } 108 | 109 | .output__speech-input--nothing { 110 | color: rgba($color__black, 0.2); 111 | } -------------------------------------------------------------------------------- /style/components/outputs.styl: -------------------------------------------------------------------------------- 1 | .output__loading-screen { 2 | position: absolute 3 | width: 100% 4 | height: 100% 5 | background: $color__background 6 | } 7 | 8 | .output__loading-title { 9 | position: absolute 10 | top: 50% 11 | left: 50% 12 | transform: translate3d(-50%, -50%, 0) 13 | color: $color__text 14 | } 15 | 16 | @import 'output__gif' 17 | @import 'output__sound' 18 | @import 'output__speech' 19 | -------------------------------------------------------------------------------- /style/components/recording.styl: -------------------------------------------------------------------------------- 1 | .recording { 2 | transition: opacity 0.3s linear 3 | position: absolute 4 | z-index: 101 5 | width: 100% 6 | height: 100% 7 | background: rgba(0, 0, 0, 0.8) 8 | opacity: 0 9 | top: 0 10 | left: 0 11 | display: flex 12 | align-items: center 13 | justify-content: center 14 | opacity: 0 15 | pointer-events: none 16 | 17 | &.fadein { 18 | opacity: 1 19 | } 20 | } 21 | 22 | .recording__canvas { 23 | width: 680px 24 | height: 340px 25 | max-width: 100vw 26 | } 27 | 28 | .close-container { 29 | padding: 20px 30 | cursor: pointer 31 | position: absolute 32 | top: 0 33 | right: 0 34 | } 35 | 36 | .close__button { 37 | width: 30px 38 | height: 30px 39 | padding: 20px 40 | background-image: url(assets/close.svg) 41 | background-repeat: no-repeat 42 | background-size: 100% 43 | background-position-y: 0px 44 | } 45 | 46 | .record__timer { 47 | text-align: center 48 | } 49 | 50 | .record__success { 51 | color: $white 52 | display: none 53 | text-align: center 54 | } 55 | 56 | .recording-start__button, 57 | .recording-share__button { 58 | position: relative 59 | } 60 | 61 | .recording-start__button--disabled { 62 | pointer-events: none 63 | filter: grayscale(40%) 64 | } 65 | 66 | #recording__legal { 67 | margin-top: space(2) 68 | } 69 | 70 | #recording__checkbox { 71 | margin-right: space(1) 72 | } 73 | 74 | .button-container { 75 | text-align: center 76 | } 77 | 78 | .animate { 79 | overflow: hidden 80 | 81 | &:after { 82 | position: relative 83 | align-items: center 84 | color: #fff 85 | display: flex 86 | filter: brightness(3); 87 | height: 48px 88 | width: 100% 89 | text-decoration: none 90 | content: "" 91 | opacity: 0.2 92 | background: #ed7572 93 | position: absolute 94 | animation: loader 10s 95 | animation-iteration-count: infinite 96 | animation-timing-function: linear 97 | bottom: 0px 98 | left: 0 99 | width: 100% 100 | margin-left: 0 101 | z-index: 2 102 | } 103 | } 104 | 105 | .sharing-notice { 106 | display: none 107 | position: relative 108 | top: 50px 109 | } 110 | 111 | .message { 112 | color: #fff 113 | text-align: center 114 | } 115 | 116 | .restart { 117 | color: #fff 118 | text-align: center 119 | 120 | a { 121 | color: #fff 122 | } 123 | } 124 | 125 | @keyframes loader { 126 | 0% { 127 | transform: translateX(0%) 128 | } 129 | 100% { 130 | transform: translateX(100%) 131 | } 132 | } 133 | 134 | @media screen and (max-width: 900px) { 135 | .recording { 136 | display: none 137 | } 138 | .recording__canvas { 139 | display: none 140 | } 141 | } -------------------------------------------------------------------------------- /style/components/wizard.styl: -------------------------------------------------------------------------------- 1 | .wizard { 2 | width: 100vw 3 | background: $color__black 4 | position: relative 5 | } 6 | 7 | .wizard--fixed { 8 | position: fixed 9 | bottom: 0px 10 | } 11 | 12 | .wizard__wrapper { 13 | position: relative 14 | overflow: hidden 15 | width: 100vw 16 | z-index: 100 17 | } 18 | 19 | .wizard__inner { 20 | width: 100vw 21 | max-width: 1180px 22 | padding: space(2) space(2) 23 | margin: 0 auto 24 | display: flex 25 | vertical-align: middle 26 | justify-content: space-between 27 | } 28 | 29 | .wizard__sound-button { 30 | display: inline-block 31 | width: 40px 32 | height: 40px 33 | margin: auto space(2) auto 0 34 | background: $color__black 35 | display: none 36 | } 37 | 38 | .wizard__sound-icon { 39 | width: 30px 40 | height: 30px 41 | background-image: url(assets/speaker-icon.svg) 42 | background-repeat: no-repeat 43 | background-size: 100% 44 | background-position-y: 0px 45 | margin: 5px 46 | } 47 | 48 | .wizard__sound-icon.wizard__sound-icon--on { 49 | background-position-y: -30px 50 | } 51 | 52 | .wizard__text { 53 | flex: 1 54 | flex-direction: column; 55 | justify-content: center; 56 | margin: auto 0 auto 0 57 | height: 52px 58 | text-align: left 59 | display: flex 60 | 61 | @extend .font__size--normal 62 | } 63 | 64 | .wizard__timer { 65 | position: relative 66 | margin-top: space(0.5) 67 | width: 80px 68 | height: 2px 69 | background: rgba($color__white, 0.27) 70 | overflow: hidden 71 | } 72 | 73 | .wizard__timer-fill { 74 | width: 0px 75 | height: 2px 76 | background: $color__white 77 | } 78 | 79 | .wizard__skip-button { 80 | 81 | } 82 | 83 | .wizard__text-inner { 84 | width: 80% 85 | vertical-align: middle 86 | align-items: center 87 | } 88 | 89 | 90 | .wizard__skip-button { 91 | display: inline-block 92 | flex: none 93 | padding: 0 space(1) 94 | height: 40px 95 | line-height: 40px 96 | text-transform: uppercase 97 | background: black 98 | margin: auto 0 auto space(2) 99 | cursor: pointer 100 | color: $color__white 101 | text-decoration: none 102 | } 103 | 104 | 105 | .wizard__arrow { 106 | position: absolute 107 | z-index: 100 !important 108 | top: 0 109 | left: 0 110 | pointer-events: none 111 | display: none 112 | } 113 | 114 | .wizard__gif { 115 | z-index: 100000 116 | position: absolute 117 | width: 180px 118 | height: 194px 119 | background-image: url(assets/wizard/gif-backdrop.svg) 120 | background-size: contain 121 | background-position: 0 0 122 | background-repeat: no-repeat 123 | display: none 124 | } 125 | 126 | .wizard__gif-mask { 127 | position: relative 128 | width: 140px 129 | height: 140px 130 | overflow: hidden 131 | margin-top: 35px 132 | margin-left: 20px 133 | } 134 | 135 | .wizard__gif-image { 136 | width: 100% 137 | } 138 | 139 | @media screen and (max-width: 900px) { 140 | 141 | .wizard__arrow { 142 | opacity: 0 143 | } 144 | 145 | .wizard__sound-button { 146 | margin-right: 5px 147 | } 148 | 149 | .wizard__text-inner { 150 | font-size: 11px 151 | width: 100% 152 | } 153 | 154 | .wizard__skip-button { 155 | 156 | span { 157 | display: none 158 | } 159 | } 160 | } 161 | @media screen and (max-height: 680px) { 162 | 163 | .wizard__gif { 164 | top: -130px 165 | left: -100px 166 | } 167 | } -------------------------------------------------------------------------------- /style/config.styl: -------------------------------------------------------------------------------- 1 | $padding = 10px 2 | 3 | space(n) { 4 | return $padding * n 5 | } -------------------------------------------------------------------------------- /style/icons.styl: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: inline-block 3 | width: 14px 4 | height: 14px 5 | position: relative 6 | top: 2px 7 | margin-right: space(0.5) 8 | background-size: contain 9 | background-position: center center 10 | background-repeat: no-repeat 11 | } 12 | 13 | .icon--mic { 14 | background-image url(assets/mic-icon.svg) 15 | } 16 | 17 | .icon--cam { 18 | background-image url(assets/cam-icon.svg) 19 | } 20 | 21 | .icon--record { 22 | background-image url(assets/outputs/record-icon.svg) 23 | } 24 | 25 | .icon--stop { 26 | background-image url(assets/outputs/stop-icon.svg) 27 | } 28 | 29 | .icon--share { 30 | background-image: url(assets/sharing-facebook.svg) 31 | } 32 | 33 | .icon--recorder { 34 | background-image url(assets/outputs/recorder-icon.svg) 35 | } 36 | 37 | .icon--speaker { 38 | background-image url(assets/speaker-icon.svg) 39 | background-position-y: 100% 40 | background-size: 100% 41 | } 42 | 43 | .icon--large { 44 | width: 24px 45 | height: 24px 46 | top: 5px 47 | } 48 | 49 | .icon--facebook { 50 | position: absolute 51 | width: 18px 52 | height: 24px 53 | bottom: 0px 54 | right: 0px 55 | display: inline-block 56 | background-image: url(assets/sharing-facebook.svg) 57 | background-size: contain 58 | background-repeat: no-repeat 59 | } 60 | 61 | .icon--twitter { 62 | position: absolute 63 | width: 28px 64 | height: 30px 65 | left: 0px 66 | top: 0px 67 | 68 | display: inline-block 69 | background-image: url(assets/sharing-twitter.svg) 70 | background-size: 20px 71 | background-repeat: no-repeat 72 | background-position: center center 73 | } 74 | 75 | -------------------------------------------------------------------------------- /style/links.styl: -------------------------------------------------------------------------------- 1 | .link { 2 | color: $color__blue--primary 3 | text-decoration: underline 4 | } 5 | 6 | .link__icon { 7 | height: 50px 8 | vertical-align: middle 9 | padding-right: $padding 10 | } 11 | 12 | .link__text { 13 | color: $color__blue--primary 14 | display: inline-block 15 | text-decoration: underline 16 | } 17 | 18 | .link--grey { 19 | color: rgba($color__black, 0.20) 20 | } 21 | 22 | .link:hover .link__text { 23 | text-decoration: none 24 | } 25 | 26 | .link:hover { 27 | text-decoration: none 28 | } -------------------------------------------------------------------------------- /style/main.styl: -------------------------------------------------------------------------------- 1 | @import 'config' 2 | @import 'colors' 3 | @import 'typography' 4 | @import 'icons' 5 | @import 'buttons' 6 | @import 'links' 7 | @import 'base' 8 | 9 | @import 'components/intro' 10 | @import 'components/faq' 11 | @import 'components/footer' 12 | @import 'components/machine' 13 | @import 'components/outputs' 14 | @import 'components/wizard' 15 | @import 'components/recording' 16 | 17 | 18 | .wrapper { 19 | overflow-x: hidden 20 | } -------------------------------------------------------------------------------- /style/typography.styl: -------------------------------------------------------------------------------- 1 | $font = 'Poppins', sans-serif 2 | 3 | .font__weight--regular { 4 | font-weight: 400 5 | } 6 | 7 | .font__weight--medium { 8 | font-weight: 500 9 | } 10 | 11 | .font__weight--semi-bold { 12 | font-weight: 600 13 | } 14 | 15 | .font__weight--bold { 16 | font-weight: 700 17 | } 18 | 19 | .font__size--xsmall { 20 | font-size: 11px 21 | } 22 | 23 | .font__size--small { 24 | font-size: 14px 25 | } 26 | 27 | .font__size--normal { 28 | font-size: 16px 29 | } 30 | 31 | .font__size--large { 32 | font-size: 18px 33 | } 34 | 35 | .font__size--xlarge { 36 | font-size: 24px 37 | } 38 | 39 | .font__size--xxlarge { 40 | font-size: 30px 41 | } 42 | 43 | .uppercase { 44 | text-transform: uppercase 45 | } 46 | 47 | html, body, input { 48 | font-family: $font 49 | @extend .font__weight--regular 50 | @extend .font__size--normal 51 | } 52 | 53 | h1, h2, h3, h4 { 54 | margin: 0 55 | @extend .font__weight--medium 56 | } --------------------------------------------------------------------------------