├── .gitignore ├── Gruntfile.coffee ├── css ├── flexbox.less ├── normalize.less ├── preboot.less ├── scissor.css └── scissor.less ├── index.html ├── js ├── knob.js ├── mousetrap.js ├── scissor.coffee ├── scissor.js └── tuna.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | # Project configuration. 4 | grunt.initConfig 5 | coffee: 6 | compile: 7 | files: 8 | 'js/scissor.js': 'js/scissor.coffee' 9 | less: 10 | compile: 11 | files: 12 | 'css/scissor.css': 'css/scissor.less' 13 | watch: 14 | all: 15 | files: ['js/*.coffee', 'css/*.less'] 16 | tasks: ['coffee', 'less'] 17 | connect: 18 | server: 19 | options: 20 | port: 1337 21 | hostname: '*' 22 | 23 | # These plugins provide necessary tasks. 24 | grunt.loadNpmTasks 'grunt-contrib-coffee' 25 | grunt.loadNpmTasks 'grunt-contrib-less' 26 | grunt.loadNpmTasks 'grunt-contrib-watch' 27 | grunt.loadNpmTasks 'grunt-contrib-connect' 28 | 29 | # Default task. 30 | grunt.registerTask 'default', ['connect', 'watch'] 31 | -------------------------------------------------------------------------------- /css/flexbox.less: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------- 2 | // Flexbox LESS mixins 3 | // The spec: http://www.w3.org/TR/css3-flexbox 4 | // -------------------------------------------------- 5 | 6 | // Flexbox display 7 | // flex or inline-flex 8 | .flex-display(@display: flex) { 9 | display: ~"-webkit-@{display}"; 10 | display: ~"-moz-@{display}"; 11 | display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox 12 | display: ~"-ms-@{display}"; // IE11 13 | display: @display; 14 | } 15 | 16 | // The 'flex' shorthand 17 | // - applies to: flex items 18 | // , initial, auto, or none 19 | .flex(@columns: initial) { 20 | -webkit-flex: @columns; 21 | -moz-flex: @columns; 22 | -ms-flex: @columns; 23 | flex: @columns; 24 | } 25 | 26 | // Flex Flow Direction 27 | // - applies to: flex containers 28 | // row | row-reverse | column | column-reverse 29 | .flex-direction(@direction: row) { 30 | -webkit-flex-direction: @direction; 31 | -moz-flex-direction: @direction; 32 | -ms-flex-direction: @direction; 33 | flex-direction: @direction; 34 | } 35 | 36 | // Flex Line Wrapping 37 | // - applies to: flex containers 38 | // nowrap | wrap | wrap-reverse 39 | .flex-wrap(@wrap: nowrap) { 40 | -webkit-flex-wrap: @wrap; 41 | -moz-flex-wrap: @wrap; 42 | -ms-flex-wrap: @wrap; 43 | flex-wrap: @wrap; 44 | } 45 | 46 | // Flex Direction and Wrap 47 | // - applies to: flex containers 48 | // || 49 | .flex-flow(@flow) { 50 | -webkit-flex-flow: @flow; 51 | -moz-flex-flow: @flow; 52 | -ms-flex-flow: @flow; 53 | flex-flow: @flow; 54 | } 55 | 56 | // Display Order 57 | // - applies to: flex items 58 | // 59 | .flex-order(@order: 0) { 60 | -webkit-order: @order; 61 | -moz-order: @order; 62 | -ms-order: @order; 63 | order: @order; 64 | } 65 | 66 | // Flex grow factor 67 | // - applies to: flex items 68 | // 69 | .flex-grow(@grow: 0) { 70 | -webkit-flex-grow: @grow; 71 | -moz-flex-grow: @grow; 72 | -ms-flex-grow: @grow; 73 | flex-grow: @grow; 74 | } 75 | 76 | // Flex shr 77 | // - applies to: flex itemsink factor 78 | // 79 | .flex-shrink(@shrink: 1) { 80 | -webkit-flex-shrink: @shrink; 81 | -moz-flex-shrink: @shrink; 82 | -ms-flex-shrink: @shrink; 83 | flex-shrink: @shrink; 84 | } 85 | 86 | // Flex basis 87 | // - the initial main size of the flex item 88 | // - applies to: flex itemsnitial main size of the flex item 89 | // 90 | .flex-basis(@width: auto) { 91 | -webkit-flex-basis: @width; 92 | -moz-flex-basis: @width; 93 | -ms-flex-basis: @width; 94 | flex-basis: @width; 95 | } 96 | 97 | // Axis Alignment 98 | // - applies to: flex containers 99 | // flex-start | flex-end | center | space-between | space-around 100 | .justify-content(@justify: flex-start) { 101 | -webkit-justify-content: @justify; 102 | -moz-justify-content: @justify; 103 | -ms-justify-content: @justify; 104 | justify-content: @justify; 105 | } 106 | 107 | // Packing Flex Lines 108 | // - applies to: multi-line flex containers 109 | // flex-start | flex-end | center | space-between | space-around | stretch 110 | .align-content(@align: stretch) { 111 | -webkit-align-content: @align; 112 | -moz-align-content: @align; 113 | -ms-align-content: @align; 114 | align-content: @align; 115 | } 116 | 117 | // Cross-axis Alignment 118 | // - applies to: flex containers 119 | // flex-start | flex-end | center | baseline | stretch 120 | .align-items(@align: stretch) { 121 | -webkit-align-items: @align; 122 | -moz-align-items: @align; 123 | -ms-align-items: @align; 124 | align-items: @align; 125 | } 126 | 127 | // Cross-axis Alignment 128 | // - applies to: flex items 129 | // auto | flex-start | flex-end | center | baseline | stretch 130 | .align-self(@align: auto) { 131 | -webkit-align-self: @align; 132 | -moz-align-self: @align; 133 | -ms-align-self: @align; 134 | align-self: @align; 135 | } -------------------------------------------------------------------------------- /css/normalize.less: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.2 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /** 8 | * Correct `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | main, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | 26 | /** 27 | * Correct `inline-block` display not defined in IE 8/9. 28 | */ 29 | 30 | audio, 31 | canvas, 32 | video { 33 | display: inline-block; 34 | } 35 | 36 | /** 37 | * Prevent modern browsers from displaying `audio` without controls. 38 | * Remove excess height in iOS 5 devices. 39 | */ 40 | 41 | audio:not([controls]) { 42 | display: none; 43 | height: 0; 44 | } 45 | 46 | /** 47 | * Address `[hidden]` styling not present in IE 8/9. 48 | * Hide the `template` element in IE, Safari, and Firefox < 22. 49 | */ 50 | 51 | [hidden], 52 | template { 53 | display: none; 54 | } 55 | 56 | /* ========================================================================== 57 | Base 58 | ========================================================================== */ 59 | 60 | /** 61 | * 1. Set default font family to sans-serif. 62 | * 2. Prevent iOS text size adjust after orientation change, without disabling 63 | * user zoom. 64 | */ 65 | 66 | html { 67 | font-family: sans-serif; /* 1 */ 68 | -ms-text-size-adjust: 100%; /* 2 */ 69 | -webkit-text-size-adjust: 100%; /* 2 */ 70 | } 71 | 72 | /** 73 | * Remove default margin. 74 | */ 75 | 76 | body { 77 | margin: 0; 78 | } 79 | 80 | /* ========================================================================== 81 | Links 82 | ========================================================================== */ 83 | 84 | /** 85 | * Remove the gray background color from active links in IE 10. 86 | */ 87 | 88 | a { 89 | background: transparent; 90 | } 91 | 92 | /** 93 | * Address `outline` inconsistency between Chrome and other browsers. 94 | */ 95 | 96 | a:focus { 97 | outline: thin dotted; 98 | } 99 | 100 | /** 101 | * Improve readability when focused and also mouse hovered in all browsers. 102 | */ 103 | 104 | a:active, 105 | a:hover { 106 | outline: 0; 107 | } 108 | 109 | /* ========================================================================== 110 | Typography 111 | ========================================================================== */ 112 | 113 | /** 114 | * Address variable `h1` font-size and margin within `section` and `article` 115 | * contexts in Firefox 4+, Safari 5, and Chrome. 116 | */ 117 | 118 | h1 { 119 | font-size: 2em; 120 | margin: 0.67em 0; 121 | } 122 | 123 | /** 124 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 125 | */ 126 | 127 | abbr[title] { 128 | border-bottom: 1px dotted; 129 | } 130 | 131 | /** 132 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 133 | */ 134 | 135 | b, 136 | strong { 137 | font-weight: bold; 138 | } 139 | 140 | /** 141 | * Address styling not present in Safari 5 and Chrome. 142 | */ 143 | 144 | dfn { 145 | font-style: italic; 146 | } 147 | 148 | /** 149 | * Address differences between Firefox and other browsers. 150 | */ 151 | 152 | hr { 153 | -moz-box-sizing: content-box; 154 | box-sizing: content-box; 155 | height: 0; 156 | } 157 | 158 | /** 159 | * Address styling not present in IE 8/9. 160 | */ 161 | 162 | mark { 163 | background: #ff0; 164 | color: #000; 165 | } 166 | 167 | /** 168 | * Correct font family set oddly in Safari 5 and Chrome. 169 | */ 170 | 171 | code, 172 | kbd, 173 | pre, 174 | samp { 175 | font-family: monospace, serif; 176 | font-size: 1em; 177 | } 178 | 179 | /** 180 | * Improve readability of pre-formatted text in all browsers. 181 | */ 182 | 183 | pre { 184 | white-space: pre-wrap; 185 | } 186 | 187 | /** 188 | * Set consistent quote types. 189 | */ 190 | 191 | q { 192 | quotes: "\201C" "\201D" "\2018" "\2019"; 193 | } 194 | 195 | /** 196 | * Address inconsistent and variable font size in all browsers. 197 | */ 198 | 199 | small { 200 | font-size: 80%; 201 | } 202 | 203 | /** 204 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 205 | */ 206 | 207 | sub, 208 | sup { 209 | font-size: 75%; 210 | line-height: 0; 211 | position: relative; 212 | vertical-align: baseline; 213 | } 214 | 215 | sup { 216 | top: -0.5em; 217 | } 218 | 219 | sub { 220 | bottom: -0.25em; 221 | } 222 | 223 | /* ========================================================================== 224 | Embedded content 225 | ========================================================================== */ 226 | 227 | /** 228 | * Remove border when inside `a` element in IE 8/9. 229 | */ 230 | 231 | img { 232 | border: 0; 233 | } 234 | 235 | /** 236 | * Correct overflow displayed oddly in IE 9. 237 | */ 238 | 239 | svg:not(:root) { 240 | overflow: hidden; 241 | } 242 | 243 | /* ========================================================================== 244 | Figures 245 | ========================================================================== */ 246 | 247 | /** 248 | * Address margin not present in IE 8/9 and Safari 5. 249 | */ 250 | 251 | figure { 252 | margin: 0; 253 | } 254 | 255 | /* ========================================================================== 256 | Forms 257 | ========================================================================== */ 258 | 259 | /** 260 | * Define consistent border, margin, and padding. 261 | */ 262 | 263 | fieldset { 264 | border: 1px solid #c0c0c0; 265 | margin: 0 2px; 266 | padding: 0.35em 0.625em 0.75em; 267 | } 268 | 269 | /** 270 | * 1. Correct `color` not being inherited in IE 8/9. 271 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 272 | */ 273 | 274 | legend { 275 | border: 0; /* 1 */ 276 | padding: 0; /* 2 */ 277 | } 278 | 279 | /** 280 | * 1. Correct font family not being inherited in all browsers. 281 | * 2. Correct font size not being inherited in all browsers. 282 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 283 | */ 284 | 285 | button, 286 | input, 287 | select, 288 | textarea { 289 | font-family: inherit; /* 1 */ 290 | font-size: 100%; /* 2 */ 291 | margin: 0; /* 3 */ 292 | } 293 | 294 | /** 295 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 296 | * the UA stylesheet. 297 | */ 298 | 299 | button, 300 | input { 301 | line-height: normal; 302 | } 303 | 304 | /** 305 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 306 | * All other form control elements do not inherit `text-transform` values. 307 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 308 | * Correct `select` style inheritance in Firefox 4+ and Opera. 309 | */ 310 | 311 | button, 312 | select { 313 | text-transform: none; 314 | } 315 | 316 | /** 317 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 318 | * and `video` controls. 319 | * 2. Correct inability to style clickable `input` types in iOS. 320 | * 3. Improve usability and consistency of cursor style between image-type 321 | * `input` and others. 322 | */ 323 | 324 | button, 325 | html input[type="button"], /* 1 */ 326 | input[type="reset"], 327 | input[type="submit"] { 328 | -webkit-appearance: button; /* 2 */ 329 | cursor: pointer; /* 3 */ 330 | } 331 | 332 | /** 333 | * Re-set default cursor for disabled elements. 334 | */ 335 | 336 | button[disabled], 337 | html input[disabled] { 338 | cursor: default; 339 | } 340 | 341 | /** 342 | * 1. Address box sizing set to `content-box` in IE 8/9. 343 | * 2. Remove excess padding in IE 8/9. 344 | */ 345 | 346 | input[type="checkbox"], 347 | input[type="radio"] { 348 | box-sizing: border-box; /* 1 */ 349 | padding: 0; /* 2 */ 350 | } 351 | 352 | /** 353 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 354 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 355 | * (include `-moz` to future-proof). 356 | */ 357 | 358 | input[type="search"] { 359 | -webkit-appearance: textfield; /* 1 */ 360 | -moz-box-sizing: content-box; 361 | -webkit-box-sizing: content-box; /* 2 */ 362 | box-sizing: content-box; 363 | } 364 | 365 | /** 366 | * Remove inner padding and search cancel button in Safari 5 and Chrome 367 | * on OS X. 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Remove inner padding and border in Firefox 4+. 377 | */ 378 | 379 | button::-moz-focus-inner, 380 | input::-moz-focus-inner { 381 | border: 0; 382 | padding: 0; 383 | } 384 | 385 | /** 386 | * 1. Remove default vertical scrollbar in IE 8/9. 387 | * 2. Improve readability and alignment in all browsers. 388 | */ 389 | 390 | textarea { 391 | overflow: auto; /* 1 */ 392 | vertical-align: top; /* 2 */ 393 | } 394 | 395 | /* ========================================================================== 396 | Tables 397 | ========================================================================== */ 398 | 399 | /** 400 | * Remove most spacing between table cells. 401 | */ 402 | 403 | table { 404 | border-collapse: collapse; 405 | border-spacing: 0; 406 | } 407 | -------------------------------------------------------------------------------- /css/preboot.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Preboot v2 3 | * 4 | * Open sourced under MIT license by @mdo. 5 | * Some variables and mixins from Bootstrap (Apache 2 license). 6 | */ 7 | 8 | 9 | // 10 | // Variables 11 | // -------------------------------------------------- 12 | 13 | // Grayscale 14 | @black-10: darken(#fff, 10%); 15 | @black-20: darken(#fff, 20%); 16 | @black-30: darken(#fff, 30%); 17 | @black-40: darken(#fff, 40%); 18 | @black-50: darken(#fff, 50%); 19 | @black-60: darken(#fff, 60%); 20 | @black-70: darken(#fff, 70%); 21 | @black-80: darken(#fff, 80%); 22 | @black-90: darken(#fff, 90%); 23 | 24 | // Brand colors 25 | @brand-primary: #428bca; 26 | @brand-success: #5cb85c; 27 | @brand-warning: #f0ad4e; 28 | @brand-danger: #d9534f; 29 | @brand-info: #5bc0de; 30 | 31 | // Scaffolding 32 | @body-background: #fff; 33 | @text-color: @black-50; 34 | 35 | // Links 36 | @link-color: @brand-primary; 37 | @link-color-hover: darken(@link-color, 15%); 38 | 39 | // Typography 40 | @font-family-sans-serif: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; 41 | @font-family-serif: Georgia, "Times New Roman", Times, serif; 42 | @font-family-monospace: Monaco, Menlo, Consolas, "Courier New", monospace; 43 | @font-family-base: @font-family-sans-serif; 44 | 45 | @phi: 1.61803399; 46 | @size-nano: unit(pow(@phi, -4), rem); 47 | @size-micro: unit(pow(@phi, -3), rem); 48 | @size-tiny: unit(pow(@phi, -2), rem); 49 | @size-small: unit(pow(@phi, -1), rem); 50 | @size-base: unit(pow(@phi, 0), rem); 51 | @size-large: unit(pow(@phi, 1), rem); 52 | @size-huge: unit(pow(@phi, 2), rem); 53 | @size-massive: unit(pow(@phi, 3), rem); 54 | @size-epic: unit(pow(@phi, 4), rem); 55 | 56 | @line-height-base: 1.4; 57 | 58 | @headings-font-family: inherit; // empty to use BS default, @font-family-base 59 | @headings-font-weight: 500; 60 | 61 | // Forms 62 | @input-color-placeholder: lighten(@text-color, 25%); 63 | 64 | // Grid 65 | // Used with the grid mixins below 66 | @grid-columns: 12; 67 | @grid-column-padding: 15px; // Left and right inner padding 68 | @grid-float-breakpoint: 768px; 69 | 70 | 71 | 72 | // 73 | // Grid system 74 | // -------------------------------------------------- 75 | 76 | // Grid 77 | .make-row() { 78 | // Negative margin the row out to align the content of columns 79 | margin-left: -@grid-column-padding; 80 | margin-right: -@grid-column-padding; 81 | // Then clear the floated columns 82 | .clearfix(); 83 | } 84 | .make-column(@columns) { 85 | @media (min-width: @grid-float-breakpoint) { 86 | float: left; 87 | // Calculate width based on number of columns available 88 | width: percentage(@columns / @grid-columns); 89 | } 90 | // Prevent columns from collapsing when empty 91 | min-height: 1px; 92 | // Set inner padding as gutters instead of margin 93 | padding-left: @grid-column-padding; 94 | padding-right: @grid-column-padding; 95 | // Proper box-model (padding doesn't add to width) 96 | .box-sizing(border-box); 97 | } 98 | .make-column-offset(@columns) { 99 | @media (min-width: @grid-float-breakpoint) { 100 | margin-left: percentage(@columns / @grid-columns); 101 | } 102 | } 103 | 104 | 105 | 106 | // 107 | // Mixins: vendor prefixes 108 | // -------------------------------------------------- 109 | 110 | // Box sizing 111 | .box-sizing(@box-model) { 112 | -webkit-box-sizing: @box-model; // Safari <= 5 113 | -moz-box-sizing: @box-model; // Firefox <= 19 114 | box-sizing: @box-model; 115 | } 116 | 117 | // Single side border-radius 118 | .border-top-radius(@radius) { 119 | border-top-right-radius: @radius; 120 | border-top-left-radius: @radius; 121 | } 122 | .border-right-radius(@radius) { 123 | border-bottom-right-radius: @radius; 124 | border-top-right-radius: @radius; 125 | } 126 | .border-bottom-radius(@radius) { 127 | border-bottom-right-radius: @radius; 128 | border-bottom-left-radius: @radius; 129 | } 130 | .border-left-radius(@radius) { 131 | border-bottom-left-radius: @radius; 132 | border-top-left-radius: @radius; 133 | } 134 | 135 | // Drop shadows 136 | .box-shadow(@shadow) { 137 | -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1 138 | box-shadow: @shadow; 139 | } 140 | 141 | // Transitions 142 | .transition(@transition) { 143 | -webkit-transition: @transition; 144 | -moz-transition: @transition; 145 | -o-transition: @transition; 146 | transition: @transition; 147 | } 148 | .transition-delay(@transition-delay) { 149 | -webkit-transition-delay: @transition-delay; 150 | -moz-transition-delay: @transition-delay; 151 | -o-transition-delay: @transition-delay; 152 | transition-delay: @transition-delay; 153 | } 154 | .transition-duration(@transition-duration) { 155 | -webkit-transition-duration: @transition-duration; 156 | -moz-transition-duration: @transition-duration; 157 | -o-transition-duration: @transition-duration; 158 | transition-duration: @transition-duration; 159 | } 160 | 161 | // Transformations 162 | .rotate(@degrees) { 163 | -webkit-transform: rotate(@degrees); 164 | -moz-transform: rotate(@degrees); 165 | -ms-transform: rotate(@degrees); 166 | -o-transform: rotate(@degrees); 167 | transform: rotate(@degrees); 168 | } 169 | .scale(@ratio) { 170 | -webkit-transform: scale(@ratio); 171 | -moz-transform: scale(@ratio); 172 | -ms-transform: scale(@ratio); 173 | -o-transform: scale(@ratio); 174 | transform: scale(@ratio); 175 | } 176 | .translate(@x, @y) { 177 | -webkit-transform: translate(@x, @y); 178 | -moz-transform: translate(@x, @y); 179 | -ms-transform: translate(@x, @y); 180 | -o-transform: translate(@x, @y); 181 | transform: translate(@x, @y); 182 | } 183 | .skew(@x, @y) { 184 | -webkit-transform: skew(@x, @y); 185 | -moz-transform: skew(@x, @y); 186 | -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twitter/bootstrap/issues/4885 187 | -o-transform: skew(@x, @y); 188 | transform: skew(@x, @y); 189 | -webkit-backface-visibility: hidden; // See https://github.com/twitter/bootstrap/issues/5319 190 | } 191 | .translate3d(@x, @y, @z) { 192 | -webkit-transform: translate3d(@x, @y, @z); 193 | -moz-transform: translate3d(@x, @y, @z); 194 | -o-transform: translate3d(@x, @y, @z); 195 | transform: translate3d(@x, @y, @z); 196 | } 197 | 198 | // Backface visibility 199 | // 200 | // Prevent browsers from flickering when using CSS 3D transforms. 201 | // Default value is `visible`, but can be changed to `hidden 202 | // See git pull https://github.com/dannykeane/bootstrap.git backface-visibility for examples 203 | .backface-visibility(@visibility){ 204 | -webkit-backface-visibility: @visibility; 205 | -moz-backface-visibility: @visibility; 206 | backface-visibility: @visibility; 207 | } 208 | 209 | // User select 210 | // 211 | // For selecting text on the page 212 | .user-select(@select) { 213 | -webkit-user-select: @select; 214 | -moz-user-select: @select; 215 | -ms-user-select: @select; 216 | -o-user-select: @select; 217 | user-select: @select; 218 | } 219 | 220 | // Opacity 221 | .opacity(@opacity) { 222 | opacity: @opacity; 223 | @opacity-ie: @opacity * 100; 224 | filter: ~"alpha(opacity=@{opacity-ie})"; // IE8 225 | } 226 | 227 | // Placeholder text 228 | .placeholder(@color: @input-color-placeholder) { 229 | &:-moz-placeholder { color: @color; } // Firefox 4-18 230 | &::-moz-placeholder { color: @color; } // Firefox 19+ 231 | &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+ 232 | &::-webkit-input-placeholder { color: @color; } // Safari and Chrome 233 | } 234 | 235 | // Resize anything 236 | .resizable(@direction) { 237 | resize: @direction; // Options: horizontal, vertical, both 238 | overflow: auto; // Safari fix 239 | } 240 | 241 | // CSS3 Content Columns 242 | .content-columns(@width, @count, @gap) { 243 | -webkit-column-width: @width; 244 | -moz-column-width: @width; 245 | column-width: @width; 246 | -webkit-column-count: @count; 247 | -moz-column-count: @count; 248 | column-count: @count; 249 | -webkit-column-gap: @gap; 250 | -moz-column-gap: @gap; 251 | column-gap: @gap; 252 | } 253 | 254 | // Optional hyphenation 255 | .hyphens(@mode: auto) { 256 | word-wrap: break-word; 257 | -webkit-hyphens: @mode; 258 | -moz-hyphens: @mode; 259 | -ms-hyphens: @mode; 260 | -o-hyphens: @mode; 261 | hyphens: @mode; 262 | } 263 | 264 | // Gradients 265 | #gradient { 266 | .horizontal(@startColor: #555, @endColor: #333) { 267 | background-color: @endColor; 268 | background-image: -moz-linear-gradient(left, @startColor, @endColor); // FF 3.6+ 269 | background-image: -webkit-gradient(linear, 0 0, 100% 0, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ 270 | background-image: -webkit-linear-gradient(left, @startColor, @endColor); // Safari 5.1+, Chrome 10+ 271 | background-image: -o-linear-gradient(left, @startColor, @endColor); // Opera 11.10 272 | background-image: linear-gradient(to right, @startColor, @endColor); // Standard, IE10 273 | background-repeat: repeat-x; 274 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@startColor),argb(@endColor))); // IE9 and down 275 | } 276 | .vertical(@startColor: #555, @endColor: #333) { 277 | background-color: @endColor; 278 | background-image: -moz-linear-gradient(top, @startColor, @endColor); // FF 3.6+ 279 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ 280 | background-image: -webkit-linear-gradient(top, @startColor, @endColor); // Safari 5.1+, Chrome 10+ 281 | background-image: -o-linear-gradient(top, @startColor, @endColor); // Opera 11.10 282 | background-image: linear-gradient(to bottom, @startColor, @endColor); // Standard, IE10 283 | background-repeat: repeat-x; 284 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down 285 | } 286 | .directional(@startColor: #555, @endColor: #333, @deg: 45deg) { 287 | background-color: @endColor; 288 | background-repeat: repeat-x; 289 | background-image: -moz-linear-gradient(@deg, @startColor, @endColor); // FF 3.6+ 290 | background-image: -webkit-linear-gradient(@deg, @startColor, @endColor); // Safari 5.1+, Chrome 10+ 291 | background-image: -o-linear-gradient(@deg, @startColor, @endColor); // Opera 11.10 292 | background-image: linear-gradient(@deg, @startColor, @endColor); // Standard, IE10 293 | } 294 | .horizontal-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { 295 | background-color: mix(@midColor, @endColor, 80%); 296 | background-image: -webkit-gradient(left, linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor)); 297 | background-image: -webkit-linear-gradient(left, @startColor, @midColor @colorStop, @endColor); 298 | background-image: -moz-linear-gradient(left, @startColor, @midColor @colorStop, @endColor); 299 | background-image: -o-linear-gradient(left, @startColor, @midColor @colorStop, @endColor); 300 | background-image: linear-gradient(to right, @startColor, @midColor @colorStop, @endColor); 301 | background-repeat: no-repeat; 302 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down, gets no color-stop at all for proper fallback 303 | } 304 | .vertical-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { 305 | background-color: mix(@midColor, @endColor, 80%); 306 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor)); 307 | background-image: -webkit-linear-gradient(@startColor, @midColor @colorStop, @endColor); 308 | background-image: -moz-linear-gradient(top, @startColor, @midColor @colorStop, @endColor); 309 | background-image: -o-linear-gradient(@startColor, @midColor @colorStop, @endColor); 310 | background-image: linear-gradient(@startColor, @midColor @colorStop, @endColor); 311 | background-repeat: no-repeat; 312 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down, gets no color-stop at all for proper fallback 313 | } 314 | .radial(@innerColor: #555, @outerColor: #333) { 315 | background-color: @outerColor; 316 | background-image: -webkit-gradient(radial, center center, 0, center center, 460, from(@innerColor), to(@outerColor)); 317 | background-image: -webkit-radial-gradient(circle, @innerColor, @outerColor); 318 | background-image: -moz-radial-gradient(circle, @innerColor, @outerColor); 319 | background-image: -o-radial-gradient(circle, @innerColor, @outerColor); 320 | background-repeat: no-repeat; 321 | } 322 | .striped(@color: #555, @angle: 45deg) { 323 | background-color: @color; 324 | background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(.25, rgba(255,255,255,.15)), color-stop(.25, transparent), color-stop(.5, transparent), color-stop(.5, rgba(255,255,255,.15)), color-stop(.75, rgba(255,255,255,.15)), color-stop(.75, transparent), to(transparent)); 325 | background-image: -webkit-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); 326 | background-image: -moz-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); 327 | background-image: -o-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); 328 | background-image: linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); 329 | } 330 | } 331 | 332 | // Reset filters for IE 333 | // 334 | // Useful for when you want to remove a gradient from an element. 335 | .reset-filter() { 336 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)")); 337 | } 338 | 339 | 340 | 341 | // 342 | // Mixins: utilities 343 | // -------------------------------------------------- 344 | 345 | // Clearfix 346 | // 347 | // Source: http://nicolasgallagher.com/micro-clearfix-hack/ 348 | // 349 | // For modern browsers 350 | // 1. The space content is one way to avoid an Opera bug when the 351 | // contenteditable attribute is included anywhere else in the document. 352 | // Otherwise it causes space to appear at the top and bottom of elements 353 | // that are clearfixed. 354 | // 2. The use of `table` rather than `block` is only necessary if using 355 | // `:before` to contain the top-margins of child elements. 356 | .clearfix() { 357 | &:before, 358 | &:after { 359 | content: " "; // 1 360 | display: table; // 2 361 | } 362 | &:after { 363 | clear: both; 364 | } 365 | } 366 | 367 | // Center-align a block level element 368 | .center-block() { 369 | display: block; 370 | margin-left: auto; 371 | margin-right: auto; 372 | } 373 | 374 | // Sizing shortcuts 375 | .size(@width, @height) { 376 | width: @width; 377 | height: @height; 378 | } 379 | .square(@size) { 380 | .size(@size, @size); 381 | } 382 | 383 | // Text overflow 384 | // 385 | // Requires inline-block or block for proper styling 386 | .text-truncate() { 387 | overflow: hidden; 388 | text-overflow: ellipsis; 389 | white-space: nowrap; 390 | } 391 | 392 | // Retina images 393 | // 394 | // Retina background-image support with non-retina fall back 395 | .retina-image(@file-1x, @file-2x, @width-1x, @height-1x) { 396 | background-image: url("@{file-1x}"); 397 | 398 | @media 399 | only screen and (-webkit-min-device-pixel-ratio: 2), 400 | only screen and ( min--moz-device-pixel-ratio: 2), 401 | only screen and ( -o-min-device-pixel-ratio: 2/1), 402 | only screen and ( min-device-pixel-ratio: 2), 403 | only screen and ( min-resolution: 192dpi), 404 | only screen and ( min-resolution: 2dppx) { 405 | background-image: url("@{file-2x}"); 406 | background-size: @width-1x @height-1x; 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /css/scissor.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.2 | MIT License | git.io/normalize */ 2 | /* ========================================================================== 3 | HTML5 display definitions 4 | ========================================================================== */ 5 | /** 6 | * Correct `block` display not defined in IE 8/9. 7 | */ 8 | article, 9 | aside, 10 | details, 11 | figcaption, 12 | figure, 13 | footer, 14 | header, 15 | hgroup, 16 | main, 17 | nav, 18 | section, 19 | summary { 20 | display: block; 21 | } 22 | /** 23 | * Correct `inline-block` display not defined in IE 8/9. 24 | */ 25 | audio, 26 | canvas, 27 | video { 28 | display: inline-block; 29 | } 30 | /** 31 | * Prevent modern browsers from displaying `audio` without controls. 32 | * Remove excess height in iOS 5 devices. 33 | */ 34 | audio:not([controls]) { 35 | display: none; 36 | height: 0; 37 | } 38 | /** 39 | * Address `[hidden]` styling not present in IE 8/9. 40 | * Hide the `template` element in IE, Safari, and Firefox < 22. 41 | */ 42 | [hidden], 43 | template { 44 | display: none; 45 | } 46 | /* ========================================================================== 47 | Base 48 | ========================================================================== */ 49 | /** 50 | * 1. Set default font family to sans-serif. 51 | * 2. Prevent iOS text size adjust after orientation change, without disabling 52 | * user zoom. 53 | */ 54 | html { 55 | font-family: sans-serif; 56 | /* 1 */ 57 | 58 | -ms-text-size-adjust: 100%; 59 | /* 2 */ 60 | 61 | -webkit-text-size-adjust: 100%; 62 | /* 2 */ 63 | 64 | } 65 | /** 66 | * Remove default margin. 67 | */ 68 | body { 69 | margin: 0; 70 | } 71 | /* ========================================================================== 72 | Links 73 | ========================================================================== */ 74 | /** 75 | * Remove the gray background color from active links in IE 10. 76 | */ 77 | a { 78 | background: transparent; 79 | } 80 | /** 81 | * Address `outline` inconsistency between Chrome and other browsers. 82 | */ 83 | a:focus { 84 | outline: thin dotted; 85 | } 86 | /** 87 | * Improve readability when focused and also mouse hovered in all browsers. 88 | */ 89 | a:active, 90 | a:hover { 91 | outline: 0; 92 | } 93 | /* ========================================================================== 94 | Typography 95 | ========================================================================== */ 96 | /** 97 | * Address variable `h1` font-size and margin within `section` and `article` 98 | * contexts in Firefox 4+, Safari 5, and Chrome. 99 | */ 100 | h1 { 101 | font-size: 2em; 102 | margin: 0.67em 0; 103 | } 104 | /** 105 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 106 | */ 107 | abbr[title] { 108 | border-bottom: 1px dotted; 109 | } 110 | /** 111 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 112 | */ 113 | b, 114 | strong { 115 | font-weight: bold; 116 | } 117 | /** 118 | * Address styling not present in Safari 5 and Chrome. 119 | */ 120 | dfn { 121 | font-style: italic; 122 | } 123 | /** 124 | * Address differences between Firefox and other browsers. 125 | */ 126 | hr { 127 | -moz-box-sizing: content-box; 128 | box-sizing: content-box; 129 | height: 0; 130 | } 131 | /** 132 | * Address styling not present in IE 8/9. 133 | */ 134 | mark { 135 | background: #ff0; 136 | color: #000; 137 | } 138 | /** 139 | * Correct font family set oddly in Safari 5 and Chrome. 140 | */ 141 | code, 142 | kbd, 143 | pre, 144 | samp { 145 | font-family: monospace, serif; 146 | font-size: 1em; 147 | } 148 | /** 149 | * Improve readability of pre-formatted text in all browsers. 150 | */ 151 | pre { 152 | white-space: pre-wrap; 153 | } 154 | /** 155 | * Set consistent quote types. 156 | */ 157 | q { 158 | quotes: "\201C" "\201D" "\2018" "\2019"; 159 | } 160 | /** 161 | * Address inconsistent and variable font size in all browsers. 162 | */ 163 | small { 164 | font-size: 80%; 165 | } 166 | /** 167 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 168 | */ 169 | sub, 170 | sup { 171 | font-size: 75%; 172 | line-height: 0; 173 | position: relative; 174 | vertical-align: baseline; 175 | } 176 | sup { 177 | top: -0.5em; 178 | } 179 | sub { 180 | bottom: -0.25em; 181 | } 182 | /* ========================================================================== 183 | Embedded content 184 | ========================================================================== */ 185 | /** 186 | * Remove border when inside `a` element in IE 8/9. 187 | */ 188 | img { 189 | border: 0; 190 | } 191 | /** 192 | * Correct overflow displayed oddly in IE 9. 193 | */ 194 | svg:not(:root) { 195 | overflow: hidden; 196 | } 197 | /* ========================================================================== 198 | Figures 199 | ========================================================================== */ 200 | /** 201 | * Address margin not present in IE 8/9 and Safari 5. 202 | */ 203 | figure { 204 | margin: 0; 205 | } 206 | /* ========================================================================== 207 | Forms 208 | ========================================================================== */ 209 | /** 210 | * Define consistent border, margin, and padding. 211 | */ 212 | fieldset { 213 | border: 1px solid #c0c0c0; 214 | margin: 0 2px; 215 | padding: 0.35em 0.625em 0.75em; 216 | } 217 | /** 218 | * 1. Correct `color` not being inherited in IE 8/9. 219 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 220 | */ 221 | legend { 222 | border: 0; 223 | /* 1 */ 224 | 225 | padding: 0; 226 | /* 2 */ 227 | 228 | } 229 | /** 230 | * 1. Correct font family not being inherited in all browsers. 231 | * 2. Correct font size not being inherited in all browsers. 232 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 233 | */ 234 | button, 235 | input, 236 | select, 237 | textarea { 238 | font-family: inherit; 239 | /* 1 */ 240 | 241 | font-size: 100%; 242 | /* 2 */ 243 | 244 | margin: 0; 245 | /* 3 */ 246 | 247 | } 248 | /** 249 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 250 | * the UA stylesheet. 251 | */ 252 | button, 253 | input { 254 | line-height: normal; 255 | } 256 | /** 257 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 258 | * All other form control elements do not inherit `text-transform` values. 259 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 260 | * Correct `select` style inheritance in Firefox 4+ and Opera. 261 | */ 262 | button, 263 | select { 264 | text-transform: none; 265 | } 266 | /** 267 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 268 | * and `video` controls. 269 | * 2. Correct inability to style clickable `input` types in iOS. 270 | * 3. Improve usability and consistency of cursor style between image-type 271 | * `input` and others. 272 | */ 273 | button, 274 | html input[type="button"], 275 | input[type="reset"], 276 | input[type="submit"] { 277 | -webkit-appearance: button; 278 | /* 2 */ 279 | 280 | cursor: pointer; 281 | /* 3 */ 282 | 283 | } 284 | /** 285 | * Re-set default cursor for disabled elements. 286 | */ 287 | button[disabled], 288 | html input[disabled] { 289 | cursor: default; 290 | } 291 | /** 292 | * 1. Address box sizing set to `content-box` in IE 8/9. 293 | * 2. Remove excess padding in IE 8/9. 294 | */ 295 | input[type="checkbox"], 296 | input[type="radio"] { 297 | box-sizing: border-box; 298 | /* 1 */ 299 | 300 | padding: 0; 301 | /* 2 */ 302 | 303 | } 304 | /** 305 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 306 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 307 | * (include `-moz` to future-proof). 308 | */ 309 | input[type="search"] { 310 | -webkit-appearance: textfield; 311 | /* 1 */ 312 | 313 | -moz-box-sizing: content-box; 314 | -webkit-box-sizing: content-box; 315 | /* 2 */ 316 | 317 | box-sizing: content-box; 318 | } 319 | /** 320 | * Remove inner padding and search cancel button in Safari 5 and Chrome 321 | * on OS X. 322 | */ 323 | input[type="search"]::-webkit-search-cancel-button, 324 | input[type="search"]::-webkit-search-decoration { 325 | -webkit-appearance: none; 326 | } 327 | /** 328 | * Remove inner padding and border in Firefox 4+. 329 | */ 330 | button::-moz-focus-inner, 331 | input::-moz-focus-inner { 332 | border: 0; 333 | padding: 0; 334 | } 335 | /** 336 | * 1. Remove default vertical scrollbar in IE 8/9. 337 | * 2. Improve readability and alignment in all browsers. 338 | */ 339 | textarea { 340 | overflow: auto; 341 | /* 1 */ 342 | 343 | vertical-align: top; 344 | /* 2 */ 345 | 346 | } 347 | /* ========================================================================== 348 | Tables 349 | ========================================================================== */ 350 | /** 351 | * Remove most spacing between table cells. 352 | */ 353 | table { 354 | border-collapse: collapse; 355 | border-spacing: 0; 356 | } 357 | /*! 358 | * Preboot v2 359 | * 360 | * Open sourced under MIT license by @mdo. 361 | * Some variables and mixins from Bootstrap (Apache 2 license). 362 | */ 363 | html { 364 | width: 100%; 365 | height: 100%; 366 | } 367 | body { 368 | width: 100%; 369 | height: 100%; 370 | font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; 371 | background-color: #1d0101; 372 | background-image: -webkit-gradient(radial, center center, 0, center center, 460, from(#c30909), to(#1d0101)); 373 | background-image: -webkit-radial-gradient(circle, #c30909, #1d0101); 374 | background-image: -moz-radial-gradient(circle, #c30909, #1d0101); 375 | background-image: -o-radial-gradient(circle, #c30909, #1d0101); 376 | background-repeat: no-repeat; 377 | display: -webkit-flex; 378 | display: -moz-flex; 379 | display: -ms-flexbox; 380 | display: -ms-flex; 381 | display: flex; 382 | -webkit-justify-content: center; 383 | -moz-justify-content: center; 384 | -ms-justify-content: center; 385 | justify-content: center; 386 | -webkit-align-items: center; 387 | -moz-align-items: center; 388 | -ms-align-items: center; 389 | align-items: center; 390 | -webkit-user-select: none; 391 | -moz-user-select: none; 392 | -ms-user-select: none; 393 | -o-user-select: none; 394 | user-select: none; 395 | } 396 | #scissor { 397 | background-color: #737373; 398 | background-image: -moz-linear-gradient(top, #c0c0c0, #737373); 399 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#c0c0c0), to(#737373)); 400 | background-image: -webkit-linear-gradient(top, #c0c0c0, #737373); 401 | background-image: -o-linear-gradient(top, #c0c0c0, #737373); 402 | background-image: linear-gradient(to bottom, #c0c0c0, #737373); 403 | background-repeat: repeat-x; 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc0c0c0', endColorstr='#ff737373', GradientType=0); 405 | padding-left: 0.23606797695262602rem; 406 | padding-right: 0.23606797695262602rem; 407 | border-radius: 0.1458980332994278rem; 408 | -webkit-box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); 409 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); 410 | } 411 | #controls { 412 | padding: 1rem; 413 | margin-top: 0.6180339882723972rem; 414 | border-top-right-radius: 0.1458980332994278rem; 415 | border-top-left-radius: 0.1458980332994278rem; 416 | -webkit-box-sizing: border-box; 417 | -moz-box-sizing: border-box; 418 | box-sizing: border-box; 419 | display: -webkit-flex; 420 | display: -moz-flex; 421 | display: -ms-flexbox; 422 | display: -ms-flex; 423 | display: flex; 424 | } 425 | #controls .panel { 426 | -webkit-flex: 1; 427 | -moz-flex: 1; 428 | -ms-flex: 1; 429 | flex: 1; 430 | -webkit-justify-content: center; 431 | -moz-justify-content: center; 432 | -ms-justify-content: center; 433 | justify-content: center; 434 | -webkit-align-items: center; 435 | -moz-align-items: center; 436 | -ms-align-items: center; 437 | align-items: center; 438 | text-align: center; 439 | } 440 | #controls .panel .knob div { 441 | text-align: center; 442 | width: 100% !important; 443 | } 444 | #controls .panel .knob label { 445 | margin: 0; 446 | padding: 0; 447 | text-transform: uppercase; 448 | font-weight: 700; 449 | color: #666666; 450 | text-shadow: 0 1px 0 #d9d9d9; 451 | } 452 | #controls .panel.title { 453 | -webkit-flex: 2; 454 | -moz-flex: 2; 455 | -ms-flex: 2; 456 | flex: 2; 457 | display: -webkit-flex; 458 | display: -moz-flex; 459 | display: -ms-flexbox; 460 | display: -ms-flex; 461 | display: flex; 462 | -webkit-flex-direction: column; 463 | -moz-flex-direction: column; 464 | -ms-flex-direction: column; 465 | flex-direction: column; 466 | } 467 | #controls .panel.title h1 { 468 | margin: 0; 469 | padding: 0; 470 | } 471 | #controls .panel.title h1 a { 472 | color: #c30909; 473 | text-shadow: 0 1px 0 #d9d9d9; 474 | font-size: 2.6180339927953202rem; 475 | line-height: 1; 476 | font-family: "Stalinist One", sans-serif; 477 | font-weight: 400; 478 | text-transform: uppercase; 479 | text-decoration: none; 480 | } 481 | #controls .panel.title p { 482 | margin: 0; 483 | padding: 0; 484 | text-transform: uppercase; 485 | font-weight: 700; 486 | color: #666666; 487 | text-shadow: 0 1px 0 #d9d9d9; 488 | } 489 | #keyboard { 490 | cursor: pointer; 491 | } 492 | #keyboard ul { 493 | margin: 0 auto; 494 | padding: 0; 495 | list-style: none; 496 | } 497 | #keyboard ul li { 498 | float: left; 499 | width: 4.236067987318243rem; 500 | height: 6.8541019874318065rem; 501 | display: -webkit-flex; 502 | display: -moz-flex; 503 | display: -ms-flexbox; 504 | display: -ms-flex; 505 | display: flex; 506 | -webkit-align-items: flex-end; 507 | -moz-align-items: flex-end; 508 | -ms-align-items: flex-end; 509 | align-items: flex-end; 510 | text-transform: uppercase; 511 | font-style: italic; 512 | padding-left: 0.38196601065988556rem; 513 | padding-bottom: 0.23606797695262602rem; 514 | margin-right: 0.1458980332994278rem; 515 | border-bottom-right-radius: 0.23606797695262602rem; 516 | border-bottom-left-radius: 0.23606797695262602rem; 517 | -webkit-box-sizing: border-box; 518 | -moz-box-sizing: border-box; 519 | box-sizing: border-box; 520 | background-color: #ececec; 521 | color: #a0a0a0; 522 | text-shadow: 0 1px 0 #ffffff; 523 | border-top: 0.1458980332994278rem solid #d3d3d3; 524 | border-left: 0.38196601065988556rem solid #c6c6c6; 525 | border-right: 0.38196601065988556rem solid #c6c6c6; 526 | border-bottom: 0.23606797695262602rem solid #acacac; 527 | -webkit-box-shadow: 0 0.23606797695262602rem 0.6180339882723972rem rgba(0, 0, 0, 0.5); 528 | box-shadow: 0 0.23606797695262602rem 0.6180339882723972rem rgba(0, 0, 0, 0.5); 529 | } 530 | #keyboard ul li.active { 531 | -webkit-box-shadow: 0 0.1458980332994278rem 0.23606797695262602rem rgba(0, 0, 0, 0.5); 532 | box-shadow: 0 0.1458980332994278rem 0.23606797695262602rem rgba(0, 0, 0, 0.5); 533 | text-shadow: 0 1px 0 #ececec; 534 | background-color: #d3d3d3; 535 | background-image: -moz-linear-gradient(top, #ececec, #d3d3d3); 536 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ececec), to(#d3d3d3)); 537 | background-image: -webkit-linear-gradient(top, #ececec, #d3d3d3); 538 | background-image: -o-linear-gradient(top, #ececec, #d3d3d3); 539 | background-image: linear-gradient(to bottom, #ececec, #d3d3d3); 540 | background-repeat: repeat-x; 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffececec', endColorstr='#ffd3d3d3', GradientType=0); 542 | } 543 | #keyboard ul li.accidental { 544 | position: relative; 545 | margin-left: -1.61803399rem; 546 | margin-right: -3.7360679836591215rem; 547 | height: 4.236067987318243rem; 548 | background-color: #464646; 549 | color: #000000; 550 | text-shadow: 0 1px 0 #606060; 551 | border-top: 0.1458980332994278rem solid #2d2d2d; 552 | border-left: 0.38196601065988556rem solid #202020; 553 | border-right: 0.38196601065988556rem solid #202020; 554 | border-bottom: 0.23606797695262602rem solid #060606; 555 | -webkit-box-shadow: 0 0.23606797695262602rem 0.6180339882723972rem rgba(0, 0, 0, 0.5); 556 | box-shadow: 0 0.23606797695262602rem 0.6180339882723972rem rgba(0, 0, 0, 0.5); 557 | } 558 | #keyboard ul li.accidental.active { 559 | -webkit-box-shadow: 0 0.1458980332994278rem 0.23606797695262602rem rgba(0, 0, 0, 0.5); 560 | box-shadow: 0 0.1458980332994278rem 0.23606797695262602rem rgba(0, 0, 0, 0.5); 561 | text-shadow: 0 1px 0 #464646; 562 | background-color: #2d2d2d; 563 | background-image: -moz-linear-gradient(top, #464646, #2d2d2d); 564 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#464646), to(#2d2d2d)); 565 | background-image: -webkit-linear-gradient(top, #464646, #2d2d2d); 566 | background-image: -o-linear-gradient(top, #464646, #2d2d2d); 567 | background-image: linear-gradient(to bottom, #464646, #2d2d2d); 568 | background-repeat: repeat-x; 569 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff464646', endColorstr='#ff2d2d2d', GradientType=0); 570 | } 571 | #keyboard ul li:last-child { 572 | margin-right: 0; 573 | } 574 | .p2 path { 575 | stroke: none; 576 | fill: #c30909; 577 | stroke-weight: .1; 578 | -webkit-svg-shadow: 0 1px 0 #d9d9d9; 579 | } 580 | .p2 path:first-child { 581 | fill: #6e6e6e; 582 | -webkit-svg-shadow: 0 1px 0 #d9d9d9; 583 | } 584 | .p2 rect { 585 | fill: #c30909; 586 | } 587 | -------------------------------------------------------------------------------- /css/scissor.less: -------------------------------------------------------------------------------- 1 | @import "normalize.less"; 2 | @import "preboot.less"; 3 | @import "flexbox.less"; 4 | 5 | .box-shadows(@1, @2, @3, @4, @5) { 6 | -webkit-box-shadow: @1, @2, @3, @4, @5; // iOS <4.3 & Android <4.1 7 | box-shadow: @1, @2, @3, @4, @5; 8 | } 9 | 10 | @synth-color: #737373; 11 | @header-color: #c30909; 12 | @white-key: rgb(236, 236, 236); 13 | @black-key: rgb(70, 70, 70); 14 | 15 | html { 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | body { 21 | width: 100%; 22 | height: 100%; 23 | font-family: @font-family-base; 24 | #gradient.radial(@header-color, darken(@header-color, 34%)); 25 | .flex-display; 26 | .justify-content(center); 27 | .align-items(center); 28 | .user-select(none); 29 | } 30 | 31 | #scissor { 32 | #gradient.vertical(lighten(@synth-color, 30%), @synth-color); 33 | padding-left: @size-micro; 34 | padding-right: @size-micro; 35 | border-radius: @size-nano; 36 | .box-shadow(0 0 @size-base rgba(0,0,0,0.5)); 37 | } 38 | 39 | #controls { 40 | padding: @size-base; 41 | margin-top: @size-small; 42 | .border-top-radius(@size-nano); 43 | .box-sizing(border-box); 44 | .flex-display; 45 | 46 | .panel { 47 | .flex(1); 48 | .justify-content(center); 49 | .align-items(center); 50 | text-align: center; 51 | 52 | .knob { 53 | div { 54 | text-align: center; 55 | width: 100% !important; 56 | } 57 | 58 | label { 59 | margin: 0; 60 | padding: 0; 61 | text-transform: uppercase; 62 | font-weight: 700; 63 | color: darken(@synth-color, 5%); 64 | text-shadow: 0 1px 0 lighten(@synth-color, 40%); 65 | } 66 | } 67 | 68 | &.title { 69 | .flex(2); 70 | .flex-display; 71 | .flex-direction(column); 72 | 73 | h1 { 74 | margin: 0; 75 | padding: 0; 76 | 77 | a { 78 | color: @header-color; 79 | text-shadow: 0 1px 0 lighten(@synth-color, 40%); 80 | font-size: @size-huge; 81 | line-height: 1; 82 | font-family: "Stalinist One", sans-serif; 83 | font-weight: 400; 84 | text-transform: uppercase; 85 | text-decoration: none; 86 | } 87 | } 88 | 89 | p { 90 | margin: 0; 91 | padding: 0; 92 | text-transform: uppercase; 93 | font-weight: 700; 94 | color: darken(@synth-color, 5%); 95 | text-shadow: 0 1px 0 lighten(@synth-color, 40%); 96 | } 97 | } 98 | } 99 | } 100 | 101 | .piano-key(@color) { 102 | background-color: @color; 103 | color: darken(@color, 30%); 104 | text-shadow: 0 1px 0 lighten(@color, 10%); 105 | border-top: @size-nano solid darken(@color, 10%); 106 | border-left: @size-tiny solid darken(@color, 15%); 107 | border-right: @size-tiny solid darken(@color, 15%); 108 | border-bottom: @size-micro solid darken(@color, 25%); 109 | .box-shadow(0 @size-micro @size-small rgba(0,0,0,0.5)); 110 | 111 | &.active { 112 | .box-shadow(0 @size-nano @size-micro rgba(0,0,0,0.5)); 113 | text-shadow: 0 1px 0 @color; 114 | #gradient.vertical(@color, darken(@color, 10%)); 115 | } 116 | } 117 | 118 | #keyboard { 119 | cursor: pointer; 120 | 121 | ul { 122 | margin: 0 auto; 123 | padding: 0; 124 | list-style: none; 125 | 126 | li { 127 | float: left; 128 | width: @size-massive; 129 | height: @size-epic; 130 | .flex-display; 131 | .align-items(flex-end); 132 | text-transform: uppercase; 133 | font-style: italic; 134 | padding-left: @size-tiny; 135 | padding-bottom: @size-micro; 136 | margin-right: @size-nano; 137 | .border-bottom-radius(@size-micro); 138 | .box-sizing(border-box); 139 | .piano-key(@white-key); 140 | 141 | &.accidental { 142 | position: relative; 143 | margin-left: -@size-large; 144 | margin-right: -(@size-large + (@size-massive / 2)); 145 | height: @size-massive; 146 | .piano-key(@black-key); 147 | } 148 | 149 | &:last-child { 150 | margin-right: 0; 151 | } 152 | } 153 | } 154 | } 155 | 156 | .p2 path { 157 | stroke: none; 158 | fill: @header-color; 159 | stroke-weight: .1; 160 | -webkit-svg-shadow: 0 1px 0 lighten(@synth-color, 40%); 161 | } 162 | 163 | .p2 path:first-child { 164 | fill: darken(@synth-color, 2%); 165 | -webkit-svg-shadow: 0 1px 0 lighten(@synth-color, 40%); 166 | } 167 | 168 | .p2 rect { 169 | fill: @header-color; 170 | } 171 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Scissor 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |

Scissor

26 |

Web Audio Supersaw Synthesizer

27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /js/knob.js: -------------------------------------------------------------------------------- 1 | var Knob; 2 | Knob = function(input, ui) { 3 | var container = document.createElement('div'); 4 | container.setAttribute('tabindex', 0); 5 | input.parentNode.replaceChild(container, input); 6 | input.style.cssText = 'position: absolute; top: -10000px'; 7 | input.setAttribute('tabindex', -1); 8 | container.appendChild(input); 9 | 10 | var settings = this.settings = this._getSettings(input); 11 | 12 | 13 | this.value = input.value = settings.min + settings.range / 2; 14 | this.input = input; 15 | this.min = settings.min; 16 | this.onChange = function() {}; 17 | 18 | this.ui = ui; 19 | input.addEventListener('change', this.changed.bind(this), false); 20 | 21 | var events = { 22 | keydown: this._handleKeyEvents.bind(this), 23 | mousewheel: this._handleWheelEvents.bind(this), 24 | DOMMouseScroll: this._handleWheelEvents.bind(this), 25 | touchstart: this._handleMove.bind(this, 'touchmove', 'touchend'), 26 | mousedown: this._handleMove.bind(this, 'mousemove', 'mouseup') 27 | }; 28 | 29 | for (var event in events) { 30 | container.addEventListener(event, events[event], false); 31 | } 32 | 33 | container.style.cssText = 'position: relative; width:' + settings.width + 'px;' + 'height:' + settings.height + 'px;'; 34 | 35 | ui.init(container, settings); 36 | this.container = container; 37 | this.changed(0); 38 | 39 | }; 40 | 41 | Knob.prototype = { 42 | _handleKeyEvents: function(e) { 43 | var keycode = e.keyCode; 44 | if (keycode >= 37 && keycode <= 40) { 45 | e.preventDefault(); 46 | var f = 1 + e.shiftKey * 9; 47 | this.changed({37: -1, 38: 1, 39: 1, 40: -1}[keycode] * f); 48 | } 49 | }, 50 | 51 | _handleWheelEvents: function(e) { 52 | e.preventDefault(); 53 | var deltaX = -e.detail || e.wheelDeltaX; 54 | var deltaY = -e.detail || e.wheelDeltaY; 55 | var val = deltaX > 0 || deltaY > 0 ? 1 : deltaX < 0 || deltaY < 0 ? -1 : 0; 56 | this.changed(val); 57 | }, 58 | 59 | _handleMove: function(onMove, onEnd) { 60 | this.centerX = this.container.offsetLeft + this.settings.width / 2; 61 | this.centerY = this.container.offsetTop + this.settings.height / 2; 62 | var fnc = this._updateWhileMoving.bind(this); 63 | var body = document.body; 64 | body.addEventListener(onMove, fnc, false); 65 | body.addEventListener(onEnd, function() { 66 | body.removeEventListener(onMove, fnc, false); 67 | }, false); 68 | }, 69 | 70 | _updateWhileMoving: function(event) { 71 | event.preventDefault(); 72 | var e = event.changedTouches ? event.changedTouches[0] : event; 73 | var x = this.centerX - e.pageX; 74 | var y = this.centerY - e.pageY; 75 | var deg = Math.atan2(-y, -x) * 180 / Math.PI + 90 - this.settings.angleoffset; 76 | var percent; 77 | 78 | if (deg < 0) { 79 | deg += 360; 80 | } 81 | deg = deg % 360; 82 | if (deg <= this.settings.anglerange) { 83 | percent = Math.max(Math.min(1, deg / this.settings.anglerange), 0); 84 | } else { 85 | percent = +(deg - this.settings.anglerange < (360 - this.settings.anglerange) / 2); 86 | } 87 | var range = this.settings.range; 88 | var value = this.min + range * percent; 89 | 90 | var step = (this.settings.max - this.min) / range; 91 | this.value = this.input.value = Math.round(value / step) * step; 92 | this.ui.update(percent, this.value); 93 | this.onChange(this.value); 94 | }, 95 | 96 | changed: function(direction) { 97 | this.input.value = this.limit(parseFloat(this.input.value) + direction * (this.input.step || 1)); 98 | this.value = this.input.value; 99 | this.ui.update(this._valueToPercent(), this.value); 100 | this.onChange(this.value); 101 | }, 102 | 103 | _valueToPercent: function() { 104 | return this.value != null ? 100 / this.settings.range * (this.value - this.min) / 100 : this.min; 105 | }, 106 | 107 | limit: function(value) { 108 | return Math.min(Math.max(this.settings.min, value), this.settings.max); 109 | }, 110 | _getSettings: function(input) { 111 | var settings = { 112 | max: parseFloat(input.max), 113 | min: parseFloat(input.min), 114 | step: parseFloat(input.step) || 1, 115 | angleoffset: 0, 116 | anglerange: 360 117 | }; 118 | settings.range = settings.max - settings.min; 119 | var data = input.dataset; 120 | for (var i in data) { 121 | if (data.hasOwnProperty(i)) { 122 | var value = +data[i]; 123 | settings[i] = isNaN(value) ? data[i] : value; 124 | } 125 | } 126 | return settings; 127 | } 128 | }; 129 | 130 | 131 | 132 | var Ui = function() { 133 | }; 134 | 135 | Ui.prototype = { 136 | init: function(parentEl, options) { 137 | this.options || (this.options = {}); 138 | this.merge(this.options, options); 139 | this.width = options.width; 140 | this.height = options.height; 141 | this.createElement(parentEl); 142 | if (!this.components) { 143 | return; 144 | } 145 | this.components.forEach(function(component) { 146 | component.init(this.el.node, options); 147 | }.bind(this)); 148 | }, 149 | 150 | merge: function(dest, src) { 151 | for (var i in src) { 152 | if (src.hasOwnProperty(i)) { 153 | dest[i] = src[i]; 154 | } 155 | } 156 | return dest; 157 | }, 158 | 159 | addComponent: function(component) { 160 | this.components || (this.components = []); 161 | this.components.push(component); 162 | }, 163 | 164 | update: function(percent, value) { 165 | 166 | if (!this.components) { 167 | return; 168 | } 169 | this.components.forEach(function(component) { 170 | component.update(percent, value); 171 | }); 172 | }, 173 | 174 | createElement: function(parentEl) { 175 | this.el = new Ui.El(this.width, this.height); 176 | this.el.create("svg", { 177 | version: "1.2", 178 | baseProfile: "tiny", 179 | width: this.width, 180 | height: this.height 181 | }); 182 | this.appendTo(parentEl); 183 | }, 184 | appendTo: function(parent) { 185 | parent.appendChild(this.el.node); 186 | } 187 | 188 | }; 189 | 190 | Ui.Pointer = function(options) { 191 | this.options = options || {}; 192 | this.options.type && Ui.El[this.options.type] || (this.options.type = 'Triangle'); 193 | }; 194 | 195 | Ui.Pointer.prototype = Object.create(Ui.prototype); 196 | 197 | Ui.Pointer.prototype.update = function(percent) { 198 | this.el.rotate(this.options.angleoffset + percent * this.options.anglerange, this.width / 2, 199 | this.height / 2); 200 | }; 201 | 202 | Ui.Pointer.prototype.createElement = function(parentEl) { 203 | this.options.pointerHeight || (this.options.pointerHeight = this.height / 2); 204 | if (this.options.type == 'Arc') { 205 | this.el = new Ui.El.Arc(this.options); 206 | this.el.setAngle(this.options.size); 207 | } else { 208 | this.el = new Ui.El[this.options.type](this.options.pointerWidth, 209 | this.options.pointerHeight, this.width / 2, 210 | this.options.pointerHeight / 2 + this.options.offset); 211 | } 212 | this.appendTo(parentEl); 213 | 214 | }; 215 | 216 | Ui.Arc = function(options) { 217 | this.options = options || {}; 218 | }; 219 | 220 | Ui.Arc.prototype = Object.create(Ui.prototype); 221 | 222 | Ui.Arc.prototype.createElement = function(parentEl) { 223 | this.el = new Ui.El.Arc(this.options); 224 | this.appendTo(parentEl); 225 | }; 226 | 227 | Ui.Arc.prototype.update = function(percent) { 228 | this.el.setAngle(percent * this.options.anglerange); 229 | }; 230 | 231 | Ui.Scale = function(options) { 232 | this.options = this.merge({ 233 | steps: options.range / options.step, 234 | radius: this.width / 2, 235 | tickWidth: 1, 236 | tickHeight: 3 237 | }, options); 238 | this.options.type = Ui.El[this.options.type || 'Rect']; 239 | }; 240 | 241 | Ui.Scale.prototype = Object.create(Ui.prototype); 242 | 243 | Ui.Scale.prototype.createElement = function(parentEl) { 244 | this.el = new Ui.El(this.width, this.height); 245 | this.startAngle = this.options.angleoffset || 0; 246 | this.options.radius || (this.options.radius = this.height / 2.5); 247 | this.el.create("g"); 248 | this.el.addClassName('scale'); 249 | if (this.options.drawScale) { 250 | var step = this.options.anglerange / this.options.steps; 251 | var end = this.options.steps + (this.options.anglerange == 360 ? 0 : 1); 252 | this.ticks = []; 253 | var Shape = this.options.type; 254 | for (var i = 0; i < end; i++) { 255 | var rect = new Shape(this.options.tickWidth, this.options.tickHeight, this.width / 2, 256 | this.options.tickHeight / 2); 257 | rect.rotate(this.startAngle + i * step, this.width / 2, this.height / 2); 258 | this.el.append(rect); 259 | this.ticks.push(rect); 260 | } 261 | } 262 | this.appendTo(parentEl); 263 | if (this.options.drawDial) { 264 | this.dial(); 265 | } 266 | }; 267 | 268 | Ui.Scale.prototype.dial = function() { 269 | var step = this.options.anglerange / this.options.steps; 270 | var min = this.options.min; 271 | var dialStep = (this.options.max - min) / this.options.steps; 272 | var end = this.options.steps + (this.options.anglerange == 360 ? 0 : 1); 273 | this.dials = []; 274 | for (var i = 0; i < end; i++) { 275 | var text = new Ui.El.Text(Math.abs(min + dialStep * i), this.width / 2 - 2.5, 276 | this.height / 2 - this.options.radius, 5, 5); 277 | this.el.append(text); 278 | text.rotate(this.startAngle + i * step, this.width / 2, this.height / 2); 279 | this.dials.push(text); 280 | } 281 | }; 282 | 283 | Ui.Scale.prototype.update = function(percent) { 284 | if (this.ticks) { 285 | if (this.activeStep) { 286 | this.activeStep.attr('class', ''); 287 | } 288 | this.activeStep = this.ticks[Math.round(this.options.steps * percent)]; 289 | this.activeStep.attr('class', 'active'); 290 | } 291 | if (this.dials) { 292 | if (this.activeDial) { 293 | this.activeDial.attr('class', ''); 294 | } 295 | this.activeDial = this.dials[Math.round(this.options.steps * percent)]; 296 | if (this.activeDial) { 297 | this.activeDial.attr('class', 'active'); 298 | } 299 | } 300 | }; 301 | 302 | Ui.Text = function() {}; 303 | 304 | Ui.Text.prototype = Object.create(Ui.prototype); 305 | 306 | Ui.Text.prototype.createElement = function(parentEl) { 307 | this.parentEl = parentEl 308 | this.el = new Ui.El.Text('', 0, this.height); 309 | this.appendTo(parentEl); 310 | this.el.center(parentEl); 311 | }; 312 | 313 | Ui.Text.prototype.update = function(percent, value) { 314 | this.el.node.textContent = value; 315 | this.el.center(this.parentEl); 316 | }; 317 | 318 | Ui.El = function() {}; 319 | 320 | Ui.El.prototype = { 321 | svgNS: "http://www.w3.org/2000/svg", 322 | 323 | init: function(width, height, x, y) { 324 | this.width = width; 325 | this.height = height; 326 | this.x = x || 0; 327 | this.y = y || 0; 328 | this.left = this.x - width / 2; 329 | this.right = this.x + width / 2; 330 | this.top = this.y - height / 2; 331 | this.bottom = this.y + height / 2; 332 | }, 333 | create: function(type, attributes) { 334 | this.node = document.createElementNS(this.svgNS, type); 335 | for (var key in attributes) { 336 | this.attr(key, attributes[key]); 337 | } 338 | }, 339 | 340 | rotate: function(angle, x, y) { 341 | this.attr("transform", "rotate(" + angle + " " + (x || this.x) + " " + (y || this.y ) + ")"); 342 | }, 343 | 344 | attr: function(attributeName, value) { 345 | if (value == null) return this.node.getAttribute(attributeName) || ''; 346 | this.node.setAttribute(attributeName, value); 347 | }, 348 | 349 | append: function(el) { 350 | this.node.appendChild(el.node); 351 | }, 352 | 353 | addClassName: function(className) { 354 | this.attr('class', this.attr('class') + ' ' + className); 355 | } 356 | }; 357 | 358 | Ui.El.Triangle = function() { 359 | this.init.apply(this, arguments); 360 | this.create("polygon", { 361 | 'points': this.left + ',' + this.bottom + ' ' + this.x + ',' + this.top + ' ' + this.right + ',' + this.bottom 362 | }); 363 | }; 364 | 365 | Ui.El.Triangle.prototype = Object.create(Ui.El.prototype); 366 | 367 | Ui.El.Rect = function() { 368 | this.init.apply(this, arguments); 369 | this.create("rect", { 370 | x: this.x - this.width / 2, 371 | y: this.y, 372 | width: this.width, 373 | height: this.height 374 | }); 375 | }; 376 | 377 | Ui.El.Rect.prototype = Object.create(Ui.El.prototype); 378 | 379 | Ui.El.Circle = function(radius, x, y) { 380 | if (arguments.length == 4) { 381 | x = arguments[2]; 382 | y = arguments[3]; 383 | } 384 | this.init(radius * 2, radius * 2, x, y); 385 | this.create("circle", { 386 | cx: this.x, 387 | cy: this.y, 388 | r: radius 389 | }); 390 | }; 391 | 392 | Ui.El.Circle.prototype = Object.create(Ui.El.prototype); 393 | 394 | Ui.El.Text = function(text, x, y, width, height) { 395 | this.create('text', { 396 | x: x, 397 | y: y, 398 | width: width, 399 | height: height 400 | }); 401 | this.node.textContent = text; 402 | }; 403 | 404 | Ui.El.Text.prototype = Object.create(Ui.El.prototype); 405 | 406 | Ui.El.Text.prototype.center = function(element) { 407 | var width = element.getAttribute('width'); 408 | var height = element.getAttribute('height'); 409 | this.attr('x', width / 2 - this.node.getBBox().width / 2); 410 | this.attr('y', height / 2 + this.node.getBBox().height / 4); 411 | }; 412 | 413 | Ui.El.Arc = function(options) { 414 | this.options = options; 415 | this.options.angleoffset = (options.angleoffset || 0) - 90; 416 | this.create('path'); 417 | }; 418 | 419 | Ui.El.Arc.prototype = Object.create(Ui.El.prototype); 420 | 421 | Ui.El.Arc.prototype.setAngle = function(angle) { 422 | this.attr('d', this.getCoords(angle)); 423 | }; 424 | 425 | 426 | Ui.El.Arc.prototype.getCoords = function(angle) { 427 | var startAngle = this.options.angleoffset; 428 | var outerRadius = this.options.outerRadius || this.options.width / 2; 429 | var innerRadius = this.options.innerRadius || this.options.width / 2 - this.options.arcWidth; 430 | var startAngleDegree = Math.PI * startAngle / 180; 431 | var endAngleDegree = Math.PI * (startAngle + angle) / 180; 432 | var center = this.options.width / 2; 433 | 434 | var p1 = pointOnCircle(outerRadius, endAngleDegree); 435 | var p2 = pointOnCircle(outerRadius, startAngleDegree); 436 | var p3 = pointOnCircle(innerRadius, startAngleDegree); 437 | var p4 = pointOnCircle(innerRadius, endAngleDegree); 438 | 439 | var path = 'M' + p1.x + ',' + p1.y; 440 | var largeArcFlag = ( angle < 180 ? 0 : 1); 441 | path += ' A' + outerRadius + ',' + outerRadius + ' 0 ' + largeArcFlag + ' 0 ' + p2.x + ',' + p2.y; 442 | path += 'L' + p3.x + ',' + p3.y; 443 | path += ' A' + innerRadius + ',' + innerRadius + ' 0 ' + largeArcFlag + ' 1 ' + p4.x + ',' + p4.y; 444 | path += 'L' + p1.x + ',' + p1.y; 445 | return path; 446 | 447 | function pointOnCircle(radius, angle) { 448 | return { 449 | x: center + radius * Math.cos(angle), 450 | y: center + radius * Math.sin(angle) 451 | }; 452 | } 453 | }; 454 | 455 | Ui.P2 = function() { 456 | }; 457 | 458 | Ui.P2.prototype = Object.create(Ui.prototype); 459 | 460 | Ui.P2.prototype.createElement = function() { 461 | "use strict"; 462 | Ui.prototype.createElement.apply(this, arguments); 463 | this.addComponent(new Ui.Arc({ 464 | arcWidth: this.width / 10 465 | })); 466 | 467 | this.addComponent(new Ui.Pointer(this.merge(this.options, { 468 | type: 'Rect', 469 | pointerWidth: this.width / 10 470 | }))); 471 | 472 | this.merge(this.options, {arcWidth: this.width / 10}); 473 | var arc = new Ui.El.Arc(this.options); 474 | arc.setAngle(this.options.anglerange); 475 | this.el.node.appendChild(arc.node); 476 | this.el.node.setAttribute("class", "p2"); 477 | }; 478 | -------------------------------------------------------------------------------- /js/mousetrap.js: -------------------------------------------------------------------------------- 1 | /*global define:false */ 2 | /** 3 | * Copyright 2013 Craig Campbell 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | * Mousetrap is a simple keyboard shortcut library for Javascript with 18 | * no external dependencies 19 | * 20 | * @version 1.4.4 21 | * @url craig.is/killing/mice 22 | */ 23 | (function() { 24 | 25 | /** 26 | * mapping of special keycodes to their corresponding keys 27 | * 28 | * everything in this dictionary cannot use keypress events 29 | * so it has to be here to map to the correct keycodes for 30 | * keyup/keydown events 31 | * 32 | * @type {Object} 33 | */ 34 | var _MAP = { 35 | 8: 'backspace', 36 | 9: 'tab', 37 | 13: 'enter', 38 | 16: 'shift', 39 | 17: 'ctrl', 40 | 18: 'alt', 41 | 20: 'capslock', 42 | 27: 'esc', 43 | 32: 'space', 44 | 33: 'pageup', 45 | 34: 'pagedown', 46 | 35: 'end', 47 | 36: 'home', 48 | 37: 'left', 49 | 38: 'up', 50 | 39: 'right', 51 | 40: 'down', 52 | 45: 'ins', 53 | 46: 'del', 54 | 91: 'meta', 55 | 93: 'meta', 56 | 224: 'meta' 57 | }, 58 | 59 | /** 60 | * mapping for special characters so they can support 61 | * 62 | * this dictionary is only used incase you want to bind a 63 | * keyup or keydown event to one of these keys 64 | * 65 | * @type {Object} 66 | */ 67 | _KEYCODE_MAP = { 68 | 106: '*', 69 | 107: '+', 70 | 109: '-', 71 | 110: '.', 72 | 111 : '/', 73 | 186: ';', 74 | 187: '=', 75 | 188: ',', 76 | 189: '-', 77 | 190: '.', 78 | 191: '/', 79 | 192: '`', 80 | 219: '[', 81 | 220: '\\', 82 | 221: ']', 83 | 222: '\'' 84 | }, 85 | 86 | /** 87 | * this is a mapping of keys that require shift on a US keypad 88 | * back to the non shift equivelents 89 | * 90 | * this is so you can use keyup events with these keys 91 | * 92 | * note that this will only work reliably on US keyboards 93 | * 94 | * @type {Object} 95 | */ 96 | _SHIFT_MAP = { 97 | '~': '`', 98 | '!': '1', 99 | '@': '2', 100 | '#': '3', 101 | '$': '4', 102 | '%': '5', 103 | '^': '6', 104 | '&': '7', 105 | '*': '8', 106 | '(': '9', 107 | ')': '0', 108 | '_': '-', 109 | '+': '=', 110 | ':': ';', 111 | '\"': '\'', 112 | '<': ',', 113 | '>': '.', 114 | '?': '/', 115 | '|': '\\' 116 | }, 117 | 118 | /** 119 | * this is a list of special strings you can use to map 120 | * to modifier keys when you specify your keyboard shortcuts 121 | * 122 | * @type {Object} 123 | */ 124 | _SPECIAL_ALIASES = { 125 | 'option': 'alt', 126 | 'command': 'meta', 127 | 'return': 'enter', 128 | 'escape': 'esc', 129 | 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl' 130 | }, 131 | 132 | /** 133 | * variable to store the flipped version of _MAP from above 134 | * needed to check if we should use keypress or not when no action 135 | * is specified 136 | * 137 | * @type {Object|undefined} 138 | */ 139 | _REVERSE_MAP, 140 | 141 | /** 142 | * a list of all the callbacks setup via Mousetrap.bind() 143 | * 144 | * @type {Object} 145 | */ 146 | _callbacks = {}, 147 | 148 | /** 149 | * direct map of string combinations to callbacks used for trigger() 150 | * 151 | * @type {Object} 152 | */ 153 | _directMap = {}, 154 | 155 | /** 156 | * keeps track of what level each sequence is at since multiple 157 | * sequences can start out with the same sequence 158 | * 159 | * @type {Object} 160 | */ 161 | _sequenceLevels = {}, 162 | 163 | /** 164 | * variable to store the setTimeout call 165 | * 166 | * @type {null|number} 167 | */ 168 | _resetTimer, 169 | 170 | /** 171 | * temporary state where we will ignore the next keyup 172 | * 173 | * @type {boolean|string} 174 | */ 175 | _ignoreNextKeyup = false, 176 | 177 | /** 178 | * temporary state where we will ignore the next keypress 179 | * 180 | * @type {boolean} 181 | */ 182 | _ignoreNextKeypress = false, 183 | 184 | /** 185 | * are we currently inside of a sequence? 186 | * type of action ("keyup" or "keydown" or "keypress") or false 187 | * 188 | * @type {boolean|string} 189 | */ 190 | _nextExpectedAction = false; 191 | 192 | /** 193 | * loop through the f keys, f1 to f19 and add them to the map 194 | * programatically 195 | */ 196 | for (var i = 1; i < 20; ++i) { 197 | _MAP[111 + i] = 'f' + i; 198 | } 199 | 200 | /** 201 | * loop through to map numbers on the numeric keypad 202 | */ 203 | for (i = 0; i <= 9; ++i) { 204 | _MAP[i + 96] = i; 205 | } 206 | 207 | /** 208 | * cross browser add event method 209 | * 210 | * @param {Element|HTMLDocument} object 211 | * @param {string} type 212 | * @param {Function} callback 213 | * @returns void 214 | */ 215 | function _addEvent(object, type, callback) { 216 | if (object.addEventListener) { 217 | object.addEventListener(type, callback, false); 218 | return; 219 | } 220 | 221 | object.attachEvent('on' + type, callback); 222 | } 223 | 224 | /** 225 | * takes the event and returns the key character 226 | * 227 | * @param {Event} e 228 | * @return {string} 229 | */ 230 | function _characterFromEvent(e) { 231 | 232 | // for keypress events we should return the character as is 233 | if (e.type == 'keypress') { 234 | var character = String.fromCharCode(e.which); 235 | 236 | // if the shift key is not pressed then it is safe to assume 237 | // that we want the character to be lowercase. this means if 238 | // you accidentally have caps lock on then your key bindings 239 | // will continue to work 240 | // 241 | // the only side effect that might not be desired is if you 242 | // bind something like 'A' cause you want to trigger an 243 | // event when capital A is pressed caps lock will no longer 244 | // trigger the event. shift+a will though. 245 | if (!e.shiftKey) { 246 | character = character.toLowerCase(); 247 | } 248 | 249 | return character; 250 | } 251 | 252 | // for non keypress events the special maps are needed 253 | if (_MAP[e.which]) { 254 | return _MAP[e.which]; 255 | } 256 | 257 | if (_KEYCODE_MAP[e.which]) { 258 | return _KEYCODE_MAP[e.which]; 259 | } 260 | 261 | // if it is not in the special map 262 | 263 | // with keydown and keyup events the character seems to always 264 | // come in as an uppercase character whether you are pressing shift 265 | // or not. we should make sure it is always lowercase for comparisons 266 | return String.fromCharCode(e.which).toLowerCase(); 267 | } 268 | 269 | /** 270 | * checks if two arrays are equal 271 | * 272 | * @param {Array} modifiers1 273 | * @param {Array} modifiers2 274 | * @returns {boolean} 275 | */ 276 | function _modifiersMatch(modifiers1, modifiers2) { 277 | return modifiers1.sort().join(',') === modifiers2.sort().join(','); 278 | } 279 | 280 | /** 281 | * resets all sequence counters except for the ones passed in 282 | * 283 | * @param {Object} doNotReset 284 | * @returns void 285 | */ 286 | function _resetSequences(doNotReset) { 287 | doNotReset = doNotReset || {}; 288 | 289 | var activeSequences = false, 290 | key; 291 | 292 | for (key in _sequenceLevels) { 293 | if (doNotReset[key]) { 294 | activeSequences = true; 295 | continue; 296 | } 297 | _sequenceLevels[key] = 0; 298 | } 299 | 300 | if (!activeSequences) { 301 | _nextExpectedAction = false; 302 | } 303 | } 304 | 305 | /** 306 | * finds all callbacks that match based on the keycode, modifiers, 307 | * and action 308 | * 309 | * @param {string} character 310 | * @param {Array} modifiers 311 | * @param {Event|Object} e 312 | * @param {string=} sequenceName - name of the sequence we are looking for 313 | * @param {string=} combination 314 | * @param {number=} level 315 | * @returns {Array} 316 | */ 317 | function _getMatches(character, modifiers, e, sequenceName, combination, level) { 318 | var i, 319 | callback, 320 | matches = [], 321 | action = e.type; 322 | 323 | // if there are no events related to this keycode 324 | if (!_callbacks[character]) { 325 | return []; 326 | } 327 | 328 | // if a modifier key is coming up on its own we should allow it 329 | if (action == 'keyup' && _isModifier(character)) { 330 | modifiers = [character]; 331 | } 332 | 333 | // loop through all callbacks for the key that was pressed 334 | // and see if any of them match 335 | for (i = 0; i < _callbacks[character].length; ++i) { 336 | callback = _callbacks[character][i]; 337 | 338 | // if a sequence name is not specified, but this is a sequence at 339 | // the wrong level then move onto the next match 340 | if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) { 341 | continue; 342 | } 343 | 344 | // if the action we are looking for doesn't match the action we got 345 | // then we should keep going 346 | if (action != callback.action) { 347 | continue; 348 | } 349 | 350 | // if this is a keypress event and the meta key and control key 351 | // are not pressed that means that we need to only look at the 352 | // character, otherwise check the modifiers as well 353 | // 354 | // chrome will not fire a keypress if meta or control is down 355 | // safari will fire a keypress if meta or meta+shift is down 356 | // firefox will fire a keypress if meta or control is down 357 | if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { 358 | 359 | // when you bind a combination or sequence a second time it 360 | // should overwrite the first one. if a sequenceName or 361 | // combination is specified in this call it does just that 362 | // 363 | // @todo make deleting its own method? 364 | var deleteCombo = !sequenceName && callback.combo == combination; 365 | var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level; 366 | if (deleteCombo || deleteSequence) { 367 | _callbacks[character].splice(i, 1); 368 | } 369 | 370 | matches.push(callback); 371 | } 372 | } 373 | 374 | return matches; 375 | } 376 | 377 | /** 378 | * takes a key event and figures out what the modifiers are 379 | * 380 | * @param {Event} e 381 | * @returns {Array} 382 | */ 383 | function _eventModifiers(e) { 384 | var modifiers = []; 385 | 386 | if (e.shiftKey) { 387 | modifiers.push('shift'); 388 | } 389 | 390 | if (e.altKey) { 391 | modifiers.push('alt'); 392 | } 393 | 394 | if (e.ctrlKey) { 395 | modifiers.push('ctrl'); 396 | } 397 | 398 | if (e.metaKey) { 399 | modifiers.push('meta'); 400 | } 401 | 402 | return modifiers; 403 | } 404 | 405 | /** 406 | * actually calls the callback function 407 | * 408 | * if your callback function returns false this will use the jquery 409 | * convention - prevent default and stop propogation on the event 410 | * 411 | * @param {Function} callback 412 | * @param {Event} e 413 | * @returns void 414 | */ 415 | function _fireCallback(callback, e, combo) { 416 | 417 | // if this event should not happen stop here 418 | if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo)) { 419 | return; 420 | } 421 | 422 | if (callback(e, combo) === false) { 423 | if (e.preventDefault) { 424 | e.preventDefault(); 425 | } 426 | 427 | if (e.stopPropagation) { 428 | e.stopPropagation(); 429 | } 430 | 431 | e.returnValue = false; 432 | e.cancelBubble = true; 433 | } 434 | } 435 | 436 | /** 437 | * handles a character key event 438 | * 439 | * @param {string} character 440 | * @param {Array} modifiers 441 | * @param {Event} e 442 | * @returns void 443 | */ 444 | function _handleKey(character, modifiers, e) { 445 | var callbacks = _getMatches(character, modifiers, e), 446 | i, 447 | doNotReset = {}, 448 | maxLevel = 0, 449 | processedSequenceCallback = false; 450 | 451 | // Calculate the maxLevel for sequences so we can only execute the longest callback sequence 452 | for (i = 0; i < callbacks.length; ++i) { 453 | if (callbacks[i].seq) { 454 | maxLevel = Math.max(maxLevel, callbacks[i].level); 455 | } 456 | } 457 | 458 | // loop through matching callbacks for this key event 459 | for (i = 0; i < callbacks.length; ++i) { 460 | 461 | // fire for all sequence callbacks 462 | // this is because if for example you have multiple sequences 463 | // bound such as "g i" and "g t" they both need to fire the 464 | // callback for matching g cause otherwise you can only ever 465 | // match the first one 466 | if (callbacks[i].seq) { 467 | 468 | // only fire callbacks for the maxLevel to prevent 469 | // subsequences from also firing 470 | // 471 | // for example 'a option b' should not cause 'option b' to fire 472 | // even though 'option b' is part of the other sequence 473 | // 474 | // any sequences that do not match here will be discarded 475 | // below by the _resetSequences call 476 | if (callbacks[i].level != maxLevel) { 477 | continue; 478 | } 479 | 480 | processedSequenceCallback = true; 481 | 482 | // keep a list of which sequences were matches for later 483 | doNotReset[callbacks[i].seq] = 1; 484 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); 485 | continue; 486 | } 487 | 488 | // if there were no sequence matches but we are still here 489 | // that means this is a regular match so we should fire that 490 | if (!processedSequenceCallback) { 491 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); 492 | } 493 | } 494 | 495 | // if the key you pressed matches the type of sequence without 496 | // being a modifier (ie "keyup" or "keypress") then we should 497 | // reset all sequences that were not matched by this event 498 | // 499 | // this is so, for example, if you have the sequence "h a t" and you 500 | // type "h e a r t" it does not match. in this case the "e" will 501 | // cause the sequence to reset 502 | // 503 | // modifier keys are ignored because you can have a sequence 504 | // that contains modifiers such as "enter ctrl+space" and in most 505 | // cases the modifier key will be pressed before the next key 506 | // 507 | // also if you have a sequence such as "ctrl+b a" then pressing the 508 | // "b" key will trigger a "keypress" and a "keydown" 509 | // 510 | // the "keydown" is expected when there is a modifier, but the 511 | // "keypress" ends up matching the _nextExpectedAction since it occurs 512 | // after and that causes the sequence to reset 513 | // 514 | // we ignore keypresses in a sequence that directly follow a keydown 515 | // for the same character 516 | var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress; 517 | if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) { 518 | _resetSequences(doNotReset); 519 | } 520 | 521 | _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown'; 522 | } 523 | 524 | /** 525 | * handles a keydown event 526 | * 527 | * @param {Event} e 528 | * @returns void 529 | */ 530 | function _handleKeyEvent(e) { 531 | 532 | // normalize e.which for key events 533 | // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion 534 | if (typeof e.which !== 'number') { 535 | e.which = e.keyCode; 536 | } 537 | 538 | var character = _characterFromEvent(e); 539 | 540 | // no character found then stop 541 | if (!character) { 542 | return; 543 | } 544 | 545 | // need to use === for the character check because the character can be 0 546 | if (e.type == 'keyup' && _ignoreNextKeyup === character) { 547 | _ignoreNextKeyup = false; 548 | return; 549 | } 550 | 551 | Mousetrap.handleKey(character, _eventModifiers(e), e); 552 | } 553 | 554 | /** 555 | * determines if the keycode specified is a modifier key or not 556 | * 557 | * @param {string} key 558 | * @returns {boolean} 559 | */ 560 | function _isModifier(key) { 561 | return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; 562 | } 563 | 564 | /** 565 | * called to set a 1 second timeout on the specified sequence 566 | * 567 | * this is so after each key press in the sequence you have 1 second 568 | * to press the next key before you have to start over 569 | * 570 | * @returns void 571 | */ 572 | function _resetSequenceTimer() { 573 | clearTimeout(_resetTimer); 574 | _resetTimer = setTimeout(_resetSequences, 1000); 575 | } 576 | 577 | /** 578 | * reverses the map lookup so that we can look for specific keys 579 | * to see what can and can't use keypress 580 | * 581 | * @return {Object} 582 | */ 583 | function _getReverseMap() { 584 | if (!_REVERSE_MAP) { 585 | _REVERSE_MAP = {}; 586 | for (var key in _MAP) { 587 | 588 | // pull out the numeric keypad from here cause keypress should 589 | // be able to detect the keys from the character 590 | if (key > 95 && key < 112) { 591 | continue; 592 | } 593 | 594 | if (_MAP.hasOwnProperty(key)) { 595 | _REVERSE_MAP[_MAP[key]] = key; 596 | } 597 | } 598 | } 599 | return _REVERSE_MAP; 600 | } 601 | 602 | /** 603 | * picks the best action based on the key combination 604 | * 605 | * @param {string} key - character for key 606 | * @param {Array} modifiers 607 | * @param {string=} action passed in 608 | */ 609 | function _pickBestAction(key, modifiers, action) { 610 | 611 | // if no action was picked in we should try to pick the one 612 | // that we think would work best for this key 613 | if (!action) { 614 | action = _getReverseMap()[key] ? 'keydown' : 'keypress'; 615 | } 616 | 617 | // modifier keys don't work as expected with keypress, 618 | // switch to keydown 619 | if (action == 'keypress' && modifiers.length) { 620 | action = 'keydown'; 621 | } 622 | 623 | return action; 624 | } 625 | 626 | /** 627 | * binds a key sequence to an event 628 | * 629 | * @param {string} combo - combo specified in bind call 630 | * @param {Array} keys 631 | * @param {Function} callback 632 | * @param {string=} action 633 | * @returns void 634 | */ 635 | function _bindSequence(combo, keys, callback, action) { 636 | 637 | // start off by adding a sequence level record for this combination 638 | // and setting the level to 0 639 | _sequenceLevels[combo] = 0; 640 | 641 | /** 642 | * callback to increase the sequence level for this sequence and reset 643 | * all other sequences that were active 644 | * 645 | * @param {string} nextAction 646 | * @returns {Function} 647 | */ 648 | function _increaseSequence(nextAction) { 649 | return function() { 650 | _nextExpectedAction = nextAction; 651 | ++_sequenceLevels[combo]; 652 | _resetSequenceTimer(); 653 | }; 654 | } 655 | 656 | /** 657 | * wraps the specified callback inside of another function in order 658 | * to reset all sequence counters as soon as this sequence is done 659 | * 660 | * @param {Event} e 661 | * @returns void 662 | */ 663 | function _callbackAndReset(e) { 664 | _fireCallback(callback, e, combo); 665 | 666 | // we should ignore the next key up if the action is key down 667 | // or keypress. this is so if you finish a sequence and 668 | // release the key the final key will not trigger a keyup 669 | if (action !== 'keyup') { 670 | _ignoreNextKeyup = _characterFromEvent(e); 671 | } 672 | 673 | // weird race condition if a sequence ends with the key 674 | // another sequence begins with 675 | setTimeout(_resetSequences, 10); 676 | } 677 | 678 | // loop through keys one at a time and bind the appropriate callback 679 | // function. for any key leading up to the final one it should 680 | // increase the sequence. after the final, it should reset all sequences 681 | // 682 | // if an action is specified in the original bind call then that will 683 | // be used throughout. otherwise we will pass the action that the 684 | // next key in the sequence should match. this allows a sequence 685 | // to mix and match keypress and keydown events depending on which 686 | // ones are better suited to the key provided 687 | for (var i = 0; i < keys.length; ++i) { 688 | var isFinal = i + 1 === keys.length; 689 | var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action); 690 | _bindSingle(keys[i], wrappedCallback, action, combo, i); 691 | } 692 | } 693 | 694 | /** 695 | * Converts from a string key combination to an array 696 | * 697 | * @param {string} combination like "command+shift+l" 698 | * @return {Array} 699 | */ 700 | function _keysFromString(combination) { 701 | if (combination === '+') { 702 | return ['+']; 703 | } 704 | 705 | return combination.split('+'); 706 | } 707 | 708 | /** 709 | * Gets info for a specific key combination 710 | * 711 | * @param {string} combination key combination ("command+s" or "a" or "*") 712 | * @param {string=} action 713 | * @returns {Object} 714 | */ 715 | function _getKeyInfo(combination, action) { 716 | var keys, 717 | key, 718 | i, 719 | modifiers = []; 720 | 721 | // take the keys from this pattern and figure out what the actual 722 | // pattern is all about 723 | keys = _keysFromString(combination); 724 | 725 | for (i = 0; i < keys.length; ++i) { 726 | key = keys[i]; 727 | 728 | // normalize key names 729 | if (_SPECIAL_ALIASES[key]) { 730 | key = _SPECIAL_ALIASES[key]; 731 | } 732 | 733 | // if this is not a keypress event then we should 734 | // be smart about using shift keys 735 | // this will only work for US keyboards however 736 | if (action && action != 'keypress' && _SHIFT_MAP[key]) { 737 | key = _SHIFT_MAP[key]; 738 | modifiers.push('shift'); 739 | } 740 | 741 | // if this key is a modifier then add it to the list of modifiers 742 | if (_isModifier(key)) { 743 | modifiers.push(key); 744 | } 745 | } 746 | 747 | // depending on what the key combination is 748 | // we will try to pick the best event for it 749 | action = _pickBestAction(key, modifiers, action); 750 | 751 | return { 752 | key: key, 753 | modifiers: modifiers, 754 | action: action 755 | }; 756 | } 757 | 758 | /** 759 | * binds a single keyboard combination 760 | * 761 | * @param {string} combination 762 | * @param {Function} callback 763 | * @param {string=} action 764 | * @param {string=} sequenceName - name of sequence if part of sequence 765 | * @param {number=} level - what part of the sequence the command is 766 | * @returns void 767 | */ 768 | function _bindSingle(combination, callback, action, sequenceName, level) { 769 | 770 | // store a direct mapped reference for use with Mousetrap.trigger 771 | _directMap[combination + ':' + action] = callback; 772 | 773 | // make sure multiple spaces in a row become a single space 774 | combination = combination.replace(/\s+/g, ' '); 775 | 776 | var sequence = combination.split(' '), 777 | info; 778 | 779 | // if this pattern is a sequence of keys then run through this method 780 | // to reprocess each pattern one key at a time 781 | if (sequence.length > 1) { 782 | _bindSequence(combination, sequence, callback, action); 783 | return; 784 | } 785 | 786 | info = _getKeyInfo(combination, action); 787 | 788 | // make sure to initialize array if this is the first time 789 | // a callback is added for this key 790 | _callbacks[info.key] = _callbacks[info.key] || []; 791 | 792 | // remove an existing match if there is one 793 | _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level); 794 | 795 | // add this call back to the array 796 | // if it is a sequence put it at the beginning 797 | // if not put it at the end 798 | // 799 | // this is important because the way these are processed expects 800 | // the sequence ones to come first 801 | _callbacks[info.key][sequenceName ? 'unshift' : 'push']({ 802 | callback: callback, 803 | modifiers: info.modifiers, 804 | action: info.action, 805 | seq: sequenceName, 806 | level: level, 807 | combo: combination 808 | }); 809 | } 810 | 811 | /** 812 | * binds multiple combinations to the same callback 813 | * 814 | * @param {Array} combinations 815 | * @param {Function} callback 816 | * @param {string|undefined} action 817 | * @returns void 818 | */ 819 | function _bindMultiple(combinations, callback, action) { 820 | for (var i = 0; i < combinations.length; ++i) { 821 | _bindSingle(combinations[i], callback, action); 822 | } 823 | } 824 | 825 | // start! 826 | _addEvent(document, 'keypress', _handleKeyEvent); 827 | _addEvent(document, 'keydown', _handleKeyEvent); 828 | _addEvent(document, 'keyup', _handleKeyEvent); 829 | 830 | var Mousetrap = { 831 | 832 | /** 833 | * binds an event to mousetrap 834 | * 835 | * can be a single key, a combination of keys separated with +, 836 | * an array of keys, or a sequence of keys separated by spaces 837 | * 838 | * be sure to list the modifier keys first to make sure that the 839 | * correct key ends up getting bound (the last key in the pattern) 840 | * 841 | * @param {string|Array} keys 842 | * @param {Function} callback 843 | * @param {string=} action - 'keypress', 'keydown', or 'keyup' 844 | * @returns void 845 | */ 846 | bind: function(keys, callback, action) { 847 | keys = keys instanceof Array ? keys : [keys]; 848 | _bindMultiple(keys, callback, action); 849 | return this; 850 | }, 851 | 852 | /** 853 | * unbinds an event to mousetrap 854 | * 855 | * the unbinding sets the callback function of the specified key combo 856 | * to an empty function and deletes the corresponding key in the 857 | * _directMap dict. 858 | * 859 | * TODO: actually remove this from the _callbacks dictionary instead 860 | * of binding an empty function 861 | * 862 | * the keycombo+action has to be exactly the same as 863 | * it was defined in the bind method 864 | * 865 | * @param {string|Array} keys 866 | * @param {string} action 867 | * @returns void 868 | */ 869 | unbind: function(keys, action) { 870 | return Mousetrap.bind(keys, function() {}, action); 871 | }, 872 | 873 | /** 874 | * triggers an event that has already been bound 875 | * 876 | * @param {string} keys 877 | * @param {string=} action 878 | * @returns void 879 | */ 880 | trigger: function(keys, action) { 881 | if (_directMap[keys + ':' + action]) { 882 | _directMap[keys + ':' + action]({}, keys); 883 | } 884 | return this; 885 | }, 886 | 887 | /** 888 | * resets the library back to its initial state. this is useful 889 | * if you want to clear out the current keyboard shortcuts and bind 890 | * new ones - for example if you switch to another page 891 | * 892 | * @returns void 893 | */ 894 | reset: function() { 895 | _callbacks = {}; 896 | _directMap = {}; 897 | return this; 898 | }, 899 | 900 | /** 901 | * should we stop this event before firing off callbacks 902 | * 903 | * @param {Event} e 904 | * @param {Element} element 905 | * @return {boolean} 906 | */ 907 | stopCallback: function(e, element) { 908 | 909 | // if the element has the class "mousetrap" then no need to stop 910 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 911 | return false; 912 | } 913 | 914 | // stop for input, select, and textarea 915 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true'); 916 | }, 917 | 918 | /** 919 | * exposes _handleKey publicly so it can be overwritten by extensions 920 | */ 921 | handleKey: _handleKey 922 | }; 923 | 924 | // expose mousetrap to the global object 925 | window.Mousetrap = Mousetrap; 926 | 927 | // expose mousetrap as an AMD module 928 | if (typeof define === 'function' && define.amd) { 929 | define(Mousetrap); 930 | } 931 | }) (); 932 | -------------------------------------------------------------------------------- /js/scissor.coffee: -------------------------------------------------------------------------------- 1 | class Scissor 2 | constructor: (@context) -> 3 | @tuna = new Tuna(@context) 4 | @output = @context.createGain() 5 | @delay = new @tuna.Delay(cutoff: 3000) 6 | @delay.connect @output 7 | 8 | @voices = [] 9 | @numSaws = 3 10 | @detune = 12 11 | 12 | noteOn: (note, time) -> 13 | return if @voices[note]? 14 | time ?= @context.currentTime 15 | freq = noteToFrequency note 16 | voice = new ScissorVoice(@context, freq, @numSaws, @detune) 17 | voice.connect @delay.input 18 | voice.start time 19 | @voices[note] = voice 20 | 21 | noteOff: (note, time) -> 22 | return unless @voices[note]? 23 | time ?= @context.currentTime 24 | @voices[note].stop time 25 | delete @voices[note] 26 | 27 | connect: (target) -> 28 | @output.connect target 29 | 30 | class ScissorVoice 31 | constructor: (@context, @frequency, @numSaws, @detune) -> 32 | @output = @context.createGain() 33 | @maxGain = 1 / @numSaws 34 | @attack = 0.001 35 | @decay = 0.015 36 | @release = 0.4 37 | @saws = [] 38 | for i in [0...@numSaws] 39 | saw = @context.createOscillator() 40 | saw.type = 'sawtooth' 41 | saw.frequency.value = @frequency 42 | saw.detune.value = -@detune + i * 2 * @detune / (@numSaws - 1) 43 | saw.start @context.currentTime 44 | saw.connect @output 45 | @saws.push saw 46 | 47 | start: (time) -> 48 | @output.gain.value = 0 49 | @output.gain.setValueAtTime 0, time 50 | @output.gain.setTargetAtTime @maxGain, time + @attack, @decay + 0.001 51 | 52 | stop: (time) -> 53 | @output.gain.cancelScheduledValues time 54 | @output.gain.setValueAtTime @output.gain.value, time 55 | @output.gain.setTargetAtTime 0, time, @release / 10 56 | @saws.forEach (saw) => 57 | saw.stop(time + @release) 58 | 59 | connect: (target) -> 60 | @output.connect target 61 | 62 | class VirtualKeyboard 63 | constructor: (@$el, params) -> 64 | @lowestNote = params.lowestNote ? 48 65 | @letters = params.letters ? "awsedftgyhujkolp;'".split '' 66 | @noteOn = params.noteOn ? (note) -> console.log "noteOn: #{note}" 67 | @noteOff = params.noteOff ? (note) -> console.log "noteOff: #{note}" 68 | @keysPressed = {} 69 | @render() 70 | @bindKeys() 71 | @bindMouse() 72 | 73 | _noteOn: (note) -> 74 | return if note of @keysPressed 75 | $(@$el.find('li').get(note - @lowestNote)).addClass 'active' 76 | @keysPressed[note] = true 77 | @noteOn note 78 | 79 | _noteOff: (note) -> 80 | return unless note of @keysPressed 81 | $(@$el.find('li').get(note - @lowestNote)).removeClass 'active' 82 | delete @keysPressed[note] 83 | @noteOff note 84 | 85 | bindKeys: -> 86 | for letter, i in @letters 87 | do (letter, i) => 88 | Mousetrap.bind letter, (=> 89 | @_noteOn (@lowestNote + i) 90 | ), 'keydown' 91 | Mousetrap.bind letter, (=> 92 | @_noteOff (@lowestNote + i) 93 | ), 'keyup' 94 | 95 | Mousetrap.bind 'z', => 96 | # shift one octave down 97 | @lowestNote -= 12 98 | 99 | Mousetrap.bind 'x', => 100 | # shift one octave up 101 | @lowestNote += 12 102 | 103 | bindMouse: -> 104 | @$el.find('li').each (i, key) => 105 | $(key).mousedown => 106 | @_noteOn (@lowestNote + i) 107 | $(key).mouseup => 108 | @_noteOff (@lowestNote + i) 109 | 110 | render: -> 111 | @$el.empty() 112 | $ul = $("
    ") 113 | for letter, i in @letters 114 | $key = $("
  • #{letter}
  • ") 115 | if i in [1, 3, 6, 8, 10, 13, 15] 116 | $key.addClass 'accidental' 117 | $ul.append $key 118 | @$el.append $ul 119 | 120 | noteToFrequency = (note) -> 121 | Math.pow(2, (note - 69) / 12) * 440.0 122 | 123 | $ -> 124 | audioContext = new (AudioContext ? webkitAudioContext) 125 | masterGain = audioContext.createGain() 126 | masterGain.gain.value = 0.7 127 | masterGain.connect audioContext.destination 128 | window.scissor = new Scissor(audioContext) 129 | scissor.connect masterGain 130 | 131 | keyboard = new VirtualKeyboard $("#keyboard"), 132 | noteOn: (note) -> 133 | scissor.noteOn note 134 | noteOff: (note) -> 135 | scissor.noteOff note 136 | 137 | setNumSaws = (numSaws) -> 138 | scissor.numSaws = numSaws 139 | 140 | setDetune = (detune) -> 141 | scissor.detune = detune 142 | 143 | sawsKnob = new Knob($("#saws")[0], new Ui.P2()) 144 | sawsKnob.onChange = (value) -> 145 | setNumSaws value 146 | $("#saws").val scissor.numSaws 147 | sawsKnob.changed 0 148 | 149 | detuneKnob = new Knob($("#detune")[0], new Ui.P2()) 150 | detuneKnob.onChange = (value) -> 151 | setDetune value 152 | $("#detune").val scissor.detune 153 | detuneKnob.changed 0 154 | -------------------------------------------------------------------------------- /js/scissor.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Scissor, ScissorVoice, VirtualKeyboard, noteToFrequency; 3 | 4 | Scissor = (function() { 5 | function Scissor(context) { 6 | this.context = context; 7 | this.tuna = new Tuna(this.context); 8 | this.output = this.context.createGain(); 9 | this.delay = new this.tuna.Delay({ 10 | cutoff: 3000 11 | }); 12 | this.delay.connect(this.output); 13 | this.voices = []; 14 | this.numSaws = 3; 15 | this.detune = 12; 16 | } 17 | 18 | Scissor.prototype.noteOn = function(note, time) { 19 | var freq, voice; 20 | if (this.voices[note] != null) { 21 | return; 22 | } 23 | if (time == null) { 24 | time = this.context.currentTime; 25 | } 26 | freq = noteToFrequency(note); 27 | voice = new ScissorVoice(this.context, freq, this.numSaws, this.detune); 28 | voice.connect(this.delay.input); 29 | voice.start(time); 30 | return this.voices[note] = voice; 31 | }; 32 | 33 | Scissor.prototype.noteOff = function(note, time) { 34 | if (this.voices[note] == null) { 35 | return; 36 | } 37 | if (time == null) { 38 | time = this.context.currentTime; 39 | } 40 | this.voices[note].stop(time); 41 | return delete this.voices[note]; 42 | }; 43 | 44 | Scissor.prototype.connect = function(target) { 45 | return this.output.connect(target); 46 | }; 47 | 48 | return Scissor; 49 | 50 | })(); 51 | 52 | ScissorVoice = (function() { 53 | function ScissorVoice(context, frequency, numSaws, detune) { 54 | var i, saw, _i, _ref; 55 | this.context = context; 56 | this.frequency = frequency; 57 | this.numSaws = numSaws; 58 | this.detune = detune; 59 | this.output = this.context.createGain(); 60 | this.maxGain = 1 / this.numSaws; 61 | this.attack = 0.001; 62 | this.decay = 0.015; 63 | this.release = 0.4; 64 | this.saws = []; 65 | for (i = _i = 0, _ref = this.numSaws; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { 66 | saw = this.context.createOscillator(); 67 | saw.type = 'sawtooth'; 68 | saw.frequency.value = this.frequency; 69 | saw.detune.value = -this.detune + i * 2 * this.detune / (this.numSaws - 1); 70 | saw.start(this.context.currentTime); 71 | saw.connect(this.output); 72 | this.saws.push(saw); 73 | } 74 | } 75 | 76 | ScissorVoice.prototype.start = function(time) { 77 | this.output.gain.value = 0; 78 | this.output.gain.setValueAtTime(0, time); 79 | return this.output.gain.setTargetAtTime(this.maxGain, time + this.attack, this.decay + 0.001); 80 | }; 81 | 82 | ScissorVoice.prototype.stop = function(time) { 83 | var _this = this; 84 | this.output.gain.cancelScheduledValues(time); 85 | this.output.gain.setValueAtTime(this.output.gain.value, time); 86 | this.output.gain.setTargetAtTime(0, time, this.release / 10); 87 | return this.saws.forEach(function(saw) { 88 | return saw.stop(time + _this.release); 89 | }); 90 | }; 91 | 92 | ScissorVoice.prototype.connect = function(target) { 93 | return this.output.connect(target); 94 | }; 95 | 96 | return ScissorVoice; 97 | 98 | })(); 99 | 100 | VirtualKeyboard = (function() { 101 | function VirtualKeyboard($el, params) { 102 | var _ref, _ref1, _ref2, _ref3; 103 | this.$el = $el; 104 | this.lowestNote = (_ref = params.lowestNote) != null ? _ref : 48; 105 | this.letters = (_ref1 = params.letters) != null ? _ref1 : "awsedftgyhujkolp;'".split(''); 106 | this.noteOn = (_ref2 = params.noteOn) != null ? _ref2 : function(note) { 107 | return console.log("noteOn: " + note); 108 | }; 109 | this.noteOff = (_ref3 = params.noteOff) != null ? _ref3 : function(note) { 110 | return console.log("noteOff: " + note); 111 | }; 112 | this.keysPressed = {}; 113 | this.render(); 114 | this.bindKeys(); 115 | this.bindMouse(); 116 | } 117 | 118 | VirtualKeyboard.prototype._noteOn = function(note) { 119 | if (note in this.keysPressed) { 120 | return; 121 | } 122 | $(this.$el.find('li').get(note - this.lowestNote)).addClass('active'); 123 | this.keysPressed[note] = true; 124 | return this.noteOn(note); 125 | }; 126 | 127 | VirtualKeyboard.prototype._noteOff = function(note) { 128 | if (!(note in this.keysPressed)) { 129 | return; 130 | } 131 | $(this.$el.find('li').get(note - this.lowestNote)).removeClass('active'); 132 | delete this.keysPressed[note]; 133 | return this.noteOff(note); 134 | }; 135 | 136 | VirtualKeyboard.prototype.bindKeys = function() { 137 | var i, letter, _fn, _i, _len, _ref, 138 | _this = this; 139 | _ref = this.letters; 140 | _fn = function(letter, i) { 141 | Mousetrap.bind(letter, (function() { 142 | return _this._noteOn(_this.lowestNote + i); 143 | }), 'keydown'); 144 | return Mousetrap.bind(letter, (function() { 145 | return _this._noteOff(_this.lowestNote + i); 146 | }), 'keyup'); 147 | }; 148 | for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { 149 | letter = _ref[i]; 150 | _fn(letter, i); 151 | } 152 | Mousetrap.bind('z', function() { 153 | return _this.lowestNote -= 12; 154 | }); 155 | return Mousetrap.bind('x', function() { 156 | return _this.lowestNote += 12; 157 | }); 158 | }; 159 | 160 | VirtualKeyboard.prototype.bindMouse = function() { 161 | var _this = this; 162 | return this.$el.find('li').each(function(i, key) { 163 | $(key).mousedown(function() { 164 | return _this._noteOn(_this.lowestNote + i); 165 | }); 166 | return $(key).mouseup(function() { 167 | return _this._noteOff(_this.lowestNote + i); 168 | }); 169 | }); 170 | }; 171 | 172 | VirtualKeyboard.prototype.render = function() { 173 | var $key, $ul, i, letter, _i, _len, _ref; 174 | this.$el.empty(); 175 | $ul = $("
      "); 176 | _ref = this.letters; 177 | for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { 178 | letter = _ref[i]; 179 | $key = $("
    • " + letter + "
    • "); 180 | if (i === 1 || i === 3 || i === 6 || i === 8 || i === 10 || i === 13 || i === 15) { 181 | $key.addClass('accidental'); 182 | } 183 | $ul.append($key); 184 | } 185 | return this.$el.append($ul); 186 | }; 187 | 188 | return VirtualKeyboard; 189 | 190 | })(); 191 | 192 | noteToFrequency = function(note) { 193 | return Math.pow(2, (note - 69) / 12) * 440.0; 194 | }; 195 | 196 | $(function() { 197 | var audioContext, detuneKnob, keyboard, masterGain, sawsKnob, setDetune, setNumSaws; 198 | audioContext = new (typeof AudioContext !== "undefined" && AudioContext !== null ? AudioContext : webkitAudioContext); 199 | masterGain = audioContext.createGain(); 200 | masterGain.gain.value = 0.7; 201 | masterGain.connect(audioContext.destination); 202 | window.scissor = new Scissor(audioContext); 203 | scissor.connect(masterGain); 204 | keyboard = new VirtualKeyboard($("#keyboard"), { 205 | noteOn: function(note) { 206 | return scissor.noteOn(note); 207 | }, 208 | noteOff: function(note) { 209 | return scissor.noteOff(note); 210 | } 211 | }); 212 | setNumSaws = function(numSaws) { 213 | return scissor.numSaws = numSaws; 214 | }; 215 | setDetune = function(detune) { 216 | return scissor.detune = detune; 217 | }; 218 | sawsKnob = new Knob($("#saws")[0], new Ui.P2()); 219 | sawsKnob.onChange = function(value) { 220 | return setNumSaws(value); 221 | }; 222 | $("#saws").val(scissor.numSaws); 223 | sawsKnob.changed(0); 224 | detuneKnob = new Knob($("#detune")[0], new Ui.P2()); 225 | detuneKnob.onChange = function(value) { 226 | return setDetune(value); 227 | }; 228 | $("#detune").val(scissor.detune); 229 | return detuneKnob.changed(0); 230 | }); 231 | 232 | }).call(this); 233 | -------------------------------------------------------------------------------- /js/tuna.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Copyright (c) 2012 DinahMoe AB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, 7 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 14 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 15 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | //Originally written by Alessandro Saccoia, Chris Coniglio and Oskar Eriksson 18 | (function (window) { 19 | var userContext, userInstance, Tuna = function (context) { 20 | if (! window.AudioContext) { 21 | window.AudioContext = window.webkitAudioContext; 22 | } 23 | 24 | if(!context) { 25 | console.log("tuna.js: Missing audio context! Creating a new context for you."); 26 | context = window.AudioContext && (new window.AudioContext()); 27 | } 28 | userContext = context; 29 | userInstance = this; 30 | }, 31 | version = "0.1", 32 | set = "setValueAtTime", 33 | linear = "linearRampToValueAtTime", 34 | pipe = function (param, val) { 35 | param.value = val; 36 | }, 37 | Super = Object.create(null, { 38 | activate: { 39 | writable: true, 40 | value: function (doActivate) { 41 | if(doActivate) { 42 | this.input.disconnect(); 43 | this.input.connect(this.activateNode); 44 | if(this.activateCallback) { 45 | this.activateCallback(doActivate); 46 | } 47 | } else { 48 | this.input.disconnect(); 49 | this.input.connect(this.output); 50 | } 51 | } 52 | }, 53 | bypass: { 54 | get: function () { 55 | return this._bypass; 56 | }, 57 | set: function (value) { 58 | if(this._lastBypassValue === value) { 59 | return; 60 | } 61 | this._bypass = value; 62 | this.activate(!value); 63 | this._lastBypassValue = value; 64 | } 65 | }, 66 | connect: { 67 | value: function (target) { 68 | this.output.connect(target); 69 | } 70 | }, 71 | disconnect: { 72 | value: function (target) { 73 | this.output.disconnect(target); 74 | } 75 | }, 76 | connectInOrder: { 77 | value: function (nodeArray) { 78 | var i = nodeArray.length - 1; 79 | while(i--) { 80 | if(!nodeArray[i].connect) { 81 | return console.error("AudioNode.connectInOrder: TypeError: Not an AudioNode.", nodeArray[i]); 82 | } 83 | if(nodeArray[i + 1].input) { 84 | nodeArray[i].connect(nodeArray[i + 1].input); 85 | } else { 86 | nodeArray[i].connect(nodeArray[i + 1]); 87 | } 88 | } 89 | } 90 | }, 91 | getDefaults: { 92 | value: function () { 93 | var result = {}; 94 | for(var key in this.defaults) { 95 | result[key] = this.defaults[key].value; 96 | } 97 | return result; 98 | } 99 | }, 100 | automate: { 101 | value: function (property, value, duration, startTime) { 102 | var start = startTime ? ~~ (startTime / 1000) : userContext.currentTime, 103 | dur = duration ? ~~ (duration / 1000) : 0, 104 | _is = this.defaults[property], 105 | param = this[property], 106 | method; 107 | 108 | if(param) { 109 | if(_is.automatable) { 110 | if(!duration) { 111 | method = set; 112 | } else { 113 | method = linear; 114 | param.cancelScheduledValues(start); 115 | param.setValueAtTime(param.value, start); 116 | } 117 | param[method](value, dur + start); 118 | } else { 119 | param = value; 120 | } 121 | } else { 122 | console.error("Invalid Property for " + this.name); 123 | } 124 | } 125 | } 126 | }), 127 | FLOAT = "float", 128 | BOOLEAN = "boolean", 129 | STRING = "string", 130 | INT = "int"; 131 | 132 | function dbToWAVolume(db) { 133 | return Math.max(0, Math.round(100 * Math.pow(2, db / 6)) / 100); 134 | } 135 | 136 | function fmod(x, y) { 137 | // http://kevin.vanzonneveld.net 138 | // + original by: Onno Marsman 139 | // + input by: Brett Zamir (http://brett-zamir.me) 140 | // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 141 | // * example 1: fmod(5.7, 1.3); 142 | // * returns 1: 0.5 143 | var tmp, tmp2, p = 0, 144 | pY = 0, 145 | l = 0.0, 146 | l2 = 0.0; 147 | 148 | tmp = x.toExponential().match(/^.\.?(.*)e(.+)$/); 149 | p = parseInt(tmp[2], 10) - (tmp[1] + '').length; 150 | tmp = y.toExponential().match(/^.\.?(.*)e(.+)$/); 151 | pY = parseInt(tmp[2], 10) - (tmp[1] + '').length; 152 | 153 | if(pY > p) { 154 | p = pY; 155 | } 156 | 157 | tmp2 = (x % y); 158 | 159 | if(p < -100 || p > 20) { 160 | // toFixed will give an out of bound error so we fix it like this: 161 | l = Math.round(Math.log(tmp2) / Math.log(10)); 162 | l2 = Math.pow(10, l); 163 | 164 | return(tmp2 / l2).toFixed(l - p) * l2; 165 | } else { 166 | return parseFloat(tmp2.toFixed(-p)); 167 | } 168 | } 169 | 170 | function sign(x) { 171 | if(x === 0) { 172 | return 1; 173 | } else { 174 | return Math.abs(x) / x; 175 | } 176 | } 177 | 178 | function tanh(n) { 179 | return(Math.exp(n) - Math.exp(-n)) / (Math.exp(n) + Math.exp(-n)); 180 | } 181 | Tuna.prototype.Filter = function (properties) { 182 | if(!properties) { 183 | properties = this.getDefaults(); 184 | } 185 | this.input = userContext.createGain(); 186 | this.activateNode = userContext.createGain(); 187 | this.filter = userContext.createBiquadFilter(); 188 | this.output = userContext.createGain(); 189 | 190 | this.activateNode.connect(this.filter); 191 | this.filter.connect(this.output); 192 | 193 | this.frequency = properties.frequency || this.defaults.frequency.value; 194 | this.Q = properties.resonance || this.defaults.Q.value; 195 | this.filterType = properties.filterType || this.defaults.filterType.value; 196 | this.gain = properties.gain || this.defaults.gain.value; 197 | this.bypass = properties.bypass || false; 198 | }; 199 | Tuna.prototype.Filter.prototype = Object.create(Super, { 200 | name: { 201 | value: "Filter" 202 | }, 203 | defaults: { 204 | writable:true, 205 | value: { 206 | frequency: { 207 | value: 800, 208 | min: 20, 209 | max: 22050, 210 | automatable: true, 211 | type: FLOAT 212 | }, 213 | Q: { 214 | value: 1, 215 | min: 0.001, 216 | max: 100, 217 | automatable: true, 218 | type: FLOAT 219 | }, 220 | gain: { 221 | value: 0, 222 | min: -40, 223 | max: 40, 224 | automatable: true, 225 | type: FLOAT 226 | }, 227 | bypass: { 228 | value: true, 229 | automatable: false, 230 | type: BOOLEAN 231 | }, 232 | filterType: { 233 | value: 1, 234 | min: 0, 235 | max: 7, 236 | automatable: false, 237 | type: INT 238 | } 239 | } 240 | }, 241 | filterType: { 242 | enumerable: true, 243 | get: function () { 244 | return this.filter.type; 245 | }, 246 | set: function (value) { 247 | this.filter.type = value; 248 | } 249 | }, 250 | Q: { 251 | enumerable: true, 252 | get: function () { 253 | return this.filter.Q; 254 | }, 255 | set: function (value) { 256 | this.filter.Q.value = value; 257 | } 258 | }, 259 | gain: { 260 | enumerable: true, 261 | get: function () { 262 | return this.filter.gain; 263 | }, 264 | set: function (value) { 265 | this.filter.gain.value = value; 266 | } 267 | }, 268 | frequency: { 269 | enumerable: true, 270 | get: function () { 271 | return this.filter.frequency; 272 | }, 273 | set: function (value) { 274 | this.filter.frequency.value = value; 275 | } 276 | } 277 | }); 278 | Tuna.prototype.Cabinet = function (properties) { 279 | if(!properties) { 280 | properties = this.getDefaults(); 281 | } 282 | this.input = userContext.createGain(); 283 | this.activateNode = userContext.createGain(); 284 | this.convolver = this.newConvolver(properties.impulsePath || "../impulses/impulse_guitar.wav"); 285 | this.makeupNode = userContext.createGain(); 286 | this.output = userContext.createGain(); 287 | 288 | this.activateNode.connect(this.convolver.input); 289 | this.convolver.output.connect(this.makeupNode); 290 | this.makeupNode.connect(this.output); 291 | 292 | this.makeupGain = properties.makeupGain || this.defaults.makeupGain; 293 | this.bypass = properties.bypass || false; 294 | }; 295 | Tuna.prototype.Cabinet.prototype = Object.create(Super, { 296 | name: { 297 | value: "Cabinet" 298 | }, 299 | defaults: { 300 | writable:true, 301 | value: { 302 | makeupGain: { 303 | value: 1, 304 | min: 0, 305 | max: 20, 306 | automatable: true, 307 | type: FLOAT 308 | }, 309 | bypass: { 310 | value: false, 311 | automatable: false, 312 | type: BOOLEAN 313 | } 314 | } 315 | }, 316 | makeupGain: { 317 | enumerable: true, 318 | get: function () { 319 | return this.makeupNode.gain; 320 | }, 321 | set: function (value) { 322 | this.makeupNode.gain.value = value; 323 | } 324 | }, 325 | newConvolver: { 326 | value: function (impulsePath) { 327 | return new userInstance.Convolver({ 328 | impulse: impulsePath, 329 | dryLevel: 0, 330 | wetLevel: 1 331 | }); 332 | } 333 | } 334 | }); 335 | Tuna.prototype.Chorus = function (properties) { 336 | if(!properties) { 337 | properties = this.getDefaults(); 338 | } 339 | this.input = userContext.createGain(); 340 | this.attenuator = this.activateNode = userContext.createGain(); 341 | this.splitter = userContext.createChannelSplitter(2); 342 | this.delayL = userContext.createDelay(); 343 | this.delayR = userContext.createDelay(); 344 | this.feedbackGainNodeLR = userContext.createGain(); 345 | this.feedbackGainNodeRL = userContext.createGain(); 346 | this.merger = userContext.createChannelMerger(2); 347 | this.output = userContext.createGain(); 348 | 349 | this.lfoL = new userInstance.LFO({ 350 | target: this.delayL.delayTime, 351 | callback: pipe 352 | }); 353 | this.lfoR = new userInstance.LFO({ 354 | target: this.delayR.delayTime, 355 | callback: pipe 356 | }); 357 | 358 | this.input.connect(this.attenuator); 359 | this.attenuator.connect(this.output); 360 | this.attenuator.connect(this.splitter); 361 | this.splitter.connect(this.delayL, 0); 362 | this.splitter.connect(this.delayR, 1); 363 | this.delayL.connect(this.feedbackGainNodeLR); 364 | this.delayR.connect(this.feedbackGainNodeRL); 365 | this.feedbackGainNodeLR.connect(this.delayR); 366 | this.feedbackGainNodeRL.connect(this.delayL); 367 | this.delayL.connect(this.merger, 0, 0); 368 | this.delayR.connect(this.merger, 0, 1); 369 | this.merger.connect(this.output); 370 | 371 | this.feedback = properties.feedback || this.defaults.feedback.value; 372 | this.rate = properties.rate || this.defaults.rate.value; 373 | this.delay = properties.delay || this.defaults.delay.value; 374 | this.depth = properties.depth || this.defaults.depth.value; 375 | this.lfoR.phase = Math.PI / 2; 376 | this.attenuator.gain.value = 0.6934; // 1 / (10 ^ (((20 * log10(3)) / 3) / 20)) 377 | this.lfoL.activate(true); 378 | this.lfoR.activate(true); 379 | this.bypass = properties.bypass || false; 380 | }; 381 | Tuna.prototype.Chorus.prototype = Object.create(Super, { 382 | name: { 383 | value: "Chorus" 384 | }, 385 | defaults: { 386 | writable:true, 387 | value: { 388 | feedback: { 389 | value: 0.4, 390 | min: 0, 391 | max: 0.95, 392 | automatable: false, 393 | type: FLOAT 394 | }, 395 | delay: { 396 | value: 0.0045, 397 | min: 0, 398 | max: 1, 399 | automatable: false, 400 | type: FLOAT 401 | }, 402 | depth: { 403 | value: 0.7, 404 | min: 0, 405 | max: 1, 406 | automatable: false, 407 | type: FLOAT 408 | }, 409 | rate: { 410 | value: 1.5, 411 | min: 0, 412 | max: 8, 413 | automatable: false, 414 | type: FLOAT 415 | }, 416 | bypass: { 417 | value: true, 418 | automatable: false, 419 | type: BOOLEAN 420 | } 421 | } 422 | }, 423 | delay: { 424 | enumerable: true, 425 | get: function () { 426 | return this._delay; 427 | }, 428 | set: function (value) { 429 | this._delay = 0.0002 * (Math.pow(10, value) * 2); 430 | this.lfoL.offset = this._delay; 431 | this.lfoR.offset = this._delay; 432 | this._depth = this._depth; 433 | } 434 | }, 435 | depth: { 436 | enumerable: true, 437 | get: function () { 438 | return this._depth; 439 | }, 440 | set: function (value) { 441 | this._depth = value; 442 | this.lfoL.oscillation = this._depth * this._delay; 443 | this.lfoR.oscillation = this._depth * this._delay; 444 | } 445 | }, 446 | feedback: { 447 | enumerable: true, 448 | get: function () { 449 | return this._feedback; 450 | }, 451 | set: function (value) { 452 | this._feedback = value; 453 | this.feedbackGainNodeLR.gain.value = this._feedback; 454 | this.feedbackGainNodeRL.gain.value = this._feedback; 455 | } 456 | }, 457 | rate: { 458 | enumerable: true, 459 | get: function () { 460 | return this._rate; 461 | }, 462 | set: function (value) { 463 | this._rate = value; 464 | this.lfoL.frequency = this._rate; 465 | this.lfoR.frequency = this._rate; 466 | } 467 | } 468 | }); 469 | Tuna.prototype.Compressor = function (properties) { 470 | if(!properties) { 471 | properties = this.getDefaults(); 472 | } 473 | this.input = userContext.createGain(); 474 | this.compNode = this.activateNode = userContext.createDynamicsCompressor(); 475 | this.makeupNode = userContext.createGain(); 476 | this.output = userContext.createGain(); 477 | 478 | this.compNode.connect(this.makeupNode); 479 | this.makeupNode.connect(this.output); 480 | 481 | this.automakeup = properties.automakeup || this.defaults.automakeup.value; 482 | this.makeupGain = properties.makeupGain || this.defaults.makeupGain.value; 483 | this.threshold = properties.threshold || this.defaults.threshold.value; 484 | this.release = properties.release || this.defaults.release.value; 485 | this.attack = properties.attack || this.defaults.attack.value; 486 | this.ratio = properties.ratio || this.defaults.ratio.value; 487 | this.knee = properties.knee || this.defaults.knee.value; 488 | this.bypass = properties.bypass || false; 489 | }; 490 | Tuna.prototype.Compressor.prototype = Object.create(Super, { 491 | name: { 492 | value: "Compressor" 493 | }, 494 | defaults: { 495 | writable:true, 496 | value: { 497 | threshold: { 498 | value: -20, 499 | min: -60, 500 | max: 0, 501 | automatable: true, 502 | type: FLOAT 503 | }, 504 | release: { 505 | value: 250, 506 | min: 10, 507 | max: 2000, 508 | automatable: true, 509 | type: FLOAT 510 | }, 511 | makeupGain: { 512 | value: 1, 513 | min: 1, 514 | max: 100, 515 | automatable: true, 516 | type: FLOAT 517 | }, 518 | attack: { 519 | value: 1, 520 | min: 0, 521 | max: 1000, 522 | automatable: true, 523 | type: FLOAT 524 | }, 525 | ratio: { 526 | value: 4, 527 | min: 1, 528 | max: 50, 529 | automatable: true, 530 | type: FLOAT 531 | }, 532 | knee: { 533 | value: 5, 534 | min: 0, 535 | max: 40, 536 | automatable: true, 537 | type: FLOAT 538 | }, 539 | automakeup: { 540 | value: false, 541 | automatable: false, 542 | type: BOOLEAN 543 | }, 544 | bypass: { 545 | value: true, 546 | automatable: false, 547 | type: BOOLEAN 548 | } 549 | } 550 | }, 551 | computeMakeup: { 552 | value: function () { 553 | var magicCoefficient = 4, 554 | // raise me if the output is too hot 555 | c = this.compNode; 556 | return -(c.threshold.value - c.threshold.value / c.ratio.value) / magicCoefficient; 557 | } 558 | }, 559 | automakeup: { 560 | enumerable: true, 561 | get: function () { 562 | return this._automakeup; 563 | }, 564 | set: function (value) { 565 | this._automakeup = value; 566 | if(this._automakeup) this.makeupGain = this.computeMakeup(); 567 | } 568 | }, 569 | threshold: { 570 | enumerable: true, 571 | get: function () { 572 | return this.compNode.threshold; 573 | }, 574 | set: function (value) { 575 | this.compNode.threshold.value = value; 576 | if(this._automakeup) this.makeupGain = this.computeMakeup(); 577 | } 578 | }, 579 | ratio: { 580 | enumerable: true, 581 | get: function () { 582 | return this.compNode.ratio; 583 | }, 584 | set: function (value) { 585 | this.compNode.ratio.value = value; 586 | if(this._automakeup) this.makeupGain = this.computeMakeup(); 587 | } 588 | }, 589 | knee: { 590 | enumerable: true, 591 | get: function () { 592 | return this.compNode.knee; 593 | }, 594 | set: function (value) { 595 | this.compNode.knee.value = value; 596 | if(this._automakeup) this.makeupGain = this.computeMakeup(); 597 | } 598 | }, 599 | attack: { 600 | enumerable: true, 601 | get: function () { 602 | return this.compNode.attack; 603 | }, 604 | set: function (value) { 605 | this.compNode.attack.value = value / 1000; 606 | } 607 | }, 608 | release: { 609 | enumerable: true, 610 | get: function () { 611 | return this.compNode.release; 612 | }, 613 | set: function (value) { 614 | this.compNode.release = value / 1000; 615 | } 616 | }, 617 | makeupGain: { 618 | enumerable: true, 619 | get: function () { 620 | return this.makeupNode.gain; 621 | }, 622 | set: function (value) { 623 | this.makeupNode.gain.value = dbToWAVolume(value); 624 | } 625 | } 626 | }); 627 | Tuna.prototype.Convolver = function (properties) { 628 | if(!properties) { 629 | properties = this.getDefaults(); 630 | } 631 | this.input = userContext.createGain(); 632 | this.activateNode = userContext.createGain(); 633 | this.convolver = userContext.createConvolver(); 634 | this.dry = userContext.createGain(); 635 | this.filterLow = userContext.createBiquadFilter(); 636 | this.filterHigh = userContext.createBiquadFilter(); 637 | this.wet = userContext.createGain(); 638 | this.output = userContext.createGain(); 639 | 640 | this.activateNode.connect(this.filterLow); 641 | this.activateNode.connect(this.dry); 642 | this.filterLow.connect(this.filterHigh); 643 | this.filterHigh.connect(this.convolver); 644 | this.convolver.connect(this.wet); 645 | this.wet.connect(this.output); 646 | this.dry.connect(this.output); 647 | 648 | this.dryLevel = properties.dryLevel || this.defaults.dryLevel.value; 649 | this.wetLevel = properties.wetLevel || this.defaults.wetLevel.value; 650 | this.highCut = properties.highCut || this.defaults.highCut.value; 651 | this.buffer = properties.impulse || "../impulses/ir_rev_short.wav"; 652 | this.lowCut = properties.lowCut || this.defaults.lowCut.value; 653 | this.level = properties.level || this.defaults.level.value; 654 | this.filterHigh.type = "lowpass"; 655 | this.filterLow.type = "highpass"; 656 | this.bypass = properties.bypass || false; 657 | }; 658 | Tuna.prototype.Convolver.prototype = Object.create(Super, { 659 | name: { 660 | value: "Convolver" 661 | }, 662 | defaults: { 663 | writable:true, 664 | value: { 665 | highCut: { 666 | value: 22050, 667 | min: 20, 668 | max: 22050, 669 | automatable: true, 670 | type: FLOAT 671 | }, 672 | lowCut: { 673 | value: 20, 674 | min: 20, 675 | max: 22050, 676 | automatable: true, 677 | type: FLOAT 678 | }, 679 | dryLevel: { 680 | value: 1, 681 | min: 0, 682 | max: 1, 683 | automatable: true, 684 | type: FLOAT 685 | }, 686 | wetLevel: { 687 | value: 1, 688 | min: 0, 689 | max: 1, 690 | automatable: true, 691 | type: FLOAT 692 | }, 693 | level: { 694 | value: 1, 695 | min: 0, 696 | max: 1, 697 | automatable: true, 698 | type: FLOAT 699 | } 700 | } 701 | }, 702 | lowCut: { 703 | get: function () { 704 | return this.filterLow.frequency; 705 | }, 706 | set: function (value) { 707 | this.filterLow.frequency.value = value; 708 | } 709 | }, 710 | highCut: { 711 | get: function () { 712 | return this.filterHigh.frequency; 713 | }, 714 | set: function (value) { 715 | this.filterHigh.frequency.value = value; 716 | } 717 | }, 718 | level: { 719 | get: function () { 720 | return this.output.gain; 721 | }, 722 | set: function (value) { 723 | this.output.gain.value = value; 724 | } 725 | }, 726 | dryLevel: { 727 | get: function () { 728 | return this.dry.gain 729 | }, 730 | set: function (value) { 731 | this.dry.gain.value = value; 732 | } 733 | }, 734 | wetLevel: { 735 | get: function () { 736 | return this.wet.gain; 737 | }, 738 | set: function (value) { 739 | this.wet.gain.value = value; 740 | } 741 | }, 742 | buffer: { 743 | enumerable: false, 744 | get: function () { 745 | return this.convolver.buffer; 746 | }, 747 | set: function (impulse) { 748 | var convolver = this.convolver, 749 | xhr = new XMLHttpRequest(); 750 | if(!impulse) { 751 | console.log("Tuna.Convolver.setBuffer: Missing impulse path!"); 752 | return; 753 | } 754 | xhr.open("GET", impulse, true); 755 | xhr.responseType = "arraybuffer"; 756 | xhr.onreadystatechange = function () { 757 | if(xhr.readyState === 4) { 758 | if(xhr.status < 300 && xhr.status > 199 || xhr.status === 302) { 759 | userContext.decodeAudioData(xhr.response, function (buffer) { 760 | convolver.buffer = buffer; 761 | }, function (e) { 762 | if(e) console.log("Tuna.Convolver.setBuffer: Error decoding data" + e); 763 | }); 764 | } 765 | } 766 | }; 767 | xhr.send(null); 768 | } 769 | } 770 | }); 771 | Tuna.prototype.Delay = function (properties) { 772 | if(!properties) { 773 | properties = this.getDefaults(); 774 | } 775 | this.input = userContext.createGain(); 776 | this.activateNode = userContext.createGain(); 777 | this.dry = userContext.createGain(); 778 | this.wet = userContext.createGain(); 779 | this.filter = userContext.createBiquadFilter(); 780 | this.delay = userContext.createDelay(); 781 | this.feedbackNode = userContext.createGain(); 782 | this.output = userContext.createGain(); 783 | 784 | this.activateNode.connect(this.delay); 785 | this.activateNode.connect(this.dry); 786 | this.delay.connect(this.filter); 787 | this.filter.connect(this.feedbackNode); 788 | this.feedbackNode.connect(this.delay); 789 | this.feedbackNode.connect(this.wet); 790 | this.wet.connect(this.output); 791 | this.dry.connect(this.output); 792 | 793 | this.delayTime = properties.delayTime || this.defaults.delayTime.value; 794 | this.feedback = properties.feedback || this.defaults.feedback.value; 795 | this.wetLevel = properties.wetLevel || this.defaults.wetLevel.value; 796 | this.dryLevel = properties.dryLevel || this.defaults.dryLevel.value; 797 | this.cutoff = properties.cutoff || this.defaults.cutoff.value; 798 | this.filter.type = "lowpass"; 799 | this.bypass = properties.bypass || false; 800 | }; 801 | Tuna.prototype.Delay.prototype = Object.create(Super, { 802 | name: { 803 | value: "Delay" 804 | }, 805 | defaults: { 806 | writable:true, 807 | value: { 808 | delayTime: { 809 | value: 100, 810 | min: 20, 811 | max: 1000, 812 | automatable: false, 813 | type: FLOAT 814 | }, 815 | feedback: { 816 | value: 0.45, 817 | min: 0, 818 | max: 0.9, 819 | automatable: true, 820 | type: FLOAT 821 | }, 822 | cutoff: { 823 | value: 20000, 824 | min: 20, 825 | max: 20000, 826 | automatable: true, 827 | type: FLOAT 828 | }, 829 | wetLevel: { 830 | value: 0.5, 831 | min: 0, 832 | max: 1, 833 | automatable: true, 834 | type: FLOAT 835 | }, 836 | dryLevel: { 837 | value: 1, 838 | min: 0, 839 | max: 1, 840 | automatable: true, 841 | type: FLOAT 842 | } 843 | } 844 | }, 845 | delayTime: { 846 | enumerable: true, 847 | get: function () { 848 | return this.delay.delayTime; 849 | }, 850 | set: function (value) { 851 | this.delay.delayTime.value = value / 1000; 852 | } 853 | }, 854 | wetLevel: { 855 | enumerable: true, 856 | get: function () { 857 | return this.wet.gain; 858 | }, 859 | set: function (value) { 860 | this.wet.gain.value = value; 861 | } 862 | }, 863 | dryLevel: { 864 | enumerable: true, 865 | get: function () { 866 | return this.dry.gain; 867 | }, 868 | set: function (value) { 869 | this.dry.gain.value = value; 870 | } 871 | }, 872 | feedback: { 873 | enumerable: true, 874 | get: function () { 875 | return this.feedbackNode.gain; 876 | }, 877 | set: function (value) { 878 | this.feedbackNode.gain.value = value; 879 | } 880 | }, 881 | cutoff: { 882 | enumerable: true, 883 | get: function () { 884 | return this.filter.frequency; 885 | }, 886 | set: function (value) { 887 | this.filter.frequency.value = value; 888 | } 889 | } 890 | }); 891 | Tuna.prototype.Overdrive = function (properties) { 892 | if(!properties) { 893 | properties = this.getDefaults(); 894 | } 895 | this.input = userContext.createGain(); 896 | this.activateNode = userContext.createGain(); 897 | this.inputDrive = userContext.createGain(); 898 | this.waveshaper = userContext.createWaveShaper(); 899 | this.outputDrive = userContext.createGain(); 900 | this.output = userContext.createGain(); 901 | 902 | this.activateNode.connect(this.inputDrive); 903 | this.inputDrive.connect(this.waveshaper); 904 | this.waveshaper.connect(this.outputDrive); 905 | this.outputDrive.connect(this.output); 906 | 907 | this.ws_table = new Float32Array(this.k_nSamples); 908 | this.drive = properties.drive || this.defaults.drive.value; 909 | this.outputGain = properties.outputGain || this.defaults.outputGain.value; 910 | this.curveAmount = properties.curveAmount || this.defaults.curveAmount.value; 911 | this.algorithmIndex = properties.algorithmIndex || this.defaults.algorithmIndex.value; 912 | this.bypass = properties.bypass || false; 913 | }; 914 | Tuna.prototype.Overdrive.prototype = Object.create(Super, { 915 | name: { 916 | value: "Overdrive" 917 | }, 918 | defaults: { 919 | writable:true, 920 | value: { 921 | drive: { 922 | value: 1, 923 | min: 0, 924 | max: 1, 925 | automatable: true, 926 | type: FLOAT, 927 | scaled: true 928 | }, 929 | outputGain: { 930 | value: 1, 931 | min: 0, 932 | max: 1, 933 | automatable: true, 934 | type: FLOAT, 935 | scaled: true 936 | }, 937 | curveAmount: { 938 | value: 0.725, 939 | min: 0, 940 | max: 1, 941 | automatable: false, 942 | type: FLOAT 943 | }, 944 | algorithmIndex: { 945 | value: 0, 946 | min: 0, 947 | max: 5, 948 | automatable: false, 949 | type: INT 950 | } 951 | } 952 | }, 953 | k_nSamples: { 954 | value: 8192 955 | }, 956 | drive: { 957 | get: function () { 958 | return this.inputDrive.gain; 959 | }, 960 | set: function (value) { 961 | this._drive = value; 962 | } 963 | }, 964 | curveAmount: { 965 | get: function () { 966 | return this._curveAmount; 967 | }, 968 | set: function (value) { 969 | this._curveAmount = value; 970 | if(this._algorithmIndex === undefined) { 971 | this._algorithmIndex = 0; 972 | } 973 | this.waveshaperAlgorithms[this._algorithmIndex](this._curveAmount, this.k_nSamples, this.ws_table); 974 | this.waveshaper.curve = this.ws_table; 975 | } 976 | }, 977 | outputGain: { 978 | get: function () { 979 | return this.outputDrive.gain; 980 | }, 981 | set: function (value) { 982 | this._outputGain = dbToWAVolume(value); 983 | } 984 | }, 985 | algorithmIndex: { 986 | get: function () { 987 | return this._algorithmIndex; 988 | }, 989 | set: function (value) { 990 | this._algorithmIndex = value; 991 | this.curveAmount = this._curveAmount; 992 | } 993 | }, 994 | waveshaperAlgorithms: { 995 | value: [ 996 | 997 | function (amount, n_samples, ws_table) { 998 | amount = Math.min(amount, 0.9999); 999 | var k = 2 * amount / (1 - amount), 1000 | i, x; 1001 | for(i = 0; i < n_samples; i++) { 1002 | x = i * 2 / n_samples - 1; 1003 | ws_table[i] = (1 + k) * x / (1 + k * Math.abs(x)); 1004 | } 1005 | }, function (amount, n_samples, ws_table) { 1006 | var i, x, y; 1007 | for(i = 0; i < n_samples; i++) { 1008 | x = i * 2 / n_samples - 1; 1009 | y = ((0.5 * Math.pow((x + 1.4), 2)) - 1) * y >= 0 ? 5.8 : 1.2; 1010 | ws_table[i] = tanh(y); 1011 | } 1012 | }, function (amount, n_samples, ws_table) { 1013 | var i, x, y, a = 1 - amount; 1014 | for(i = 0; i < n_samples; i++) { 1015 | x = i * 2 / n_samples - 1; 1016 | y = x < 0 ? -Math.pow(Math.abs(x), a + 0.04) : Math.pow(x, a); 1017 | ws_table[i] = tanh(y * 2); 1018 | } 1019 | }, function (amount, n_samples, ws_table) { 1020 | var i, x, y, abx, a = 1 - amount > 0.99 ? 0.99 : 1 - amount; 1021 | for(i = 0; i < n_samples; i++) { 1022 | x = i * 2 / n_samples - 1; 1023 | abx = Math.abs(x); 1024 | if(abx < a) y = abx; 1025 | else if(abx > a) y = a + (abx - a) / (1 + Math.pow((abx - a) / (1 - a), 2)); 1026 | else if(abx > 1) y = abx; 1027 | ws_table[i] = sign(x) * y * (1 / ((a + 1) / 2)); 1028 | } 1029 | }, function (amount, n_samples, ws_table) { // fixed curve, amount doesn't do anything, the distortion is just from the drive 1030 | var i, x; 1031 | for(i = 0; i < n_samples; i++) { 1032 | x = i * 2 / n_samples - 1; 1033 | if(x < -0.08905) { 1034 | ws_table[i] = (-3 / 4) * (1 - (Math.pow((1 - (Math.abs(x) - 0.032857)), 12)) + (1 / 3) * (Math.abs(x) - 0.032847)) + 0.01; 1035 | } else if(x >= -0.08905 && x < 0.320018) { 1036 | ws_table[i] = (-6.153 * (x * x)) + 3.9375 * x; 1037 | } else { 1038 | ws_table[i] = 0.630035; 1039 | } 1040 | } 1041 | }, function (amount, n_samples, ws_table) { 1042 | var a = 2 + Math.round(amount * 14), 1043 | // we go from 2 to 16 bits, keep in mind for the UI 1044 | bits = Math.round(Math.pow(2, a - 1)), 1045 | // real number of quantization steps divided by 2 1046 | i, x; 1047 | for(i = 0; i < n_samples; i++) { 1048 | x = i * 2 / n_samples - 1; 1049 | ws_table[i] = Math.round(x * bits) / bits; 1050 | } 1051 | }] 1052 | } 1053 | }); 1054 | Tuna.prototype.Phaser = function (properties) { 1055 | if(!properties) { 1056 | properties = this.getDefaults(); 1057 | } 1058 | this.input = userContext.createGain(); 1059 | this.splitter = this.activateNode = userContext.createChannelSplitter(2); 1060 | this.filtersL = []; 1061 | this.filtersR = []; 1062 | this.feedbackGainNodeL = userContext.createGain(); 1063 | this.feedbackGainNodeR = userContext.createGain(); 1064 | this.merger = userContext.createChannelMerger(2); 1065 | this.filteredSignal = userContext.createGain(); 1066 | this.output = userContext.createGain(); 1067 | this.lfoL = new userInstance.LFO({ 1068 | target: this.filtersL, 1069 | callback: this.callback 1070 | }); 1071 | this.lfoR = new userInstance.LFO({ 1072 | target: this.filtersR, 1073 | callback: this.callback 1074 | }); 1075 | 1076 | var i = this.stage; 1077 | while(i--) { 1078 | this.filtersL[i] = userContext.createBiquadFilter(); 1079 | this.filtersR[i] = userContext.createBiquadFilter(); 1080 | this.filtersL[i].type = "allpass"; 1081 | this.filtersR[i].type = "allpass"; 1082 | } 1083 | this.input.connect(this.splitter); 1084 | this.input.connect(this.output); 1085 | this.splitter.connect(this.filtersL[0], 0, 0); 1086 | this.splitter.connect(this.filtersR[0], 1, 0); 1087 | this.connectInOrder(this.filtersL); 1088 | this.connectInOrder(this.filtersR); 1089 | this.filtersL[this.stage - 1].connect(this.feedbackGainNodeL); 1090 | this.filtersL[this.stage - 1].connect(this.merger, 0, 0); 1091 | this.filtersR[this.stage - 1].connect(this.feedbackGainNodeR); 1092 | this.filtersR[this.stage - 1].connect(this.merger, 0, 1); 1093 | this.feedbackGainNodeL.connect(this.filtersL[0]); 1094 | this.feedbackGainNodeR.connect(this.filtersR[0]); 1095 | this.merger.connect(this.output); 1096 | 1097 | this.rate = properties.rate || this.defaults.rate.value; 1098 | this.baseModulationFrequency = properties.baseModulationFrequency || this.defaults.baseModulationFrequency.value; 1099 | this.depth = properties.depth || this.defaults.depth.value; 1100 | this.feedback = properties.feedback || this.defaults.feedback.value; 1101 | this.stereoPhase = properties.stereoPhase || this.defaults.stereoPhase.value; 1102 | 1103 | this.lfoL.activate(true); 1104 | this.lfoR.activate(true); 1105 | this.bypass = properties.bypass || false; 1106 | }; 1107 | Tuna.prototype.Phaser.prototype = Object.create(Super, { 1108 | name: { 1109 | value: "Phaser" 1110 | }, 1111 | stage: { 1112 | value: 4 1113 | }, 1114 | defaults: { 1115 | writable:true, 1116 | value: { 1117 | rate: { 1118 | value: 0.1, 1119 | min: 0, 1120 | max: 8, 1121 | automatable: false, 1122 | type: FLOAT 1123 | }, 1124 | depth: { 1125 | value: 0.6, 1126 | min: 0, 1127 | max: 1, 1128 | automatable: false, 1129 | type: FLOAT 1130 | }, 1131 | feedback: { 1132 | value: 0.7, 1133 | min: 0, 1134 | max: 1, 1135 | automatable: false, 1136 | type: FLOAT 1137 | }, 1138 | stereoPhase: { 1139 | value: 40, 1140 | min: 0, 1141 | max: 180, 1142 | automatable: false, 1143 | type: FLOAT 1144 | }, 1145 | baseModulationFrequency: { 1146 | value: 700, 1147 | min: 500, 1148 | max: 1500, 1149 | automatable: false, 1150 | type: FLOAT 1151 | } 1152 | } 1153 | }, 1154 | callback: { 1155 | value: function (filters, value) { 1156 | for(var stage = 0; stage < 4; stage++) { 1157 | filters[stage].frequency.value = value; 1158 | } 1159 | } 1160 | }, 1161 | depth: { 1162 | get: function () { 1163 | return this._depth; 1164 | }, 1165 | set: function (value) { 1166 | this._depth = value; 1167 | this.lfoL.oscillation = this._baseModulationFrequency * this._depth; 1168 | this.lfoR.oscillation = this._baseModulationFrequency * this._depth; 1169 | } 1170 | }, 1171 | rate: { 1172 | get: function () { 1173 | return this._rate; 1174 | }, 1175 | set: function (value) { 1176 | this._rate = value; 1177 | this.lfoL.frequency = this._rate; 1178 | this.lfoR.frequency = this._rate; 1179 | } 1180 | }, 1181 | baseModulationFrequency: { 1182 | enumerable: true, 1183 | get: function () { 1184 | return this._baseModulationFrequency; 1185 | }, 1186 | set: function (value) { 1187 | this._baseModulationFrequency = value; 1188 | this.lfoL.offset = this._baseModulationFrequency; 1189 | this.lfoR.offset = this._baseModulationFrequency; 1190 | this._depth = this._depth; 1191 | } 1192 | }, 1193 | feedback: { 1194 | get: function () { 1195 | return this._feedback; 1196 | }, 1197 | set: function (value) { 1198 | this._feedback = value; 1199 | this.feedbackGainNodeL.gain.value = this._feedback; 1200 | this.feedbackGainNodeR.gain.value = this._feedback; 1201 | } 1202 | }, 1203 | stereoPhase: { 1204 | get: function () { 1205 | return this._stereoPhase; 1206 | }, 1207 | set: function (value) { 1208 | this._stereoPhase = value; 1209 | var newPhase = this.lfoL._phase + this._stereoPhase * Math.PI / 180; 1210 | newPhase = fmod(newPhase, 2 * Math.PI); 1211 | this.lfoR._phase = newPhase; 1212 | } 1213 | } 1214 | }); 1215 | Tuna.prototype.Tremolo = function (properties) { 1216 | if(!properties) { 1217 | properties = this.getDefaults(); 1218 | } 1219 | this.input = userContext.createGain(); 1220 | this.splitter = this.activateNode = userContext.createChannelSplitter(2), this.amplitudeL = userContext.createGain(), this.amplitudeR = userContext.createGain(), this.merger = userContext.createChannelMerger(2), this.output = userContext.createGain(); 1221 | this.lfoL = new userInstance.LFO({ 1222 | target: this.amplitudeL.gain, 1223 | callback: pipe 1224 | }); 1225 | this.lfoR = new userInstance.LFO({ 1226 | target: this.amplitudeR.gain, 1227 | callback: pipe 1228 | }); 1229 | 1230 | this.input.connect(this.splitter); 1231 | this.splitter.connect(this.amplitudeL, 0); 1232 | this.splitter.connect(this.amplitudeR, 1); 1233 | this.amplitudeL.connect(this.merger, 0, 0); 1234 | this.amplitudeR.connect(this.merger, 0, 1); 1235 | this.merger.connect(this.output); 1236 | 1237 | this.rate = properties.rate || this.defaults.rate.value; 1238 | this.intensity = properties.intensity || this.defaults.intensity.value; 1239 | this.stereoPhase = properties.stereoPhase || this.defaults.stereoPhase.value; 1240 | 1241 | this.lfoL.offset = 1 - (this.intensity / 2); 1242 | this.lfoR.offset = 1 - (this.intensity / 2); 1243 | this.lfoL.phase = this.stereoPhase * Math.PI / 180; 1244 | 1245 | this.lfoL.activate(true); 1246 | this.lfoR.activate(true); 1247 | this.bypass = properties.bypass || false; 1248 | }; 1249 | Tuna.prototype.Tremolo.prototype = Object.create(Super, { 1250 | name: { 1251 | value: "Tremolo" 1252 | }, 1253 | defaults: { 1254 | writable:true, 1255 | value: { 1256 | intensity: { 1257 | value: 0.3, 1258 | min: 0, 1259 | max: 1, 1260 | automatable: false, 1261 | type: FLOAT 1262 | }, 1263 | stereoPhase: { 1264 | value: 0, 1265 | min: 0, 1266 | max: 180, 1267 | automatable: false, 1268 | type: FLOAT 1269 | }, 1270 | rate: { 1271 | value: 5, 1272 | min: 0.1, 1273 | max: 11, 1274 | automatable: false, 1275 | type: FLOAT 1276 | } 1277 | } 1278 | }, 1279 | intensity: { 1280 | enumerable: true, 1281 | get: function () { 1282 | return this._intensity; 1283 | }, 1284 | set: function (value) { 1285 | this._intensity = value; 1286 | this.lfoL.offset = 1 - this._intensity / 2; 1287 | this.lfoR.offset = 1 - this._intensity / 2; 1288 | this.lfoL.oscillation = this._intensity; 1289 | this.lfoR.oscillation = this._intensity; 1290 | } 1291 | }, 1292 | rate: { 1293 | enumerable: true, 1294 | get: function () { 1295 | return this._rate; 1296 | }, 1297 | set: function (value) { 1298 | this._rate = value; 1299 | this.lfoL.frequency = this._rate; 1300 | this.lfoR.frequency = this._rate; 1301 | } 1302 | }, 1303 | stereoPhase: { 1304 | enumerable: true, 1305 | get: function () { 1306 | return this._rate; 1307 | }, 1308 | set: function (value) { 1309 | this._stereoPhase = value; 1310 | var newPhase = this.lfoL._phase + this._stereoPhase * Math.PI / 180; 1311 | newPhase = fmod(newPhase, 2 * Math.PI); 1312 | this.lfoR.phase = newPhase; 1313 | } 1314 | } 1315 | }); 1316 | Tuna.prototype.WahWah = function (properties) { 1317 | if(!properties) { 1318 | properties = this.getDefaults(); 1319 | } 1320 | this.input = userContext.createGain(); 1321 | this.activateNode = userContext.createGain(); 1322 | this.envelopeFollower = new userInstance.EnvelopeFollower({ 1323 | target: this, 1324 | callback: function (context, value) { 1325 | context.sweep = value; 1326 | } 1327 | }); 1328 | this.filterBp = userContext.createBiquadFilter(); 1329 | this.filterPeaking = userContext.createBiquadFilter(); 1330 | this.output = userContext.createGain(); 1331 | 1332 | //Connect AudioNodes 1333 | this.activateNode.connect(this.filterBp); 1334 | this.filterBp.connect(this.filterPeaking); 1335 | this.filterPeaking.connect(this.output); 1336 | 1337 | //Set Properties 1338 | this.init(); 1339 | this.automode = properties.enableAutoMode || this.defaults.automode.value; 1340 | this.resonance = properties.resonance || this.defaults.resonance.value; 1341 | this.sensitivity = properties.sensitivity || this.defaults.sensitivity.value; 1342 | this.baseFrequency = properties.baseFrequency || this.defaults.baseFrequency.value; 1343 | this.excursionOctaves = properties.excursionOctaves || this.defaults.excursionOctaves.value; 1344 | this.sweep = properties.sweep || this.defaults.sweep.value; 1345 | 1346 | this.activateNode.gain.value = 2; 1347 | this.envelopeFollower.activate(true); 1348 | this.bypass = properties.bypass || false; 1349 | }; 1350 | Tuna.prototype.WahWah.prototype = Object.create(Super, { 1351 | name: { 1352 | value: "WahWah" 1353 | }, 1354 | defaults: { 1355 | writable:true, 1356 | value: { 1357 | automode: { 1358 | value: true, 1359 | automatable: false, 1360 | type: BOOLEAN 1361 | }, 1362 | baseFrequency: { 1363 | value: 0.5, 1364 | min: 0, 1365 | max: 1, 1366 | automatable: false, 1367 | type: FLOAT 1368 | }, 1369 | excursionOctaves: { 1370 | value: 2, 1371 | min: 1, 1372 | max: 6, 1373 | automatable: false, 1374 | type: FLOAT 1375 | }, 1376 | sweep: { 1377 | value: 0.2, 1378 | min: 0, 1379 | max: 1, 1380 | automatable: false, 1381 | type: FLOAT 1382 | }, 1383 | resonance: { 1384 | value: 10, 1385 | min: 1, 1386 | max: 100, 1387 | automatable: false, 1388 | type: FLOAT 1389 | }, 1390 | sensitivity: { 1391 | value: 0.5, 1392 | min: -1, 1393 | max: 1, 1394 | automatable: false, 1395 | type: FLOAT 1396 | } 1397 | } 1398 | }, 1399 | activateCallback: { 1400 | value: function (value) { 1401 | this.automode = value; 1402 | } 1403 | }, 1404 | automode: { 1405 | get: function () { 1406 | return this._automode; 1407 | }, 1408 | set: function (value) { 1409 | this._automode = value; 1410 | if(value) { 1411 | this.activateNode.connect(this.envelopeFollower.input); 1412 | this.envelopeFollower.activate(true); 1413 | } else { 1414 | this.envelopeFollower.activate(false); 1415 | this.activateNode.disconnect(); 1416 | this.activateNode.connect(this.filterBp); 1417 | } 1418 | } 1419 | }, 1420 | sweep: { 1421 | enumerable: true, 1422 | get: function () { 1423 | return this._sweep.value; 1424 | }, 1425 | set: function (value) { 1426 | this._sweep = Math.pow(value > 1 ? 1 : value < 0 ? 0 : value, this._sensitivity); 1427 | this.filterBp.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; 1428 | this.filterPeaking.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; 1429 | } 1430 | }, 1431 | baseFrequency: { 1432 | enumerable: true, 1433 | get: function () { 1434 | return this._baseFrequency; 1435 | }, 1436 | set: function (value) { 1437 | this._baseFrequency = 50 * Math.pow(10, value * 2); 1438 | this._excursionFrequency = Math.min(this.sampleRate / 2, this.baseFrequency * Math.pow(2, this._excursionOctaves)); 1439 | this.filterBp.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; 1440 | this.filterPeaking.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; 1441 | } 1442 | }, 1443 | excursionOctaves: { 1444 | enumerable: true, 1445 | get: function () { 1446 | return this._excursionOctaves; 1447 | }, 1448 | set: function (value) { 1449 | this._excursionOctaves = value; 1450 | this._excursionFrequency = Math.min(this.sampleRate / 2, this.baseFrequency * Math.pow(2, this._excursionOctaves)); 1451 | this.filterBp.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; 1452 | this.filterPeaking.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; 1453 | } 1454 | }, 1455 | sensitivity: { 1456 | enumerable: true, 1457 | get: function () { 1458 | return this._sensitivity; 1459 | }, 1460 | set: function (value) { 1461 | this._sensitivity = Math.pow(10, value); 1462 | } 1463 | }, 1464 | resonance: { 1465 | enumerable: true, 1466 | get: function () { 1467 | return this._resonance; 1468 | }, 1469 | set: function (value) { 1470 | this._resonance = value; 1471 | this.filterPeaking.Q = this._resonance; 1472 | } 1473 | }, 1474 | init: { 1475 | value: function () { 1476 | this.output.gain.value = 1; 1477 | this.filterPeaking.type = "peaking"; 1478 | this.filterBp.type = "bandpass"; 1479 | this.filterPeaking.frequency.value = 100; 1480 | this.filterPeaking.gain.value = 20; 1481 | this.filterPeaking.Q.value = 5; 1482 | this.filterBp.frequency.value = 100; 1483 | this.filterBp.Q.value = 1; 1484 | this.sampleRate = userContext.sampleRate; 1485 | } 1486 | } 1487 | }); 1488 | Tuna.prototype.EnvelopeFollower = function (properties) { 1489 | if(!properties) { 1490 | properties = this.getDefaults(); 1491 | } 1492 | this.input = userContext.createGain(); 1493 | this.jsNode = this.output = userContext.createScriptProcessor(this.buffersize, 1, 1); 1494 | 1495 | this.input.connect(this.output); 1496 | 1497 | this.attackTime = properties.attackTime || this.defaults.attackTime.value; 1498 | this.releaseTime = properties.releaseTime || this.defaults.releaseTime.value; 1499 | this._envelope = 0; 1500 | this.target = properties.target || {}; 1501 | this.callback = properties.callback || function () {}; 1502 | }; 1503 | Tuna.prototype.EnvelopeFollower.prototype = Object.create(Super, { 1504 | name: { 1505 | value: "EnvelopeFollower" 1506 | }, 1507 | defaults: { 1508 | value: { 1509 | attackTime: { 1510 | value: 0.003, 1511 | min: 0, 1512 | max: 0.5, 1513 | automatable: false, 1514 | type: FLOAT 1515 | }, 1516 | releaseTime: { 1517 | value: 0.5, 1518 | min: 0, 1519 | max: 0.5, 1520 | automatable: false, 1521 | type: FLOAT 1522 | } 1523 | } 1524 | }, 1525 | buffersize: { 1526 | value: 256 1527 | }, 1528 | envelope: { 1529 | value: 0 1530 | }, 1531 | sampleRate: { 1532 | value: 44100 1533 | }, 1534 | attackTime: { 1535 | enumerable: true, 1536 | get: function () { 1537 | return this._attackTime; 1538 | }, 1539 | set: function (value) { 1540 | this._attackTime = value; 1541 | this._attackC = Math.exp(-1 / this._attackTime * this.sampleRate / this.buffersize); 1542 | } 1543 | }, 1544 | releaseTime: { 1545 | enumerable: true, 1546 | get: function () { 1547 | return this._releaseTime; 1548 | }, 1549 | set: function (value) { 1550 | this._releaseTime = value; 1551 | this._releaseC = Math.exp(-1 / this._releaseTime * this.sampleRate / this.buffersize); 1552 | } 1553 | }, 1554 | callback: { 1555 | get: function () { 1556 | return this._callback; 1557 | }, 1558 | set: function (value) { 1559 | if(typeof value === "function") { 1560 | this._callback = value; 1561 | } else { 1562 | console.error("tuna.js: " + this.name + ": Callback must be a function!"); 1563 | } 1564 | } 1565 | }, 1566 | target: { 1567 | get: function () { 1568 | return this._target; 1569 | }, 1570 | set: function (value) { 1571 | this._target = value; 1572 | } 1573 | }, 1574 | activate: { 1575 | value: function (doActivate) { 1576 | this.activated = doActivate; 1577 | if(doActivate) { 1578 | this.jsNode.connect(userContext.destination); 1579 | this.jsNode.onaudioprocess = this.returnCompute(this); 1580 | } else { 1581 | this.jsNode.disconnect(); 1582 | this.jsNode.onaudioprocess = null; 1583 | } 1584 | } 1585 | }, 1586 | returnCompute: { 1587 | value: function (instance) { 1588 | return function (event) { 1589 | instance.compute(event); 1590 | }; 1591 | } 1592 | }, 1593 | compute: { 1594 | value: function (event) { 1595 | var count = event.inputBuffer.getChannelData(0).length, 1596 | channels = event.inputBuffer.numberOfChannels, 1597 | current, chan, rms, i; 1598 | chan = rms = i = 0; 1599 | if(channels > 1) { //need to mixdown 1600 | for(i = 0; i < count; ++i) { 1601 | for(; chan < channels; ++chan) { 1602 | current = event.inputBuffer.getChannelData(chan)[i]; 1603 | rms += (current * current) / channels; 1604 | } 1605 | } 1606 | } else { 1607 | for(i = 0; i < count; ++i) { 1608 | current = event.inputBuffer.getChannelData(0)[i]; 1609 | rms += (current * current); 1610 | } 1611 | } 1612 | rms = Math.sqrt(rms); 1613 | 1614 | if(this._envelope < rms) { 1615 | this._envelope *= this._attackC; 1616 | this._envelope += (1 - this._attackC) * rms; 1617 | } else { 1618 | this._envelope *= this._releaseC; 1619 | this._envelope += (1 - this._releaseC) * rms; 1620 | } 1621 | this._callback(this._target, this._envelope); 1622 | } 1623 | } 1624 | }); 1625 | Tuna.prototype.LFO = function (properties) { 1626 | //Instantiate AudioNode 1627 | this.output = userContext.createScriptProcessor(256, 1, 1); 1628 | this.activateNode = userContext.destination; 1629 | 1630 | //Set Properties 1631 | this.frequency = properties.frequency || this.defaults.frequency.value; 1632 | this.offset = properties.offset || this.defaults.offset.value; 1633 | this.oscillation = properties.oscillation || this.defaults.oscillation.value; 1634 | this.phase = properties.phase || this.defaults.phase.value; 1635 | this.target = properties.target || {}; 1636 | this.output.onaudioprocess = this.callback(properties.callback || 1637 | function () {}); 1638 | this.bypass = properties.bypass || false; 1639 | }; 1640 | Tuna.prototype.LFO.prototype = Object.create(Super, { 1641 | name: { 1642 | value: "LFO" 1643 | }, 1644 | bufferSize: { 1645 | value: 256 1646 | }, 1647 | sampleRate: { 1648 | value: 44100 1649 | }, 1650 | defaults: { 1651 | value: { 1652 | frequency: { 1653 | value: 1, 1654 | min: 0, 1655 | max: 20, 1656 | automatable: false, 1657 | type: FLOAT 1658 | }, 1659 | offset: { 1660 | value: 0.85, 1661 | min: 0, 1662 | max: 22049, 1663 | automatable: false, 1664 | type: FLOAT 1665 | }, 1666 | oscillation: { 1667 | value: 0.3, 1668 | min: -22050, 1669 | max: 22050, 1670 | automatable: false, 1671 | type: FLOAT 1672 | }, 1673 | phase: { 1674 | value: 0, 1675 | min: 0, 1676 | max: 2 * Math.PI, 1677 | automatable: false, 1678 | type: FLOAT 1679 | } 1680 | } 1681 | }, 1682 | frequency: { 1683 | get: function () { 1684 | return this._frequency; 1685 | }, 1686 | set: function (value) { 1687 | this._frequency = value; 1688 | this._phaseInc = 2 * Math.PI * this._frequency * this.bufferSize / this.sampleRate; 1689 | } 1690 | }, 1691 | offset: { 1692 | get: function () { 1693 | return this._offset; 1694 | }, 1695 | set: function (value) { 1696 | this._offset = value; 1697 | } 1698 | }, 1699 | oscillation: { 1700 | get: function () { 1701 | return this._oscillation; 1702 | }, 1703 | set: function (value) { 1704 | this._oscillation = value; 1705 | } 1706 | }, 1707 | phase: { 1708 | get: function () { 1709 | return this._phase; 1710 | }, 1711 | set: function (value) { 1712 | this._phase = value; 1713 | } 1714 | }, 1715 | target: { 1716 | get: function () { 1717 | return this._target; 1718 | }, 1719 | set: function (value) { 1720 | this._target = value; 1721 | } 1722 | }, 1723 | activate: { 1724 | value: function (doActivate) { 1725 | if(!doActivate) { 1726 | this.output.disconnect(userContext.destination); 1727 | } else { 1728 | this.output.connect(userContext.destination); 1729 | } 1730 | } 1731 | }, 1732 | callback: { 1733 | value: function (callback) { 1734 | var that = this; 1735 | return function () { 1736 | that._phase += that._phaseInc; 1737 | if(that._phase > 2 * Math.PI) { 1738 | that._phase = 0; 1739 | } 1740 | callback(that._target, that._offset + that._oscillation * Math.sin(that._phase)); 1741 | }; 1742 | } 1743 | } 1744 | }); 1745 | Tuna.toString = Tuna.prototype.toString = function () { 1746 | return "You are running Tuna version " + version + " by Dinahmoe!"; 1747 | }; 1748 | if(typeof define === "function") { 1749 | define("Tuna", [], function () { 1750 | return Tuna; 1751 | }); 1752 | } else { 1753 | window.Tuna = Tuna; 1754 | } 1755 | })(this); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scissor", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "grunt": "~0.4.0", 6 | "grunt-contrib-coffee": "~0.7.0", 7 | "grunt-contrib-less": "~0.7.0", 8 | "grunt-contrib-watch": "~0.3.0", 9 | "grunt-contrib-connect": "~0.3.0" 10 | } 11 | } 12 | --------------------------------------------------------------------------------