├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── firebase.json ├── package-lock.json ├── package.json ├── public ├── 404.html ├── css │ └── main.css ├── favicon.ico ├── google0afd8760fd68f119.html ├── img │ ├── hello-rotavo-square.png │ ├── hello-rotavo-widescreen.png │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg ├── index.html ├── manifest.json └── sw.js ├── rollup.config.js └── src ├── DrawingMode.js ├── OptionsMode.js ├── RotavoApp.js ├── RotavoKiosk.js ├── Sketch.js ├── main.js ├── toggle-fullscreen.js └── touch-knob.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.firebase/ 2 | /.firebaserc 3 | /firebase-debug.log 4 | /functions/firebase-debug.log 5 | /functions/node_modules 6 | /node_modules/ 7 | /public/js/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | # How to Contribute 18 | 19 | We'd love to accept your patches and contributions to this project. There are 20 | just a few small guidelines you need to follow. 21 | 22 | ## Contributor License Agreement 23 | 24 | Contributions to this project must be accompanied by a Contributor License 25 | Agreement. You (or your employer) retain the copyright to your contribution; 26 | this simply gives us permission to use and redistribute your contributions as 27 | part of the project. Head over to to see 28 | your current agreements on file or to sign a new one. 29 | 30 | You generally only need to submit a CLA once, so if you've already submitted one 31 | (even if it was for a different project), you probably don't need to do it 32 | again. 33 | 34 | ## Code reviews 35 | 36 | All submissions, including submissions by project members, require review. We 37 | use GitHub pull requests for this purpose. Consult 38 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 39 | information on using pull requests. 40 | 41 | ## Community Guidelines 42 | 43 | This project follows 44 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Google Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | # Rotavo 🥑 18 | 19 | Enjoy a healthy diversion over at https://rotavo-pwa.firebaseapp.com 20 | 21 | ![Screenshot of Rotavo from ChromeOS showing a depiction of the Chrome Dino](https://user-images.githubusercontent.com/108052/55230605-2f6ee400-5218-11e9-9ce9-6c951d0026be.png) 22 | 23 | ## Installation 24 | 25 | 1. From the project root directory: 26 | ```sh 27 | cd rotavo/ 28 | ``` 29 | 2. Install the dependencies 30 | ```sh 31 | npm install 32 | ``` 33 | 3. Build the bundle 34 | ```sh 35 | npm run build 36 | ``` 37 | 4. Serve the `public` directory using the web server of your choice. The project is configured for Firebase hosting 38 | ```sh 39 | firebase serve 40 | ``` 41 | 42 | ## Contributing 43 | 44 | This is a playground project, so there is so much room for improvement! Issues and pull requests are always welcome. For details, see [CONTRIBUTING](CONTRIBUTING.md) 45 | 46 | This is not an officially supported Google product. 47 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "ignore": [ 4 | "firebase.json", 5 | "**/.*", 6 | "**/node_modules/**" 7 | ], 8 | "public": "public" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rotavo", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.10.1", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", 10 | "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", 11 | "requires": { 12 | "@babel/highlight": "^7.10.1" 13 | } 14 | }, 15 | "@babel/helper-validator-identifier": { 16 | "version": "7.10.1", 17 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz", 18 | "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==" 19 | }, 20 | "@babel/highlight": { 21 | "version": "7.10.1", 22 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", 23 | "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", 24 | "requires": { 25 | "@babel/helper-validator-identifier": "^7.10.1", 26 | "chalk": "^2.0.0", 27 | "js-tokens": "^4.0.0" 28 | } 29 | }, 30 | "@types/estree": { 31 | "version": "0.0.39", 32 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 33 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" 34 | }, 35 | "@types/node": { 36 | "version": "12.7.2", 37 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz", 38 | "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==" 39 | }, 40 | "@types/resolve": { 41 | "version": "0.0.8", 42 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", 43 | "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", 44 | "requires": { 45 | "@types/node": "*" 46 | } 47 | }, 48 | "@webcomponents/custom-elements": { 49 | "version": "1.4.1", 50 | "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.4.1.tgz", 51 | "integrity": "sha512-vNCS1+3sxJOpoIsBjUQiXjGLngakEAGOD5Ale+6ikg6OZG5qI5O39frm3raPhud/IwnF4vec5ags05YBsgzcuA==" 52 | }, 53 | "ansi-styles": { 54 | "version": "3.2.1", 55 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 56 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 57 | "requires": { 58 | "color-convert": "^1.9.0" 59 | } 60 | }, 61 | "buffer-from": { 62 | "version": "1.1.1", 63 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 64 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 65 | }, 66 | "builtin-modules": { 67 | "version": "3.1.0", 68 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", 69 | "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==" 70 | }, 71 | "chalk": { 72 | "version": "2.4.2", 73 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 74 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 75 | "requires": { 76 | "ansi-styles": "^3.2.1", 77 | "escape-string-regexp": "^1.0.5", 78 | "supports-color": "^5.3.0" 79 | } 80 | }, 81 | "color-convert": { 82 | "version": "1.9.3", 83 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 84 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 85 | "requires": { 86 | "color-name": "1.1.3" 87 | } 88 | }, 89 | "color-name": { 90 | "version": "1.1.3", 91 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 92 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 93 | }, 94 | "commander": { 95 | "version": "2.20.3", 96 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 97 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 98 | }, 99 | "escape-string-regexp": { 100 | "version": "1.0.5", 101 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 102 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 103 | }, 104 | "estree-walker": { 105 | "version": "0.6.1", 106 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 107 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==" 108 | }, 109 | "fsevents": { 110 | "version": "2.1.3", 111 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", 112 | "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", 113 | "optional": true 114 | }, 115 | "has-flag": { 116 | "version": "3.0.0", 117 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 118 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 119 | }, 120 | "is-module": { 121 | "version": "1.0.0", 122 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 123 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=" 124 | }, 125 | "is-reference": { 126 | "version": "1.1.3", 127 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.3.tgz", 128 | "integrity": "sha512-W1iHHv/oyBb2pPxkBxtaewxa1BC58Pn5J0hogyCdefwUIvb6R+TGbAcIa4qPNYLqLhb3EnOgUf2MQkkF76BcKw==", 129 | "requires": { 130 | "@types/estree": "0.0.39" 131 | } 132 | }, 133 | "jest-worker": { 134 | "version": "26.0.0", 135 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz", 136 | "integrity": "sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw==", 137 | "requires": { 138 | "merge-stream": "^2.0.0", 139 | "supports-color": "^7.0.0" 140 | }, 141 | "dependencies": { 142 | "has-flag": { 143 | "version": "4.0.0", 144 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 145 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 146 | }, 147 | "supports-color": { 148 | "version": "7.1.0", 149 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", 150 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", 151 | "requires": { 152 | "has-flag": "^4.0.0" 153 | } 154 | } 155 | } 156 | }, 157 | "js-tokens": { 158 | "version": "4.0.0", 159 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 160 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 161 | }, 162 | "magic-string": { 163 | "version": "0.25.3", 164 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.3.tgz", 165 | "integrity": "sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA==", 166 | "requires": { 167 | "sourcemap-codec": "^1.4.4" 168 | } 169 | }, 170 | "merge-stream": { 171 | "version": "2.0.0", 172 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 173 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" 174 | }, 175 | "path-parse": { 176 | "version": "1.0.6", 177 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 178 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" 179 | }, 180 | "randombytes": { 181 | "version": "2.1.0", 182 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 183 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 184 | "requires": { 185 | "safe-buffer": "^5.1.0" 186 | } 187 | }, 188 | "resolve": { 189 | "version": "1.12.0", 190 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", 191 | "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", 192 | "requires": { 193 | "path-parse": "^1.0.6" 194 | } 195 | }, 196 | "rollup": { 197 | "version": "2.11.2", 198 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.11.2.tgz", 199 | "integrity": "sha512-pJT6mfH+/gh1sOWyNMAWxjbYGL5x2AfsaR0SWLRwq2e7vxOKt/0mBjtYDTVYF8JXxVzmnuDzA+EpsPLWt/oyrg==", 200 | "requires": { 201 | "fsevents": "~2.1.2" 202 | } 203 | }, 204 | "rollup-plugin-commonjs": { 205 | "version": "10.1.0", 206 | "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", 207 | "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", 208 | "requires": { 209 | "estree-walker": "^0.6.1", 210 | "is-reference": "^1.1.2", 211 | "magic-string": "^0.25.2", 212 | "resolve": "^1.11.0", 213 | "rollup-pluginutils": "^2.8.1" 214 | } 215 | }, 216 | "rollup-plugin-node-resolve": { 217 | "version": "5.2.0", 218 | "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", 219 | "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", 220 | "requires": { 221 | "@types/resolve": "0.0.8", 222 | "builtin-modules": "^3.1.0", 223 | "is-module": "^1.0.0", 224 | "resolve": "^1.11.1", 225 | "rollup-pluginutils": "^2.8.1" 226 | } 227 | }, 228 | "rollup-plugin-terser": { 229 | "version": "6.1.0", 230 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-6.1.0.tgz", 231 | "integrity": "sha512-4fB3M9nuoWxrwm39habpd4hvrbrde2W2GG4zEGPQg1YITNkM3Tqur5jSuXlWNzbv/2aMLJ+dZJaySc3GCD8oDw==", 232 | "requires": { 233 | "@babel/code-frame": "^7.8.3", 234 | "jest-worker": "^26.0.0", 235 | "serialize-javascript": "^3.0.0", 236 | "terser": "^4.7.0" 237 | } 238 | }, 239 | "rollup-pluginutils": { 240 | "version": "2.8.1", 241 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz", 242 | "integrity": "sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg==", 243 | "requires": { 244 | "estree-walker": "^0.6.1" 245 | } 246 | }, 247 | "safe-buffer": { 248 | "version": "5.2.1", 249 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 250 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 251 | }, 252 | "serialize-javascript": { 253 | "version": "3.1.0", 254 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", 255 | "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", 256 | "requires": { 257 | "randombytes": "^2.1.0" 258 | } 259 | }, 260 | "shake.js": { 261 | "version": "1.2.2", 262 | "resolved": "https://registry.npmjs.org/shake.js/-/shake.js-1.2.2.tgz", 263 | "integrity": "sha1-sqxWCoKr5o14oCliOnCI4bMLrP8=" 264 | }, 265 | "simplify-js": { 266 | "version": "1.2.4", 267 | "resolved": "https://registry.npmjs.org/simplify-js/-/simplify-js-1.2.4.tgz", 268 | "integrity": "sha512-vITfSlwt7h/oyrU42R83mtzFpwYk3+mkH9bOHqq/Qw6n8rtR7aE3NZQ5fbcyCUVVmuMJR6ynsAhOfK2qoah8Jg==" 269 | }, 270 | "source-map": { 271 | "version": "0.6.1", 272 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 273 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 274 | }, 275 | "source-map-support": { 276 | "version": "0.5.19", 277 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 278 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 279 | "requires": { 280 | "buffer-from": "^1.0.0", 281 | "source-map": "^0.6.0" 282 | } 283 | }, 284 | "sourcemap-codec": { 285 | "version": "1.4.6", 286 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", 287 | "integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==" 288 | }, 289 | "supports-color": { 290 | "version": "5.5.0", 291 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 292 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 293 | "requires": { 294 | "has-flag": "^3.0.0" 295 | } 296 | }, 297 | "terser": { 298 | "version": "4.7.0", 299 | "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz", 300 | "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==", 301 | "requires": { 302 | "commander": "^2.20.0", 303 | "source-map": "~0.6.1", 304 | "source-map-support": "~0.5.12" 305 | } 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rotavo", 3 | "version": "1.0.0", 4 | "description": "Create a whole drawing with just one line using the on-screen controls in this progressive web app.", 5 | "main": "public/bundle.js", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "build:watch": "rollup -cw" 9 | }, 10 | "author": "Rowan Merewood ", 11 | "license": "Apache-2.0", 12 | "repository": "https://github.com/GoogleChromeLabs/rotavo", 13 | "dependencies": { 14 | "@webcomponents/custom-elements": "^1.4.1", 15 | "rollup": "^2.11.2", 16 | "rollup-plugin-commonjs": "^10.1.0", 17 | "rollup-plugin-node-resolve": "^5.2.0", 18 | "rollup-plugin-terser": "^6.1.0", 19 | "shake.js": "^1.2.2", 20 | "simplify-js": "^1.2.4" 21 | }, 22 | "devDependencies": {} 23 | } 24 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Page Not Found 23 | 24 | 39 | 40 | 41 |
42 |

404

43 |

Page Not Found

44 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

45 |

Why am I seeing this?

46 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | html { 19 | margin: 0; 20 | padding: 0; 21 | overflow: hidden; 22 | overscroll-behavior: none; 23 | } 24 | 25 | body { 26 | font-family: sans-serif; 27 | margin: 0; 28 | padding: 0; 29 | overflow: hidden; 30 | background: #356211; 31 | color: #AA471F; 32 | } 33 | 34 | .casing { 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: space-between; 38 | align-items: flex-end; 39 | height: calc(96vh - 12px); 40 | padding: 2vh 3vw 2vh 3vw; 41 | border: 6px solid #356211; 42 | border-radius: 12px; 43 | background: #F2E880; 44 | box-shadow: inset 0 20px 40px 20px #6C8F32; 45 | } 46 | 47 | .screen { 48 | position: absolute; 49 | top: 2px; 50 | right: 2px; 51 | bottom: 2px; 52 | left: 2px; 53 | } 54 | 55 | .controls { 56 | width: 100%; 57 | margin-bottom: 4px; 58 | display: flex; 59 | flex-flow: row nowrap; 60 | justify-content: space-between; 61 | align-items: flex-end; 62 | } 63 | 64 | touch-knob { 65 | flex: 0 0 80px; 66 | width: 80px; 67 | height: 80px; 68 | color: #AA471F; 69 | opacity: 0.6; 70 | font-weight: bolder; 71 | text-align: center; 72 | user-select: none; 73 | -webkit-user-select: none; 74 | touch-action: none; 75 | will-change: transform; 76 | margin: 0; 77 | overflow: hidden; 78 | contain: strict; 79 | } 80 | 81 | .knob-shape { 82 | stroke-width: 5; 83 | stroke: #356211; 84 | stroke-linejoin: round; 85 | fill: url(#avoflesh); 86 | } 87 | 88 | .knob-seed { 89 | stroke-width: 5; 90 | stroke: #AA471F; 91 | stroke-linejoin: round; 92 | fill: #C15C37; 93 | } 94 | 95 | .knob-label { 96 | font-size: 20px; 97 | fill: #AA471F; 98 | } 99 | 100 | .knob-arrow { 101 | font-size: 20px; 102 | fill: #F2E880; 103 | } 104 | 105 | .buttons { 106 | display: flex; 107 | flex-wrap: wrap; 108 | justify-content: center; 109 | align-content: flex-end; 110 | } 111 | 112 | button { 113 | width: 90px; 114 | user-select: none; 115 | -webkit-user-select: none; 116 | transition: transform 0.1s ease; 117 | background: #6C8F32; 118 | opacity: 0.6; 119 | border: 1px solid #356211; 120 | border-radius: 4px; 121 | outline: 0; 122 | padding: 7px; 123 | margin: 3px; 124 | color: #F2E880; 125 | font-weight: bold; 126 | } 127 | 128 | button:focus { 129 | border: 1px solid #558303; 130 | } 131 | 132 | button:active { 133 | transform: scale(0.9); 134 | } 135 | 136 | .active-sketch, 137 | .erased-sketch { 138 | position: absolute; 139 | top: 0.5%; 140 | left: 0.5%; 141 | width: 99%; 142 | height: 99%; 143 | stroke-width: 4; 144 | stroke-linecap: round; 145 | stroke-linejoin: round; 146 | } 147 | 148 | .sketch-stylus { 149 | stroke: #dd9e87; 150 | vector-effect: non-scaling-stroke; 151 | } 152 | 153 | .sketch-path { 154 | stroke: #AA471F; 155 | vector-effect: non-scaling-stroke; 156 | fill: none; 157 | } 158 | 159 | .fancy>.active-sketch, 160 | .fancy>.erased-sketch { 161 | filter: url(#pencil-effect); 162 | } 163 | 164 | .fancy>.active-sketch>.sketch-path, 165 | .fancy>.erased-sketch>.sketch-path { 166 | filter: url(#drop-shadow); 167 | } 168 | 169 | .fanciest>.active-sketch, 170 | .fanciest>.erased-sketch { 171 | filter: url(#pencil-effect); 172 | } 173 | 174 | .fanciest>.active-sketch>.sketch-path, 175 | .fanciest>.erased-sketch>.sketch-path { 176 | stroke: url(#rainbow-effect); 177 | filter: url(#drop-shadow); 178 | } 179 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/rotavo/7844314fae8b502f0eeafbbb0ae5f23a914fdecf/public/favicon.ico -------------------------------------------------------------------------------- /public/google0afd8760fd68f119.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google0afd8760fd68f119.html -------------------------------------------------------------------------------- /public/img/hello-rotavo-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/rotavo/7844314fae8b502f0eeafbbb0ae5f23a914fdecf/public/img/hello-rotavo-square.png -------------------------------------------------------------------------------- /public/img/hello-rotavo-widescreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/rotavo/7844314fae8b502f0eeafbbb0ae5f23a914fdecf/public/img/hello-rotavo-widescreen.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/rotavo/7844314fae8b502f0eeafbbb0ae5f23a914fdecf/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/rotavo/7844314fae8b502f0eeafbbb0ae5f23a914fdecf/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/rotavo/7844314fae8b502f0eeafbbb0ae5f23a914fdecf/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #6c8f32 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/rotavo/7844314fae8b502f0eeafbbb0ae5f23a914fdecf/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/rotavo/7844314fae8b502f0eeafbbb0ae5f23a914fdecf/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/rotavo/7844314fae8b502f0eeafbbb0ae5f23a914fdecf/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | Rotavo 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | 56 | 57 | 58 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 69 | 70 | 71 | 73 | 74 | 75 | 77 | 78 | 79 | 81 | 82 | 83 | 85 | 86 | 87 | 89 | 90 | 91 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 108 | 109 |
110 |
111 | 137 |
138 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rotavo", 3 | "short_name": "Rotavo", 4 | "icons": [ 5 | { 6 | "src": "/img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#356211", 17 | "background_color": "#6c8f32", 18 | "start_url": "/", 19 | "display": "standalone", 20 | "orientation": "any" 21 | } 22 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | var cacheName = 'rotavo-v20190828a'; 15 | 16 | // Cache a very basic selection of resources 17 | self.addEventListener('install', (event) => { 18 | event.waitUntil( 19 | caches.open(cacheName).then((cache) => { 20 | return cache.addAll([ 21 | '/', 22 | '/css/main.css', 23 | '/favicon.ico', 24 | '/img/icons/android-chrome-192x192.png', 25 | '/img/icons/android-chrome-512x512.png', 26 | '/img/icons/favicon-16x16.png', 27 | '/img/icons/favicon-32x32.png', 28 | '/index.html', 29 | '/js/main.js', 30 | ]); 31 | }).then(() => { 32 | return self.skipWaiting(); 33 | }) 34 | ); 35 | }); 36 | 37 | // Clean out old caches 38 | self.addEventListener('activate', (event) => { 39 | event.waitUntil( 40 | caches.keys().then((existingCacheNames) => { 41 | return Promise.all( 42 | existingCacheNames.map((existingCacheName) => { 43 | if (existingCacheName !== cacheName) { 44 | return caches.delete(existingCacheName); 45 | } 46 | }) 47 | ); 48 | }).then(() => { 49 | return self.clients.claim(); 50 | }) 51 | ); 52 | }); 53 | 54 | self.addEventListener('fetch', (event) => { 55 | event.respondWith( 56 | caches.match(event.request).then((response) => { 57 | return response || fetch(event.request); 58 | }) 59 | ); 60 | }); 61 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 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 resolve from 'rollup-plugin-node-resolve'; 16 | import commonjs from 'rollup-plugin-commonjs'; 17 | import { terser } from 'rollup-plugin-terser'; 18 | 19 | export default [{ 20 | input: 'src/main.js', 21 | output: { 22 | file: 'public/js/main.js', 23 | format: 'iife', 24 | sourcemap: true 25 | }, 26 | plugins: [ 27 | resolve(), 28 | commonjs({ 29 | exclude: [ 30 | 'node_modules/simplify-js/**', 31 | 'node_modules/shake.js/**', 32 | ] 33 | }), 34 | terser(), 35 | ] 36 | }]; 37 | -------------------------------------------------------------------------------- /src/DrawingMode.js: -------------------------------------------------------------------------------- 1 | 2 | import 'shake.js'; // imports Shake 3 | 4 | export class DrawingMode { 5 | 6 | constructor(rootElement, app) { 7 | this._rootElement = rootElement; 8 | this._app = app; 9 | 10 | this._canDraw = true; 11 | this._lastSketchAngle = null; 12 | this._lastDraw = 0; 13 | this._drawInterval = 0; 14 | this._keyInterval = 200; 15 | this._horzKeys = { 16 | w: { 17 | pressed: false, 18 | timestamp: 0, 19 | value: 0.01, 20 | prev: { key: 'a', value: -0.785 }, 21 | next: { key: 'd', value: 0.785 } 22 | }, 23 | d: { 24 | pressed: false, 25 | timestamp: 0, 26 | value: 1.57, 27 | prev: { key: 'w', value: 0.785 }, 28 | next: { key: 's', value: 2.355 } 29 | }, 30 | s: { 31 | pressed: false, 32 | timestamp: 0, 33 | value: 3.14, 34 | prev: { key: 'd', value: 2.355 }, 35 | next: { key: 'a', value: -2.355 } 36 | }, 37 | a: { 38 | pressed: false, 39 | timestamp: 0, 40 | value: -1.56, 41 | prev: { key: 's', value: -2.355 }, 42 | next: { key: 'w', value: -0.785 } 43 | } 44 | }; 45 | 46 | this._vertKeys = { 47 | i: { 48 | pressed: false, 49 | timestamp: 0, 50 | value: 0.01, 51 | prev: { key: 'j', value: -0.785 }, 52 | next: { key: 'l', value: 0.785 } 53 | }, 54 | l: { 55 | pressed: false, 56 | timestamp: 0, 57 | value: 1.57, 58 | prev: { key: 'i', value: 0.785 }, 59 | next: { key: 'k', value: 2.355 } 60 | }, 61 | k: { 62 | pressed: false, 63 | timestamp: 0, 64 | value: 3.14, 65 | prev: { key: 'l', value: 2.355 }, 66 | next: { key: 'j', value: -2.355 } 67 | }, 68 | j: { 69 | pressed: false, 70 | timestamp: 0, 71 | value: -1.56, 72 | prev: { key: 'k', value: -2.355 }, 73 | next: { key: 'i', value: -0.785 } 74 | } 75 | }; 76 | 77 | this.activate = this.activate.bind(this); 78 | this._handleKeydown = this._handleKeydown.bind(this); 79 | this._handleKeyup = this._handleKeyup.bind(this); 80 | this._onShake = this._onShake.bind(this); 81 | this._optimizeSketch = this._optimizeSketch.bind(this); 82 | this._requestOptimizeSketch = this._requestOptimizeSketch.bind(this); 83 | this._requestUpdateSketch = this._requestUpdateSketch.bind(this); 84 | this._toggleDetail = this._toggleDetail.bind(this); 85 | this._updateSketch = this._updateSketch.bind(this); 86 | 87 | this._screen = this._rootElement.querySelector('.screen'); 88 | this._activeSketch = this._rootElement.querySelector('.active-sketch'); 89 | this._path = this._rootElement.querySelector('.sketch-path'); 90 | this._stylus = this._rootElement.querySelector('.sketch-stylus'); 91 | 92 | this._horzKnob = this._rootElement.querySelector('.horz-knob'); 93 | this._vertKnob = this._rootElement.querySelector('.vert-knob'); 94 | } 95 | 96 | activate() { 97 | this._rootElement.addEventListener('touch-knob-move', this._requestUpdateSketch, { capture: false, passive: true }); 98 | this._rootElement.addEventListener('touch-knob-end', this._requestOptimizeSketch, { capture: false, passive: true }); 99 | 100 | this._shakeDetector = new Shake({ threshold: 5, timeout: 200 }); 101 | this._shakeDetector.start(); 102 | window.addEventListener('shake', this._onShake, { capture: false, passive: true }); 103 | this._jiggleButton = this._rootElement.querySelector('.button-jiggle'); 104 | 105 | if (this._jiggleButton) { 106 | this._jiggleButton.addEventListener('click', this._onShake, { capture: false, passive: true }); 107 | } 108 | 109 | this._detailButton = this._rootElement.querySelector('.button-detail'); 110 | 111 | if (this._detailButton) { 112 | this._detailButton.addEventListener('click', this._toggleDetail, { capture: false, passive: true }); 113 | } 114 | 115 | this._rootElement.addEventListener('keydown', this._handleKeydown, { capture: false, passive: true }); 116 | this._rootElement.addEventListener('keyup', this._handleKeyup, { capture: false, passive: true }); 117 | } 118 | 119 | _isAnyKeyPressed(config) { 120 | for (let key in config) { 121 | if (config.hasOwnProperty(key) && config[key]['pressed'] === true) { 122 | return true; 123 | } 124 | } 125 | } 126 | _handleRotateKeydown(key, config, knob) { 127 | if (config[key]['pressed'] != false) { 128 | return; 129 | } 130 | 131 | const current = config[key]; 132 | const prev = config[config[key]['prev']['key']]; 133 | const next = config[config[key]['next']['key']]; 134 | 135 | if (prev['pressed'] === true || Date.now() - prev['timestamp'] < this._keyInterval) { 136 | knob.rotateToContinue(current['prev']['value']); 137 | } else if (next['pressed'] === true || Date.now() - next['timestamp'] < this._keyInterval) { 138 | knob.rotateToContinue(current['next']['value']); 139 | } else if (this._isAnyKeyPressed(config)) { 140 | knob.rotateToContinue(current['value']); 141 | } else { 142 | knob.rotateToStart(current['value']); 143 | } 144 | 145 | current['pressed'] = true; 146 | current['timestamp'] = Date.now(); 147 | } 148 | 149 | _handleRotateKeyup(key, config, knob) { 150 | config[key]['pressed'] = false; 151 | 152 | const prev = config[config[key]['prev']['key']]; 153 | const next = config[config[key]['next']['key']]; 154 | 155 | if (prev['pressed'] === true) { 156 | knob.rotateToContinue(prev['value']); 157 | } else if (next['pressed'] == true) { 158 | knob.rotateToContinue(next['value']); 159 | } else { 160 | knob.rotateToEnd(); 161 | } 162 | } 163 | 164 | _handleKeydown(event) { 165 | if (['w', 'd', 's', 'a'].includes(event.key)) { 166 | this._handleRotateKeydown(event.key, this._horzKeys, this._horzKnob); 167 | } else if (['i', 'l', 'k', 'j'].includes(event.key)) { 168 | this._handleRotateKeydown(event.key, this._vertKeys, this._vertKnob); 169 | } else if (event.key === 'x') { 170 | this._onShake(); 171 | } 172 | } 173 | 174 | _handleKeyup(event) { 175 | if (['w', 'd', 's', 'a'].includes(event.key)) { 176 | this._handleRotateKeyup(event.key, this._horzKeys, this._horzKnob); 177 | } else if (['i', 'l', 'k', 'j'].includes(event.key)) { 178 | this._handleRotateKeyup(event.key, this._vertKeys, this._vertKnob); 179 | } else if (event.key === 'r') { 180 | this._app.activateOptionsMode(); 181 | } 182 | } 183 | 184 | setDrawInterval(milliseconds) { 185 | this._drawInterval = milliseconds; 186 | } 187 | 188 | _requestUpdateSketch() { 189 | if (this._canDraw === true && Date.now() - this._lastDraw > this._drawInterval) { 190 | this._canDraw = false; 191 | this._lastDraw = Date.now(); 192 | window.requestAnimationFrame(this._updateSketch); 193 | } 194 | } 195 | 196 | _requestOptimizeSketch() { 197 | if (this._canDraw === true) { 198 | this._canDraw = false; 199 | window.requestAnimationFrame(this._optimizeSketch); 200 | } 201 | } 202 | 203 | _updateSketch() { 204 | this._app.sketchModel.moveTo({ 205 | x: this._horzKnob.value, 206 | y: this._vertKnob.value 207 | }); 208 | this._drawSketch(); 209 | this._canDraw = true; 210 | } 211 | 212 | _optimizeSketch() { 213 | this._app.sketchModel.simplifyPath(); 214 | this._drawSketch(); 215 | this._canDraw = true; 216 | } 217 | 218 | _drawSketch() { 219 | const path = this._app.sketchModel.path; 220 | const start = path[0]; 221 | let svgPath = `M ${start.x - 2} ${start.y - 2} M ${start.x + 2} ${start.y + 2} M ${start.x} ${start.y}`; 222 | 223 | for (let i = 1; i < path.length; i++) { 224 | const point = path[i]; 225 | svgPath += ` L ${point.x} ${point.y}`; 226 | } 227 | 228 | this._path.setAttribute('d', svgPath); 229 | 230 | const stylus = this._app.sketchModel.lastPoint; 231 | this._stylus.setAttribute('x1', stylus.x); 232 | this._stylus.setAttribute('x2', stylus.x); 233 | this._stylus.setAttribute('y1', stylus.y); 234 | this._stylus.setAttribute('y2', stylus.y); 235 | } 236 | 237 | _onShake() { 238 | if (this._app.sketchModel.jiggle()) { 239 | const newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 240 | const start = this._app.sketchModel.path[0]; 241 | const initialPath = `M ${start.x - 2} ${start.y - 2} M ${start.x + 2} ${start.y + 2} M ${start.x} ${start.y}`; 242 | newPath.setAttributeNS(null, 'd', initialPath); 243 | newPath.classList.add('sketch-path'); 244 | 245 | const erasedSketch = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 246 | erasedSketch.classList.add('erased-sketch'); 247 | erasedSketch.setAttributeNS(null, 'preserveAspectRatio', 'none'); 248 | erasedSketch.setAttributeNS(null, 'x', '0'); 249 | erasedSketch.setAttributeNS(null, 'y', '0'); 250 | erasedSketch.setAttributeNS(null, 'viewBox', '-2 -2 302 302'); 251 | erasedSketch.style.opacity = '1'; 252 | erasedSketch.appendChild(this._path); 253 | 254 | this._path = newPath; 255 | this._activeSketch.insertBefore(this._path, this._stylus); 256 | this._screen.insertBefore(erasedSketch, this._activeSketch); 257 | } 258 | 259 | this._screen.querySelectorAll('.erased-sketch').forEach(erasedSketch => { 260 | let opacity = Number.parseFloat(erasedSketch.style.opacity); 261 | 262 | if (opacity <= 0) { 263 | this._screen.removeChild(erasedSketch); 264 | } else { 265 | opacity -= 0.21; 266 | const blur = 1 - opacity; 267 | erasedSketch.style.opacity = opacity; 268 | erasedSketch.style.filter = 'blur(' + blur + 'vw)'; 269 | } 270 | }); 271 | } 272 | 273 | _toggleDetail() { 274 | switch (this._detailButton.value) { 275 | case 'fast': 276 | this._screen.classList.add('fast'); 277 | this._screen.classList.remove('fanciest', 'fancy'); 278 | this._detailButton.textContent = '✨ Fancy'; 279 | this._detailButton.value = 'fancy'; 280 | break; 281 | case 'fancy': 282 | this._screen.classList.add('fancy'); 283 | this._screen.classList.remove('fanciest', 'fast'); 284 | this._detailButton.textContent = '🌈 Fanciest'; 285 | this._detailButton.value = 'fanciest'; 286 | break; 287 | case 'fanciest': 288 | this._screen.classList.add('fanciest'); 289 | this._screen.classList.remove('fancy', 'fast'); 290 | this._detailButton.textContent = '🚀 Fast'; 291 | this._detailButton.value = 'fast'; 292 | break; 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/OptionsMode.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import 'firebase/firestore'; 3 | import QRious from 'qrious'; 4 | 5 | export class OptionsMode { 6 | constructor(rootElement, app) { 7 | this._rootElement = rootElement; 8 | this._app = app; 9 | this._db = null; 10 | 11 | this._screen = this._rootElement.querySelector('.screen'); 12 | this._detailButton = this._rootElement.querySelector('.button-detail'); 13 | this._options = this._rootElement.querySelector('.options'); 14 | this._qrcode = this._rootElement.querySelector('.qrcode'); 15 | this._drawingLink = this._rootElement.querySelector('.drawing-link>a'); 16 | 17 | this.activate = this.activate.bind(this); 18 | this.deactivate = this.deactivate.bind(this); 19 | this._handleKeydown = this._handleKeydown.bind(this); 20 | this._handleKeyup = this._handleKeyup.bind(this); 21 | this._toggleDetail = this._toggleDetail.bind(this); 22 | 23 | this._initFirebase(); 24 | } 25 | 26 | _initFirebase() { 27 | if (!this._db) { 28 | // Initialize Cloud Firestore through Firebase 29 | firebase.initializeApp({ 30 | apiKey: 'AIzaSyCV444-QKFwglGHmhJbVgCN6Cj2GXf5KIM', 31 | authDomain: 'rotavo-pwa.firebaseapp.com', 32 | databaseURL: 'https://rotavo-pwa.firebaseio.com', 33 | projectId: 'rotavo-pwa', 34 | storageBucket: 'rotavo-pwa.appspot.com', 35 | messagingSenderId: '646348013955' 36 | }); 37 | 38 | this._db = firebase.firestore(); 39 | } 40 | } 41 | 42 | activate() { 43 | const qrcode = this._qrcode; 44 | const drawingLink = this._drawingLink; 45 | this._db.collection('drawings').add({ 46 | path: this._app.sketchModel.path 47 | }).then(function (docRef) { 48 | const url = 'https://rotavo-pwa.firebaseapp.com/sharing/' + docRef.id; 49 | drawingLink.href = url; 50 | drawingLink.textContent = url; 51 | 52 | const qr = new QRious({ 53 | element: qrcode, 54 | value: url, 55 | size: 200, 56 | foreground: 'black', 57 | background: 'white' 58 | }); 59 | }) 60 | .catch(function (error) { 61 | console.error("Error adding document: ", error); 62 | });; 63 | 64 | this._options.classList.add('options-active'); 65 | 66 | this._rootElement.addEventListener('keydown', this._handleKeydown, { capture: false, passive: true }); 67 | this._rootElement.addEventListener('keyup', this._handleKeyup, { capture: false, passive: true }); 68 | } 69 | 70 | deactivate() { 71 | this._options.classList.remove('options-active'); 72 | 73 | this._rootElement.removeEventListener('keydown', this._handleKeydown, { capture: false, passive: true }); 74 | this._rootElement.removeEventListener('keyup', this._handleKeyup, { capture: false, passive: true }); 75 | } 76 | 77 | _handleKeydown(event) { 78 | switch (event.key) { 79 | case 'w': 80 | break; 81 | case 'd': 82 | break; 83 | case 's': 84 | break; 85 | case 'a': 86 | break; 87 | case 'i': 88 | break; 89 | case 'l': 90 | break; 91 | case 'k': 92 | break; 93 | case 'j': 94 | break; 95 | case 'x': 96 | this._toggleDetail(); 97 | break; 98 | case 'r': 99 | break; 100 | } 101 | } 102 | 103 | _handleKeyup(event) { 104 | switch (event.key) { 105 | case 'w': 106 | break; 107 | case 'd': 108 | break; 109 | case 's': 110 | break; 111 | case 'a': 112 | break; 113 | case 'i': 114 | break; 115 | case 'l': 116 | break; 117 | case 'k': 118 | break; 119 | case 'j': 120 | break; 121 | case 'x': 122 | break; 123 | case 'r': 124 | this._app.activateDrawingMode(); 125 | break; 126 | } 127 | } 128 | 129 | _toggleDetail() { 130 | console.log(this._detailButton.value); 131 | switch (this._detailButton.value) { 132 | case 'fast': 133 | this._screen.classList.add('fast'); 134 | this._screen.classList.remove('fanciest', 'fancy'); 135 | this._detailButton.textContent = '✨ Fancy'; 136 | this._detailButton.value = 'fancy'; 137 | break; 138 | case 'fancy': 139 | this._screen.classList.add('fancy'); 140 | this._screen.classList.remove('fanciest', 'fast'); 141 | this._detailButton.textContent = '🌈 Fanciest'; 142 | this._detailButton.value = 'fanciest'; 143 | break; 144 | case 'fanciest': 145 | this._screen.classList.add('fanciest'); 146 | this._screen.classList.remove('fancy', 'fast'); 147 | this._detailButton.textContent = '🚀 Fast'; 148 | this._detailButton.value = 'fast'; 149 | break; 150 | } 151 | } 152 | 153 | _toggleDetail() { 154 | switch (this._detailButton.value) { 155 | case 'fast': 156 | this._screen.classList.add('fast'); 157 | this._screen.classList.remove('fanciest', 'fancy'); 158 | this._detailButton.textContent = '✨ Fancy'; 159 | this._detailButton.value = 'fancy'; 160 | break; 161 | case 'fancy': 162 | this._screen.classList.add('fancy'); 163 | this._screen.classList.remove('fanciest', 'fast'); 164 | this._detailButton.textContent = '🌈 Fanciest'; 165 | this._detailButton.value = 'fanciest'; 166 | break; 167 | case 'fanciest': 168 | this._screen.classList.add('fanciest'); 169 | this._screen.classList.remove('fancy', 'fast'); 170 | this._detailButton.textContent = '🚀 Fast'; 171 | this._detailButton.value = 'fast'; 172 | break; 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/RotavoApp.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 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 { DrawingMode } from './DrawingMode'; 16 | import { Sketch } from './Sketch'; 17 | 18 | export class RotavoApp { 19 | constructor(rootElement, optionsModeEnabled) { 20 | this._drawingMode = new DrawingMode(rootElement, this); 21 | this.sketchModel = new Sketch({ x: 10, y: 10 }); 22 | this._drawingMode.activate(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/RotavoKiosk.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 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 { DrawingMode } from './DrawingMode'; 16 | import { OptionsMode } from './OptionsMode'; 17 | import { Sketch } from './Sketch'; 18 | 19 | export class RotavoKiosk { 20 | constructor(rootElement) { 21 | this._drawingMode = new DrawingMode(rootElement, this); 22 | this._drawingMode.setDrawInterval(150); 23 | this._optionsMode = new OptionsMode(rootElement, this); 24 | this.sketchModel = new Sketch({ x: 2, y: 2 }); 25 | this._drawingMode.activate(); 26 | } 27 | 28 | activateOptionsMode() { 29 | this._drawingMode.deactivate(); 30 | this._optionsMode.activate(); 31 | } 32 | 33 | activateDrawingMode() { 34 | this._optionsMode.deactivate(); 35 | this._drawingMode.activate(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Sketch.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 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 'simplify-js'; // imports simplify() 16 | 17 | export class Sketch { 18 | constructor(startingPoint) { 19 | this.path = [startingPoint]; 20 | this.erasedPaths = []; 21 | this._lastAngle = null; 22 | } 23 | 24 | get lastPoint() { 25 | return this.path[this.path.length - 1]; 26 | } 27 | 28 | get lastAngle() { 29 | if (this.path.length < 2) { 30 | return null; 31 | } 32 | 33 | const penultimatePoint = this.path[this.path.length - 2]; 34 | 35 | return Math.atan2( 36 | this.lastPoint.x - penultimatePoint.x, 37 | this.lastPoint.y - penultimatePoint.y 38 | ); 39 | } 40 | 41 | moveTo(point) { 42 | point = { 43 | x: Math.round(parseFloat(point.x)*1000)/1000, 44 | y: Math.round(parseFloat(point.y)*1000)/1000 45 | } 46 | 47 | const angle = Math.atan2( 48 | point.x - this.lastPoint.x, 49 | point.y - this.lastPoint.y 50 | ); 51 | 52 | if (angle === this.lastAngle && (this.lastPoint.x !== point.x || this.lastPoint.y !== point.y)) { 53 | this.path.pop(); 54 | } 55 | 56 | this.path.push(point); 57 | } 58 | 59 | simplifyPath() { 60 | this.path = simplify(this.path, 0.5); 61 | } 62 | 63 | jiggle() { 64 | if (this.path.length > 1) { 65 | this.erasedPaths.push(this.path); 66 | this.path = [this.lastPoint]; 67 | this._lastAngle = null; 68 | return true; 69 | } 70 | 71 | return false; 72 | } 73 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 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 | import '@webcomponents/custom-elements'; 15 | import { TouchKnob } from './touch-knob'; 16 | import { RotavoApp } from './RotavoApp'; 17 | import { ToggleFullscreen } from './toggle-fullscreen'; 18 | 19 | window.customElements.define('touch-knob', TouchKnob); 20 | window.customElements.define('toggle-fullscreen', ToggleFullscreen); 21 | 22 | window.customElements.whenDefined('touch-knob').then(initApp()); 23 | 24 | function initApp() { 25 | const app = new RotavoApp(document, false); 26 | } 27 | 28 | if ('serviceWorker' in navigator) { 29 | window.addEventListener('load', function () { 30 | navigator.serviceWorker 31 | .register('/sw.js') 32 | .then(function (registration) { }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/toggle-fullscreen.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 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 | export class ToggleFullscreen extends HTMLElement { 16 | 17 | constructor() { 18 | super(); 19 | 20 | this._onFullscreenchange = this._onFullscreenchange.bind(this); 21 | this._toggleFullscreen = this._toggleFullscreen.bind(this); 22 | } 23 | 24 | 25 | connectedCallback() { 26 | document.addEventListener('fullscreenchange', this._onFullscreenchange, { capture: false, passive: true }); 27 | 28 | this._fullscreenButton = this.querySelector('.button-fullscreen'); 29 | this._fullscreenButton.addEventListener('click', this._toggleFullscreen, { capture: false, passive: true }); 30 | } 31 | 32 | disconnectedCallback() { 33 | this._fullscreenButton.removeEventListener('click', this._toggleFullscreen); 34 | } 35 | 36 | _onFullscreenchange() { 37 | if (document.fullscreenElement !== null) { 38 | this._fullscreenButton.textContent = '⤵️ Return'; 39 | } else { 40 | this._fullscreenButton.textContent = '⤴️ Full'; 41 | } 42 | } 43 | 44 | _toggleFullscreen() { 45 | if (!document.fullscreenElement) { 46 | document.documentElement.requestFullscreen(); 47 | } else { 48 | if (document.exitFullscreen) { 49 | document.exitFullscreen(); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/touch-knob.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 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 | export class TouchKnob extends HTMLElement { 16 | constructor() { 17 | super(); 18 | this._angle = 0; 19 | this._canDraw = true; 20 | this._rotations = 0; 21 | this._TWO_PI = 2 * Math.PI; 22 | this.min = 0; 23 | this.max = 298; 24 | 25 | this._drawState = this._drawState.bind(this); 26 | this._onMousedown = this._onMousedown.bind(this); 27 | this._onMousemove = this._onMousemove.bind(this); 28 | this._onMouseup = this._onMouseup.bind(this); 29 | this._onPointerdown = this._onPointerdown.bind(this); 30 | this._onPointermove = this._onPointermove.bind(this); 31 | this._onPointerup = this._onPointerup.bind(this); 32 | this._onTouchend = this._onTouchend.bind(this); 33 | this._onTouchmove = this._onTouchmove.bind(this); 34 | this._onTouchstart = this._onTouchstart.bind(this); 35 | } 36 | 37 | get value() { 38 | return this.hasAttribute('value') ? this.getAttribute('value') : 0; 39 | } 40 | 41 | set value(value) { 42 | this.setAttribute('value', value); 43 | } 44 | 45 | get scale() { 46 | return this.hasAttribute('scale') ? this.getAttribute('scale') : 1; 47 | } 48 | 49 | set scale(scale) { 50 | this.setAttribute('scale', scale); 51 | } 52 | 53 | connectedCallback() { 54 | this._angle = this.value / this.scale; 55 | this.style.setProperty('transform', 'rotate(' + this._angle + 'rad)'); 56 | if ('PointerEvent' in window) { 57 | this.addEventListener('pointerdown', this._onPointerdown); 58 | } else { 59 | this.addEventListener('touchstart', this._onTouchstart); 60 | this.addEventListener('mousedown', this._onMousedown); 61 | } 62 | } 63 | 64 | disconnectedCallback() { 65 | this.removeEventListener('pointerdown', this._onPointerdown); 66 | this.removeEventListener('touchstart', this._onTouchstart); 67 | this.removeEventListener('mousedown', this._onMousedown); 68 | } 69 | 70 | rotateTo(angle) { 71 | this._handleStart(); 72 | 73 | this._previousAttemptedAngle = this._attemptedAngle; 74 | this._attemptedAngle = angle; 75 | 76 | this._handleMove(); 77 | this._handleEnd(); 78 | } 79 | 80 | rotateToStart(angle) { 81 | this._initCenter(); 82 | this._touchX = this._centerX + Math.sin(angle) * 30; 83 | this._touchY = this._centerY - Math.cos(angle) * 30; 84 | this._handleStart(); 85 | } 86 | 87 | rotateToContinue(angle) { 88 | this._touchX = this._centerX + Math.sin(angle) * 30; 89 | this._touchY = this._centerY - Math.cos(angle) * 30; 90 | this._calcTouchAngle(); 91 | this._handleMove(); 92 | } 93 | 94 | rotateToEnd() { 95 | this._handleEnd(); 96 | } 97 | 98 | _onMousedown(e) { 99 | this._touchX = e.clientX; 100 | this._touchY = e.clientY; 101 | 102 | this._initCenter(); 103 | this._handleStart(); 104 | 105 | document.addEventListener('mousemove', this._onMousemove); 106 | document.addEventListener('mouseup', this._onMouseup); 107 | } 108 | 109 | _onMousemove(e) { 110 | e.preventDefault(); 111 | this._touchX = e.clientX; 112 | this._touchY = e.clientY; 113 | 114 | this._calcTouchAngle(); 115 | this._handleMove(); 116 | } 117 | 118 | _onMouseup(e) { 119 | e.preventDefault(); 120 | 121 | document.removeEventListener('mousemove', this._onMousemove); 122 | document.removeEventListener('mouseup', this._onMouseup); 123 | 124 | this._handleEnd(); 125 | } 126 | 127 | _onTouchstart(e) { 128 | e.preventDefault(); 129 | window.oncontextmenu = () => { return false; }; 130 | 131 | this._touchX = e.changedTouches[0].clientX; 132 | this._touchY = e.changedTouches[0].clientY; 133 | 134 | this._initCenter(); 135 | this._handleStart(); 136 | 137 | this.addEventListener('touchmove', this._onTouchmove); 138 | this.addEventListener('touchend', this._onTouchend); 139 | this.addEventListener('touchcancel', this._onTouchend); 140 | } 141 | 142 | _onTouchmove(e) { 143 | e.preventDefault(); 144 | 145 | this._touchX = e.targetTouches[0].clientX; 146 | this._touchY = e.targetTouches[0].clientY; 147 | 148 | this._calcTouchAngle(); 149 | this._handleMove(); 150 | } 151 | 152 | _onTouchend(e) { 153 | e.preventDefault(); 154 | window.oncontextmenu = null; 155 | 156 | this.removeEventListener('touchmove', this._onTouchmove); 157 | this.removeEventListener('touchend', this._onTouchend); 158 | this.removeEventListener('touchcancel', this._onTouchend); 159 | 160 | this._handleEnd(); 161 | } 162 | 163 | _onPointerdown(e) { 164 | e.preventDefault(); 165 | window.oncontextmenu = () => { return false; }; 166 | 167 | this._touchX = e.clientX; 168 | this._touchY = e.clientY; 169 | this.setPointerCapture(e.pointerId); 170 | 171 | this._initCenter(); 172 | this._handleStart(); 173 | 174 | this.addEventListener('pointermove', this._onPointermove); 175 | this.addEventListener('pointerup', this._onPointerup); 176 | this.addEventListener('pointercancel', this._onPointerup); 177 | } 178 | 179 | _onPointermove(e) { 180 | e.preventDefault(); 181 | this._touchX = e.clientX; 182 | this._touchY = e.clientY; 183 | 184 | this._calcTouchAngle(); 185 | this._handleMove(); 186 | } 187 | 188 | _onPointerup(e) { 189 | e.preventDefault(); 190 | window.oncontextmenu = null; 191 | 192 | this.releasePointerCapture(e.pointerId); 193 | this.removeEventListener('pointermove', this._onPointermove); 194 | this.removeEventListener('pointerup', this._onPointerup); 195 | this.removeEventListener('pointercancel', this._onPointerup); 196 | 197 | this._handleEnd(); 198 | } 199 | 200 | _handleStart() { 201 | this._initTouchAngle(); 202 | this._initialAngle = this._angle; 203 | this._initialValue = parseFloat(this.value); 204 | this._attemptedAngle = this._angle; 205 | this._attemptedRotations = this._rotations; 206 | this._attemptedValue = this.value; 207 | 208 | const evt = new CustomEvent('touch-knob-start', { bubbles: true }); 209 | this.dispatchEvent(evt); 210 | } 211 | 212 | _handleMove() { 213 | if (this._canDraw === true) { 214 | this._canDraw = false; 215 | // window.requestAnimationFrame(this._drawState); 216 | this._drawState(); 217 | } 218 | 219 | const evt = new CustomEvent('touch-knob-move', { bubbles: true }); 220 | this.dispatchEvent(evt); 221 | } 222 | 223 | _handleEnd() { 224 | const evt = new CustomEvent('touch-knob-end', { bubbles: true }); 225 | this.dispatchEvent(evt); 226 | } 227 | 228 | _initCenter() { 229 | this._centerX = this.offsetLeft - this.scrollLeft + this.clientLeft + this.offsetWidth / 2; 230 | this._centerY = this.offsetTop - this.scrollTop + this.clientTop + this.offsetHeight / 2; 231 | } 232 | 233 | _initTouchAngle() { 234 | this._initialTouchAngle = Math.atan2( 235 | this._touchY - this._centerY, 236 | this._touchX - this._centerX 237 | ); 238 | } 239 | 240 | _calcTouchAngle() { 241 | this._previousAttemptedAngle = this._attemptedAngle; 242 | this._attemptedAngle = 243 | this._initialAngle - this._initialTouchAngle 244 | + Math.atan2(this._touchY - this._centerY, this._touchX - this._centerX); 245 | this._attemptedAngle = this._attemptedAngle 246 | - this._TWO_PI * Math.floor((this._attemptedAngle + Math.PI) / this._TWO_PI); 247 | } 248 | 249 | _drawState() { 250 | if ( 251 | this._previousAttemptedAngle > -1.57 && this._previousAttemptedAngle < 0 252 | && this._attemptedAngle >= 0 && this._attemptedAngle <= 1.57 253 | ) { 254 | this._attemptedRotations++; 255 | } else if ( 256 | this._previousAttemptedAngle < 1.57 && this._previousAttemptedAngle > 0 257 | && this._attemptedAngle <= 0 && this._attemptedAngle >= -1.57 258 | ) { 259 | this._attemptedRotations--; 260 | } 261 | 262 | if (this._attemptedAngle >= 0) { 263 | this._attemptedValue = 264 | (this._attemptedAngle + this._TWO_PI * this._attemptedRotations) * this.scale; 265 | } else if (this._attemptedAngle < 0) { 266 | this._attemptedValue = 267 | (this._attemptedAngle + this._TWO_PI * (this._attemptedRotations + 1)) * this.scale; 268 | } 269 | 270 | if (this._attemptedValue >= this.min && this._attemptedValue <= this.max) { 271 | this.value = this._attemptedValue; 272 | this._rotations = this._attemptedRotations; 273 | this._angle = this._attemptedAngle; 274 | this.style.setProperty('transform', `rotate(${this._angle}rad)`); 275 | } 276 | 277 | this._canDraw = true; 278 | } 279 | } --------------------------------------------------------------------------------