├── .gitignore ├── LICENSE ├── README.md ├── css ├── main.css └── reset.css ├── images ├── background.png └── background.psd ├── index.html └── js ├── header.js ├── libs └── audiolet.js ├── radar.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store? 4 | ehthumbs.db 5 | Icon? 6 | Thumbs.db 7 | 8 | .settings 9 | .project 10 | .tmp 11 | .svn 12 | 13 | deploy -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Hakim El Hattab, http://hakim.se 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Radar 2 | 3 | An audio-visual experiment that uses [Audiolet](https://github.com/oampo/Audiolet) to synthesize sound in real-time. The visuals are rendered on ``````. 4 | 5 | [Check out the demo](http://lab.hakim.se/radar/). 6 | 7 | # License 8 | 9 | MIT licensed 10 | 11 | Copyright (C) 2014 Hakim El Hattab, http://hakim.se -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /********************************************* 4 | * GLOBAL 5 | *********************************************/ 6 | 7 | body, html { 8 | overflow: hidden; 9 | font-family: Helvetica, Arial, sans-serif; 10 | color: #fff; 11 | font-size: 11px; 12 | background: #111; 13 | height: 100%; 14 | } 15 | 16 | .main-container { 17 | background: #111; 18 | } 19 | 20 | * { 21 | -webkit-box-sizing: border-box; 22 | -moz-box-sizing: border-box; 23 | box-sizing: border-box; 24 | } 25 | 26 | 27 | /********************************************* 28 | * HEADER 29 | *********************************************/ 30 | 31 | header { 32 | position: relative; 33 | width: 100%; 34 | height: 36px; 35 | margin: 0; 36 | padding: 0 8px 8px 8px; 37 | overflow: hidden; 38 | z-index: 5; 39 | 40 | background: #1e2121; 41 | color: #eee; 42 | 43 | -webkit-transition: height .22s ease-out; 44 | -moz-transition: height .22s ease-out; 45 | -o-transition: height .22s ease-out; 46 | transition: height .22s ease-out; 47 | } 48 | 49 | header.open { 50 | height: 165px; 51 | } 52 | 53 | header h1 { 54 | font-family: Molengo, Helvetica, Arial, sans-serif; 55 | float: left; 56 | margin-top: 1px; 57 | } 58 | 59 | header .header-instruction { 60 | float: left; 61 | margin: 12px 0 0 15px; 62 | 63 | font-size: 10px; 64 | font-style: italic; 65 | color: #999; 66 | 67 | -webkit-transition: opacity .18s linear; 68 | -moz-transition: opacity .18s linear; 69 | -o-transition: opacity .18s linear; 70 | transition: opacity .18s linear; 71 | } 72 | header.open .header-instruction { 73 | opacity: 0; 74 | } 75 | header div.extra { 76 | margin: 45px 0 0 20px; 77 | clear: both; 78 | 79 | -webkit-transition: opacity .18s linear; 80 | -moz-transition: opacity .18s linear; 81 | -o-transition: opacity .18s linear; 82 | transition: opacity .18s linear; 83 | } 84 | header div.extra h3 { 85 | margin-bottom: 10px; 86 | } 87 | header a { 88 | color: #19d75a; 89 | text-decoration: underline; 90 | 91 | -webkit-transition: all .1s ease-out; 92 | -moz-transition: all .1s ease-out; 93 | -o-transition: all .1s ease-out; 94 | transition: all .1s ease-in; 95 | } 96 | header a:hover { 97 | color: #67f38f; 98 | } 99 | header section { 100 | height: 120px; 101 | padding: 0 25px; 102 | float: left; 103 | 104 | border-left: 1px #333 solid; 105 | } 106 | header p { 107 | margin-bottom: 5px; 108 | 109 | font-size: 12px; 110 | letter-spacing: 0.05em; 111 | } 112 | #about { 113 | padding-left: 0; 114 | border: none; 115 | } 116 | #about p.credits { 117 | margin: 15px 0 2px 0; 118 | 119 | font-style: italic; 120 | color: #666; 121 | font-size: 11px; 122 | line-height: 1.4em; 123 | } 124 | #share iframe, 125 | #share div { 126 | display: inline-block; 127 | } 128 | #retweet-button { 129 | margin-right: 6px; 130 | } 131 | 132 | .no-canvas { 133 | color: #999999; 134 | font-size: 24px; 135 | text-align: center; 136 | margin-top: 150px; 137 | } 138 | 139 | 140 | /********************************************* 141 | * EXPERIMENT STYLES 142 | *********************************************/ 143 | 144 | #wrapper { 145 | position: absolute; 146 | 147 | font-size: 12px; 148 | color: #f4f4f4; 149 | cursor: default; 150 | } 151 | 152 | #wrapper canvas { 153 | float: left; 154 | 155 | background: #000; 156 | background: url('../images/background.png') no-repeat; 157 | border: 1px solid #222; 158 | border-radius: 2px; 159 | 160 | box-shadow: 0px 0px 20px rgba(0,0,0,0.5); 161 | } 162 | 163 | #wrapper .instructions { 164 | display: block; 165 | position: absolute; 166 | left: 0px; 167 | top: -22px; 168 | opacity: 0; 169 | 170 | -webkit-transition: all .12s ease; 171 | -moz-transition: all .12s ease; 172 | -ms-transition: all .12s ease; 173 | -o-transition: all .12s ease; 174 | transition: all .12s ease; 175 | } 176 | #wrapper.empty .instructions { 177 | opacity: 1; 178 | } 179 | 180 | #wrapper .sidebar { 181 | position: absolute; 182 | right: 0; 183 | top: 0; 184 | width: 100px; 185 | 186 | color: #fff; 187 | opacity: 1; 188 | font-size: 12px; 189 | 190 | -webkit-transition: all .12s ease; 191 | -moz-transition: all .12s ease; 192 | -ms-transition: all .12s ease; 193 | -o-transition: all .12s ease; 194 | transition: all .12s ease; 195 | } 196 | #wrapper.empty .sidebar { 197 | opacity: 0; 198 | visibility: hidden; 199 | } 200 | 201 | #wrapper .sidebar button { 202 | display: inline-block; 203 | width: 48%; 204 | padding: 6px; 205 | margin-bottom: 14px; 206 | 207 | color: #bbb; 208 | background: rgba( 255, 255, 255, 0.05 ); 209 | opacity: 1; 210 | border: none; 211 | border-radius: 2px; 212 | cursor: pointer; 213 | font-size: 12px; 214 | text-align: center; 215 | 216 | -webkit-transition: all .12s ease; 217 | -moz-transition: all .12s ease; 218 | -ms-transition: all .12s ease; 219 | -o-transition: all .12s ease; 220 | transition: all .12s ease; 221 | } 222 | #wrapper .sidebar button:hover { 223 | opacity: 1; 224 | color: #fff; 225 | background: rgba( 255, 255, 255, 0.2 ); 226 | } 227 | 228 | /* SEQUENCER */ 229 | #wrapper .sidebar .sequencer { 230 | padding-top: 14px; 231 | border-top: 1px dotted rgba( 255, 255, 255, 0.08 ); 232 | } 233 | #wrapper .sidebar .sequencer li { 234 | display: block; 235 | position: relative; 236 | padding: 10px 6px; 237 | margin-bottom: 4px; 238 | width: 100%; 239 | cursor: pointer; 240 | 241 | background: #222; 242 | 243 | box-shadow: 0px 0px 20px rgba(0,0,0,0.5); 244 | border-radius: 2px; 245 | 246 | -webkit-transition: all .12s ease; 247 | -moz-transition: all .12s ease; 248 | -ms-transition: all .12s ease; 249 | -o-transition: all .12s ease; 250 | transition: all .12s ease; 251 | } 252 | #wrapper .sidebar .sequencer li:hover { 253 | background: #333; 254 | } 255 | #wrapper .sidebar .sequencer li .index { 256 | opacity: 0.5; 257 | } 258 | #wrapper .sidebar .sequencer li .delete { 259 | position: absolute; 260 | right: 7px; 261 | top: 3px; 262 | font-family: Helvetica; 263 | font-size: 20px; 264 | opacity: 0.1; 265 | } 266 | #wrapper .sidebar .sequencer li:hover .delete { 267 | opacity: 0.6; 268 | } 269 | #wrapper .sidebar .sequencer li .delete:hover { 270 | opacity: 1; 271 | } 272 | #wrapper .sidebar .sequencer li .background { 273 | position: absolute; 274 | top: 0; 275 | left: 0; 276 | width: 100%; 277 | height: 100%; 278 | opacity: 0; 279 | 280 | -webkit-transition: opacity 1.5s linear; 281 | -moz-transition: opacity 1.5s linear; 282 | -ms-transition: opacity 1.5s linear; 283 | -o-transition: opacity 1.5s linear; 284 | transition: opacity 1.5s linear; 285 | } 286 | #wrapper .sidebar .sequencer li .background.instant { 287 | -webkit-transition: none; 288 | -moz-transition: none; 289 | -ms-transition: none; 290 | -o-transition: none; 291 | transition: none; 292 | } 293 | #wrapper .sidebar .sequencer li.add-key { 294 | background: #111; 295 | color: #fff; 296 | border: 1px solid rgba( 255, 255, 255, 0.05 ); 297 | } 298 | #wrapper .sidebar .sequencer li.add-key:hover { 299 | background: #222; 300 | } 301 | 302 | #wrapper .sidebar .sequencer-input { 303 | visibility: hidden; 304 | position: absolute; 305 | width: 100px; 306 | left: 0; 307 | border-radius: 2px; 308 | 309 | background: #222; 310 | } 311 | #wrapper .sidebar .sequencer-input:before { 312 | content: '\2023'; 313 | 314 | position: absolute; 315 | margin: 76px 0 0 100%; 316 | left: -4px; 317 | 318 | color: #222; 319 | font-size: 50px; 320 | } 321 | 322 | #wrapper .sidebar .sequencer-input li { 323 | padding: 10px 6px; 324 | background: #222; 325 | cursor: pointer; 326 | 327 | -webkit-transition: all .12s ease; 328 | -moz-transition: all .12s ease; 329 | -ms-transition: all .12s ease; 330 | -o-transition: all .12s ease; 331 | transition: all .12s ease; 332 | } 333 | #wrapper .sidebar .sequencer-input li:hover { 334 | background: #333; 335 | } 336 | #wrapper .sidebar .sequencer-input li+li { 337 | border-top: 1px solid #333; 338 | } 339 | -------------------------------------------------------------------------------- /css/reset.css: -------------------------------------------------------------------------------- 1 | html{color:#000;background:#222222;} 2 | a{cursor:pointer;} 3 | html,body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;} 4 | table{border-collapse:collapse;border-spacing:0;} 5 | fieldset,img{border:0;} 6 | address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;} 7 | li{list-style:none;} 8 | caption,th{text-align:left;} 9 | /* h1,h2,h3,h4,h5,h6{font-size:100%;} */ 10 | q:before,q:after{content:'';} 11 | abbr,acronym {border:0;font-variant:normal;} 12 | sup {vertical-align:text-top;} 13 | sub {vertical-align:text-bottom;} 14 | input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;outline-style:none;outline-width:0pt;} 15 | legend{color:#000;} 16 | a:focus,object,h1,h2,h3,h4,h5,h6{-moz-outline-style: none; border:0px;} 17 | /*input[type="Submit"]{cursor:pointer;}*/ 18 | strong {font-weight: bold;} 19 | *{margin: 0; padding: 0;} -------------------------------------------------------------------------------- /images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/Radar/a763799d6884d461405e99d63be3e15c1bbe9fab/images/background.png -------------------------------------------------------------------------------- /images/background.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/Radar/a763799d6884d461405e99d63be3e15c1bbe9fab/images/background.psd -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Radar - An audio-visual HTML experiment 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 |

Radar

31 | Expand for more info. 32 | 33 | 34 |
35 | 36 | 37 |
38 |

About

39 |

40 | An experiment with real-time audio synthesis.
41 | Works well in Chrome & Safari but runs slowly in Firefox.
42 | Thanks to Tom Ashworth for harmonising the sound! 43 |

44 |

45 | Created by Hakim El Hattab | @hakimel 46 |

47 |
48 | 49 | 50 |
51 |

Tech

52 |

53 | Radar uses Audiolet to generate sound and
54 | the visuals are rendered on HTML5 <canvas>.

55 | Source code available at github.com/hakimel/Radar. 56 |

57 |
58 | 59 | 60 |
61 |

Share

62 | 63 |
64 | 65 | 66 |
67 |
68 | 69 |
70 |
71 | 72 |
73 | 74 | 89 | Click on a pin to generate sound 90 |
91 | 92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /js/header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controls the showing and hiding of the expandable 3 | * header. 4 | * 5 | * @author Hakim El Hattab / http://hakim.se 6 | */ 7 | window.onload = function() { 8 | 9 | var header = document.getElementsByTagName('header')[0]; 10 | var headerToggleTimeOut = -1; 11 | var headerMouseDown = false; 12 | 13 | document.addEventListener( 'mousedown', function() { 14 | headerMouseDown = true; 15 | }, false ); 16 | 17 | document.addEventListener( 'mouseup', function() { 18 | headerMouseDown = false; 19 | }, false ); 20 | 21 | header.addEventListener('mouseover', function() { 22 | if (!headerMouseDown) { 23 | // Make sure no previous call to toggle the header are 24 | // queued up 25 | clearTimeout( headerToggleTimeOut ); 26 | 27 | // Avoid accidentally opening the header by setting 28 | // a short time out 29 | headerToggleTimeOut = setTimeout( function() { 30 | header.setAttribute( 'class', 'open' ) 31 | }, 100 ); 32 | } 33 | }, false); 34 | 35 | header.addEventListener('mouseout', function() { 36 | // Make sure no previous call to toggle the header are 37 | // queued up 38 | clearTimeout( headerToggleTimeOut ); 39 | 40 | // Avoid accidentally closing the header by setting 41 | // a short time out 42 | headerToggleTimeOut = setTimeout( function() { 43 | header.setAttribute( 'class', '' ) 44 | }, 100 ); 45 | }, false); 46 | 47 | }; 48 | -------------------------------------------------------------------------------- /js/radar.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @author Hakim El Hattab 4 | */ 5 | var Radar = (function(){ 6 | 7 | var NODES_X = 12, 8 | NODES_Y = 12, 9 | 10 | BEAT_VELOCITY = 0.01, 11 | BEAT_FREQUENCY = 2, 12 | BEAT_LIMIT = 10, 13 | 14 | // Distance threshold between active node and beat 15 | ACTIVATION_DISTANCE = 20, 16 | 17 | // Number of neighboring nodes to push aside on impact 18 | WAVE_RADIUS = 3; 19 | 20 | // The world dimensions 21 | var world = { 22 | width: 600, 23 | height: 500, 24 | center: new Point( 300, 250 ) 25 | }; 26 | 27 | // Mouse input tracking 28 | var mouse = { 29 | // The current position 30 | x: 0, 31 | y: 0, 32 | 33 | // The position previous to the current 34 | previousX: 0, 35 | previousY: 0, 36 | 37 | // The velocity, based on the difference between 38 | // the current and next positions 39 | velocityX: 0, 40 | velocityY: 0, 41 | 42 | // Flags if the mouse is currently pressed down 43 | down: false, 44 | 45 | // When dragging the action is defined by the first nodes 46 | // reaction (activate/deactivate) 47 | action: null, 48 | 49 | // A list of node ID's for which action should not be 50 | // taken until the next time the mouse is pressed down 51 | exclude: [] 52 | 53 | }; 54 | 55 | var id = 0, 56 | 57 | container, 58 | 59 | canvas, 60 | context, 61 | 62 | sidebar, 63 | sequencer, 64 | sequencerInput, 65 | sequencerInputElements, 66 | sequencerAddButton, 67 | 68 | saveButton, 69 | resetButton, 70 | 71 | query = {}, 72 | 73 | currentBeat = null, 74 | defaultBeats = [ 75 | [ 'a', 'min' ], 76 | [ 'a', 'min' ] 77 | ], 78 | 79 | activateNodeDistance = 0, 80 | 81 | // Seed is used to generate the note field so that random 82 | // one persons's grid can be saved & replicated 83 | // Some patterns to try: 84 | // ?8643+d+maj+8+30+43+55+66+69 85 | // ?8643+a+min+30+43+44+55+58+93+106+141 86 | seed = Math.floor( Math.random() * 10000 ), 87 | 88 | nodes = [], 89 | savedNodes = [], 90 | beats = []; 91 | 92 | // Generate some scales (a, d & e) 93 | // Frequencies from http://www.seventhstring.com/resources/notefrequencies.html 94 | // Delta ratios are musical harmonies, like http://modularscale.com/ 95 | var notes = {}; 96 | notes.a = { 97 | min: [ 220.0,246.9,261.6,293.7,329.6,349.2,415.3,440.0,493.9,523.3 ], 98 | maj: [ 220.0,246.9,277.2,293.7,329.6,370.0,415.3,440.0,493.9,554.4 ], 99 | minColor: 'hsl(180, 90%, 50%)', 100 | majColor: 'hsl(160, 90%, 50%)' 101 | }; 102 | 103 | notes.d = { 104 | min: generateScaleFrom( notes.a.min, 4/3 ), 105 | maj: generateScaleFrom( notes.a.maj, 4/3 ), 106 | minColor: 'hsl(140, 90%, 50%)', 107 | majColor: 'hsl(120, 90%, 50%)' 108 | }; 109 | 110 | notes.e = { 111 | min: generateScaleFrom( notes.a.min, 3/2 ), 112 | maj: generateScaleFrom( notes.a.maj, 3/2 ), 113 | minColor: 'hsl(100, 90%, 50%)', 114 | majColor: 'hsl(80, 90%, 50%)' 115 | }; 116 | 117 | /** 118 | * 119 | */ 120 | function initialize() { 121 | // Run selectors and cache element references 122 | container = document.getElementById( 'wrapper' ); 123 | canvas = document.querySelector( '#wrapper canvas' ); 124 | sidebar = document.querySelector( '#wrapper .sidebar' ); 125 | resetButton = document.querySelector( '#wrapper .sidebar .reset' ); 126 | saveButton = document.querySelector( '#wrapper .sidebar .save' ); 127 | sequencer = document.querySelector( '#wrapper .sequencer' ); 128 | sequencerInput = document.querySelector( '#wrapper .sequencer-input' ); 129 | sequencerInputElements = sequencerInput.querySelectorAll( 'li' ); 130 | sequencerAddButton = document.querySelector( '#wrapper .sequencer .add-key' ); 131 | 132 | if ( canvas && canvas.getContext ) { 133 | context = canvas.getContext('2d'); 134 | context.globalCompositeOperation = 'lighter'; 135 | 136 | // Split the query values into a key/value object 137 | location.search.replace( /[A-Z0-9]+?=([\w|\-|\+]*)/gi, function(a) { 138 | query[ a.split( '=' ).shift() ] = a.split( '=' ).pop(); 139 | } ); 140 | 141 | if( query.seed ) { 142 | seed = parseInt( query.seed ); 143 | } 144 | 145 | addEventListeners(); 146 | 147 | // Force an initial layout 148 | onWindowResize(); 149 | 150 | setup(); 151 | load(); 152 | update(); 153 | } 154 | else { 155 | alert( 'Doesn\'t seem like your browser supports the HTML5 canvas element :(' ); 156 | } 157 | 158 | } 159 | 160 | function addEventListeners() { 161 | resetButton.addEventListener('click', onResetButtonClicked, false); 162 | saveButton.addEventListener('click', onSaveButtonClicked, false); 163 | sequencerAddButton.addEventListener('click', onSequencerAddButtonClick, false); 164 | 165 | canvas.addEventListener('mousedown', onCanvasMouseDown, false); 166 | document.addEventListener('mousemove', onDocumentMouseMove, false); 167 | document.addEventListener('mouseup', onDocumentMouseUp, false); 168 | canvas.addEventListener('touchstart', onCanvasTouchStart, false); 169 | canvas.addEventListener('touchmove', onCanvasTouchMove, false); 170 | canvas.addEventListener('touchend', onCanvasTouchEnd, false); 171 | window.addEventListener('resize', onWindowResize, false); 172 | 173 | for( var i = 0, len = sequencerInputElements.length; i < len; i++ ) { 174 | sequencerInputElements[i].addEventListener( 'click', onSequencerInputElementClick, false ); 175 | } 176 | } 177 | 178 | function setup() { 179 | // Distance between nodes 180 | var cx = world.width / ( NODES_X + 1 ), 181 | cy = world.height / ( NODES_Y + 1 ); 182 | 183 | activateNodeDistance = Math.min( cx, cy ) * 0.5; 184 | 185 | var i, 186 | j, 187 | x = 0, 188 | y = 0, 189 | len = NODES_X * NODES_Y, 190 | length; 191 | 192 | // Generate nodes 193 | for( y = 0; y < NODES_Y; y++ ) { 194 | for( x = 0; x < NODES_X; x++ ) { 195 | nodes.push( new Node( cx + x * cx, cy + y * cy, x, y ) ); 196 | } 197 | } 198 | 199 | // Determine node neighbors 200 | for( i = 0; i < len; i++ ) { 201 | var nodeA = nodes[i]; 202 | 203 | for( j = 0; j < len; j++ ) { 204 | var nodeB = nodes[j]; 205 | 206 | if( nodeA !== nodeB && nodeB.distanceToNode( nodeA ) < WAVE_RADIUS ) { 207 | nodeA.neighbors.push( nodeB ); 208 | } 209 | } 210 | } 211 | } 212 | 213 | function load() { 214 | // Restore grid from query string 215 | if( document.location.search.length > 0 ) { 216 | var isRunning = false; 217 | 218 | if( query.beats ) { 219 | var beatData = query.beats.split( '+' ); 220 | 221 | for( var i = 0, len = beatData.length; i < len; i++ ) { 222 | var key = beatData[i].split( '-' )[0], 223 | scale = beatData[i].split( '-' )[1]; 224 | 225 | addBeat( key, scale ); 226 | 227 | isRunning = true; 228 | } 229 | } 230 | 231 | if( query.nodes ) { 232 | var nodeData = query.nodes.split( '+' ); 233 | 234 | for( var i = 0, len = nodeData.length; i < len; i++ ) { 235 | var index = parseInt( nodeData[i], 10 ); 236 | 237 | if( nodes[ index ] ) { 238 | nodes[ index ].activate(); 239 | isRunning = true; 240 | } 241 | } 242 | } 243 | 244 | if( isRunning ) { 245 | container.className = container.className.replace( 'empty', '' ); 246 | } 247 | } 248 | else { 249 | for( var i = 0, len = defaultBeats.length; i < len; i++ ) { 250 | addBeat( defaultBeats[i][0], defaultBeats[i][1] ); 251 | } 252 | } 253 | } 254 | 255 | function reset() { 256 | var i; 257 | 258 | for( i = 0, len = nodes.length; i < len; i++ ) { 259 | nodes[i].deactivate(); 260 | } 261 | 262 | while( beats.length ) { 263 | beats.pop().destroy(); 264 | } 265 | 266 | for( i = 0, len = defaultBeats.length; i < len; i++ ) { 267 | addBeat( defaultBeats[i][0], defaultBeats[i][1] ); 268 | } 269 | 270 | currentBeat = null; 271 | } 272 | 273 | function addBeat() { 274 | var element; 275 | 276 | if( arguments.length === 2 ) { 277 | element = document.createElement( 'li' ); 278 | element.setAttribute( 'data-key', arguments[0] ); 279 | element.setAttribute( 'data-scale', arguments[1] ); 280 | sequencer.insertBefore( element, sequencerAddButton ); 281 | } 282 | else { 283 | element = arguments[0]; 284 | } 285 | 286 | var elementKey = element.getAttribute( 'data-key' ), 287 | elementScale = element.getAttribute( 'data-scale' ); 288 | 289 | var beat = new Beat( 290 | world.center.x, 291 | world.center.y, 292 | element, 293 | elementKey, 294 | elementScale, 295 | beats.length 296 | ); 297 | 298 | beats.push( beat ); 299 | 300 | updateBeats(); 301 | 302 | return beat; 303 | } 304 | 305 | function removeBeat( index ) { 306 | var beat = beats[ index ]; 307 | 308 | if( beat ) { 309 | if( beat === currentBeat ) { 310 | currentBeat = null; 311 | } 312 | 313 | beats.splice( beat.index, 1 ); 314 | beat.destroy(); 315 | } 316 | 317 | updateBeats(); 318 | } 319 | 320 | function updateBeats() { 321 | if( beats.length > BEAT_LIMIT - 1 ) { 322 | sequencerAddButton.style.visibility = 'hidden'; 323 | } 324 | else { 325 | sequencerAddButton.style.visibility = 'visible'; 326 | } 327 | 328 | // Update indices of all beats 329 | for( var i = 0, len = beats.length; i < len; i++ ) { 330 | beats[i].changeIndex( i ); 331 | }; 332 | } 333 | 334 | function update() { 335 | clear(); 336 | render(); 337 | 338 | requestAnimFrame( update ); 339 | } 340 | 341 | function clear() { 342 | context.clearRect( 0, 0, world.width, world.height ); 343 | } 344 | 345 | function render() { 346 | // Render nodes 347 | for( var i = 0, len = nodes.length; i < len; i++ ) { 348 | var node = nodes[i]; 349 | 350 | updateNode( node ); 351 | renderNode( node ); 352 | } 353 | 354 | // Render beats 355 | context.save(); 356 | 357 | var activeBeats = 0, 358 | firstActiveBeatStrength = 0; 359 | 360 | for( var i = 0, len = beats.length; i < len; i++ ) { 361 | var beat = beats[i]; 362 | 363 | updateBeat( beat ); 364 | renderBeat( beat ); 365 | 366 | if( beat.active ) { 367 | activeBeats += 1; 368 | 369 | if( firstActiveBeatStrength === 0 ) { 370 | firstActiveBeatStrength = beat.strength; 371 | } 372 | } 373 | } 374 | 375 | context.restore(); 376 | 377 | // Trigger a new beat when needed 378 | if( beats.length ) { 379 | var nextBeat = currentBeat ? beats[ ( currentBeat.index + 1 ) % beats.length ] : null; 380 | 381 | if( !currentBeat ) { 382 | currentBeat = beats[0]; 383 | currentBeat.activate(); 384 | } 385 | else if( !nextBeat.active && activeBeats < BEAT_FREQUENCY && currentBeat.strength > 1 / BEAT_FREQUENCY ) { 386 | currentBeat = nextBeat; 387 | currentBeat.activate(); 388 | } 389 | } 390 | } 391 | 392 | function updateNode( node ) { 393 | // Active nodes that the mouse touches when pressed down 394 | if( mouse.down ) { 395 | if( node.distanceTo( mouse.x, mouse.y ) < activateNodeDistance && mouse.exclude.indexOf( node.id ) === -1 ) { 396 | if( mouse.action !== 'deactivate' && node.active === false ) { 397 | mouse.action = 'activate'; 398 | node.activate(); 399 | 400 | container.className = container.className.replace( 'empty', '' ); 401 | } 402 | else if( mouse.action !== 'activate' && node.active === true ) { 403 | mouse.action = 'deactivate'; 404 | node.deactivate(); 405 | } 406 | 407 | mouse.exclude.push( node.id ); 408 | } 409 | } 410 | 411 | node.strength = Math.max( node.strength - 0.01, 0 ); 412 | node.size += ( node.sizeTarget - node.size ) * 0.25; 413 | 414 | node.offsetTargetX *= 0.6; 415 | node.offsetTargetY *= 0.6; 416 | 417 | node.offsetX += ( node.offsetTargetX - node.offsetX ) * 0.2; 418 | node.offsetY += ( node.offsetTargetY - node.offsetY ) * 0.2; 419 | 420 | if( node.strength > 0.1 ) { 421 | for( j = 0, jlen = node.neighbors.length; j < jlen; j++ ) { 422 | var neighbor = node.neighbors[j]; 423 | 424 | var radians = Math.atan2( node.indexh - neighbor.indexh, node.indexv - neighbor.indexv ), 425 | distance = node.distanceToNode( neighbor ); 426 | 427 | neighbor.offsetX += Math.sin( radians - Math.PI ) * node.strength * ( WAVE_RADIUS - distance ); 428 | neighbor.offsetY += Math.cos( radians - Math.PI ) * node.strength * ( WAVE_RADIUS - distance ); 429 | } 430 | } 431 | } 432 | 433 | function renderNode( node ) { 434 | // Angle and distance between node and center 435 | var radians = Math.atan2( world.center.y - node.y, world.center.x - node.x ), 436 | distance = node.distanceTo( world.center.x, world.center.y ); 437 | 438 | var distanceFactor = distance / Math.min( world.width, world.height ); 439 | 440 | // Offset for the pin head 441 | var ox = node.offsetX + Math.cos( radians - Math.PI ) * ( 30 * distanceFactor ) * node.strength, 442 | oy = node.offsetY + Math.sin( radians - Math.PI ) * ( 30 * distanceFactor ) * node.strength; 443 | 444 | if( node.strength ) { 445 | var radius = 4 + node.size * 16 * node.strength; 446 | 447 | context.beginPath(); 448 | context.arc( node.x, node.y, radius, 0, Math.PI * 2, true ); 449 | 450 | var gradient = context.createRadialGradient( node.x, node.y, 0, node.x, node.y, radius ); 451 | gradient.addColorStop( 0, node.activeColorA ); 452 | gradient.addColorStop( 1, node.activeColorB ); 453 | 454 | context.fillStyle = gradient; 455 | context.fill(); 456 | } 457 | 458 | // Offset for the pin body 459 | var tx = Math.cos( radians ) * ( 30 * distanceFactor ), 460 | ty = Math.sin( radians ) * ( 30 * distanceFactor ); 461 | 462 | // Pin body 463 | context.beginPath(); 464 | context.moveTo( node.x + ox, node.y + oy ); 465 | context.lineTo( node.x + tx, node.y + ty ); 466 | context.lineWidth = 1; 467 | context.strokeStyle = 'rgba(255,255,255,0.2)'; 468 | context.stroke(); 469 | 470 | // Pin head 471 | context.beginPath(); 472 | context.arc( node.x + ox, node.y + oy, node.size, 0, Math.PI * 2, true ); 473 | context.fillStyle = node.color; 474 | context.fill(); 475 | } 476 | 477 | function updateBeat( beat ) { 478 | if( beat.active ) { 479 | beat.strength += BEAT_VELOCITY; 480 | } 481 | 482 | // Remove used up beats 483 | if( beat.strength > 1 ) { 484 | beat.deactivate(); 485 | } 486 | else if( beat.active ) { 487 | // Check for collision with nodes 488 | for( var j = 0, len = nodes.length; j < len; j++ ) { 489 | var node = nodes[j]; 490 | 491 | if( node.active && node.collisionLevel < beat.level ) { 492 | // Distance between the beat wave and node 493 | var distance = Math.abs( node.distanceTo( beat.x, beat.y ) - ( beat.size * beat.strength ) ); 494 | 495 | if( distance < ACTIVATION_DISTANCE ) { 496 | node.collisionLevel = beat.level; 497 | node.play( beat.key, beat.scale ); 498 | node.highlight( 100 ); 499 | } 500 | } 501 | } 502 | } 503 | } 504 | 505 | function renderBeat( beat ) { 506 | if( beat.active && beat.strength > 0 ) { 507 | context.beginPath(); 508 | context.arc( beat.x, beat.y, Math.max( (beat.size * beat.strength)-2, 0 ), 0, Math.PI * 2, true ); 509 | context.lineWidth = 8; 510 | context.globalAlpha = 0.2 * ( 1 - beat.strength ); 511 | context.strokeStyle = beat.color; 512 | context.stroke(); 513 | 514 | context.beginPath(); 515 | context.arc( beat.x, beat.y, beat.size * beat.strength, 0, Math.PI * 2, true ); 516 | context.lineWidth = 2; 517 | context.globalAlpha = 0.8 * ( 1 - beat.strength ); 518 | context.strokeStyle = beat.color; 519 | context.stroke(); 520 | } 521 | } 522 | 523 | function generateScaleFrom(originalScale, delta) { 524 | var newScale = []; 525 | originalScale.forEach(function (freq) { 526 | newScale.push(freq * delta); 527 | }); 528 | return newScale; 529 | } 530 | 531 | function onResetButtonClicked( event ) { 532 | reset(); 533 | } 534 | 535 | function onSaveButtonClicked( event ) { 536 | var data = { 537 | seed: seed, 538 | beats: [], 539 | nodes: [] 540 | }; 541 | 542 | nodes.forEach(function ( node, index ) { 543 | if( node.active ) { 544 | data.nodes.push( index ); 545 | } 546 | }); 547 | 548 | beats.forEach(function ( beat, index ) { 549 | data.beats.push( beat.key + '-' + beat.scale ); 550 | }); 551 | 552 | var query = '', 553 | value; 554 | 555 | for( var i in data ) { 556 | value = data[i] instanceof Array ? data[i].join( '+' ) : data[i]; 557 | query += ( query.length > 0 ? '&' : '' ) + ( i + '=' + value ); 558 | } 559 | 560 | var url = document.location.protocol + '//' + document.location.host + document.location.pathname + '?' + query; 561 | 562 | if( 'history' in window && 'pushState' in window.history ) { 563 | window.history.pushState( null, null, url ); 564 | } 565 | 566 | prompt( 'Copy the unique URL and save it or share with friends.', url ); 567 | } 568 | 569 | function onSequencerAddButtonClick( event ) { 570 | var lastBeat = beats[ beats.length - 1 ]; 571 | 572 | if( lastBeat ) { 573 | addBeat( lastBeat.key, lastBeat.scale ).openSelector(); 574 | } 575 | else { 576 | addBeat( 'a', 'min' ).openSelector(); 577 | } 578 | } 579 | 580 | function onSequencerInputElementClick( event ) { 581 | sequencerInput.style.visibility = 'hidden'; 582 | 583 | var element = event.target; 584 | 585 | if( element ) { 586 | event.preventDefault(); 587 | 588 | var index = parseInt( sequencerInput.getAttribute( 'data-index' ) ), 589 | key = element.getAttribute( 'data-key' ), 590 | scale = element.getAttribute( 'data-scale' ); 591 | 592 | if( !isNaN( index ) && key && scale ) { 593 | var beat = beats[ index ]; 594 | 595 | if( beat ) { 596 | beat.generate( key, scale ); 597 | } 598 | } 599 | } 600 | } 601 | 602 | function onCanvasMouseDown( event ) { 603 | mouse.down = true; 604 | mouse.action = null; 605 | mouse.exclude.length = 0; 606 | } 607 | 608 | function onDocumentMouseMove( event ) { 609 | mouse.previousX = mouse.x; 610 | mouse.previousY = mouse.y; 611 | 612 | mouse.x = event.clientX - (window.innerWidth - world.width) * 0.5; 613 | mouse.y = event.clientY - (window.innerHeight - world.height) * 0.5; 614 | 615 | mouse.velocityX = Math.abs( mouse.x - mouse.previousX ) / world.width; 616 | mouse.velocityY = Math.abs( mouse.y - mouse.previousY ) / world.height; 617 | } 618 | 619 | function onDocumentMouseUp( event ) { 620 | mouse.down = false; 621 | sequencerInput.style.visibility = 'hidden'; 622 | } 623 | 624 | function onCanvasTouchStart( event ) { 625 | if(event.touches.length == 1) { 626 | event.preventDefault(); 627 | 628 | mouse.x = event.touches[0].pageX - (window.innerWidth - world.width) * 0.5; 629 | mouse.y = event.touches[0].pageY - (window.innerHeight - world.height) * 0.5; 630 | 631 | mouse.down = true; 632 | mouse.action = null; 633 | mouse.exclude.length = 0; 634 | } 635 | } 636 | 637 | function onCanvasTouchMove( event ) { 638 | if(event.touches.length == 1) { 639 | event.preventDefault(); 640 | 641 | mouse.x = event.touches[0].pageX - (window.innerWidth - world.width) * 0.5; 642 | mouse.y = event.touches[0].pageY - (window.innerHeight - world.height) * 0.5 - 20; 643 | } 644 | } 645 | 646 | function onCanvasTouchEnd( event ) { 647 | mouse.down = false; 648 | } 649 | 650 | function onWindowResize() { 651 | var containerWidth = world.width + sidebar.offsetWidth + 20; 652 | 653 | // Resize the container 654 | container.style.width = containerWidth + 'px'; 655 | container.style.height = world.height + 'px'; 656 | container.style.left = ( window.innerWidth - world.width ) / 2 + 'px'; 657 | container.style.top = ( window.innerHeight - world.height ) / 2 + 'px'; 658 | 659 | // Resize the canvas 660 | canvas.width = world.width; 661 | canvas.height = world.height; 662 | } 663 | 664 | /** 665 | * Represets one node/point in the grid. 666 | */ 667 | function Node( x, y, indexh, indexv ) { 668 | // invoke super 669 | this.constructor.apply( this, arguments ); 670 | 671 | this.indexh = indexh; 672 | this.indexv = indexv; 673 | 674 | this.id = ++id; 675 | this.neighbors = []; 676 | this.collisionLevel = 0; 677 | this.active = false; 678 | this.strength = 0; 679 | this.size = 1; 680 | this.sizeTarget = this.size; 681 | 682 | // This bit of randomness should make sure that the notes are different 683 | // and unpredictable, yet reproducably so when the same seed is used. 684 | // indexv * NODES_X + indexh reproduces the overall node number, 685 | // this * seed % the number of notes in the current scale in the key of A 686 | // produces something reproducable with the same seed, although there's still 687 | // a degree of linearity becuase of the modulus: notes rise from right to left 688 | // with a repeating patter top to bottom. 689 | this.note = seed * (indexv * NODES_X + indexh) % notes.a[ 'maj' ].length; 690 | 691 | this.offsetX = 0; 692 | this.offsetY = 0; 693 | 694 | this.offsetTargetX = 0; 695 | this.offsetTargetY = 0; 696 | 697 | this.color = '#fff'; 698 | this.activeColorA = 'rgba(90,255,230,0.2)'; 699 | this.activeColorB = 'rgba(90,255,230,0.0)'; 700 | } 701 | Node.prototype = new Point(); 702 | Node.prototype.generate = function() { 703 | this.audiolet = new Audiolet( 44100, 2 ); 704 | 705 | var factorY = 1 - ( this.y / world.height ), 706 | factorD = this.distanceTo( world.center.x, world.center.y ); 707 | 708 | this.attack = 0.01; 709 | this.release = 0.6; 710 | }; 711 | Node.prototype.distanceToNode = function( node ) { 712 | var dx = node.indexh - this.indexh; 713 | var dy = node.indexv - this.indexv; 714 | 715 | return Math.sqrt(dx*dx + dy*dy); 716 | }; 717 | Node.prototype.activate = function() { 718 | this.active = true; 719 | this.sizeTarget = 5; 720 | this.color = 'rgba(110,255,210,0.8)'; 721 | }; 722 | Node.prototype.deactivate = function() { 723 | this.active = false; 724 | this.sizeTarget = 1; 725 | this.color = '#fff'; 726 | }; 727 | Node.prototype.play = function( key, scale ) { 728 | if( !this.audiolet ) { 729 | this.generate(); 730 | } 731 | 732 | this.frequency = notes[ key ][ scale ][ this.note ]; 733 | 734 | // This is horribly bad for performance and memory.. Need 735 | // to find a way to cache 736 | this.synth = new Synth( this.audiolet, this.frequency, this.attack, this.release ); 737 | this.synth.connect( this.audiolet.output ); 738 | }; 739 | Node.prototype.highlight = function( delay ) { 740 | if( delay ) { 741 | setTimeout( function() { 742 | 743 | this.strength = 1; 744 | 745 | }.bind( this ), delay ); 746 | } 747 | else { 748 | this.strength = 1; 749 | } 750 | }; 751 | 752 | /** 753 | * Represents a beatwave that triggers nodes. 754 | */ 755 | function Beat( x, y, element, key, scale, index ) { 756 | // invoke super 757 | this.constructor.apply( this, arguments ); 758 | 759 | this.element = element; 760 | 761 | this.changeIndex( index ); 762 | this.generate( key, scale ); 763 | 764 | this.level = ++id; 765 | this.size = Math.max( world.width, world.height ) * 0.65; 766 | this.active = false; 767 | this.strength = 0; 768 | 769 | this.openSelector = this.openSelector.bind( this ); 770 | 771 | this.element.addEventListener( 'click', this.openSelector, false ); 772 | }; 773 | Beat.prototype = new Point(); 774 | Beat.prototype.changeIndex = function( index ) { 775 | this.index = index; 776 | this.element.setAttribute( 'data-index', this.index ); 777 | }; 778 | Beat.prototype.generate = function( key, scale ) { 779 | this.key = key; 780 | this.scale = scale; 781 | 782 | this.color = notes[ this.key ][ scale + 'Color' ]; 783 | 784 | this.element.innerHTML = ''; 785 | 786 | this.backgroundElement = document.createElement( 'div' ); 787 | this.backgroundElement.className = 'background'; 788 | this.backgroundElement.style.backgroundColor = this.color; 789 | this.element.appendChild( this.backgroundElement ); 790 | 791 | this.element.setAttribute( 'data-key', this.key ); 792 | this.element.setAttribute( 'data-scale', this.scale ); 793 | this.element.innerHTML += key.toUpperCase() + ' ' + scale + 'or'; 794 | 795 | this.deleteElement = document.createElement( 'span' ); 796 | this.deleteElement.innerHTML = '×'; 797 | this.deleteElement.className = 'delete'; 798 | this.element.appendChild( this.deleteElement ); 799 | 800 | this.deleteElement.addEventListener( 'click', function() { 801 | removeBeat( this.index ); 802 | return false; 803 | }.bind( this ), false ); 804 | }; 805 | Beat.prototype.activate = function() { 806 | this.level = ++id; 807 | this.active = true; 808 | this.strength = 0; 809 | 810 | // For some reason this.backgroundElement isn't reacting 811 | var background = this.element.querySelector( '.background' ); 812 | 813 | background.className = 'background instant'; 814 | background.style.opacity = 0.4; 815 | 816 | setTimeout( function() { 817 | background.className = 'background'; 818 | background.style.opacity = 0; 819 | }, 1 ); 820 | }; 821 | Beat.prototype.deactivate = function() { 822 | this.active = false; 823 | }; 824 | Beat.prototype.destroy = function() { 825 | if( this.element && this.element.parentElement ) { 826 | this.element.removeEventListener( 'click', this.openSelector, false ); 827 | this.element.parentElement.removeChild( this.element ); 828 | this.element = null; 829 | } 830 | }; 831 | Beat.prototype.openSelector = function( event ) { 832 | // If the user clicks on the same beat twice, hide the input 833 | if( sequencerInput.style.visibility === 'visible' && parseInt( sequencerInput.getAttribute( 'data-index' ) ) === this.index ) { 834 | sequencerInput.style.visibility = 'hidden'; 835 | } 836 | else { 837 | sequencerInput.style.visibility = 'visible'; 838 | sequencerInput.style.left = -sequencerInput.offsetWidth - 15 + 'px'; 839 | sequencerInput.style.top = this.element.offsetTop + ( ( this.element.offsetHeight - sequencerInput.offsetHeight ) / 2 ) + 'px'; 840 | sequencerInput.setAttribute( 'data-index', this.index ); 841 | } 842 | }; 843 | 844 | /** 845 | * Plays a short sound effect based on arguments. 846 | */ 847 | function Synth( audiolet, frequency, attack, release ) { 848 | AudioletGroup.apply(this, [audiolet, 0, 1]); 849 | // Basic wave 850 | this.sine = new Sine(audiolet, frequency); 851 | 852 | // Gain envelope 853 | this.gain = new Gain(audiolet); 854 | this.env = new PercussiveEnvelope(audiolet, 1, attack, release, 855 | function() { 856 | this.audiolet.scheduler.addRelative(0, this.remove.bind(this)); 857 | }.bind(this) 858 | ); 859 | this.envMulAdd = new MulAdd(audiolet, 0.2, 0); 860 | 861 | // Main signal path 862 | this.sine.connect(this.gain); 863 | this.gain.connect(this.outputs[0]); 864 | 865 | // Envelope 866 | this.env.connect(this.envMulAdd); 867 | this.envMulAdd.connect(this.gain, 0, 1); 868 | }; 869 | Synth.prototype = new AudioletGroup(); 870 | 871 | initialize(); 872 | 873 | })(); 874 | -------------------------------------------------------------------------------- /js/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | var Capabilities = { 5 | isOnline: function() { 6 | return navigator.onLine; 7 | }, 8 | 9 | isTouchDevice: function() { 10 | return navigator.userAgent.match( /(iphone|ipad|ipod|android)/gi ); 11 | }, 12 | 13 | suportsLocalStorage: function() { 14 | return ('localStorage' in window) && window['localStorage'] !== null; 15 | } 16 | }; 17 | 18 | /** 19 | * Defines a 2D position. 20 | */ 21 | function Point( x, y ) { 22 | this.x = x || 0; 23 | this.y = y || 0; 24 | } 25 | 26 | Point.prototype.distanceTo = function( x, y ) { 27 | var dx = x-this.x; 28 | var dy = y-this.y; 29 | return Math.sqrt(dx*dx + dy*dy); 30 | }; 31 | 32 | Point.prototype.clonePosition = function() { 33 | return { x: this.x, y: this.y }; 34 | }; 35 | 36 | Point.prototype.interpolate = function( x, y, amp ) { 37 | this.x += ( x - this.x ) * amp; 38 | this.y += ( y - this.y ) * amp; 39 | }; 40 | 41 | /** 42 | * Defines of a rectangular region. 43 | */ 44 | function Region() { 45 | this.left = 999999; 46 | this.top = 999999; 47 | this.right = 0; 48 | this.bottom = 0; 49 | } 50 | 51 | Region.prototype.reset = function() { 52 | this.left = 999999; 53 | this.top = 999999; 54 | this.right = 0; 55 | this.bottom = 0; 56 | }; 57 | 58 | Region.prototype.inflate = function( x, y ) { 59 | this.left = Math.min(this.left, x); 60 | this.top = Math.min(this.top, y); 61 | this.right = Math.max(this.right, x); 62 | this.bottom = Math.max(this.bottom, y); 63 | }; 64 | 65 | Region.prototype.expand = function( x, y ) { 66 | this.left -= x; 67 | this.top -= y; 68 | this.right += x; 69 | this.bottom += y; 70 | }; 71 | 72 | Region.prototype.contains = function( x, y ) { 73 | return x > this.left && x < this.right && y > this.top && y < this.bottom; 74 | }; 75 | 76 | Region.prototype.size = function() { 77 | return ( ( this.right - this.left ) + ( this.bottom - this.top ) ) / 2; 78 | }; 79 | 80 | Region.prototype.center = function() { 81 | return new Point( this.left + (this.right - this.left) / 2, this.top + (this.bottom - this.top) / 2 ); 82 | }; 83 | 84 | Region.prototype.toRectangle = function() { 85 | return { x: this.left, y: this.top, width: this.right - this.left, height: this.bottom - this.top }; 86 | }; 87 | 88 | 89 | 90 | // shim layer with setTimeout fallback from http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 91 | window.requestAnimFrame = (function(){ 92 | return window.requestAnimationFrame || 93 | window.webkitRequestAnimationFrame || 94 | window.mozRequestAnimationFrame || 95 | window.oRequestAnimationFrame || 96 | window.msRequestAnimationFrame || 97 | function(/* function */ callback, /* DOMElement */ element){ 98 | window.setTimeout(callback, 1000 / 60); 99 | }; 100 | })(); 101 | 102 | --------------------------------------------------------------------------------