├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── css └── main.css ├── favicon.png ├── index.html └── js ├── RotaryDial.js └── main.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: victorqribeiro 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Victor Ribeiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rotary Dial 2 | 3 | A [Rotary Dial](https://en.wikipedia.org/wiki/Rotary_dial) for input numbers. 4 | 5 | ![RotaryDial](favicon.png) 6 | 7 | Live version [here](https://victorribeiro.com/dial) | Alternative link [here](https://victorqribeiro.github.io/dial/index.html) 8 | 9 | # How to use it 10 | 11 | Click / Touch the disc and drag it until the arrow. When the number reaches the arrow, it will turn red, then let go and that's your number. 12 | 13 | # How to use it on your web app 14 | 15 | Include the RotaryDial.js file. 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | Then create a new RotaryDial 22 | 23 | ```javascript 24 | const rd = new RotaryDial(); 25 | ``` 26 | 27 | Creating a callback is easy, just define what your function will do with the number it receives from the RotaryDial. 28 | 29 | ```javascript 30 | const func = function(number){ 31 | alert( number ) 32 | } 33 | 34 | const rd = new RotaryDial({callback: func}); 35 | 36 | ``` 37 | 38 | By default the RotaryDial has the console.log function as the callback. 39 | 40 | # Documentation 41 | 42 | The RotaryDial accepts a configuration object on its constructor. The most import parts are the size and the callback. The size will determine the size of your rotary dial menu, and the callback determines which function will be called when a number is selected. Besides that, there are some color configurations you can fiddle with. 43 | 44 | **size** - The size of the menu. *Default 400px* 45 | 46 | **callback** - The function that will be called when a number is selected. *Default console.log* 47 | 48 | **discFillColor** - The disc color. 49 | 50 | **discStrokeColor** - The disc border color. 51 | 52 | **circlesFillColor** - The circles (where the numbers are displayed) color. 53 | 54 | **circlesStrokeColor** - The circles (where the numbers are displayed) border color. 55 | 56 | **circlesHighlightColor** - The color that a circle will be displayed when a number is selected. 57 | 58 | **textFillColor** - The text color. 59 | 60 | **textStrokeColor** - The text border color. 61 | 62 | **arrowFillColor** - The arrow color. 63 | 64 | **arrowStrokeColor** - The arrow border color. 65 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: space-around; 9 | } 10 | 11 | canvas { 12 | display: block; 13 | } 14 | 15 | input { 16 | width: 300px; 17 | height: 4em; 18 | padding-left: 0.5em; 19 | box-sizing: border-box; 20 | } 21 | 22 | button { 23 | width: 50px; 24 | height: 4em; 25 | margin-left: 0.5em; 26 | box-sizing: border-box; 27 | } 28 | 29 | @media only screen and (max-width: 600px) { 30 | canvas{ 31 | width: 100%; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorqribeiro/dial/d3ff0ed2c7ceb8f047e28011e570de6223cc3e50/favicon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Rotary Dial 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /js/RotaryDial.js: -------------------------------------------------------------------------------- 1 | class RotaryDial { 2 | 3 | constructor({ 4 | size, 5 | discFillColor, 6 | discStrokeColor, 7 | circlesFillColor, 8 | circlesStrokeColor, 9 | circlesHighlightColor, 10 | textFillColor, 11 | textStrokeColor, 12 | arrowFillColor, 13 | arrowStrokeColor, 14 | callback} = {}){ 15 | 16 | this.canvasSize = size || 400; 17 | 18 | this.size = this.canvasSize-2; 19 | 20 | this.discFillColor = discFillColor || 'transparent'; 21 | 22 | this.discStrokeColor = discStrokeColor || 'black'; 23 | 24 | this.circlesFillColor = circlesFillColor || 'black'; 25 | 26 | this.circlesStrokeColor = circlesStrokeColor || 'transparent'; 27 | 28 | this.circlesHighlightColor = circlesHighlightColor || 'red'; 29 | 30 | this.textFillColor = textFillColor || 'white'; 31 | 32 | this.textStrokeColor = textStrokeColor || 'transparent'; 33 | 34 | this.arrowFillColor = arrowFillColor || 'black'; 35 | 36 | this.arrowStrokeColor = arrowStrokeColor || 'transparent'; 37 | 38 | this.canvas = document.createElement('canvas'); 39 | 40 | this.canvas.width = this.w = this.canvasSize; 41 | 42 | this.canvas.height = this.h = this.canvasSize; 43 | 44 | this.w2 = this.w / 2; 45 | 46 | this.h2 = this.h / 2; 47 | 48 | this.TWOPI = Math.PI * 2; 49 | 50 | this.offset = this.size * 0.25; 51 | 52 | this.outerCircle = this.size/2; 53 | 54 | this.innerCircle = this.outerCircle-this.offset; 55 | 56 | this.c = this.canvas.getContext('2d'); 57 | 58 | this.c.font = this.size * 0.08+"px Arial"; 59 | 60 | this.c.textAlign = "center"; 61 | 62 | this.a = 1; 63 | 64 | this.clicking = false; 65 | 66 | this.number = null; 67 | 68 | this.callback = callback || console.log; 69 | 70 | document.body.appendChild(this.canvas); 71 | 72 | this.draw(); 73 | 74 | this.addEvent(); 75 | 76 | } 77 | 78 | draw(){ 79 | 80 | this.c.clearRect(0,0,this.w,this.h); 81 | 82 | this.c.beginPath(); 83 | 84 | this.c.arc(this.w2, this.h2, this.outerCircle, 0, this.TWOPI, false ); 85 | 86 | this.c.moveTo(this.w2+this.innerCircle, this.h2 ); 87 | 88 | this.c.arc(this.w2, this.h2, this.innerCircle, this.TWOPI, 0, true); 89 | 90 | this.c.fillStyle = this.discFillColor; 91 | 92 | this.c.fill(); 93 | 94 | this.c.strokeStyle = this.discStrokeColor; 95 | 96 | this.c.stroke(); 97 | 98 | this.c.save(); 99 | 100 | this.c.translate(this.w2, this.h2); 101 | 102 | for(let i = 0; i < 10; i++){ 103 | 104 | const a = this.a+i/2; 105 | 106 | const center = this.innerCircle + (this.outerCircle-this.innerCircle)/2; 107 | 108 | const x = Math.cos(a)*center; 109 | 110 | const y = Math.sin(a)*center; 111 | 112 | const n = (10-i)%10; 113 | 114 | this.c.beginPath(); 115 | 116 | this.c.arc(x, y, this.size * 0.08, 0, this.TWOPI); 117 | 118 | this.c.fillStyle = this.circlesFillColor; 119 | 120 | this.c.strokeStyle = this.circlesStrokeColor; 121 | 122 | if( this.number != null && this.number%10 == n) 123 | 124 | this.c.fillStyle = this.circlesHighlightColor 125 | 126 | this.c.fill(); 127 | 128 | this.c.stroke(); 129 | 130 | this.c.fillStyle = this.textFillColor; 131 | 132 | this.c.strokeStyle = this.textStrokeColor; 133 | 134 | this.c.fillText(n, x, y+this.size * 0.02); 135 | 136 | this.c.strokeText(n, x, y+this.size * 0.02); 137 | 138 | } 139 | 140 | this.c.restore(); 141 | 142 | this.c.beginPath() 143 | 144 | this.c.moveTo(this.w - this.size * 0.08, this.h2); 145 | 146 | this.c.lineTo(this.w, this.h2 - this.size * 0.04); 147 | 148 | this.c.lineTo(this.w, this.h2 + this.size * 0.04); 149 | 150 | this.c.closePath(); 151 | 152 | this.c.fillStyle = this.arrowFillColor; 153 | 154 | this.c.strokeStyle = this.arrowStrokeColor; 155 | 156 | this.c.fill(); 157 | 158 | this.c.stroke(); 159 | 160 | 161 | } 162 | 163 | 164 | addEvent(){ 165 | 166 | this.canvas.addEventListener('mousedown', e => { this.isClicking(e) }); 167 | 168 | this.canvas.addEventListener('mouseup', _ => { this.result() }); 169 | 170 | this.canvas.addEventListener('mouseout', _ => { this.clear() }); 171 | 172 | this.canvas.addEventListener('mousemove', e => { this.rotate(e) }); 173 | 174 | this.canvas.addEventListener('touchstart', e =>{ this.isClicking(e) }); 175 | 176 | this.canvas.addEventListener('touchmove', e =>{ this.rotate(e) }); 177 | 178 | this.canvas.addEventListener('touchend', _ => { this.result() }); 179 | 180 | } 181 | 182 | result(){ 183 | 184 | if( this.number ) 185 | 186 | this.callback( this.number%10 ) 187 | 188 | this.clear() 189 | 190 | } 191 | 192 | isClicking(e){ 193 | 194 | e.preventDefault() 195 | 196 | const pos = this.getPos(e); 197 | 198 | const dist = this.getDist( pos.x, pos.y, this.w2, this.h2 ) 199 | 200 | if( dist > this.size/2 || dist < this.size/2-this.offset ) 201 | 202 | return 203 | 204 | this.lastAngle = Math.atan2( pos.y - this.h2, pos.x - this.w2 ); 205 | 206 | this.number = null; 207 | 208 | this.clicking = true; 209 | 210 | } 211 | 212 | getDist(x1, y1, x2, y2){ 213 | 214 | return Math.sqrt( (x2-x1) ** 2 + (y2-y1) ** 2); 215 | 216 | } 217 | 218 | getPos(e){ 219 | 220 | e.preventDefault(); 221 | 222 | let x, y; 223 | 224 | const rect = this.canvas.getBoundingClientRect(); 225 | 226 | const _x = this.canvas.width/rect.width; 227 | const _y = this.canvas.height/rect.height; 228 | 229 | if( e.touches ){ 230 | 231 | x = (e.targetTouches[0].pageX - rect.left) * _x; 232 | 233 | y = (e.targetTouches[0].pageY - rect.top) *_y; 234 | 235 | }else{ 236 | 237 | x = e.offsetX * _x; 238 | 239 | y = e.offsetY * _y; 240 | 241 | } 242 | 243 | return {x, y}; 244 | 245 | } 246 | 247 | clear(){ 248 | 249 | this.number = null; 250 | 251 | this.clicking = false; 252 | 253 | this.goBack(); 254 | 255 | } 256 | 257 | rotate(e){ 258 | 259 | if( !this.clicking || this.a >= this.TWOPI) return; 260 | 261 | if( this.a < 1 ){ 262 | 263 | this.a = 1; 264 | 265 | return; 266 | 267 | } 268 | 269 | const pos = this.getPos(e); 270 | 271 | const dist = this.getDist( pos.x, pos.y, this.w2, this.h2 ) 272 | 273 | if( dist > this.size/2 || dist < this.size/2-this.offset ){ 274 | 275 | this.clear() 276 | 277 | return 278 | 279 | } 280 | 281 | const n = Math.floor((this.a-1.1) / (this.TWOPI-0.8) * 11); 282 | 283 | this.number = n > 0 ? n : null; 284 | 285 | this.newAngle = Math.atan2( pos.y - this.h2, pos.x - this.w2 ); 286 | 287 | const delta = (this.a - (this.lastAngle - this.newAngle)); 288 | 289 | this.a = delta > 0 ? delta : this.TWOPI+delta; 290 | 291 | this.lastAngle = this.newAngle; 292 | 293 | this.draw(); 294 | } 295 | 296 | goBack(){ 297 | 298 | if( this.a > 1 ){ 299 | 300 | this.a -= 0.1 301 | 302 | this.draw(); 303 | 304 | requestAnimationFrame( _=>{ this.goBack() } ); 305 | 306 | if( this.a - 1 < 0.05 ) 307 | 308 | this.a = 1; 309 | } 310 | } 311 | 312 | } 313 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | const init = function(){ 2 | 3 | const div = document.createElement('div'); 4 | 5 | const input = document.createElement('input'); 6 | 7 | const btn = document.createElement('button'); 8 | 9 | btn.innerText = "Clear"; 10 | 11 | btn.addEventListener("click", e => { 12 | input.value = ""; 13 | }); 14 | 15 | input.type = "text"; 16 | 17 | div.append(input); 18 | 19 | div.append(btn); 20 | 21 | document.body.appendChild( div ); 22 | 23 | const func = function(value){ 24 | input.value += value; 25 | } 26 | 27 | const rd = new RotaryDial({callback: func}); 28 | 29 | } 30 | 31 | init(); 32 | --------------------------------------------------------------------------------