├── .gitignore ├── README.md ├── assets ├── audio │ ├── alkaloid │ │ ├── clap.wav │ │ ├── closedhat.wav │ │ ├── kick.wav │ │ ├── openhat.wav │ │ ├── snare.wav │ │ └── tom.wav │ ├── alphabetical │ │ ├── clap.wav │ │ ├── closedhat.wav │ │ ├── kick.wav │ │ ├── openhat.wav │ │ ├── snare.wav │ │ └── tom.wav │ └── glitch-baby │ │ ├── clap.wav │ │ ├── closedhat.wav │ │ ├── kick.wav │ │ ├── openhat.wav │ │ ├── snare.wav │ │ └── tom.wav ├── bpmn-font │ ├── css │ │ ├── bpmn-embedded.css │ │ └── bpmn.css │ └── font │ │ ├── bpmn.eot │ │ ├── bpmn.svg │ │ ├── bpmn.ttf │ │ └── bpmn.woff ├── css │ ├── diagram-js.css │ ├── hot-cues.css │ ├── node-sequencer.css │ └── tempo-control.css └── icons │ └── remove.svg ├── docs └── screenshot.png ├── index.js ├── package.json └── src ├── NodeSequencer.js ├── config.js ├── core ├── Audio.js ├── ExportConfig.js ├── LoadingOverlay.js ├── NodeSequencerElementFactory.js ├── NodeSequencerRenderer.js ├── Sounds.js ├── cmd │ ├── AddSequenceHandler.js │ ├── RemoveSequenceHandler.js │ └── UpdateSequenceHandler.js └── index.js ├── features ├── auto-connect │ ├── AutoConnect.js │ └── index.js ├── cropping │ ├── Cropping.js │ ├── NodeSequencerConnectionCropping.js │ └── index.js ├── emission-animation │ ├── EmissionAnimation.js │ └── index.js ├── emitter-animation │ ├── EmitterAnimation.js │ └── index.js ├── emitter-preview │ ├── EmitterPreview.js │ └── index.js ├── hot-cues │ ├── HotCues.js │ ├── ProgressIndicator.js │ └── index.js ├── keyboard-bindings │ ├── KeyboardBindings.js │ └── index.js ├── kit-select │ ├── KitSelect.js │ └── index.js ├── listener-animation │ ├── ListenerAnimation.js │ └── index.js ├── modeling │ ├── Modeling.js │ ├── NodeSequencerUpdater.js │ ├── cmd │ │ └── ChangePropertiesHandler.js │ └── index.js ├── move-preview │ ├── NodeSequencerMovePreview.js │ └── index.js ├── ordering │ ├── NodeSequencerOrderingProvider.js │ └── index.js ├── overridden │ ├── Outline.js │ └── index.js ├── palette │ ├── NodeSequencerPalette.js │ └── index.js ├── radial-menu │ ├── RadialMenu.js │ └── index.js ├── rules │ ├── NodeSequencerRules.js │ └── index.js ├── save-midi │ ├── SaveMidi.js │ └── index.js ├── sequences │ ├── Sequences.js │ └── index.js └── tempo-control │ ├── TempoControl.js │ └── index.js └── util ├── GeometryUtil.js ├── NodeSequencerUtil.js ├── SequenceUtil.js └── TweenUtil.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Sequencer 2 | 3 | A node-based sequencer for the web. 4 | 5 | ![Screenshot](docs/screenshot.png) 6 | 7 | ## License 8 | 9 | MIT 10 | -------------------------------------------------------------------------------- /assets/audio/alkaloid/clap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alkaloid/clap.wav -------------------------------------------------------------------------------- /assets/audio/alkaloid/closedhat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alkaloid/closedhat.wav -------------------------------------------------------------------------------- /assets/audio/alkaloid/kick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alkaloid/kick.wav -------------------------------------------------------------------------------- /assets/audio/alkaloid/openhat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alkaloid/openhat.wav -------------------------------------------------------------------------------- /assets/audio/alkaloid/snare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alkaloid/snare.wav -------------------------------------------------------------------------------- /assets/audio/alkaloid/tom.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alkaloid/tom.wav -------------------------------------------------------------------------------- /assets/audio/alphabetical/clap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alphabetical/clap.wav -------------------------------------------------------------------------------- /assets/audio/alphabetical/closedhat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alphabetical/closedhat.wav -------------------------------------------------------------------------------- /assets/audio/alphabetical/kick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alphabetical/kick.wav -------------------------------------------------------------------------------- /assets/audio/alphabetical/openhat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alphabetical/openhat.wav -------------------------------------------------------------------------------- /assets/audio/alphabetical/snare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alphabetical/snare.wav -------------------------------------------------------------------------------- /assets/audio/alphabetical/tom.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/alphabetical/tom.wav -------------------------------------------------------------------------------- /assets/audio/glitch-baby/clap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/glitch-baby/clap.wav -------------------------------------------------------------------------------- /assets/audio/glitch-baby/closedhat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/glitch-baby/closedhat.wav -------------------------------------------------------------------------------- /assets/audio/glitch-baby/kick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/glitch-baby/kick.wav -------------------------------------------------------------------------------- /assets/audio/glitch-baby/openhat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/glitch-baby/openhat.wav -------------------------------------------------------------------------------- /assets/audio/glitch-baby/snare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/glitch-baby/snare.wav -------------------------------------------------------------------------------- /assets/audio/glitch-baby/tom.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/audio/glitch-baby/tom.wav -------------------------------------------------------------------------------- /assets/bpmn-font/css/bpmn.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'bpmn'; 3 | src: url('../font/bpmn.eot?70672887'); 4 | src: url('../font/bpmn.eot?70672887#iefix') format('embedded-opentype'), 5 | url('../font/bpmn.woff?70672887') format('woff'), 6 | url('../font/bpmn.ttf?70672887') format('truetype'), 7 | url('../font/bpmn.svg?70672887#bpmn') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 12 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 13 | /* 14 | @media screen and (-webkit-min-device-pixel-ratio:0) { 15 | @font-face { 16 | font-family: 'bpmn'; 17 | src: url('../font/bpmn.svg?70672887#bpmn') format('svg'); 18 | } 19 | } 20 | */ 21 | 22 | [class^="bpmn-icon-"]:before, [class*=" bpmn-icon-"]:before { 23 | font-family: "bpmn"; 24 | font-style: normal; 25 | font-weight: normal; 26 | speak: none; 27 | 28 | display: inline-block; 29 | text-decoration: inherit; 30 | width: 1em; 31 | margin-right: .2em; 32 | text-align: center; 33 | /* opacity: .8; */ 34 | 35 | /* For safety - reset parent styles, that can break glyph codes*/ 36 | font-variant: normal; 37 | text-transform: none; 38 | 39 | /* fix buttons height, for twitter bootstrap */ 40 | line-height: 1em; 41 | 42 | /* Animation center compensation - margins should be symmetric */ 43 | /* remove if not needed */ 44 | margin-left: .2em; 45 | 46 | /* you can be more comfortable with increased icons size */ 47 | /* font-size: 120%; */ 48 | 49 | /* Font smoothing. That was taken from TWBS */ 50 | -webkit-font-smoothing: antialiased; 51 | -moz-osx-font-smoothing: grayscale; 52 | 53 | /* Uncomment for 3D effect */ 54 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 55 | } 56 | 57 | .bpmn-icon-screw-wrench:before { content: '\e800'; } /* '' */ 58 | .bpmn-icon-trash:before { content: '\e801'; } /* '' */ 59 | .bpmn-icon-conditional-flow:before { content: '\e802'; } /* '' */ 60 | .bpmn-icon-default-flow:before { content: '\e803'; } /* '' */ 61 | .bpmn-icon-gateway-parallel:before { content: '\e804'; } /* '' */ 62 | .bpmn-icon-intermediate-event-catch-cancel:before { content: '\e805'; } /* '' */ 63 | .bpmn-icon-intermediate-event-catch-non-interrupting-message:before { content: '\e806'; } /* '' */ 64 | .bpmn-icon-start-event-compensation:before { content: '\e807'; } /* '' */ 65 | .bpmn-icon-start-event-non-interrupting-parallel-multiple:before { content: '\e808'; } /* '' */ 66 | .bpmn-icon-loop-marker:before { content: '\e809'; } /* '' */ 67 | .bpmn-icon-parallel-mi-marker:before { content: '\e80a'; } /* '' */ 68 | .bpmn-icon-start-event-non-interrupting-signal:before { content: '\e80b'; } /* '' */ 69 | .bpmn-icon-intermediate-event-catch-non-interrupting-timer:before { content: '\e80c'; } /* '' */ 70 | .bpmn-icon-intermediate-event-catch-parallel-multiple:before { content: '\e80d'; } /* '' */ 71 | .bpmn-icon-intermediate-event-catch-compensation:before { content: '\e80e'; } /* '' */ 72 | .bpmn-icon-gateway-xor:before { content: '\e80f'; } /* '' */ 73 | .bpmn-icon-connection:before { content: '\e810'; } /* '' */ 74 | .bpmn-icon-end-event-cancel:before { content: '\e811'; } /* '' */ 75 | .bpmn-icon-intermediate-event-catch-condition:before { content: '\e812'; } /* '' */ 76 | .bpmn-icon-intermediate-event-catch-non-interrupting-parallel-multiple:before { content: '\e813'; } /* '' */ 77 | .bpmn-icon-start-event-condition:before { content: '\e814'; } /* '' */ 78 | .bpmn-icon-start-event-non-interrupting-timer:before { content: '\e815'; } /* '' */ 79 | .bpmn-icon-sequential-mi-marker:before { content: '\e816'; } /* '' */ 80 | .bpmn-icon-user-task:before { content: '\e817'; } /* '' */ 81 | .bpmn-icon-business-rule:before { content: '\e818'; } /* '' */ 82 | .bpmn-icon-sub-process-marker:before { content: '\e819'; } /* '' */ 83 | .bpmn-icon-start-event-parallel-multiple:before { content: '\e81a'; } /* '' */ 84 | .bpmn-icon-start-event-error:before { content: '\e81b'; } /* '' */ 85 | .bpmn-icon-intermediate-event-catch-signal:before { content: '\e81c'; } /* '' */ 86 | .bpmn-icon-intermediate-event-catch-error:before { content: '\e81d'; } /* '' */ 87 | .bpmn-icon-end-event-compensation:before { content: '\e81e'; } /* '' */ 88 | .bpmn-icon-subprocess-collapsed:before { content: '\e81f'; } /* '' */ 89 | .bpmn-icon-subprocess-expanded:before { content: '\e820'; } /* '' */ 90 | .bpmn-icon-task:before { content: '\e821'; } /* '' */ 91 | .bpmn-icon-end-event-error:before { content: '\e822'; } /* '' */ 92 | .bpmn-icon-intermediate-event-catch-escalation:before { content: '\e823'; } /* '' */ 93 | .bpmn-icon-intermediate-event-catch-timer:before { content: '\e824'; } /* '' */ 94 | .bpmn-icon-start-event-escalation:before { content: '\e825'; } /* '' */ 95 | .bpmn-icon-start-event-signal:before { content: '\e826'; } /* '' */ 96 | .bpmn-icon-business-rule-task:before { content: '\e827'; } /* '' */ 97 | .bpmn-icon-manual:before { content: '\e828'; } /* '' */ 98 | .bpmn-icon-receive:before { content: '\e829'; } /* '' */ 99 | .bpmn-icon-call-activity:before { content: '\e82a'; } /* '' */ 100 | .bpmn-icon-start-event-timer:before { content: '\e82b'; } /* '' */ 101 | .bpmn-icon-start-event-message:before { content: '\e82c'; } /* '' */ 102 | .bpmn-icon-intermediate-event-none:before { content: '\e82d'; } /* '' */ 103 | .bpmn-icon-intermediate-event-catch-link:before { content: '\e82e'; } /* '' */ 104 | .bpmn-icon-end-event-escalation:before { content: '\e82f'; } /* '' */ 105 | .bpmn-icon-text-annotation:before { content: '\e830'; } /* '' */ 106 | .bpmn-icon-bpmn-io:before { content: '\e831'; } /* '' */ 107 | .bpmn-icon-gateway-complex:before { content: '\e832'; } /* '' */ 108 | .bpmn-icon-gateway-eventbased:before { content: '\e833'; } /* '' */ 109 | .bpmn-icon-gateway-none:before { content: '\e834'; } /* '' */ 110 | .bpmn-icon-gateway-or:before { content: '\e835'; } /* '' */ 111 | .bpmn-icon-end-event-terminate:before { content: '\e836'; } /* '' */ 112 | .bpmn-icon-end-event-signal:before { content: '\e837'; } /* '' */ 113 | .bpmn-icon-end-event-none:before { content: '\e838'; } /* '' */ 114 | .bpmn-icon-end-event-multiple:before { content: '\e839'; } /* '' */ 115 | .bpmn-icon-end-event-message:before { content: '\e83a'; } /* '' */ 116 | .bpmn-icon-end-event-link:before { content: '\e83b'; } /* '' */ 117 | .bpmn-icon-intermediate-event-catch-message:before { content: '\e83c'; } /* '' */ 118 | .bpmn-icon-intermediate-event-throw-compensation:before { content: '\e83d'; } /* '' */ 119 | .bpmn-icon-start-event-multiple:before { content: '\e83e'; } /* '' */ 120 | .bpmn-icon-script:before { content: '\e83f'; } /* '' */ 121 | .bpmn-icon-manual-task:before { content: '\e840'; } /* '' */ 122 | .bpmn-icon-send:before { content: '\e841'; } /* '' */ 123 | .bpmn-icon-service:before { content: '\e842'; } /* '' */ 124 | .bpmn-icon-receive-task:before { content: '\e843'; } /* '' */ 125 | .bpmn-icon-user:before { content: '\e844'; } /* '' */ 126 | .bpmn-icon-start-event-none:before { content: '\e845'; } /* '' */ 127 | .bpmn-icon-intermediate-event-throw-escalation:before { content: '\e846'; } /* '' */ 128 | .bpmn-icon-intermediate-event-catch-multiple:before { content: '\e847'; } /* '' */ 129 | .bpmn-icon-intermediate-event-catch-non-interrupting-escalation:before { content: '\e848'; } /* '' */ 130 | .bpmn-icon-intermediate-event-throw-link:before { content: '\e849'; } /* '' */ 131 | .bpmn-icon-start-event-non-interrupting-condition:before { content: '\e84a'; } /* '' */ 132 | .bpmn-icon-data-object:before { content: '\e84b'; } /* '' */ 133 | .bpmn-icon-script-task:before { content: '\e84c'; } /* '' */ 134 | .bpmn-icon-send-task:before { content: '\e84d'; } /* '' */ 135 | .bpmn-icon-data-store:before { content: '\e84e'; } /* '' */ 136 | .bpmn-icon-start-event-non-interrupting-escalation:before { content: '\e84f'; } /* '' */ 137 | .bpmn-icon-intermediate-event-throw-message:before { content: '\e850'; } /* '' */ 138 | .bpmn-icon-intermediate-event-catch-non-interrupting-multiple:before { content: '\e851'; } /* '' */ 139 | .bpmn-icon-intermediate-event-catch-non-interrupting-signal:before { content: '\e852'; } /* '' */ 140 | .bpmn-icon-intermediate-event-throw-multiple:before { content: '\e853'; } /* '' */ 141 | .bpmn-icon-start-event-non-interrupting-message:before { content: '\e854'; } /* '' */ 142 | .bpmn-icon-ad-hoc-marker:before { content: '\e855'; } /* '' */ 143 | .bpmn-icon-service-task:before { content: '\e856'; } /* '' */ 144 | .bpmn-icon-task-none:before { content: '\e857'; } /* '' */ 145 | .bpmn-icon-compensation-marker:before { content: '\e858'; } /* '' */ 146 | .bpmn-icon-start-event-non-interrupting-multiple:before { content: '\e859'; } /* '' */ 147 | .bpmn-icon-intermediate-event-throw-signal:before { content: '\e85a'; } /* '' */ 148 | .bpmn-icon-intermediate-event-catch-non-interrupting-condition:before { content: '\e85b'; } /* '' */ 149 | .bpmn-icon-participant:before { content: '\e85c'; } /* '' */ 150 | .bpmn-icon-event-subprocess-expanded:before { content: '\e85d'; } /* '' */ 151 | .bpmn-icon-lane-insert-below:before { content: '\e85e'; } /* '' */ 152 | .bpmn-icon-space-tool:before { content: '\e85f'; } /* '' */ 153 | .bpmn-icon-connection-multi:before { content: '\e860'; } /* '' */ 154 | .bpmn-icon-lane:before { content: '\e861'; } /* '' */ 155 | .bpmn-icon-lasso-tool:before { content: '\e862'; } /* '' */ 156 | .bpmn-icon-lane-insert-above:before { content: '\e863'; } /* '' */ 157 | .bpmn-icon-lane-divide-three:before { content: '\e864'; } /* '' */ 158 | .bpmn-icon-lane-divide-two:before { content: '\e865'; } /* '' */ 159 | .bpmn-icon-data-input:before { content: '\e866'; } /* '' */ 160 | .bpmn-icon-data-output:before { content: '\e867'; } /* '' */ 161 | .bpmn-icon-hand-tool:before { content: '\e868'; } /* '' */ 162 | .bpmn-icon-transaction:before { content: '\e8c4'; } /* '' */ -------------------------------------------------------------------------------- /assets/bpmn-font/font/bpmn.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/bpmn-font/font/bpmn.eot -------------------------------------------------------------------------------- /assets/bpmn-font/font/bpmn.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/bpmn-font/font/bpmn.ttf -------------------------------------------------------------------------------- /assets/bpmn-font/font/bpmn.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/assets/bpmn-font/font/bpmn.woff -------------------------------------------------------------------------------- /assets/css/diagram-js.css: -------------------------------------------------------------------------------- 1 | /** 2 | * outline styles 3 | */ 4 | 5 | .djs-outline { 6 | fill: none; 7 | visibility: hidden; 8 | } 9 | 10 | .djs-element.hover .djs-outline, 11 | .djs-element.selected .djs-outline { 12 | visibility: visible; 13 | shape-rendering: crispEdges; 14 | stroke-dasharray: 3,3; 15 | } 16 | 17 | .djs-element.selected .djs-outline { 18 | stroke: #8888FF; 19 | stroke-width: 1px; 20 | } 21 | 22 | .djs-element.hover .djs-outline { 23 | stroke: #FF8888; 24 | stroke-width: 1px; 25 | } 26 | 27 | .djs-shape.connect-ok .djs-visual > :nth-child(1) { 28 | fill: #DCFECC /* light-green */ !important; 29 | } 30 | 31 | .djs-shape.connect-not-ok .djs-visual > :nth-child(1), 32 | .djs-shape.drop-not-ok .djs-visual > :nth-child(1) { 33 | fill: #f9dee5 /* light-red */ !important; 34 | } 35 | 36 | .djs-shape.new-parent .djs-visual > :nth-child(1) { 37 | fill: #F7F9FF !important; 38 | } 39 | 40 | svg.drop-not-ok { 41 | background: #f9dee5 /* light-red */ !important; 42 | } 43 | 44 | svg.new-parent { 45 | background: #F7F9FF /* light-blue */ !important; 46 | } 47 | 48 | .djs-connection.connect-ok .djs-visual > :nth-child(1), 49 | .djs-connection.drop-ok .djs-visual > :nth-child(1) { 50 | stroke: #90DD5F /* light-green */ !important; 51 | } 52 | 53 | .djs-connection.connect-not-ok .djs-visual > :nth-child(1), 54 | .djs-connection.drop-not-ok .djs-visual > :nth-child(1) { 55 | stroke: #E56283 /* light-red */ !important; 56 | } 57 | 58 | .drop-not-ok, 59 | .connect-not-ok { 60 | cursor: not-allowed; 61 | } 62 | 63 | .djs-element.attach-ok .djs-visual > :nth-child(1) { 64 | stroke-width: 5px !important; 65 | stroke: rgba(255, 116, 0, 0.7) !important; 66 | } 67 | 68 | 69 | /** 70 | * Selection box style 71 | * 72 | */ 73 | .djs-lasso-overlay { 74 | fill: rgb(255, 116, 0); 75 | fill-opacity: 0.1; 76 | 77 | stroke-dasharray: 5 1 3 1; 78 | stroke: rgb(255, 116, 0); 79 | 80 | shape-rendering: crispEdges; 81 | pointer-events: none; 82 | } 83 | 84 | /** 85 | * Resize styles 86 | */ 87 | .djs-resize-overlay { 88 | fill: none; 89 | 90 | stroke-dasharray: 5 1 3 1; 91 | stroke: rgb(255, 116, 0); 92 | 93 | pointer-events: none; 94 | } 95 | 96 | .djs-resizer-hit { 97 | fill: none; 98 | pointer-events: all; 99 | } 100 | 101 | .djs-resizer-visual { 102 | fill: white; 103 | stroke-width: 1px; 104 | stroke: black; 105 | shape-rendering: crispEdges; 106 | stroke-opacity: 0.2; 107 | } 108 | 109 | .djs-cursor-resize-nwse, 110 | .djs-resizer-nw, 111 | .djs-resizer-se { 112 | cursor: nwse-resize; 113 | } 114 | 115 | .djs-cursor-resize-nesw, 116 | .djs-resizer-ne, 117 | .djs-resizer-sw { 118 | cursor: nesw-resize; 119 | } 120 | 121 | .djs-shape.djs-resizing > .djs-outline { 122 | visibility: hidden !important; 123 | } 124 | 125 | .djs-shape.djs-resizing > .djs-resizer { 126 | visibility: hidden; 127 | } 128 | 129 | .djs-dragger > .djs-resizer { 130 | visibility: hidden; 131 | } 132 | 133 | /** 134 | * drag styles 135 | */ 136 | /* .djs-dragger .djs-visual circle, 137 | .djs-dragger .djs-visual path, 138 | .djs-dragger .djs-visual polygon, 139 | .djs-dragger .djs-visual polyline, 140 | .djs-dragger .djs-visual rect, 141 | .djs-dragger .djs-visual text { 142 | fill: none !important; 143 | stroke: red !important; 144 | } */ 145 | 146 | .djs-dragging { 147 | opacity: 0.3; 148 | } 149 | 150 | .djs-dragging, 151 | .djs-dragging > * { 152 | pointer-events: none !important; 153 | } 154 | 155 | .djs-dragging .djs-context-pad, 156 | .djs-dragging .djs-outline { 157 | display: none !important; 158 | } 159 | 160 | /** 161 | * no pointer events for visual 162 | */ 163 | .djs-visual, 164 | .djs-outline { 165 | pointer-events: none; 166 | } 167 | 168 | /** 169 | * all pointer events for hit shape 170 | */ 171 | .djs-shape .djs-hit { 172 | pointer-events: all; 173 | } 174 | 175 | .djs-connection .djs-hit { 176 | pointer-events: stroke; 177 | } 178 | 179 | /** 180 | * shape / connection basic styles 181 | */ 182 | .djs-connection .djs-visual { 183 | stroke-width: 2px; 184 | fill: none; 185 | } 186 | 187 | .djs-cursor-grab { 188 | cursor: -webkit-grab; 189 | cursor: -moz-grab; 190 | cursor: grab; 191 | } 192 | 193 | .djs-cursor-grabbing { 194 | cursor: -webkit-grabbing; 195 | cursor: -moz-grabbing; 196 | cursor: grabbing; 197 | } 198 | 199 | .djs-cursor-crosshair { 200 | cursor: crosshair; 201 | } 202 | 203 | .djs-cursor-move { 204 | cursor: move; 205 | } 206 | 207 | .djs-cursor-resize-ns { 208 | cursor: ns-resize; 209 | } 210 | 211 | .djs-cursor-resize-ew { 212 | cursor: ew-resize; 213 | } 214 | 215 | 216 | /** 217 | * snapping 218 | */ 219 | .djs-snap-line { 220 | stroke: rgb(255, 195, 66); 221 | stroke: rgba(255, 195, 66, 0.50); 222 | stroke-linecap: round; 223 | stroke-width: 2px; 224 | pointer-events: none; 225 | } 226 | 227 | /** 228 | * snapping 229 | */ 230 | .djs-crosshair { 231 | stroke: #555; 232 | stroke-linecap: round; 233 | stroke-width: 1px; 234 | pointer-events: none; 235 | shape-rendering: crispEdges; 236 | stroke-dasharray: 5, 5; 237 | } 238 | 239 | /** 240 | * palette 241 | */ 242 | 243 | .djs-palette { 244 | position: absolute; 245 | left: 20px; 246 | top: 20px; 247 | 248 | width: 46px; 249 | } 250 | 251 | .djs-container.two-column .djs-palette.open { 252 | width: 94px; 253 | } 254 | 255 | .djs-palette .separator { 256 | margin: 3px 5px 5px 5px; 257 | border: none; 258 | border-top: solid 1px #DDD; 259 | 260 | clear: both; 261 | } 262 | 263 | .djs-palette .entry:before { 264 | vertical-align: middle; 265 | } 266 | 267 | .djs-palette .djs-palette-toggle { 268 | cursor: pointer; 269 | } 270 | 271 | .djs-palette .entry, 272 | .djs-palette .djs-palette-toggle { 273 | color: green; 274 | font-size: 30px; 275 | 276 | text-align: center; 277 | } 278 | 279 | .djs-palette .entry { 280 | float: left; 281 | } 282 | 283 | .djs-palette .entry img { 284 | max-width: 100%; 285 | } 286 | 287 | .djs-palette.open .djs-palette-toggle { 288 | height: 10px; 289 | } 290 | 291 | .djs-palette .djs-palette-entries:after { 292 | content: ''; 293 | display: table; 294 | clear: both; 295 | } 296 | 297 | .djs-palette:not(.open) .djs-palette-entries { 298 | display: none; 299 | } 300 | 301 | .djs-palette .djs-palette-toggle:hover { 302 | background: #666; 303 | } 304 | 305 | .djs-palette .entry:hover { 306 | color: rgb(255, 116, 0); 307 | } 308 | 309 | .highlighted-entry { 310 | color: rgb(255, 116, 0) !important; 311 | } 312 | 313 | .djs-palette:not(.open) { 314 | overflow: hidden; 315 | } 316 | 317 | .djs-palette .entry, 318 | .djs-palette .djs-palette-toggle { 319 | width: 46px; 320 | height: 46px; 321 | line-height: 46px; 322 | cursor: default; 323 | } 324 | 325 | .djs-palette.open .djs-palette-toggle { 326 | width: 100%; 327 | } 328 | 329 | /** 330 | * context-pad 331 | */ 332 | .djs-overlay-context-pad { 333 | width: 72px; 334 | } 335 | 336 | .djs-context-pad { 337 | position: absolute; 338 | display: none; 339 | pointer-events: none; 340 | } 341 | 342 | .djs-context-pad .entry { 343 | width: 22px; 344 | height: 22px; 345 | text-align: center; 346 | display: inline-block; 347 | font-size: 22px; 348 | margin: 0 2px 2px 0; 349 | 350 | border-radius: 3px; 351 | 352 | cursor: default; 353 | 354 | background-color: #FEFEFE; 355 | box-shadow: 0 0 2px 1px #FEFEFE; 356 | 357 | pointer-events: all; 358 | } 359 | 360 | .djs-context-pad .entry:before { 361 | vertical-align: top; 362 | } 363 | 364 | .djs-context-pad .entry:hover { 365 | background: rgb(255, 252, 176); 366 | } 367 | 368 | .djs-context-pad.open { 369 | display: block; 370 | } 371 | 372 | /** 373 | * popup styles 374 | */ 375 | .djs-popup .entry { 376 | line-height: 20px; 377 | white-space: nowrap; 378 | border: solid 1px transparent; 379 | cursor: default; 380 | } 381 | 382 | /* larger font for prefixed icons */ 383 | .djs-popup .entry:before { 384 | vertical-align: middle; 385 | font-size: 20px; 386 | } 387 | 388 | .djs-popup .entry > span { 389 | vertical-align: middle; 390 | font-size: 14px; 391 | } 392 | 393 | .djs-popup .entry:hover, 394 | .djs-popup .entry.active:hover { 395 | background: rgb(255, 252, 176); 396 | } 397 | 398 | .djs-popup .entry.disabled { 399 | background: inherit; 400 | } 401 | 402 | .djs-popup .entry.active { 403 | color: rgb(255, 116, 0); 404 | border: solid 1px rgb(255, 116, 0); 405 | border-radius: 3px; 406 | background-color: #F6F6F6; 407 | } 408 | 409 | .djs-popup-body .entry { 410 | padding: 2px 10px 2px 5px; 411 | } 412 | 413 | .djs-popup-header .entry { 414 | display: inline-block; 415 | padding: 2px 3px 2px 3px; 416 | } 417 | 418 | .djs-popup-body .entry > span { 419 | margin-left: 5px; 420 | } 421 | 422 | .djs-popup-body { 423 | background-color: #FEFEFE; 424 | } 425 | 426 | .djs-popup-header { 427 | border-bottom: 1px solid #DDD; 428 | } 429 | 430 | .djs-popup-header .entry { 431 | margin: 1px; 432 | margin-left: 3px; 433 | } 434 | 435 | .djs-popup-header .entry:last-child { 436 | margin-right: 3px; 437 | } 438 | 439 | /** 440 | * popup / palette styles 441 | */ 442 | .djs-popup, .djs-palette { 443 | background: #FAFAFA; 444 | border: solid 1px #CCC; 445 | border-radius: 2px; 446 | box-shadow: 0 1px 2px rgba(0,0,0,0.3); 447 | } 448 | 449 | /** 450 | * touch 451 | */ 452 | 453 | .djs-shape, 454 | .djs-connection { 455 | touch-action: none; 456 | } 457 | 458 | .djs-segment-dragger, 459 | .djs-bendpoint { 460 | display: none; 461 | } 462 | 463 | /** 464 | * bendpoints 465 | */ 466 | .djs-segment-dragger .djs-visual { 467 | fill: rgba(255, 255, 121, 0.2); 468 | stroke-width: 1px; 469 | stroke-opacity: 1; 470 | stroke: rgba(255, 255, 121, 0.3); 471 | } 472 | 473 | .djs-bendpoint .djs-visual { 474 | fill: rgba(255, 255, 121, 0.8); 475 | stroke-width: 1px; 476 | stroke-opacity: 0.5; 477 | stroke: black; 478 | } 479 | 480 | .djs-segment-dragger:hover, 481 | .djs-bendpoints.hover .djs-segment-dragger, 482 | .djs-bendpoints.selected .djs-segment-dragger, 483 | .djs-bendpoint:hover, 484 | .djs-bendpoints.hover .djs-bendpoint, 485 | .djs-bendpoints.selected .djs-bendpoint { 486 | display: block; 487 | } 488 | 489 | .djs-drag-active .djs-bendpoints * { 490 | display: none; 491 | } 492 | 493 | .djs-bendpoints:not(.hover) .floating { 494 | display: none; 495 | } 496 | 497 | .djs-segment-dragger:hover .djs-visual, 498 | .djs-segment-dragger.djs-dragging .djs-visual, 499 | .djs-bendpoint:hover .djs-visual, 500 | .djs-bendpoint.floating .djs-visual { 501 | fill: yellow; 502 | stroke-opacity: 0.5; 503 | stroke: black; 504 | } 505 | 506 | .djs-bendpoint.floating .djs-hit { 507 | pointer-events: none; 508 | } 509 | 510 | .djs-segment-dragger .djs-hit, 511 | .djs-bendpoint .djs-hit { 512 | pointer-events: all; 513 | fill: none; 514 | } 515 | 516 | .djs-segment-dragger.horizontal .djs-hit { 517 | cursor: ns-resize; 518 | } 519 | 520 | .djs-segment-dragger.vertical .djs-hit { 521 | cursor: ew-resize; 522 | } 523 | 524 | .djs-segment-dragger.djs-dragging .djs-hit { 525 | pointer-events: none; 526 | } 527 | 528 | .djs-updating, 529 | .djs-updating > * { 530 | pointer-events: none !important; 531 | } 532 | 533 | .djs-updating .djs-context-pad, 534 | .djs-updating .djs-outline, 535 | .djs-updating .djs-bendpoint, 536 | .connect-ok .djs-bendpoint, 537 | .connect-not-ok .djs-bendpoint, 538 | .drop-ok .djs-bendpoint, 539 | .drop-not-ok .djs-bendpoint { 540 | display: none !important; 541 | } 542 | 543 | .djs-segment-dragger.djs-dragging, 544 | .djs-bendpoint.djs-dragging { 545 | display: block; 546 | opacity: 1.0; 547 | } 548 | 549 | .djs-segment-dragger.djs-dragging .djs-visual, 550 | .djs-bendpoint.djs-dragging .djs-visual { 551 | fill: yellow; 552 | stroke-opacity: 0.5; 553 | } 554 | 555 | 556 | /** 557 | * tooltips 558 | */ 559 | .djs-tooltip-error { 560 | font-size: 11px; 561 | line-height: 18px; 562 | text-align: left; 563 | 564 | padding: 5px; 565 | 566 | opacity: 0.7; 567 | } 568 | 569 | .djs-tooltip-error > * { 570 | width: 160px; 571 | 572 | background: rgb(252, 236, 240); 573 | color: rgb(158, 76, 76); 574 | padding: 3px 7px; 575 | box-shadow: 0 1px 2px rgba(0,0,0, 0.2); 576 | border-radius: 5px; 577 | border-left: solid 5px rgb(174, 73, 73); 578 | } 579 | 580 | .djs-tooltip-error:hover { 581 | opacity: 1; 582 | } 583 | 584 | 585 | /** 586 | * search pad 587 | */ 588 | .djs-search-container { 589 | position: absolute; 590 | top: 20px; 591 | left: 0; 592 | right: 0; 593 | margin-left: auto; 594 | margin-right: auto; 595 | 596 | width: 25%; 597 | min-width: 300px; 598 | max-width: 400px; 599 | z-index: 10; 600 | 601 | font-size: 1.05em; 602 | opacity: 0.9; 603 | background: #FAFAFA; 604 | border: solid 1px #CCC; 605 | border-radius: 2px; 606 | box-shadow: 0 1px 2px rgba(0,0,0,0.3); 607 | } 608 | 609 | .djs-search-container:not(.open) { 610 | display: none; 611 | } 612 | 613 | .djs-search-input input { 614 | font-size: 1.05em; 615 | width: 100%; 616 | padding: 6px 10px; 617 | border: 1px solid #ccc; 618 | } 619 | 620 | .djs-search-input input:focus{ 621 | outline: none; 622 | border-color: #52B415; 623 | box-shadow: 0 0 1px 2px rgba(82, 180, 21, 0.2); 624 | } 625 | 626 | .djs-search-results { 627 | position: relative; 628 | overflow-y: auto; 629 | max-height: 200px; 630 | } 631 | 632 | .djs-search-results:hover { 633 | /*background: #fffdd7;*/ 634 | cursor: pointer; 635 | } 636 | 637 | .djs-search-result { 638 | width: 100%; 639 | padding: 6px 10px; 640 | background: white; 641 | border-bottom: solid 1px #AAA; 642 | border-radius: 1px; 643 | } 644 | 645 | .djs-search-highlight { 646 | color: black; 647 | } 648 | 649 | .djs-search-result-primary { 650 | margin: 0 0 10px; 651 | } 652 | 653 | .djs-search-result-secondary { 654 | font-family: monospace; 655 | margin: 0; 656 | } 657 | 658 | .djs-search-result:hover { 659 | background: #fdffd6; 660 | } 661 | 662 | .djs-search-result-selected { 663 | background: #fffcb0; 664 | } 665 | 666 | .djs-search-result-selected:hover { 667 | background: #f7f388; 668 | } 669 | 670 | .djs-search-overlay { 671 | background: yellow; 672 | opacity: 0.3; 673 | } 674 | -------------------------------------------------------------------------------- /assets/css/hot-cues.css: -------------------------------------------------------------------------------- 1 | /** 2 | * hot cues 3 | */ 4 | 5 | .hot-cues { 6 | height: 50px; 7 | top: 20px; 8 | left: 50%; 9 | position: absolute; 10 | transform: translate(-50%); 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | } 15 | 16 | .hot-cues .slots .slot { 17 | box-sizing: border-box; 18 | background: rgba(255, 255, 255, 0.5); 19 | margin-right: 4px; 20 | width: 50px; 21 | height: 50px; 22 | display: inline-flex; 23 | flex: 1; 24 | flex-flow: column; 25 | flex-direction: row; 26 | align-items: center; 27 | justify-content: center; 28 | position: relative; 29 | border: solid 2px rgba(0, 0, 255, 0.5); 30 | border-radius: 4px; 31 | color: rgba(0, 0, 255, 0.5); 32 | overflow: hidden; 33 | user-select: none; 34 | cursor: pointer; 35 | transition: all 0.2s ease-in-out; 36 | } 37 | 38 | .hot-cues .slots .slot:not(.existing):hover { 39 | background-color: blue; 40 | color: white; 41 | cursor: pointer; 42 | } 43 | 44 | .hot-cues .slots .slot.existing:hover { 45 | background-color: blue; 46 | color: white; 47 | } 48 | 49 | .hot-cues .slots .slot:last-child { 50 | margin-right: 0; 51 | } 52 | 53 | .hot-cues .dirty-indicator { 54 | display: none; 55 | } 56 | 57 | .hot-cues .dirty .dirty-indicator { 58 | display: inline; 59 | } 60 | 61 | .hot-cues .slot.existing { 62 | border-color: blue; 63 | color: blue; 64 | } 65 | 66 | .hot-cues .slot.saved { 67 | animation: saved .25s linear 1; 68 | } 69 | 70 | .hot-cues .slot.active { 71 | background-color: blue; 72 | color: white; 73 | } 74 | 75 | .hot-cues #open-close { 76 | cursor: pointer; 77 | } 78 | 79 | .hot-cues:not(.hidden) #open-close { 80 | margin-top: 10px; 81 | } 82 | 83 | .hot-cues .slot.jumping:not(.active) { 84 | animation: jumping 0.5s linear infinite; 85 | } 86 | 87 | .hot-cues.hidden .slots { 88 | display: none; 89 | } 90 | 91 | @keyframes jumping { 92 | 50% { 93 | opacity: 0.5; 94 | } 95 | } 96 | 97 | @keyframes saved { 98 | from { 99 | transform: scale(1.0); 100 | } 101 | 102 | 50% { 103 | transform: scale(1.1); 104 | } 105 | 106 | to { 107 | transform: scale(1.0); 108 | } 109 | } 110 | 111 | /** 112 | * progress indicator 113 | */ 114 | 115 | .progress-indicator { 116 | position: absolute; 117 | top: 0; 118 | left: 0; 119 | right: 0; 120 | height: 3px; 121 | } 122 | 123 | .progress-indicator .progress { 124 | background: white; 125 | height: 100%; 126 | } -------------------------------------------------------------------------------- /assets/css/node-sequencer.css: -------------------------------------------------------------------------------- 1 | .djs-container { 2 | background-color: white; 3 | } 4 | 5 | .djs-connection .djs-hit { 6 | pointer-events: none; 7 | } 8 | 9 | .djs-outline { 10 | shape-rendering: geometricPrecision !important; 11 | } 12 | 13 | .djs-connection.selected .djs-outline { 14 | stroke: transparent !important; 15 | } 16 | 17 | .djs-element.selected .djs-outline { 18 | stroke: rgba(0, 0, 255, 0.5); 19 | } 20 | 21 | .djs-element.hover .djs-outline { 22 | stroke: blue; 23 | } 24 | 25 | .djs-shape.connect-ok .djs-visual > :nth-child(1) { 26 | fill: #fff /* light-green */ !important; 27 | } 28 | 29 | .djs-shape.connect-not-ok .djs-visual > :nth-child(1), 30 | .djs-shape.drop-not-ok .djs-visual > :nth-child(1) { 31 | stroke: orangered /* light-red */ !important; 32 | fill: white !important; 33 | } 34 | 35 | .djs-shape.new-parent .djs-visual > :nth-child(1) { 36 | fill: none !important; 37 | } 38 | 39 | svg.drop-not-ok { 40 | background: white /* light-red */ !important; 41 | } 42 | 43 | svg.new-parent { 44 | background: white /* light-blue */ !important; 45 | } 46 | 47 | .djs-connection.connect-ok .djs-visual > :nth-child(1), 48 | .djs-connection.drop-ok .djs-visual > :nth-child(1) { 49 | stroke: orange /* light-green */ !important; 50 | } 51 | 52 | .djs-connection.connect-not-ok .djs-visual > :nth-child(1), 53 | .djs-connection.drop-not-ok .djs-visual > :nth-child(1) { 54 | stroke: yellow /* light-red */ !important; 55 | } 56 | 57 | .drop-not-ok, 58 | .connect-not-ok { 59 | cursor: inherit; 60 | } 61 | 62 | /* .djs-dragger .djs-visual circle, 63 | .djs-dragger .djs-visual path, 64 | .djs-dragger .djs-visual polygon, 65 | .djs-dragger .djs-visual polyline, 66 | .djs-dragger .djs-visual rect, 67 | .djs-dragger .djs-visual text { 68 | fill: none !important; 69 | stroke: red !important; 70 | } */ 71 | 72 | /** 73 | * popup / palette styles 74 | */ 75 | .djs-popup, .djs-palette { 76 | background: transparent; 77 | border: none; 78 | border-radius: 0; 79 | box-shadow: none; 80 | } 81 | 82 | /** 83 | * palette 84 | */ 85 | 86 | .djs-palette { 87 | position: absolute; 88 | left: 15px; 89 | bottom: 15px; 90 | top: inherit; 91 | 92 | width: 46px; 93 | } 94 | 95 | .djs-container.two-column .djs-palette.open { 96 | width: 46px; 97 | } 98 | 99 | .djs-palette .separator { 100 | margin: 0; 101 | padding-top: 5px; 102 | border: none; 103 | border-bottom: solid 1px #999; 104 | clear: both; 105 | } 106 | 107 | .djs-palette .entry:before { 108 | vertical-align: middle; 109 | } 110 | 111 | .djs-palette .djs-palette-toggle { 112 | cursor: pointer; 113 | } 114 | 115 | .djs-palette .djs-palette-toggle { 116 | display: none; 117 | } 118 | 119 | .djs-palette .entry { 120 | color: rgba(0, 0, 255, 0.5); 121 | font-size: 30px; 122 | text-align: center; 123 | } 124 | 125 | .djs-palette .entry { 126 | float: left; 127 | } 128 | 129 | .djs-palette .entry img { 130 | max-width: 100%; 131 | } 132 | 133 | .djs-palette.open .djs-palette-toggle { 134 | height: 10px; 135 | } 136 | 137 | .djs-palette .djs-palette-entries:after { 138 | content: ''; 139 | display: table; 140 | clear: both; 141 | } 142 | 143 | .djs-palette:not(.open) .djs-palette-entries { 144 | display: none; 145 | } 146 | 147 | .djs-palette .djs-palette-toggle:hover { 148 | background: #666; 149 | } 150 | 151 | .djs-palette .entry:hover { 152 | color: #fff; 153 | } 154 | 155 | .highlighted-entry { 156 | color: orange !important; 157 | } 158 | 159 | .djs-palette:not(.open) { 160 | overflow: hidden; 161 | } 162 | 163 | .djs-palette .entry, 164 | .djs-palette .djs-palette-toggle { 165 | width: 46px; 166 | height: 46px; 167 | line-height: 46px; 168 | cursor: default; 169 | } 170 | 171 | .djs-palette.open .djs-palette-toggle { 172 | width: 100%; 173 | } 174 | 175 | /** 176 | * #fff doesn't work in FF, we have to use %23fff instead 177 | */ 178 | .icon-node-sequencer-emitter { 179 | background: url('data:image/svg+xml;utf8,'); 180 | } 181 | 182 | .icon-node-sequencer-emitter:hover { 183 | background: url('data:image/svg+xml;utf8,'); 184 | } 185 | 186 | .icon-node-sequencer-listener { 187 | background: url('data:image/svg+xml;utf8,'); 188 | } 189 | 190 | .icon-node-sequencer-listener:hover { 191 | background: url('data:image/svg+xml;utf8,'); 192 | } 193 | 194 | .icon-lasso-tool { 195 | 196 | } 197 | 198 | /** 199 | * context-pad 200 | */ 201 | .djs-overlay-context-pad { 202 | width: 72px; 203 | } 204 | 205 | .djs-context-pad { 206 | position: absolute; 207 | display: none; 208 | pointer-events: none; 209 | } 210 | 211 | .djs-context-pad .entry { 212 | width: 22px; 213 | height: 22px; 214 | text-align: center; 215 | display: inline-block; 216 | font-size: 22px; 217 | margin: 0; 218 | 219 | border-radius: 0; 220 | 221 | cursor: default; 222 | 223 | background-color: transparent; 224 | box-shadow: none; 225 | 226 | pointer-events: all; 227 | color: #999; 228 | } 229 | 230 | .djs-context-pad .entry:before { 231 | vertical-align: top; 232 | } 233 | 234 | .djs-context-pad .entry:hover { 235 | background: blue; 236 | color: white; 237 | } 238 | 239 | .djs-context-pad.open { 240 | display: block; 241 | } 242 | 243 | /** 244 | * Selection box style 245 | * 246 | */ 247 | .djs-lasso-overlay { 248 | fill: #fff; 249 | fill-opacity: 0.1; 250 | 251 | stroke-dasharray: 6 6; 252 | stroke: #fff; 253 | 254 | shape-rendering: crispEdges; 255 | pointer-events: none; 256 | } 257 | 258 | /** 259 | * drag styles 260 | */ 261 | .djs-dragger .djs-visual circle, 262 | .djs-dragger .djs-visual path, 263 | .djs-dragger .djs-visual polygon, 264 | .djs-dragger .djs-visual polyline, 265 | .djs-dragger .djs-visual rect, 266 | .djs-dragger .djs-visual text { 267 | fill: none !important; 268 | stroke: rgba(0, 0, 255, 0.5) !important; 269 | } 270 | 271 | /** 272 | * notifications styles 273 | */ 274 | .notifications { 275 | position: absolute; 276 | top: 10px; 277 | } 278 | 279 | .notifications .notification { 280 | color: #999; 281 | border-right: solid 1px #999; 282 | background: rgba(0, 0, 0, 0.75); 283 | padding: 10px; 284 | text-align: right; 285 | } 286 | 287 | .notifications .notification:hover { 288 | cursor: default !important; 289 | color: #fff; 290 | border-right: solid 1px #fff; 291 | } 292 | 293 | /** 294 | * loading overlay 295 | */ 296 | .loading-overlay { 297 | position: absolute; 298 | height: 100%; 299 | width: 100%; 300 | display: flex; 301 | justify-content: center; 302 | align-items: center; 303 | flex-direction: column; 304 | z-index: 100000; 305 | top: 0; 306 | left: 0; 307 | /* background: rgba(255, 255, 255, 0.5); */ 308 | color: blue; 309 | font-size: 18px; 310 | } 311 | 312 | .loading-overlay p { 313 | margin: 4px; 314 | } 315 | 316 | .loading-overlay.hidden { 317 | display: none; 318 | } 319 | 320 | /** radial menu */ 321 | .radial-menu .entry { 322 | position: absolute; 323 | border-radius: 100%; 324 | border: solid 1.5px blue; 325 | background-color: white; 326 | display: flex; 327 | justify-content: center; 328 | align-items: center; 329 | color: blue; 330 | font-size: 6px; 331 | user-select: none; 332 | cursor: default; 333 | } 334 | 335 | .radial-menu .entry svg { 336 | fill: blue; 337 | stroke: blue; 338 | } 339 | 340 | .radial-menu .entry:hover, 341 | .radial-menu .entry.active { 342 | border-color: blue; 343 | color: white; 344 | background-color: blue; 345 | } 346 | 347 | .radial-menu .entry:hover svg, 348 | .radial-menu .entry.active svg { 349 | fill: blue !important; 350 | stroke: blue !important; 351 | } 352 | 353 | .radial-menu .entry-remove { 354 | border-color: rgba(0, 0, 255, 0.5); 355 | } 356 | 357 | .radial-menu .entry-remove svg { 358 | fill: rgba(0, 0, 255, 0.5); 359 | stroke: rgba(0, 0, 255, 0.5); 360 | } 361 | 362 | .radial-menu .entry-remove:hover svg { 363 | fill: white !important; 364 | stroke: white !important; 365 | } 366 | 367 | .radial-menu .entry svg { 368 | width: 75%; 369 | } 370 | 371 | .kit-select { 372 | position: absolute; 373 | bottom: 20px; 374 | right: 20px; 375 | color: blue; 376 | cursor: default; 377 | display: flex; 378 | flex-direction: row; 379 | align-items: center; 380 | } 381 | 382 | .kit-select .preset { 383 | margin-left: 10px; 384 | } 385 | 386 | .kit-select .preset.active { 387 | background: blue; 388 | color: white; 389 | } -------------------------------------------------------------------------------- /assets/css/tempo-control.css: -------------------------------------------------------------------------------- 1 | /** 2 | * tempo control range styles 3 | */ 4 | 5 | .tempo-control { 6 | display: none !important; 7 | position: absolute; 8 | right: 20px; 9 | bottom: 25px; 10 | color: #999; 11 | cursor: default; 12 | user-select: none; 13 | } 14 | 15 | .tempo-control:hover { 16 | color: #fff; 17 | } 18 | 19 | .tempo-control input { 20 | background-color: transparent; 21 | } 22 | 23 | .tempo-control input[type="range"] { 24 | -webkit-appearance:none; 25 | height: 40px; 26 | margin: 0; 27 | overflow: hidden; 28 | outline: none; 29 | } 30 | 31 | /** Chrome styles */ 32 | .tempo-control input[type="range"]::-webkit-slider-runnable-track { 33 | background: linear-gradient(to right, blue 0%, blue 100%); 34 | background-size: 100% 2px; 35 | background-position: center; 36 | background-repeat: no-repeat; 37 | } 38 | 39 | .tempo-control:hover input[type="range"]::-webkit-slider-runnable-track { 40 | background: linear-gradient(to right, blue 0%, blue 100%); 41 | background-size: 100% 2px; 42 | background-position: center; 43 | background-repeat: no-repeat; 44 | } 45 | 46 | .tempo-control input[type="range"]::-webkit-slider-thumb { 47 | -webkit-appearance:none; 48 | border-radius: 100%; 49 | width: 20px; 50 | height: 20px; 51 | position: relative; 52 | z-index: 1000; 53 | background: blue; 54 | border: solid 2px blue; 55 | } 56 | 57 | .tempo-control:hover input[type="range"]::-webkit-slider-thumb { 58 | border-color: blue; 59 | } 60 | 61 | .tempo-control { 62 | display: flex; 63 | flex-direction: row; 64 | align-items: center; 65 | } 66 | 67 | .tempo-control .range { 68 | width: 200px; 69 | } 70 | 71 | .tempo-control .value { 72 | width: 50px; 73 | text-align: right; 74 | } 75 | 76 | /** Firefox styles */ 77 | .tempo-control input[type="range"]::-moz-range-track { 78 | background: #999; 79 | } 80 | 81 | .tempo-control:hover input[type="range"]::-moz-range-track { 82 | background: #fff; 83 | } 84 | 85 | .tempo-control input[type="range"]::-moz-range-thumb { 86 | border-radius: 100%; 87 | width: 20px; 88 | height: 20px; 89 | position: relative; 90 | z-index: 1000; 91 | } 92 | 93 | .tempo-control input[type="range"]::-moz-range-thumb { 94 | background: #000; 95 | border: solid 2px #999; 96 | } 97 | 98 | .tempo-control:hover input[type="range"]::-moz-range-thumb { 99 | border: solid 2px #fff; 100 | } -------------------------------------------------------------------------------- /assets/icons/remove.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippfromme/node-sequencer/b8e79865521a2f5fe2e650ab4faeaf76decb44bd/docs/screenshot.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import NodeSequencer from './src/NodeSequencer'; 2 | 3 | export default NodeSequencer; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sequencer", 3 | "version": "1.0.0", 4 | "description": "A Node-Based Sequencer for the Web", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/philippfromme/node-sequencer" 8 | }, 9 | "author": { 10 | "name": "Philipp Fromme", 11 | "url": "https://github.com/philippfromme" 12 | }, 13 | "contributors": [ 14 | { 15 | "name": "Nico Rehwaldt", 16 | "url": "https://github.com/nikku" 17 | } 18 | ], 19 | "license": "MIT", 20 | "main": "index.js", 21 | "module": "index.js", 22 | "dependencies": { 23 | "diagram-js": "^3.1.3", 24 | "file-saver": "^1.3.3", 25 | "ids": "^0.2.0", 26 | "jsmidgen": "^0.1.5", 27 | "lodash-es": "^4.17.4", 28 | "min-dom": "^3.1.1", 29 | "p5": "^0.7.3", 30 | "tiny-svg": "^2.2.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/NodeSequencer.js: -------------------------------------------------------------------------------- 1 | import Diagram from 'diagram-js'; 2 | 3 | // nodeSequencer modules 4 | import autoConnect from './features/auto-connect'; 5 | import nodeSequencerConfig from './config'; 6 | import coreModule from './core'; 7 | import cropping from './features/cropping'; 8 | import emissionAnimation from './features/emission-animation'; 9 | import emitterAnimation from './features/emitter-animation'; 10 | import nodeSequencerEmitterPreview from './features/emitter-preview'; 11 | import nodeSequencerMovePreview from './features/move-preview'; 12 | import nodeSequencerPalette from './features/palette'; 13 | import nodeSequencerRules from './features/rules'; 14 | import keyboardBindings from './features/keyboard-bindings'; 15 | import kitSelect from './features/kit-select'; 16 | import listenerAnimation from './features/listener-animation'; 17 | import nodeSequencerModeling from './features/modeling'; 18 | import ordering from './features/ordering'; 19 | import overridden from './features/overridden'; // overridden diagram-js features 20 | import radialMenu from './features/radial-menu'; 21 | import saveMidi from './features/save-midi'; 22 | import sequences from './features/sequences'; 23 | import tempoControl from './features/tempo-control'; 24 | import hotCues from './features/hot-cues'; 25 | 26 | import autoScroll from 'diagram-js/lib/features/auto-scroll'; 27 | import connect from 'diagram-js/lib/features/connect'; 28 | import contextPad from 'diagram-js/lib/features/context-pad'; 29 | import create from 'diagram-js/lib/features/create'; 30 | import editorActions from 'diagram-js/lib/features/editor-actions'; 31 | import handTool from 'diagram-js/lib/features/hand-tool'; 32 | import kayboard from 'diagram-js/lib/features/keyboard'; 33 | import lassoTool from 'diagram-js/lib/features/lasso-tool'; 34 | import modeling from 'diagram-js/lib/features/modeling'; 35 | import move from 'diagram-js/lib/features/move'; 36 | import outline from 'diagram-js/lib/features/outline'; 37 | import overlays from 'diagram-js/lib/features/overlays'; 38 | import palette from 'diagram-js/lib/features/palette'; 39 | import popupMenu from 'diagram-js/lib/features/popup-menu'; 40 | import rules from 'diagram-js/lib/features/rules'; 41 | import selection from 'diagram-js/lib/features/selection'; 42 | import toolManager from 'diagram-js/lib/features/tool-manager'; 43 | import moveCanvas from 'diagram-js/lib/navigation/movecanvas'; 44 | import zoomScroll from 'diagram-js/lib/navigation/zoomscroll'; 45 | 46 | import ConnectionDocking from 'diagram-js/lib/layout/CroppingConnectionDocking'; 47 | 48 | import { isRoot, isEmitter, isListener } from './util/NodeSequencerUtil'; 49 | import keyboard from 'diagram-js/lib/features/keyboard'; 50 | 51 | function NodeSequencer(options) { 52 | 53 | const diagramModules = [ 54 | { 55 | nodeSequencer: [ 'value', this ], 56 | nodeSequencerConfig: [ 'value', nodeSequencerConfig ], 57 | connectionDocking: [ 'type', ConnectionDocking ] 58 | }, 59 | autoScroll, 60 | connect, 61 | contextPad, 62 | create, 63 | editorActions, 64 | handTool, 65 | keyboard, 66 | lassoTool, 67 | modeling, 68 | move, 69 | outline, 70 | overlays, 71 | palette, 72 | popupMenu, 73 | rules, 74 | selection, 75 | toolManager, 76 | moveCanvas, 77 | zoomScroll, 78 | { 79 | movePreview: [ 'value', {} ] 80 | } 81 | ]; 82 | 83 | const nodeSequencerModules = [ 84 | autoConnect, 85 | coreModule, 86 | cropping, 87 | emissionAnimation, 88 | emitterAnimation, 89 | nodeSequencerEmitterPreview, 90 | nodeSequencerMovePreview, 91 | nodeSequencerPalette, 92 | nodeSequencerRules, 93 | hotCues, 94 | keyboardBindings, 95 | kitSelect, 96 | listenerAnimation, 97 | nodeSequencerModeling, 98 | ordering, 99 | overridden, 100 | radialMenu, 101 | saveMidi, 102 | sequences, 103 | tempoControl 104 | ]; 105 | 106 | const additionalModules = options.additionalModules || []; 107 | 108 | Diagram.call(this, { 109 | ...options, 110 | ...{ 111 | modules: [ 112 | ...diagramModules, 113 | ...nodeSequencerModules, 114 | ...additionalModules 115 | ] 116 | } 117 | }); 118 | }; 119 | 120 | NodeSequencer.prototype = Object.create(Diagram.prototype, { 121 | constructor: { 122 | value: NodeSequencer, 123 | enumerable: false, 124 | writable: true, 125 | configurable: true 126 | } 127 | }); 128 | 129 | NodeSequencer.prototype.create = function() { 130 | const canvas = this.get('canvas'); 131 | const eventBus = this.get('eventBus'); 132 | const nodeSequencerConfig = this.get('nodeSequencerConfig'); 133 | const nodeSequencerElementFactory = this.get('nodeSequencerElementFactory'); 134 | 135 | eventBus.fire('nodeSequencer.create.start'); 136 | 137 | const rootShape = nodeSequencerElementFactory.createRoot(); 138 | 139 | canvas.setRootElement(rootShape); 140 | 141 | const x = Math.floor(canvas.getContainer().clientWidth / 3) - 15; 142 | const y = Math.floor(canvas.getContainer().clientHeight / 2) - 15; 143 | 144 | const emitter = nodeSequencerElementFactory.createEmitter({ 145 | id: 'Emitter_1', 146 | type: 'nodeSequencer:Emitter', 147 | x, 148 | y, 149 | width: nodeSequencerConfig.shapeSize, 150 | height: nodeSequencerConfig.shapeSize 151 | }); 152 | 153 | canvas.addShape(emitter, rootShape); 154 | 155 | eventBus.fire('nodeSequencer.create.end'); 156 | }; 157 | 158 | /** 159 | * Internal load. Loads all elements. 160 | */ 161 | NodeSequencer.prototype._load = function(elements) { 162 | const nodeSequencerConfig = this.get('nodeSequencerConfig'), 163 | canvas = this.get('canvas'), 164 | modeling = this.get('modeling'), 165 | nodeSequencerElementFactory = this.get('nodeSequencerElementFactory'), 166 | sounds = this.get('sounds'); 167 | 168 | const { shapeSize } = nodeSequencerConfig; 169 | 170 | this.clear(); 171 | 172 | // add root first 173 | const rootElement = elements.filter(({ isRoot }) => isRoot)[0]; 174 | 175 | if (!rootElement) { 176 | throw new Error('root not found'); 177 | } 178 | 179 | const { tempo, soundKit } = rootElement; 180 | 181 | const rootShape = nodeSequencerElementFactory.createRoot({ 182 | tempo, 183 | soundKit 184 | }); 185 | 186 | canvas.setRootElement(rootShape); 187 | 188 | sounds.setSoundKit(soundKit); 189 | 190 | elements.forEach(element => { 191 | 192 | if (element.isRoot) { 193 | return; 194 | } else if (isEmitter(element)) { 195 | const { type, x, y, timeSignature } = element; 196 | 197 | const emitterShape = nodeSequencerElementFactory.createEmitter({ 198 | type, 199 | timeSignature, 200 | width: shapeSize, 201 | height: shapeSize 202 | }); 203 | 204 | modeling.createShape(emitterShape, { x, y }, rootShape); 205 | } else if (isListener(element)) { 206 | const { type, x, y, sound } = element; 207 | 208 | const listenerShape = nodeSequencerElementFactory.createEmitter({ 209 | type, 210 | sound, 211 | width: shapeSize, 212 | height: shapeSize 213 | }); 214 | 215 | modeling.createShape(listenerShape, { x, y }, rootShape); 216 | } 217 | 218 | }); 219 | }; 220 | 221 | /** 222 | * Loads all elements and configurations. 223 | */ 224 | NodeSequencer.prototype.load = function(descriptors) { 225 | const eventBus = this.get('eventBus'), 226 | exportConfig = this.get('exportConfig'); 227 | 228 | eventBus.fire('nodeSequencer.load.start'); 229 | 230 | try { 231 | const { elements, exportedConfigs } = JSON.parse(descriptors); 232 | 233 | this._load(elements); 234 | 235 | exportConfig.import(exportedConfigs); 236 | 237 | eventBus.fire('nodeSequencer.load.end'); 238 | } catch(e) { 239 | throw new Error('could not load', e); 240 | } 241 | }; 242 | 243 | /** 244 | * Internal save. Saves all elements. 245 | */ 246 | NodeSequencer.prototype._save = function() { 247 | const elementRegistry = this.get('elementRegistry'); 248 | 249 | let elements = []; 250 | 251 | Object.values(elementRegistry._elements).forEach(({ element }) => { 252 | 253 | let descriptor; 254 | 255 | if (isRoot(element)) { 256 | descriptor = { 257 | isRoot: true, 258 | tempo: element.tempo, 259 | soundKit: element.soundKit 260 | }; 261 | } else if (isEmitter(element)) { 262 | descriptor = { 263 | type: element.type, 264 | timeSignature: element.timeSignature, 265 | x: element.x, 266 | y: element.y 267 | }; 268 | } else if (isListener(element)) { 269 | descriptor = { 270 | type: element.type, 271 | sound: element.sound, 272 | x: element.x, 273 | y: element.y 274 | }; 275 | } 276 | 277 | if (descriptor) { 278 | elements = [ 279 | ...elements, 280 | descriptor 281 | ]; 282 | } 283 | }); 284 | 285 | return elements; 286 | }; 287 | 288 | /** 289 | * Saves all elements and additional configurations. 290 | */ 291 | NodeSequencer.prototype.save = function() { 292 | const exportConfig = this.get('exportConfig'); 293 | 294 | const exportedConfigs = exportConfig.export(); 295 | 296 | return JSON.stringify({ 297 | elements: this._save(), 298 | exportedConfigs: exportedConfigs 299 | }); 300 | }; 301 | 302 | NodeSequencer.prototype.saveMidi = function() { 303 | const saveMidi = this.get('saveMidi'); 304 | 305 | if (!saveMidi) { 306 | throw new Error('feature not found'); 307 | } 308 | 309 | saveMidi.saveMidi(); 310 | }; 311 | 312 | export default NodeSequencer; 313 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import removeSvg from '../assets/icons/remove.svg'; 2 | 3 | export default { 4 | maxDistance: 400, // px 5 | offsetDistance: 20, // px 6 | emitterColor: 'blue', 7 | listenerColor: 'blue', 8 | shapeSize: 20, // px 9 | minTempo: 70, // bpm 10 | maxTempo: 140, // bpm 11 | initialTempo: 120, // bpm 12 | initialTimeSignature: '8', 13 | timeSignatures: [ 14 | { id: '2', label: '1/2' }, 15 | { id: '4', label: '1/4' }, 16 | { id: '8', label: '1/8' }, 17 | { id: '16', label: '1/16' }, 18 | ], 19 | initialSound: undefined, 20 | initialSoundKit: 'alphabetical', 21 | soundKits: { 22 | 'alphabetical': { 23 | label: 'Alphabetical', 24 | sounds: [ 25 | { id: 'kick', label: 'Kick', path: 'public/audio/alphabetical/kick.wav' }, 26 | { id: 'clap', label: 'Clap', path: 'public/audio/alphabetical/clap.wav' }, 27 | { id: 'snare', label: 'Snare', path: 'public/audio/alphabetical/snare.wav' }, 28 | { id: 'closedhat', label: 'Closed Hihat', path: 'public/audio/alphabetical/closedhat.wav' }, 29 | { id: 'openhat', label: 'Open Hihat', path: 'public/audio/alphabetical/openhat.wav' }, 30 | { id: 'tom', label: 'Tom', path: 'public/audio/alphabetical/tom.wav' } 31 | ] 32 | }, 33 | 'glitch-baby': { 34 | label: 'Glitch Baby', 35 | sounds: [ 36 | { id: 'kick', label: 'Kick', path: 'public/audio/glitch-baby/kick.wav' }, 37 | { id: 'clap', label: 'Clap', path: 'public/audio/glitch-baby/clap.wav' }, 38 | { id: 'snare', label: 'Snare', path: 'public/audio/glitch-baby/snare.wav' }, 39 | { id: 'closedhat', label: 'Closed Hihat', path: 'public/audio/glitch-baby/closedhat.wav' }, 40 | { id: 'openhat', label: 'Open Hihat', path: 'public/audio/glitch-baby/openhat.wav' }, 41 | { id: 'tom', label: 'Tom', path: 'public/audio/glitch-baby/tom.wav' } 42 | ] 43 | }, 44 | 'alkaloid': { 45 | label: 'Alkaloid', 46 | sounds: [ 47 | { id: 'kick', label: 'Kick', path: 'public/audio/alkaloid/kick.wav' }, 48 | { id: 'clap', label: 'Clap', path: 'public/audio/alkaloid/clap.wav' }, 49 | { id: 'snare', label: 'Snare', path: 'public/audio/alkaloid/snare.wav' }, 50 | { id: 'closedhat', label: 'Closed Hihat', path: 'public/audio/alkaloid/closedhat.wav' }, 51 | { id: 'openhat', label: 'Open Hihat', path: 'public/audio/alkaloid/openhat.wav' }, 52 | { id: 'tom', label: 'Tom', path: 'public/audio/alkaloid/tom.wav' } 53 | ] 54 | } 55 | }, 56 | icons: { 57 | remove: removeSvg 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/core/Audio.js: -------------------------------------------------------------------------------- 1 | import p5 from 'p5'; 2 | import 'p5/lib/addons/p5.sound.js'; 3 | 4 | import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; 5 | 6 | import AddSequenceHandler from './cmd/AddSequenceHandler'; 7 | import RemoveSequenceHandler from './cmd/RemoveSequenceHandler'; 8 | import UpdateSequenceHandler from './cmd/UpdateSequenceHandler'; 9 | 10 | import { isRoot, isEmitter, isListener } from '../util/NodeSequencerUtil'; 11 | 12 | class Audio extends CommandInterceptor { 13 | constructor(commandStack, elementRegistry, eventBus, sounds) { 14 | super(eventBus); 15 | 16 | window.p5 = p5; 17 | 18 | this._commandStack = commandStack; 19 | this._elementRegistry = elementRegistry; 20 | this._sounds = sounds; 21 | 22 | commandStack.registerHandler('nodeSequencer.audio.addSequence', AddSequenceHandler); 23 | commandStack.registerHandler('nodeSequencer.audio.removeSequence', RemoveSequenceHandler); 24 | commandStack.registerHandler('nodeSequencer.audio.updateSequence', UpdateSequenceHandler); 25 | 26 | this.phrases = {}; 27 | 28 | this.mainPart = new p5.Part(); 29 | // this.mainPart.loop(); 30 | // this.mainPart.start(); 31 | 32 | const phrase = new p5.Phrase('loopStart', (time, playbackRate) => { 33 | eventBus.fire('nodeSequencer.audio.loopStart'); 34 | }, [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]); 35 | 36 | this.mainPart.addPhrase(phrase); 37 | 38 | window.phrases = this.phrases; 39 | 40 | const reverb = new p5.Reverb(); 41 | const delay = new p5.Delay(); 42 | 43 | eventBus.on('nodeSequencer.sounds.loaded', () => { 44 | const allSounds = sounds.getSounds(); 45 | 46 | Object.values(allSounds).forEach(soundKit => { 47 | Object.values(soundKit).forEach(soundObject => { 48 | const { sound } = soundObject; 49 | 50 | reverb.process(sound, 1, 2); // reverb time, decay rate 51 | delay.process(sound, .12, .1, 2300); // delay time, feedback, filter frequency 52 | }); 53 | }); 54 | }); 55 | 56 | // diagram clear 57 | eventBus.on('diagram.clear', () => { 58 | Object.keys(this.phrases).forEach(key => { 59 | this.mainPart.removePhrase(key); 60 | }); 61 | 62 | this.phrases = {}; 63 | }); 64 | 65 | // enable changing tempo during input 66 | eventBus.on('nodeSequencer.tempoControl.input', ({ tempo }) => { 67 | this.mainPart.setBPM(tempo); 68 | }); 69 | } 70 | 71 | addSequence(sequence, emitter, listener) { 72 | const { sound } = this._sounds.getSound(listener.sound); 73 | 74 | const onPlay = () => { 75 | this._eventBus.fire('nodeSequencer.audio.playSound', { 76 | listener 77 | }); 78 | }; 79 | 80 | this._commandStack.execute('nodeSequencer.audio.addSequence', { 81 | sequence, 82 | emitter, 83 | listener, 84 | phrases: this.phrases, 85 | mainPart: this.mainPart, 86 | sound, 87 | onPlay 88 | }); 89 | } 90 | 91 | removeSequence(emitter, listener) { 92 | this._commandStack.execute('nodeSequencer.audio.removeSequence', { 93 | emitter, 94 | listener, 95 | phrases: this.phrases, 96 | mainPart: this.mainPart 97 | }); 98 | } 99 | 100 | updateSequence(sequence, emitter, listener) { 101 | this._commandStack.execute('nodeSequencer.audio.updateSequence', { 102 | sequence, 103 | emitter, 104 | listener, 105 | phrases: this.phrases, 106 | mainPart: this.mainPart 107 | }); 108 | } 109 | 110 | getMainPart() { 111 | return this.mainPart; 112 | } 113 | 114 | getAllPhrases() { 115 | return this.phrases; 116 | } 117 | 118 | async start() { 119 | 120 | // resume audio 121 | p5.prototype.getAudioContext().resume(); 122 | 123 | this.mainPart.loop(); 124 | this.mainPart.start(); 125 | } 126 | 127 | stop() { 128 | this.mainPart.stop(); 129 | } 130 | } 131 | 132 | Audio.$inject = [ 'commandStack', 'elementRegistry', 'eventBus', 'sounds' ]; 133 | 134 | // export default doesn't work 135 | export default Audio; 136 | -------------------------------------------------------------------------------- /src/core/ExportConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lets modules register for exporting configurations on NodeSequencer#save. 3 | */ 4 | class ExportConfig { 5 | constructor() { 6 | this.registeredExports = {}; 7 | } 8 | 9 | registerExport(options) { 10 | if (!options) { 11 | throw new Error('options not found'); 12 | } 13 | 14 | if (this.registeredExports[options.id]) { 15 | throw new Error('already registered'); 16 | } 17 | 18 | this.registeredExports[options.id] = { 19 | exportConfig: options.exportConfig || function() {}, 20 | importConfig: options.importConfig || function() {} 21 | }; 22 | } 23 | 24 | export() { 25 | const exportedConfigs = {}; 26 | 27 | Object.keys(this.registeredExports).forEach(id => { 28 | exportedConfigs[id] = this.registeredExports[id].exportConfig(); 29 | }); 30 | 31 | return exportedConfigs; 32 | } 33 | 34 | import(exportedConfigs) { 35 | Object.keys(exportedConfigs).forEach(id => { 36 | this.registeredExports[id].importConfig(exportedConfigs[id]); 37 | }); 38 | } 39 | } 40 | 41 | export default ExportConfig; -------------------------------------------------------------------------------- /src/core/LoadingOverlay.js: -------------------------------------------------------------------------------- 1 | import { 2 | domify, 3 | classes as domClasses 4 | } from 'min-dom'; 5 | 6 | class LoadingOverlay { 7 | constructor(eventBus, canvas) { 8 | this._canvas = canvas; 9 | 10 | this._loadingComponents = []; 11 | 12 | this.init(); 13 | } 14 | 15 | init() { 16 | this.$overlay = domify(` 17 | 20 | `); 21 | 22 | this._canvas.getContainer().appendChild(this.$overlay); 23 | } 24 | 25 | addLoadingComponent(loadingComponent) { 26 | if (!this._loadingComponents.includes(loadingComponent)) { 27 | this._loadingComponents.push(loadingComponent); 28 | } 29 | 30 | domClasses(this.$overlay).remove('hidden'); 31 | } 32 | 33 | removeLoadingComponent(loadingComponent) { 34 | this._loadingComponents = this._loadingComponents.filter(c => { 35 | c !== loadingComponent; 36 | }); 37 | 38 | if (!this._loadingComponents.length) { 39 | domClasses(this.$overlay).add('hidden'); 40 | } 41 | } 42 | } 43 | 44 | LoadingOverlay.$inject = [ 'eventBus', 'canvas' ]; 45 | 46 | export default LoadingOverlay; 47 | -------------------------------------------------------------------------------- /src/core/NodeSequencerElementFactory.js: -------------------------------------------------------------------------------- 1 | import BaseElementFactory from 'diagram-js/lib/core/ElementFactory'; 2 | 3 | class NodeSequencerElementFactory extends BaseElementFactory { 4 | constructor(nodeSequencerConfig, sounds) { 5 | super(); 6 | 7 | this.baseCreate = BaseElementFactory.prototype.create; 8 | 9 | this.handlers = { 10 | 11 | // root 12 | root: attrs => { 13 | return this.baseCreate('root', Object.assign({ 14 | id: 'root', 15 | tempo: nodeSequencerConfig.initialTempo, 16 | soundKit: nodeSequencerConfig.initialSoundKit 17 | }, attrs)); 18 | }, 19 | 20 | // emitter 21 | emitter: attrs => { 22 | return this.baseCreate('shape', Object.assign({ 23 | type: 'nodeSequencer:Emitter', 24 | width: nodeSequencerConfig.shapeSize, 25 | height: nodeSequencerConfig.shapeSize, 26 | timeSignature: nodeSequencerConfig.initialTimeSignature 27 | }, attrs)); 28 | }, 29 | 30 | // listener 31 | listener: attrs => { 32 | return this.baseCreate('shape', Object.assign({ 33 | type: 'nodeSequencer:Listener', 34 | width: nodeSequencerConfig.shapeSize, 35 | height: nodeSequencerConfig.shapeSize, 36 | sound: nodeSequencerConfig.initialSound 37 | }, attrs)); 38 | } 39 | } 40 | } 41 | 42 | create(elementType, attrs) { 43 | return this.handlers[elementType](attrs); 44 | } 45 | 46 | createRoot(attrs) { 47 | return this.create('root', attrs); 48 | } 49 | 50 | createEmitter(attrs) { 51 | return this.create('emitter', attrs); 52 | } 53 | 54 | createListener(attrs) { 55 | return this.create('listener', attrs); 56 | } 57 | } 58 | 59 | NodeSequencerElementFactory.$inject = [ 'nodeSequencerConfig', 'sounds' ]; 60 | 61 | export default NodeSequencerElementFactory; 62 | -------------------------------------------------------------------------------- /src/core/NodeSequencerRenderer.js: -------------------------------------------------------------------------------- 1 | import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; 2 | 3 | import { componentsToPath, createLine } from 'diagram-js/lib/util/RenderUtil'; 4 | 5 | import { 6 | append as svgAppend, 7 | attr as svgAttr, 8 | create as svgCreate 9 | } from 'tiny-svg'; 10 | 11 | import { isEmitter, isListener, isConnection } from '../util/NodeSequencerUtil'; 12 | 13 | function getLabel(soundId) { 14 | switch (soundId) { 15 | case 'kick': 16 | return 'K'; 17 | case 'clap': 18 | return 'C'; 19 | case 'snare': 20 | return 'S'; 21 | case 'closedhat': 22 | return 'CH'; 23 | case 'openhat': 24 | return 'OH'; 25 | case 'tom': 26 | return 'T'; 27 | } 28 | } 29 | 30 | class CustomRenderer extends BaseRenderer { 31 | constructor(eventBus, canvas, nodeSequencerConfig) { 32 | super(eventBus, 2000); 33 | 34 | this._nodeSequencerConfig = nodeSequencerConfig; 35 | 36 | this.drawEmitter = (p, element, color) => { 37 | const { width, height, timeSignature } = element; 38 | 39 | const cx = width / 2, 40 | cy = height / 2; 41 | 42 | const attrs = { 43 | stroke: color, 44 | strokeWidth: 1, 45 | fill: 'white' 46 | }; 47 | 48 | const circle = svgCreate('circle'); 49 | 50 | svgAttr(circle, { 51 | cx: cx, 52 | cy: cy, 53 | r: Math.round((width + height) / 4) 54 | }); 55 | 56 | svgAttr(circle, attrs); 57 | 58 | svgAppend(p, circle); 59 | 60 | var text = svgCreate('text'); 61 | 62 | text.textContent = '1/' + timeSignature; 63 | 64 | svgAppend(p, text); 65 | 66 | // thanks to monospace font we can do this 67 | var translateX = (width / 2) - (text.textContent.length * 1.85); 68 | 69 | svgAttr(text, { 70 | transform: 'translate(' + translateX + ', 12)', 71 | fill: color, 72 | fontSize: '6px' 73 | }); 74 | 75 | return circle; 76 | }; 77 | 78 | this.drawListener = (p, element, outerColor, innerColor) => { 79 | const { width, height, sound } = element; 80 | 81 | const cx = width / 2, 82 | cy = height / 2; 83 | 84 | const attrs = { 85 | strokeWidth: 1, 86 | fill: 'white' 87 | }; 88 | 89 | const circle = svgCreate('circle'); 90 | 91 | svgAttr(circle, { 92 | cx: cx, 93 | cy: cy, 94 | r: Math.round((width + height) / 4), 95 | stroke: outerColor 96 | }); 97 | 98 | svgAttr(circle, attrs); 99 | 100 | svgAppend(p, circle); 101 | 102 | const innerCircle = svgCreate('circle'); 103 | 104 | svgAttr(innerCircle, { 105 | cx: cx, 106 | cy: cy, 107 | r: Math.round((width + height) / 4) - 3, 108 | stroke: innerColor 109 | }); 110 | 111 | svgAttr(innerCircle, attrs); 112 | 113 | svgAppend(p, innerCircle); 114 | 115 | var text = svgCreate('text'); 116 | 117 | text.textContent = getLabel(sound) || ''; 118 | 119 | svgAppend(p, text); 120 | 121 | // thanks to monospace font we can do this 122 | var translateX = (width / 2) - (text.textContent.length * 1.85); 123 | 124 | svgAttr(text, { 125 | transform: 'translate(' + translateX + ', 12)', 126 | fill: innerColor, 127 | fontSize: '6' 128 | }); 129 | 130 | return circle; 131 | }; 132 | 133 | this.getCirclePath = shape => { 134 | const cx = shape.x + shape.width / 2, 135 | cy = shape.y + shape.height / 2, 136 | radius = shape.width / 2; 137 | 138 | const circlePath = [ 139 | ['M', cx, cy], 140 | ['m', 0, -radius], 141 | ['a', radius, radius, 0, 1, 1, 0, 2 * radius], 142 | ['a', radius, radius, 0, 1, 1, 0, -2 * radius], 143 | ['z'] 144 | ]; 145 | 146 | return componentsToPath(circlePath); 147 | }; 148 | 149 | this.drawConnection = (p, element) => { 150 | const attrs = { 151 | strokeWidth: 1, 152 | stroke: nodeSequencerConfig.emitterColor 153 | }; 154 | 155 | return svgAppend(p, createLine(element.waypoints, attrs)); 156 | }; 157 | 158 | this.getCustomConnectionPath = connection => { 159 | const waypoints = connection.waypoints.map(function(p) { 160 | return p.original || p; 161 | }); 162 | 163 | const connectionPath = [ 164 | ['M', waypoints[0].x, waypoints[0].y] 165 | ]; 166 | 167 | waypoints.forEach(function(waypoint, index) { 168 | if (index !== 0) { 169 | connectionPath.push(['L', waypoint.x, waypoint.y]); 170 | } 171 | }); 172 | 173 | return componentsToPath(connectionPath); 174 | }; 175 | } 176 | 177 | canRender(element) { 178 | return /^nodeSequencer\:/.test(element.type); 179 | } 180 | 181 | drawShape(parent, element) { 182 | if (isEmitter(element)) { 183 | return this.drawEmitter(parent, element, this._nodeSequencerConfig.emitterColor); 184 | } else if (isListener(element)) { 185 | return this.drawListener(parent, element, this._nodeSequencerConfig.emitterColor, this._nodeSequencerConfig.listenerColor); 186 | } 187 | } 188 | 189 | getShapePath(shape) { 190 | if (isEmitter(shape) || isListener(shape)) { 191 | return this.getCirclePath(shape); 192 | } 193 | } 194 | 195 | drawConnection(p, element) { 196 | if (isConnection(element)) { 197 | return this.drawConnection(p, element); 198 | } 199 | } 200 | } 201 | 202 | CustomRenderer.$inject = [ 'eventBus', 'canvas', 'nodeSequencerConfig' ]; 203 | 204 | export default CustomRenderer; 205 | -------------------------------------------------------------------------------- /src/core/Sounds.js: -------------------------------------------------------------------------------- 1 | import p5 from 'p5'; 2 | 3 | class Sounds { 4 | constructor(eventBus, nodeSequencerConfig, loadingOverlay) { 5 | this._eventBus = eventBus; 6 | this._nodeSequencerConfig = nodeSequencerConfig; 7 | this._loadingOverlay = loadingOverlay; 8 | 9 | this.soundKit = nodeSequencerConfig.initialSoundKit; 10 | 11 | this.noneSound = { 12 | sound: { 13 | rate() {}, 14 | play() {} 15 | }, 16 | label: 'None' 17 | }; 18 | 19 | this._sounds = {}; 20 | 21 | this.loadSounds(); 22 | } 23 | 24 | loadSounds() { 25 | this._loadingOverlay.addLoadingComponent(this); 26 | 27 | this._eventBus.fire('nodeSequencer.sounds.loading'); 28 | 29 | let numberLoading = 0; 30 | 31 | Object.entries(this._nodeSequencerConfig.soundKits).forEach(entry => { 32 | const soundKit = entry[0], 33 | sounds = entry[1].sounds; 34 | 35 | this._sounds[soundKit] = {}; 36 | 37 | sounds.forEach(s => { 38 | numberLoading++; 39 | 40 | const sound = p5.prototype.loadSound(s.path, () => { 41 | numberLoading--; 42 | 43 | if (numberLoading === 0) { 44 | this._loadingOverlay.removeLoadingComponent(this); 45 | 46 | this._eventBus.fire('nodeSequencer.sounds.loaded'); 47 | } 48 | }); 49 | 50 | this._sounds[soundKit][s.id] = { 51 | sound, 52 | label: s.label 53 | }; 54 | }); 55 | }); 56 | 57 | this.soundKit = Object.keys(this._nodeSequencerConfig.soundKits)[0]; 58 | } 59 | 60 | getSound(soundId) { 61 | if (this._sounds[this.soundKit][soundId]) { 62 | return this._sounds[this.soundKit][soundId]; 63 | } else { 64 | 65 | // return mock sound 66 | return this.noneSound; 67 | } 68 | } 69 | 70 | setSoundKit(soundKit) { 71 | this.soundKit = soundKit; 72 | 73 | this._eventBus.fire('sounds.setSoundKit', { 74 | soundKit 75 | }); 76 | } 77 | 78 | getSounds() { 79 | return this._sounds; 80 | } 81 | } 82 | 83 | Sounds.$inject = [ 'eventBus', 'nodeSequencerConfig', 'loadingOverlay' ]; 84 | 85 | export default Sounds; 86 | -------------------------------------------------------------------------------- /src/core/cmd/AddSequenceHandler.js: -------------------------------------------------------------------------------- 1 | import p5 from 'p5'; 2 | 3 | import { getSequenceFromSequences } from '../../util/SequenceUtil'; 4 | 5 | class AddSequenceHandler { 6 | execute(context) { 7 | const { 8 | sequence, 9 | emitter, 10 | listener, 11 | phrases, 12 | mainPart, 13 | sound, 14 | onPlay 15 | } = context; 16 | 17 | if (!phrases[listener.id]) { 18 | context.phraseAdded = true; 19 | 20 | phrases[listener.id] = {}; 21 | } 22 | 23 | phrases[listener.id][emitter.id] = sequence; 24 | 25 | const sequenceFromSequences = getSequenceFromSequences(phrases[listener.id]); 26 | 27 | const phrase = new p5.Phrase(listener.id, (time, playbackRate) => { 28 | sound.rate(playbackRate); 29 | sound.play(time); 30 | onPlay(); 31 | }, sequenceFromSequences); 32 | 33 | if (!mainPart.getPhrase(listener.id)) { 34 | mainPart.addPhrase(phrase); 35 | } else { 36 | context.oldSequence = mainPart.getPhrase(listener.id).sequence; 37 | 38 | mainPart.getPhrase(listener.id).sequence = sequenceFromSequences; 39 | } 40 | } 41 | 42 | revert({ 43 | emitter, 44 | listener, 45 | phrases, 46 | mainPart, 47 | phraseAdded, 48 | oldSequence 49 | }) { 50 | delete phrases[listener.id][emitter.id]; 51 | 52 | if (phraseAdded) { 53 | delete phrases[listener.id] 54 | } 55 | 56 | if (oldSequence) { 57 | mainPart.getPhrase(listener.id).sequence = oldSequence; 58 | } else { 59 | mainPart.removePhrase(listener.id); 60 | } 61 | } 62 | } 63 | 64 | // export default doesn't work 65 | export default AddSequenceHandler; 66 | -------------------------------------------------------------------------------- /src/core/cmd/RemoveSequenceHandler.js: -------------------------------------------------------------------------------- 1 | import { getSequenceFromSequences } from '../../util/SequenceUtil'; 2 | 3 | class RemoveSequenceHandler { 4 | execute(context) { 5 | const { 6 | emitter, 7 | listener, 8 | phrases, 9 | mainPart 10 | } = context; 11 | 12 | // remove from phrases 13 | context.oldSequence = phrases[listener.id][emitter.id]; 14 | 15 | delete phrases[listener.id][emitter.id]; 16 | 17 | // remove from mainPart 18 | if (!Object.keys(phrases[listener.id]).length) { 19 | delete phrases[listener.id]; 20 | 21 | context.phraseDeleted = true; 22 | 23 | context.oldPhrase = mainPart.getPhrase(listener.id); 24 | 25 | mainPart.removePhrase(listener.id); 26 | } else { 27 | const sequenceFromSequences = getSequenceFromSequences(phrases[listener.id]); 28 | 29 | mainPart.getPhrase(listener.id).sequence = sequenceFromSequences; 30 | } 31 | } 32 | 33 | revert({ 34 | emitter, 35 | listener, 36 | phrases, 37 | mainPart, 38 | oldSequence, 39 | oldPhrase, 40 | phraseDeleted 41 | }) { 42 | if (phraseDeleted) { 43 | phrases[listener.id] = {}; 44 | } 45 | 46 | phrases[listener.id][emitter.id] = oldSequence; 47 | 48 | // last remaining sequence was deleted 49 | if (oldPhrase) { 50 | mainPart.addPhrase(oldPhrase); 51 | } else { 52 | mainPart.getPhrase(listener.id).sequence = oldSequence; 53 | } 54 | } 55 | } 56 | 57 | export default RemoveSequenceHandler; 58 | -------------------------------------------------------------------------------- /src/core/cmd/UpdateSequenceHandler.js: -------------------------------------------------------------------------------- 1 | import { getSequenceFromSequences } from '../../util/SequenceUtil'; 2 | 3 | class UpdateSequenceHandler { 4 | execute(context) { 5 | const { 6 | sequence, 7 | emitter, 8 | listener, 9 | phrases, 10 | mainPart 11 | } = context; 12 | 13 | context.oldSequence = phrases[listener.id][emitter.id]; 14 | 15 | phrases[listener.id][emitter.id] = sequence; 16 | 17 | const sequenceFromSequences = getSequenceFromSequences(phrases[listener.id]); 18 | 19 | context.oldSequenceFromSequences = mainPart.getPhrase(listener.id).sequence; 20 | 21 | mainPart.getPhrase(listener.id).sequence = sequenceFromSequences; 22 | } 23 | 24 | revert({ 25 | emitter, 26 | listener, 27 | phrases, 28 | mainPart, 29 | oldSequence, 30 | oldSequenceFromSequences 31 | }) { 32 | phrases[listener.id][emitter.id] = oldSequence; 33 | 34 | mainPart.getPhrase(listener.id).sequence = oldSequenceFromSequences; 35 | } 36 | } 37 | 38 | export default UpdateSequenceHandler; 39 | -------------------------------------------------------------------------------- /src/core/index.js: -------------------------------------------------------------------------------- 1 | import Audio from './Audio'; 2 | import ExportConfig from './ExportConfig'; 3 | import NodeSequencerElementFactory from './NodeSequencerElementFactory'; 4 | import NodeSequencerRenderer from './NodeSequencerRenderer'; 5 | import LoadingOverlay from './LoadingOverlay'; 6 | import Sounds from './Sounds'; 7 | 8 | export default { 9 | __init__: [ 10 | 'audio', 11 | 'exportConfig', 12 | 'nodeSequencerElementFactory', 13 | 'nodeSequencerRenderer', 14 | 'loadingOverlay', 15 | 'sounds' 16 | ], 17 | audio: [ 'type', Audio ], 18 | exportConfig: [ 'type', ExportConfig ], 19 | nodeSequencerElementFactory: [ 'type', NodeSequencerElementFactory ], 20 | nodeSequencerRenderer: [ 'type', NodeSequencerRenderer ], 21 | loadingOverlay: [ 'type', LoadingOverlay ], 22 | sounds: [ 'type', Sounds ] 23 | }; 24 | -------------------------------------------------------------------------------- /src/features/auto-connect/AutoConnect.js: -------------------------------------------------------------------------------- 1 | import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; 2 | 3 | import { isEmitter, isListener } from '../../util/NodeSequencerUtil'; 4 | import { getDistance } from '../../util/GeometryUtil'; 5 | 6 | function connected(source, target) { 7 | if (!source.outgoing.length || !target.incoming.length) { 8 | return false; 9 | } 10 | 11 | let connected = false; 12 | 13 | source.outgoing.forEach(outgoing => { 14 | target.incoming.forEach(incoming => { 15 | if (outgoing === incoming) { 16 | connected = true; 17 | } 18 | }) 19 | }); 20 | 21 | return connected; 22 | } 23 | 24 | class AutoConnect extends CommandInterceptor { 25 | constructor(eventBus, elementRegistry, modeling, nodeSequencerRules) { 26 | super(eventBus); 27 | 28 | this.postExecuted('shape.create', context => { 29 | const shape = context.context.shape; 30 | 31 | if (isEmitter(shape)) { 32 | 33 | const listeners = elementRegistry.filter(elements => { 34 | return isListener(elements); 35 | }); 36 | 37 | listeners.forEach(listener => { 38 | if (nodeSequencerRules.canConnect(shape, listener)) { 39 | modeling.connect(shape, listener, { type: 'nodeSequencer:Connection' }); 40 | } 41 | }); 42 | } 43 | 44 | if (isListener(shape)) { 45 | const emitters = elementRegistry.filter(elements => { 46 | return isEmitter(elements); 47 | }); 48 | 49 | emitters.forEach(emitter => { 50 | if (nodeSequencerRules.canConnect(emitter, shape)) { 51 | modeling.connect(emitter, shape, { type: 'nodeSequencer:Connection' }); 52 | } 53 | }); 54 | } 55 | }, this); 56 | 57 | this.postExecuted('elements.move', event => { 58 | const shapes = event.context.shapes; 59 | 60 | shapes.forEach(shape => { 61 | if (isEmitter(shape)) { 62 | var remove = []; 63 | 64 | shape.outgoing.forEach(outgoing => { 65 | if (!nodeSequencerRules.canConnect(shape, outgoing.target)) { 66 | remove.push(outgoing); 67 | } 68 | }); 69 | 70 | remove.forEach(c => modeling.removeConnection(c)); 71 | 72 | const listeners = elementRegistry.filter(elements => { 73 | return isListener(elements); 74 | }); 75 | 76 | listeners.forEach(listener => { 77 | 78 | if (nodeSequencerRules.canConnect(shape, listener) && 79 | !connected(shape, listener)) { 80 | modeling.connect(shape, listener, { type: 'nodeSequencer:Connection' }); 81 | } 82 | 83 | }); 84 | } 85 | 86 | if (isListener(shape)) { 87 | shape.incoming.forEach(incoming => { 88 | if (!nodeSequencerRules.canConnect(shape, incoming.source)) { 89 | console.log('removing connection'); 90 | modeling.removeConnection(incoming); 91 | } 92 | }); 93 | 94 | const emitters = elementRegistry.filter(elements => { 95 | return isEmitter(elements); 96 | }); 97 | 98 | emitters.forEach(emitter => { 99 | if (nodeSequencerRules.canConnect(emitter, shape) && 100 | !connected(emitter, shape)) { 101 | modeling.connect(emitter, shape, { type: 'nodeSequencer:Connection' }); 102 | } 103 | }); 104 | } 105 | }); 106 | }); 107 | } 108 | } 109 | 110 | AutoConnect.$inject = [ 'eventBus', 'elementRegistry', 'modeling', 'nodeSequencerRules' ]; 111 | 112 | export default AutoConnect; 113 | -------------------------------------------------------------------------------- /src/features/auto-connect/index.js: -------------------------------------------------------------------------------- 1 | import AutoConnect from './AutoConnect'; 2 | 3 | export default { 4 | __init__: [ 'autoConnect' ], 5 | autoConnect: [ 'type', AutoConnect ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/cropping/Cropping.js: -------------------------------------------------------------------------------- 1 | import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; 2 | 3 | class Cropping extends CommandInterceptor { 4 | constructor(eventBus, nodeSequencerConnectionCropping) { 5 | super(eventBus); 6 | 7 | function cropConnection(event) { 8 | const { context } = event; 9 | 10 | if (!context.cropped) { 11 | const connection = context.connection; 12 | connection.waypoints = nodeSequencerConnectionCropping.getCroppedWaypointsFromConnection(connection); 13 | context.cropped = true; 14 | } 15 | } 16 | 17 | this.executed([ 18 | 'connection.layout', 19 | 'connection.create', 20 | 'connection.reconnectEnd', 21 | 'connection.reconnectStart' 22 | ], cropConnection); 23 | } 24 | } 25 | 26 | Cropping.$inject = [ 'eventBus', 'nodeSequencerConnectionCropping' ]; 27 | 28 | // export default doesn't work 29 | export default Cropping; 30 | -------------------------------------------------------------------------------- /src/features/cropping/NodeSequencerConnectionCropping.js: -------------------------------------------------------------------------------- 1 | import { getMid, getVectorFromPoints, Vector } from '../../util/GeometryUtil'; 2 | 3 | const { atan2, cos, sin, PI } = Math; 4 | 5 | class NodeSequencerConnectionCropping { 6 | constructor(nodeSequencerConfig) { 7 | this.shapeRadius = nodeSequencerConfig.shapeSize / 2; 8 | } 9 | 10 | getCroppedWaypoints(a, b) { 11 | const vectorA = getVectorFromPoints(a, b).normalize().scale(10); 12 | const vectorB = getVectorFromPoints(b, a).normalize().scale(10); 13 | 14 | const intersection1 = { 15 | x: a.x + vectorA.x, 16 | y: a.y + vectorA.y 17 | }; 18 | 19 | const intersection2 = { 20 | x: b.x + vectorB.x, 21 | y: b.y + vectorB.y 22 | }; 23 | 24 | return [ 25 | intersection1, 26 | intersection2 27 | ]; 28 | } 29 | 30 | getCroppedWaypointsFromConnection({ source, target }) { 31 | const sourceMid = getMid(source), 32 | targetMid = getMid(target); 33 | 34 | return this.getCroppedWaypoints(sourceMid, targetMid); 35 | } 36 | } 37 | 38 | NodeSequencerConnectionCropping.$inject = [ 'nodeSequencerConfig' ]; 39 | 40 | export default NodeSequencerConnectionCropping; 41 | -------------------------------------------------------------------------------- /src/features/cropping/index.js: -------------------------------------------------------------------------------- 1 | import Cropping from './Cropping'; 2 | import NodeSequencerConnectionCropping from './NodeSequencerConnectionCropping'; 3 | 4 | export default { 5 | __init__: [ 'cropping', 'nodeSequencerConnectionCropping' ], 6 | cropping: [ 'type', Cropping ], 7 | nodeSequencerConnectionCropping: [ 'type', NodeSequencerConnectionCropping ] 8 | }; 9 | -------------------------------------------------------------------------------- /src/features/emission-animation/EmissionAnimation.js: -------------------------------------------------------------------------------- 1 | import p5 from 'p5'; 2 | 3 | import { 4 | append as svgAppend, 5 | attr as svgAttr, 6 | clear as svgClear, 7 | create as svgCreate, 8 | remove as svgRemove 9 | } from 'tiny-svg'; 10 | 11 | import { rotate, translate } from 'diagram-js/lib/util/SvgTransformUtil'; 12 | 13 | import { isConnection } from '../../util/NodeSequencerUtil'; 14 | 15 | import { getStepIndex } from '../../util/SequenceUtil'; 16 | 17 | import { getDistance, getMid } from '../../util/GeometryUtil'; 18 | 19 | import { tweenPoint } from '../../util/TweenUtil'; 20 | 21 | const MILLIS_PER_MINUTE = 60000; 22 | 23 | const IMPULSE_RECT_WIDTH = 10; 24 | const IMPULSE_RECT_HEIGHT = 4; 25 | 26 | const round = Math.round; 27 | 28 | class EmissionAnimation { 29 | constructor(eventBus, canvas, nodeSequencerConfig, elementRegistry) { 30 | this._canvas = canvas; 31 | this._nodeSequencerConfig = nodeSequencerConfig; 32 | 33 | this.audioContext = p5.prototype.getAudioContext(); 34 | 35 | this.emissionAnimationLayer = canvas.getLayer('nodeSequencerEmissionAnimation', -700); 36 | 37 | this.impulses = []; 38 | 39 | this.timeLastLoopStart = 0; 40 | 41 | eventBus.on('nodeSequencer.audio.loopStart', context => { 42 | this.timeLastLoopStart = Date.now(); 43 | 44 | svgClear(this.emissionAnimationLayer); 45 | 46 | this.impulses = []; 47 | 48 | const connections = elementRegistry.filter(e => isConnection(e)); 49 | 50 | connections.forEach(connection => { 51 | this.createEmitterAnimation(connection); 52 | }); 53 | }); 54 | 55 | // update during animation 56 | eventBus.on('commandStack.shape.delete.postExecuted', ({ context }) => { 57 | const { shape } = context; 58 | 59 | this.impulses.forEach(({ emitter, listener, gfxGroup}) => { 60 | if (shape === emitter || shape === listener) { 61 | svgRemove(gfxGroup); 62 | } 63 | }); 64 | 65 | this.impulses = 66 | this.impulses.filter(i => i.emitter !== shape && i.listener !== shape); 67 | }); 68 | 69 | eventBus.on('commandStack.connection.create.postExecuted', ({ context }) => { 70 | const { connection } = context; 71 | 72 | this.createEmitterAnimation(connection); 73 | }); 74 | 75 | eventBus.on('commandStack.connection.delete.postExecuted', ({ context }) => { 76 | const { source, target } = context; 77 | 78 | this.impulses.forEach(({ emitter, listener, gfxGroup}) => { 79 | if (source === emitter && target === listener) { 80 | svgRemove(gfxGroup); 81 | } 82 | }); 83 | 84 | this.impulses = 85 | this.impulses.filter(i => i.emitter.id !== source.id || i.listener.id !== target.id); 86 | }); 87 | 88 | // diagram clear 89 | eventBus.on('diagram.clear', () => { 90 | svgClear(this.emissionAnimationLayer); 91 | 92 | this.impulses = []; 93 | }); 94 | 95 | // start animation loop 96 | this.updateAnimation(); 97 | } 98 | 99 | createEmitterAnimation({ source, target }) { 100 | const { tempo } = this._canvas.getRootElement(); 101 | 102 | const { maxDistance, offsetDistance } = this._nodeSequencerConfig; 103 | 104 | const quarterNoteDuration = MILLIS_PER_MINUTE / tempo, 105 | sixteenthNoteDuration = quarterNoteDuration / 4; 106 | 107 | const emitter = source, 108 | listener = target; 109 | 110 | const { timeSignature } = emitter; 111 | 112 | const distance = getDistance(emitter, listener); 113 | 114 | const stepIndex = getStepIndex(distance, maxDistance, offsetDistance, timeSignature); 115 | 116 | if (stepIndex === 0) { 117 | 118 | // animation duration would be 0ms 119 | return; 120 | } 121 | 122 | const animationDuration = sixteenthNoteDuration * stepIndex; 123 | 124 | const emitterMid = getMid(emitter); 125 | const listenerMid = getMid(listener); 126 | 127 | const gfxGroup = svgCreate('g'); 128 | 129 | translate(gfxGroup, emitterMid.x, emitterMid.y); 130 | 131 | svgAppend(this.emissionAnimationLayer, gfxGroup); 132 | 133 | const gfxRect = svgAttr(svgCreate('rect'), { 134 | x: - round(IMPULSE_RECT_WIDTH / 2), 135 | y: - round(IMPULSE_RECT_HEIGHT / 2), 136 | width: IMPULSE_RECT_WIDTH, 137 | height: IMPULSE_RECT_HEIGHT, 138 | fill: this._nodeSequencerConfig.emitterColor 139 | }); 140 | 141 | const rotation = 142 | Math.atan2(listenerMid.x - emitterMid.x, listenerMid.y - emitterMid.y) 143 | * 180 / Math.PI; 144 | 145 | // TODO: fix 146 | rotate(gfxRect, - rotation - 90); 147 | 148 | svgAppend(gfxGroup, gfxRect); 149 | 150 | this.impulses.push({ 151 | startTime: this.timeLastLoopStart, 152 | animationDuration, 153 | emitter, 154 | listener, 155 | gfxGroup, 156 | gfxRect 157 | }); 158 | } 159 | 160 | updateAnimation() { 161 | const currentTime = Date.now(); 162 | 163 | this.impulses.forEach(impulse => { 164 | const { 165 | startTime, 166 | animationDuration, 167 | emitter, 168 | listener, 169 | gfxGroup, 170 | gfxRect 171 | } = impulse; 172 | 173 | const emitterMid = getMid(emitter); 174 | const listenerMid = getMid(listener); 175 | 176 | const startPoint = { 177 | x: emitterMid.x, 178 | y: emitterMid.y 179 | }; 180 | 181 | const endPoint = { 182 | x: listenerMid.x, 183 | y: listenerMid.y 184 | }; 185 | 186 | const time = currentTime - startTime; 187 | 188 | const newPoint = tweenPoint(startPoint, endPoint, time, animationDuration, 'eased'); 189 | 190 | translate(gfxGroup, newPoint.x, newPoint.y); 191 | 192 | let rotation = 193 | Math.atan2(listenerMid.x - emitterMid.x, listenerMid.y - emitterMid.y) 194 | * 180 / Math.PI; 195 | 196 | rotation = - rotation - 90; 197 | 198 | svgAttr(gfxRect, { 199 | transform: `rotate(${rotation})` 200 | }); 201 | 202 | if (currentTime >= startTime + animationDuration) { 203 | svgRemove(gfxGroup); 204 | 205 | this.impulses = this.impulses.filter(i => i !== impulse); 206 | } 207 | }); 208 | 209 | requestAnimationFrame(this.updateAnimation.bind(this)); 210 | } 211 | } 212 | 213 | EmissionAnimation.$inject = [ 'eventBus', 'canvas', 'nodeSequencerConfig', 'elementRegistry' ]; 214 | 215 | export default EmissionAnimation; 216 | -------------------------------------------------------------------------------- /src/features/emission-animation/index.js: -------------------------------------------------------------------------------- 1 | import EmissionAnimation from './EmissionAnimation'; 2 | 3 | export default { 4 | __init__: [ 'emissionAnimation' ], 5 | emissionAnimation: [ 'type', EmissionAnimation ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/emitter-animation/EmitterAnimation.js: -------------------------------------------------------------------------------- 1 | import { 2 | append as svgAppend, 3 | attr as svgAttr, 4 | clear as svgClear, 5 | create as svgCreate, 6 | remove as svgRemove 7 | } from 'tiny-svg'; 8 | 9 | import { isEmitter } from '../../util/NodeSequencerUtil'; 10 | 11 | class EmitterAnimation { 12 | constructor(eventBus, canvas, nodeSequencerConfig, elementRegistry) { 13 | const emitterAnimationLayer = canvas.getLayer('nodeSequencerEmitterAnimation', -900); 14 | 15 | this.circles = []; 16 | 17 | this.updateAnimation(); 18 | 19 | let soundsLoaded = false; 20 | 21 | eventBus.on('nodeSequencer.sounds.loaded', () => { 22 | soundsLoaded = true; 23 | }); 24 | 25 | eventBus.on('nodeSequencer.audio.loopStart', () => { 26 | if (!soundsLoaded) { 27 | return; 28 | } 29 | 30 | const emitters = elementRegistry.filter(e => isEmitter(e)); 31 | 32 | emitters.forEach(emitter => { 33 | const { x, y, width } = emitter; 34 | 35 | const circle = svgCreate('circle'); 36 | 37 | let radius = 20; 38 | 39 | svgAttr(circle, { 40 | cx: Math.round(x + (width / 2)), 41 | cy: Math.round(y + (width / 2)), 42 | r: radius 43 | }); 44 | 45 | svgAttr(circle, { 46 | stroke: 'none', 47 | fill: nodeSequencerConfig.emitterColor 48 | }); 49 | 50 | svgAppend(emitterAnimationLayer, circle); 51 | 52 | this.circles.push({ 53 | emitter, 54 | gfx: circle, 55 | radius, 56 | created: Date.now() 57 | }); 58 | }); 59 | }); 60 | 61 | eventBus.on('commandStack.shape.delete.postExecuted', ({ context }) => { 62 | const { shape } = context; 63 | 64 | this.circles.forEach(circle => { 65 | if (circle.emitter === shape) { 66 | svgRemove(circle.gfx); 67 | } 68 | }); 69 | 70 | this.circles = this.circles.filter(c => c.emitter !== shape); 71 | }); 72 | 73 | // diagram clear 74 | eventBus.on('diagram.clear', () => { 75 | svgClear(emitterAnimationLayer); 76 | 77 | this.circles = []; 78 | }); 79 | } 80 | 81 | updateAnimation() { 82 | requestAnimationFrame(this.updateAnimation.bind(this)); 83 | 84 | this.circles.forEach(circle => { 85 | circle.radius = Math.max(0, circle.radius - 1); 86 | 87 | svgAttr(circle.gfx, { 88 | r: circle.radius 89 | }); 90 | 91 | if (Date.now() - circle.created > 500) { 92 | svgRemove(circle.gfx); 93 | 94 | this.circles = this.circles.filter(c => c !== circle); 95 | } 96 | }); 97 | } 98 | } 99 | 100 | EmitterAnimation.$inject = [ 'eventBus', 'canvas', 'nodeSequencerConfig', 'elementRegistry' ]; 101 | 102 | export default EmitterAnimation; 103 | -------------------------------------------------------------------------------- /src/features/emitter-animation/index.js: -------------------------------------------------------------------------------- 1 | import EmitterAnimation from './EmitterAnimation'; 2 | 3 | export default { 4 | __init__: [ 'emitterAnimation' ], 5 | emitterAnimation: [ 'type', EmitterAnimation ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/emitter-preview/EmitterPreview.js: -------------------------------------------------------------------------------- 1 | import { 2 | append as svgAppend, 3 | attr as svgAttr, 4 | clear as svgClear, 5 | create as svgCreate 6 | } from 'tiny-svg'; 7 | 8 | import { translate } from 'diagram-js/lib/util/SvgTransformUtil'; 9 | 10 | import { isEmitter, isListener, isRoot } from '../../util/NodeSequencerUtil'; 11 | 12 | const STROKE_COLOR = 'rgba(0, 0, 255, 0.5)' 13 | 14 | function createEmitterPreview(cx, cy, timeSignature, maxDistance, offsetDistance) { 15 | const attrs = { 16 | stroke: STROKE_COLOR, 17 | strokeWidth: 1, 18 | fill: 'none' 19 | }; 20 | 21 | const preview = svgCreate('g'); 22 | 23 | const circleRadiusStep = (maxDistance - offsetDistance) / timeSignature; 24 | 25 | for (let i = 0; i < timeSignature; i++) { 26 | const circle = svgCreate('circle'); 27 | 28 | svgAttr(circle, { 29 | cx, 30 | cy, 31 | r: i * circleRadiusStep + circleRadiusStep 32 | }); 33 | 34 | svgAttr(circle, attrs); 35 | 36 | svgAppend(preview, circle); 37 | } 38 | 39 | return preview; 40 | } 41 | 42 | class EmitterPreview { 43 | constructor(eventBus, canvas, elementRegistry, nodeSequencerConfig) { 44 | const { maxDistance, offsetDistance } = nodeSequencerConfig; 45 | 46 | const emitterPreviewLayer = canvas.getLayer('nodeSequencerEmitterPreview', -1000); 47 | 48 | let ignoreSelectionChanged = false; 49 | 50 | eventBus.on('selection.changed', ({ newSelection }) => { 51 | 52 | if (ignoreSelectionChanged) { 53 | return; 54 | } 55 | 56 | svgClear(emitterPreviewLayer); 57 | 58 | if (newSelection.length !== 1) { 59 | return; 60 | } 61 | 62 | if (isEmitter(newSelection[0])) { 63 | const emitter = newSelection[0]; 64 | 65 | const { x, y, width, timeSignature } = emitter; 66 | 67 | const cx = Math.round(x + (width / 2)); 68 | const cy = Math.round(y + (width / 2)); 69 | 70 | const preview = createEmitterPreview(cx, cy, timeSignature, maxDistance, offsetDistance); 71 | 72 | svgAppend(emitterPreviewLayer, preview); 73 | } 74 | }); 75 | 76 | eventBus.on([ 77 | 'commandStack.nodeSequencer.changeProperties.executed', 78 | 'commandStack.nodeSequencer.changeProperties.reverted', 79 | 'commandStack.shape.move.executed', 80 | 'commandStack.shape.move.reverted' 81 | ], ({ context }) => { 82 | const element = context.element || context.shape; 83 | 84 | if (isRoot(element)) return; 85 | 86 | svgClear(emitterPreviewLayer); 87 | 88 | if (isEmitter(element)) { 89 | const emitter = element; 90 | 91 | const { x, y, width, timeSignature } = emitter; 92 | 93 | const cx = Math.round(x + (width / 2)); 94 | const cy = Math.round(y + (width / 2)); 95 | 96 | const preview = createEmitterPreview(cx, cy, timeSignature, maxDistance, offsetDistance); 97 | 98 | svgAppend(emitterPreviewLayer, preview); 99 | } 100 | }); 101 | 102 | eventBus.on('shape.move.start',({ context }) => { 103 | ignoreSelectionChanged = true; 104 | 105 | const { shapes } = context; 106 | 107 | const hasMovingListeners = shapes.filter(s => isListener(s)).length; 108 | 109 | const emitters = elementRegistry.filter(e => isEmitter(e)); 110 | 111 | const movingGroup = context.movingGroup = svgCreate('g'); 112 | const nonMovingGroup = svgCreate('g'); 113 | 114 | svgAppend(emitterPreviewLayer, movingGroup); 115 | svgAppend(emitterPreviewLayer, nonMovingGroup); 116 | 117 | emitters.forEach(emitter => { 118 | const { x, y, width, timeSignature } = emitter; 119 | 120 | const cx = Math.round(x + (width / 2)); 121 | const cy = Math.round(y + (width / 2)); 122 | 123 | const preview = createEmitterPreview(cx, cy, timeSignature, maxDistance, offsetDistance); 124 | 125 | if (shapes.includes(emitter)) { 126 | svgAppend(movingGroup, preview); 127 | } else if (hasMovingListeners) { 128 | svgAppend(nonMovingGroup, preview); 129 | } 130 | }); 131 | }); 132 | 133 | eventBus.on('shape.move.move', ({ dx, dy, context }) => { 134 | const { movingGroup } = context; 135 | 136 | translate(movingGroup, dx, dy); 137 | }); 138 | 139 | eventBus.on('create.start', ({ shape }) => { 140 | svgClear(emitterPreviewLayer); 141 | 142 | ignoreSelectionChanged = true; 143 | 144 | if (isListener(shape)) { 145 | const emitters = elementRegistry.filter(e => isEmitter(e)); 146 | 147 | emitters.forEach(emitter => { 148 | const { x, y, width, timeSignature } = emitter; 149 | 150 | const cx = Math.round(x + (width / 2)); 151 | const cy = Math.round(y + (width / 2)); 152 | 153 | const preview = createEmitterPreview(cx, cy, timeSignature, maxDistance, offsetDistance); 154 | 155 | svgAppend(emitterPreviewLayer, preview); 156 | }); 157 | } 158 | }); 159 | 160 | eventBus.on([ 161 | 'shape.move.end', 162 | 'shape.move.cancel', 163 | 'shape.move.rejected', 164 | 'create.end', 165 | 'create.cancel' 166 | ], () => { 167 | ignoreSelectionChanged = false; 168 | 169 | svgClear(emitterPreviewLayer); 170 | }); 171 | } 172 | } 173 | 174 | EmitterPreview.$inject = [ 'eventBus', 'canvas', 'elementRegistry', 'nodeSequencerConfig' ]; 175 | 176 | export default EmitterPreview; 177 | -------------------------------------------------------------------------------- /src/features/emitter-preview/index.js: -------------------------------------------------------------------------------- 1 | import EmitterPreview from './EmitterPreview'; 2 | 3 | export default { 4 | __init__: [ 'nodeSequencerEmitterPreview' ], 5 | nodeSequencerEmitterPreview: [ 'type', EmitterPreview ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/hot-cues/HotCues.js: -------------------------------------------------------------------------------- 1 | import { 2 | domify, 3 | attr as domAttr, 4 | classes as domClasses, 5 | event as domEvent, 6 | query as domQuery 7 | } from 'min-dom'; 8 | 9 | import { isEmitter, isListener } from '../../util/NodeSequencerUtil'; 10 | 11 | import ProgressIndicator from './ProgressIndicator'; 12 | 13 | class HotCues { 14 | constructor(eventBus, canvas, nodeSequencerConfig, commandStack, nodeSequencer, exportConfig) { 15 | this._eventBus = eventBus; 16 | this._canvas = canvas; 17 | this._commandStack = commandStack; 18 | this._nodeSequencer = nodeSequencer; 19 | 20 | this._progressIndicator = new ProgressIndicator(eventBus, canvas); 21 | 22 | this.init(); 23 | 24 | exportConfig.registerExport({ 25 | id: 'hot-cues', 26 | exportConfig: () => { 27 | return { 28 | slots: this._slots, 29 | activeSlot: this._activeSlot 30 | }; 31 | }, 32 | 33 | importConfig: (config) => { 34 | this._slots = config.slots; 35 | 36 | this._slots.forEach((slot, slotId) => { 37 | let slotEl = this._getSlotEl(slotId); 38 | 39 | if (slot !== null) { 40 | domClasses(slotEl).add('existing'); 41 | } 42 | }); 43 | 44 | this._setActive(config.activeSlot); 45 | } 46 | }); 47 | 48 | eventBus.on('commandStack.changed', () => { 49 | this.setDirty(); 50 | }); 51 | 52 | eventBus.on('nodeSequencer.audio.loopStart', 2000, context => { 53 | 54 | if (this._nextJump) { 55 | this._actualJumpTo(this._nextJump); 56 | 57 | this._nextJump = null; 58 | } 59 | }, this); 60 | } 61 | 62 | init() { 63 | this.$rootEl = domify(` 64 | 69 | `); 70 | 71 | this.$openCloseEl = domQuery('#open-close', this.$rootEl); 72 | 73 | this.$slotsEl = domQuery('.slots', this.$rootEl); 74 | 75 | this._slots = []; 76 | this._activeSlot = null; 77 | 78 | for (let i = 0; i < 9; i++) { 79 | 80 | let slotEl = domify(` 81 |
82 | ${i + 1} 83 | * 84 |
85 | `); 86 | 87 | if (this._slots[i]) { 88 | domClasses(slotEl).add('existing'); 89 | } 90 | 91 | domEvent.bind(slotEl, 'click', (e) => { 92 | if (e.ctrlKey) { 93 | 94 | // save slot 95 | this.saveSlot(i); 96 | } else { 97 | 98 | // jump to slot 99 | this.jumpTo(i); 100 | } 101 | }); 102 | 103 | domEvent.bind(slotEl, 'dblclick', (e) => { 104 | 105 | // save slot 106 | this.saveSlot(i); 107 | }); 108 | 109 | let active = false; 110 | 111 | this.$slotsEl.appendChild(slotEl); 112 | 113 | this.$openCloseEl.addEventListener('click', () => { 114 | if (active) { 115 | this.$rootEl.classList.add('hidden'); 116 | 117 | this.$openCloseEl.innerHTML = ''; 118 | } else { 119 | this.$rootEl.classList.remove('hidden'); 120 | 121 | this.$openCloseEl.innerHTML = ''; 122 | } 123 | 124 | active = !active; 125 | }); 126 | } 127 | 128 | domEvent.bind(this.$slotsEl, 'mousedown', event => { 129 | event.stopPropagation(); 130 | 131 | var target = event.target; 132 | 133 | var slotId = parseInt(domAttr(target, 'data-id'), 10); 134 | 135 | this.jumpTo(slotId); 136 | }); 137 | 138 | this._canvas.getContainer().appendChild(this.$rootEl); 139 | } 140 | 141 | saveSlot(slotId) { 142 | this._slots[slotId] = this._nodeSequencer._save(); 143 | 144 | let slotEl = this._getSlotEl(slotId); 145 | 146 | if (slotEl) { 147 | domClasses(slotEl).add('existing'); 148 | domClasses(slotEl).add('pulse'); 149 | domClasses(slotEl).remove('infinite'); 150 | 151 | // setTimeout(function() { 152 | // domClasses(slotEl).remove('saved'); 153 | // }, 500); 154 | 155 | this._setActive(slotId); 156 | } 157 | } 158 | 159 | _actualJumpTo(jumpTarget) { 160 | let { 161 | slotId 162 | } = jumpTarget; 163 | 164 | let slot = this._slots[slotId]; 165 | 166 | this._nodeSequencer._load(slot); 167 | 168 | this._setActive(slotId); 169 | } 170 | 171 | _setActive(slotId) { 172 | this._activeSlot = slotId; 173 | 174 | let slotEl = this._getSlotEl(slotId); 175 | 176 | var activeSlotEl = this._getActiveSlotEl(); 177 | 178 | if (activeSlotEl) { 179 | domClasses(activeSlotEl).remove('active'); 180 | domClasses(activeSlotEl).remove('dirty'); 181 | } 182 | 183 | if (slotEl) { 184 | // domClasses(slotEl).remove('jumping'); 185 | domClasses(slotEl).remove('pulse'); 186 | domClasses(slotEl).add('active'); 187 | 188 | this._progressIndicator.drawOn(slotEl); 189 | } 190 | } 191 | 192 | _getActiveSlotEl() { 193 | return domQuery(`.active`, this.$slotsEl); 194 | } 195 | 196 | _getSlotEl(slotId) { 197 | return domQuery(`[data-id='${slotId}']`, this.$slotsEl); 198 | } 199 | 200 | setDirty() { 201 | let activeEl = this._getActiveSlotEl(); 202 | 203 | if (activeEl) { 204 | domClasses(activeEl).add('dirty'); 205 | } 206 | } 207 | 208 | jumpTo(slotId) { 209 | 210 | if (!this._slots[slotId]) { 211 | return; 212 | } 213 | 214 | let slotEl = this._getSlotEl(slotId); 215 | 216 | domClasses(slotEl).add('pulse'); 217 | domClasses(slotEl).add('infinite'); 218 | 219 | this._nextJump = { slotId }; 220 | } 221 | } 222 | 223 | HotCues.$inject = [ 224 | 'eventBus', 225 | 'canvas', 226 | 'nodeSequencerConfig', 227 | 'commandStack', 228 | 'nodeSequencer', 229 | 'exportConfig' 230 | ]; 231 | 232 | export default HotCues; 233 | -------------------------------------------------------------------------------- /src/features/hot-cues/ProgressIndicator.js: -------------------------------------------------------------------------------- 1 | import { tweenLinear } from '../../util/TweenUtil'; 2 | 3 | const MILLIS_PER_MINUTE = 60000; 4 | 5 | import { 6 | domify, 7 | attr as domAttr, 8 | classes as domClasses, 9 | event as domEvent, 10 | query as domQuery 11 | } from 'min-dom'; 12 | 13 | class ProgressIndicator { 14 | 15 | constructor(eventBus, canvas) { 16 | this.timeLastLoopStart = 0; 17 | this._canvas = canvas; 18 | 19 | eventBus.on('nodeSequencer.audio.loopStart', 4000, context => { 20 | this.timeLastLoopStart = Date.now(); 21 | }); 22 | 23 | eventBus.on('diagram.destroy', () => { 24 | this._destroyed = true; 25 | }); 26 | 27 | this._init(); 28 | 29 | eventBus.on([ 30 | 'nodeSequencer.create.end', 31 | 'nodeSequencer.load.end' 32 | ], () => { 33 | this.updateAnimation(); 34 | }); 35 | } 36 | 37 | _init() { 38 | 39 | this.$rootEl = domify(` 40 |
41 |
42 |
43 | `); 44 | 45 | this.$progressEl = domQuery('.progress', this.$rootEl); 46 | } 47 | 48 | drawOn(parentEl) { 49 | 50 | if (!parentEl) { 51 | if (this.$rootEl.parentNode) { 52 | this.$rootEl.parentNode.removeChild(parentEl); 53 | } 54 | } 55 | 56 | parentEl.appendChild(this.$rootEl); 57 | } 58 | 59 | updateAnimation() { 60 | 61 | if (this._destroyed) { 62 | return; 63 | } 64 | 65 | const currentTime = Date.now(); 66 | 67 | const { tempo } = this._canvas.getRootElement(); 68 | 69 | // we are in 4/4, quarter note = beat 70 | const quarterNoteDuration = MILLIS_PER_MINUTE / tempo, 71 | sixteenthNoteDuration = quarterNoteDuration / 4; 72 | 73 | const loopDuration = sixteenthNoteDuration * 16; 74 | 75 | const elapsedLoopTime = currentTime - this.timeLastLoopStart; 76 | 77 | // TODO: fix, should be 0 to 100 78 | const newProgress = tweenLinear(0, 110, elapsedLoopTime, loopDuration); 79 | 80 | // console.log('progress: ' + newProgress); 81 | 82 | this.$progressEl.style.width = newProgress + '%'; 83 | 84 | requestAnimationFrame(this.updateAnimation.bind(this)); 85 | } 86 | } 87 | 88 | ProgressIndicator.$inject = [ 'eventBus', 'canvas' ]; 89 | 90 | export default ProgressIndicator; -------------------------------------------------------------------------------- /src/features/hot-cues/index.js: -------------------------------------------------------------------------------- 1 | import HotCues from './HotCues'; 2 | import ProgressIndicator from './ProgressIndicator'; 3 | 4 | export default { 5 | __init__: [ 6 | 'hotCues', 7 | 'progressIndicator' 8 | ], 9 | hotCues: [ 'type', HotCues ], 10 | progressIndicator: [ 'type', ProgressIndicator ] 11 | }; 12 | -------------------------------------------------------------------------------- /src/features/keyboard-bindings/KeyboardBindings.js: -------------------------------------------------------------------------------- 1 | import { event as domEvent } from 'min-dom'; 2 | 3 | class KeyboardBindings { 4 | 5 | constructor(eventBus, keyboard, canvas, elementRegistry, selection, lassoTool, handTool, hotCues) { 6 | this._canvas = canvas; 7 | this._elementRegistry = elementRegistry; 8 | this._selection = selection; 9 | 10 | let handToolActive = false; 11 | 12 | domEvent.bind(document, 'keydown', e => { 13 | if (!handToolActive && e.keyCode === 32) { 14 | handTool.toggle(); 15 | 16 | handTool.activateMove(); 17 | 18 | handToolActive = true; 19 | 20 | e.stopPropagation(); 21 | } 22 | }); 23 | 24 | domEvent.bind(document, 'keyup', e => { 25 | if (handToolActive && e.keyCode === 32) { 26 | handTool.toggle(); 27 | 28 | handToolActive = false; 29 | 30 | e.stopPropagation(); 31 | } 32 | }); 33 | 34 | keyboard.addListener((key, modifiers) => { 35 | 36 | // ctrl + a -> select all elements 37 | if (key === 65 && keyboard.isCmd(modifiers)) { 38 | this.selectAllElements(); 39 | 40 | return true; 41 | } 42 | 43 | // bind CTRL + 1..0 to 44 | // bind 1..0 to 45 | if (key >= 49 && key <= 57) { 46 | var slot = key - 49; 47 | 48 | if (keyboard.isCmd(modifiers)) { 49 | hotCues.saveSlot(slot); 50 | } else { 51 | hotCues.jumpTo(slot); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | // l -> activate lasso tool 58 | if (key === 76) { 59 | lassoTool.toggle(); 60 | 61 | return true; 62 | } 63 | 64 | // 72 -> toggle help overlay 65 | if (key === 72) { 66 | eventBus.fire('helpOverlay.toggle'); 67 | 68 | return true; 69 | } 70 | 71 | // 27 -> clear selection 72 | if (key === 27) { 73 | 74 | if (this._selection.get().length) { 75 | this._selection.select(null); 76 | } 77 | } 78 | 79 | }); 80 | } 81 | 82 | selectAllElements() { 83 | const rootElement = this._canvas.getRootElement(); 84 | 85 | const elements = this._elementRegistry.filter(function(element) { 86 | return element !== rootElement; 87 | }); 88 | 89 | this._selection.select(elements); 90 | 91 | return elements; 92 | } 93 | } 94 | 95 | KeyboardBindings.$inject = [ 96 | 'eventBus', 97 | 'keyboard', 98 | 'canvas', 99 | 'elementRegistry', 100 | 'selection', 101 | 'lassoTool', 102 | 'handTool', 103 | 'hotCues' 104 | ]; 105 | 106 | export default KeyboardBindings; 107 | -------------------------------------------------------------------------------- /src/features/keyboard-bindings/index.js: -------------------------------------------------------------------------------- 1 | import KeyboardBindings from './KeyboardBindings'; 2 | 3 | export default { 4 | __init__: [ 'keyboardBindings' ], 5 | keyboardBindings: [ 'type', KeyboardBindings ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/kit-select/KitSelect.js: -------------------------------------------------------------------------------- 1 | import { 2 | domify, 3 | classes as domClasses, 4 | event as domEvent, 5 | query as domQuery 6 | } from 'min-dom'; 7 | 8 | import { isRoot } from '../../util/NodeSequencerUtil'; 9 | 10 | class KitSelect { 11 | constructor(canvas, eventBus, nodeSequencerConfig, nodeSequencerModeling) { 12 | this._canvas = canvas; 13 | this._eventBus = eventBus; 14 | this._nodeSequencerConfig = nodeSequencerConfig; 15 | this._nodeSequencerModeling = nodeSequencerModeling; 16 | 17 | this.presets = {}; 18 | 19 | this.init(); 20 | 21 | eventBus.on([ 22 | 'commandStack.nodeSequencer.changeProperties.executed', 23 | 'commandStack.nodeSequencer.changeProperties.reverted' 24 | ], ({ context }) => { 25 | const element = context.element; 26 | 27 | if (isRoot(element)) { 28 | this.update(element.soundKit); 29 | } 30 | }); 31 | 32 | eventBus.on('sounds.setSoundKit', ({ soundKit }) => { 33 | this.update(soundKit); 34 | }); 35 | } 36 | 37 | init() { 38 | const container = this.container = domify( 39 | '
' + 40 | 'Presets' + 41 | '
' 42 | ); 43 | 44 | this._canvas.getContainer().appendChild(container); 45 | 46 | Object.keys(this._nodeSequencerConfig.soundKits).forEach((key, index) => { 47 | const label = index + 1; 48 | 49 | const preset = domify(`
${label}
`); 50 | 51 | domEvent.bind(preset, 'click', () => { 52 | const rootElement = this._canvas.getRootElement(); 53 | 54 | this._nodeSequencerModeling.changeProperties(rootElement, { 55 | soundKit: key 56 | }); 57 | 58 | this.update(key); 59 | }); 60 | 61 | this.container.appendChild(preset); 62 | 63 | this.presets[ key ] = preset; 64 | }); 65 | 66 | this.update(this._nodeSequencerConfig.initialSoundKit); 67 | } 68 | 69 | update(activeSoundKit) { 70 | Object.entries(this.presets).forEach((entry) => { 71 | const [ key, preset ] = entry; 72 | 73 | if (key === activeSoundKit) { 74 | preset.classList.add('active'); 75 | } else { 76 | preset.classList.remove('active'); 77 | } 78 | }) 79 | } 80 | } 81 | 82 | KitSelect.$inject = [ 'canvas', 'eventBus', 'nodeSequencerConfig', 'nodeSequencerModeling' ]; 83 | 84 | export default KitSelect; -------------------------------------------------------------------------------- /src/features/kit-select/index.js: -------------------------------------------------------------------------------- 1 | import KitSelect from './KitSelect'; 2 | 3 | export default { 4 | __init__: [ 'kitSelect' ], 5 | kitSelect: [ 'type', KitSelect ] 6 | }; -------------------------------------------------------------------------------- /src/features/listener-animation/ListenerAnimation.js: -------------------------------------------------------------------------------- 1 | import { 2 | append as svgAppend, 3 | attr as svgAttr, 4 | clear as svgClear, 5 | create as svgCreate, 6 | remove as svgRemove 7 | } from 'tiny-svg'; 8 | 9 | class ListenerAnimation { 10 | constructor(eventBus, canvas, nodeSequencerConfig) { 11 | const listenerAnimationLayer = canvas.getLayer('nodeSequencerListenerAnimation', -800); 12 | 13 | this.circles = []; 14 | 15 | this.updateAnimation(); 16 | 17 | eventBus.on('nodeSequencer.audio.playSound', ({ listener }) => { 18 | const { x, y, width } = listener; 19 | 20 | const circle = svgCreate('circle'); 21 | 22 | let radius = 20; 23 | 24 | svgAttr(circle, { 25 | cx: Math.round(x + (width / 2)), 26 | cy: Math.round(y + (width / 2)), 27 | r: radius 28 | }); 29 | 30 | svgAttr(circle, { 31 | stroke: 'none', 32 | fill: nodeSequencerConfig.emitterColor 33 | }); 34 | 35 | svgAppend(listenerAnimationLayer, circle); 36 | 37 | this.circles.push({ 38 | listener, 39 | gfx: circle, 40 | radius, 41 | created: Date.now() 42 | }); 43 | }); 44 | 45 | eventBus.on('commandStack.shape.delete.postExecuted', ({ context }) => { 46 | const { shape } = context; 47 | 48 | this.circles.forEach(circle => { 49 | if (circle.listener === shape) { 50 | svgRemove(circle.gfx); 51 | } 52 | }); 53 | 54 | this.circles = this.circles.filter(c => c.listener !== shape); 55 | }); 56 | 57 | // diagram clear 58 | eventBus.on('diagram.clear', () => { 59 | svgClear(listenerAnimationLayer); 60 | 61 | this.circles = []; 62 | }); 63 | } 64 | 65 | updateAnimation() { 66 | requestAnimationFrame(this.updateAnimation.bind(this)); 67 | 68 | this.circles.forEach(circle => { 69 | circle.radius = Math.max(0, circle.radius - 1); 70 | 71 | svgAttr(circle.gfx, { 72 | r: circle.radius 73 | }); 74 | 75 | if (Date.now() - circle.created > 500) { 76 | svgRemove(circle.gfx); 77 | 78 | this.circles = this.circles.filter(c => c !== circle); 79 | } 80 | }); 81 | } 82 | } 83 | 84 | ListenerAnimation.$inject = [ 'eventBus', 'canvas', 'nodeSequencerConfig' ]; 85 | 86 | export default ListenerAnimation; 87 | -------------------------------------------------------------------------------- /src/features/listener-animation/index.js: -------------------------------------------------------------------------------- 1 | import ListenerAnimation from './ListenerAnimation'; 2 | 3 | export default { 4 | __init__: [ 'listenerAnimation' ], 5 | listenerAnimation: [ 'type', ListenerAnimation ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/modeling/Modeling.js: -------------------------------------------------------------------------------- 1 | import ChangePropertiesHandler from './cmd/ChangePropertiesHandler'; 2 | 3 | class Modeling { 4 | constructor(commandStack) { 5 | this._commandStack = commandStack; 6 | 7 | commandStack.registerHandler('nodeSequencer.changeProperties', ChangePropertiesHandler); 8 | } 9 | 10 | changeProperties(element, properties) { 11 | this._commandStack.execute('nodeSequencer.changeProperties', { 12 | element, 13 | properties 14 | }); 15 | } 16 | } 17 | 18 | Modeling.$inject = [ 'commandStack' ]; 19 | 20 | export default Modeling; -------------------------------------------------------------------------------- /src/features/modeling/NodeSequencerUpdater.js: -------------------------------------------------------------------------------- 1 | import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; 2 | 3 | import { isRoot, isEmitter, isListener } from '../../util/NodeSequencerUtil'; 4 | import { getSequence } from '../../util/SequenceUtil'; 5 | import { getDistance } from '../../util/GeometryUtil'; 6 | 7 | function connected(source, target) { 8 | if (!source.outgoing.length || !target.incoming.length) { 9 | return false; 10 | } 11 | 12 | let connected = false; 13 | 14 | source.outgoing.forEach(outgoing => { 15 | target.incoming.forEach(incoming => { 16 | if (outgoing === incoming) { 17 | connected = true; 18 | } 19 | }) 20 | }); 21 | 22 | return connected; 23 | } 24 | 25 | 26 | /** 27 | * Update p5.sound properties after properties change. 28 | * 29 | * Changes are always applied immediately, i.e. during 30 | * and phases. 31 | */ 32 | class NodeSequencerUpdater extends CommandInterceptor { 33 | 34 | constructor(eventBus, audio, sounds, elementRegistry, nodeSequencerConfig) { 35 | 36 | super(eventBus); 37 | 38 | const { maxDistance, offsetDistance } = nodeSequencerConfig; 39 | 40 | const mainPart = audio.getMainPart(); 41 | 42 | 43 | function updateListener(listener, soundId) { 44 | 45 | // if mock sound and no sound assignment => no change needed 46 | if (!soundId && !listener.sound) { 47 | return; 48 | } 49 | 50 | const onPlay = function() { 51 | eventBus.fire('nodeSequencer.audio.playSound', { 52 | listener 53 | }); 54 | }; 55 | 56 | const { sound } = sounds.getSound(soundId || listener.sound); 57 | const oldPhrase = mainPart.getPhrase(listener.id); 58 | 59 | if (!oldPhrase) { 60 | 61 | // no sequence to update 62 | return; 63 | } 64 | 65 | const newPhrase = new p5.Phrase(listener.id, (time, playbackRate) => { 66 | sound.rate(playbackRate); 67 | sound.play(time); 68 | onPlay(); 69 | }, oldPhrase.sequence); 70 | 71 | mainPart.removePhrase(listener.id); 72 | mainPart.addPhrase(newPhrase); 73 | } 74 | 75 | 76 | function updateSounds(element, properties) { 77 | 78 | if (isRoot(element)) { 79 | 80 | const { tempo, soundKit } = properties; 81 | 82 | // updates BPM on p5.sound part after BPM change 83 | if (tempo) { 84 | mainPart.setBPM(tempo); 85 | } 86 | 87 | // update p5.sounds after sound kit change 88 | if (soundKit) { 89 | sounds.setSoundKit(soundKit); 90 | 91 | const listeners = elementRegistry.filter(element => isListener(element)); 92 | 93 | listeners.forEach(listener => { 94 | updateListener(listener, listener.sound); 95 | }); 96 | } 97 | } else if (isEmitter(element)) { 98 | 99 | // NOOP; we still do this in POST EXECUTE 100 | } else if (isListener(element)) { 101 | updateListener(element, properties.sound); 102 | } 103 | } 104 | 105 | this.postExecute('nodeSequencer.changeProperties', ({ context }) => { 106 | 107 | const { element, properties } = context; 108 | 109 | // update time signature 110 | if (properties.timeSignature) { 111 | const listeners = elementRegistry.filter(e => { 112 | return isListener(e) && connected(element, e); 113 | }); 114 | 115 | listeners.forEach(listener => { 116 | const distance = getDistance(element, listener); 117 | 118 | const sequence = getSequence(distance, maxDistance, offsetDistance, properties.timeSignature); 119 | 120 | audio.updateSequence(sequence, element, listener); 121 | }); 122 | } 123 | }); 124 | 125 | this.executed('nodeSequencer.changeProperties', ({ context }) => { 126 | 127 | const { element, properties } = context; 128 | 129 | updateSounds(element, properties); 130 | }); 131 | 132 | this.reverted('nodeSequencer.changeProperties', ({ context }) => { 133 | 134 | const { element, oldProperties } = context; 135 | 136 | updateSounds(element, oldProperties); 137 | }); 138 | 139 | } 140 | } 141 | 142 | NodeSequencerUpdater.$inject = [ 143 | 'eventBus', 144 | 'audio', 145 | 'sounds', 146 | 'elementRegistry', 147 | 'nodeSequencerConfig' 148 | ]; 149 | 150 | export default NodeSequencerUpdater; 151 | -------------------------------------------------------------------------------- /src/features/modeling/cmd/ChangePropertiesHandler.js: -------------------------------------------------------------------------------- 1 | class ChangePropertiesHandler { 2 | 3 | execute(context) { 4 | const { element, properties } = context; 5 | 6 | const oldProperties = {}; 7 | 8 | Object.keys(properties).forEach(propertyKey => { 9 | 10 | // copy old property 11 | oldProperties[propertyKey] = element[propertyKey]; 12 | 13 | // set new property 14 | element[propertyKey] = properties[propertyKey]; 15 | }); 16 | 17 | context.oldProperties = oldProperties; 18 | 19 | return element; 20 | } 21 | 22 | revert(context) { 23 | const { element, oldProperties } = context; 24 | 25 | Object.keys(oldProperties).forEach(propertyKey => { 26 | 27 | // set new property 28 | element[propertyKey] = oldProperties[propertyKey]; 29 | }); 30 | 31 | return element; 32 | } 33 | } 34 | 35 | export default ChangePropertiesHandler; 36 | -------------------------------------------------------------------------------- /src/features/modeling/index.js: -------------------------------------------------------------------------------- 1 | import NodeSequencerUpdater from './NodeSequencerUpdater'; 2 | import Modeling from './Modeling'; 3 | 4 | export default { 5 | __init__: [ 'nodeSequencerUpdater', 'nodeSequencerModeling' ], 6 | nodeSequencerUpdater: [ 'type', NodeSequencerUpdater ], 7 | nodeSequencerModeling: [ 'type', Modeling ] 8 | }; 9 | -------------------------------------------------------------------------------- /src/features/move-preview/NodeSequencerMovePreview.js: -------------------------------------------------------------------------------- 1 | import { 2 | append as svgAppend, 3 | clear as svgClear, 4 | create as svgCreate, 5 | remove as svgRemove 6 | } from 'tiny-svg'; 7 | 8 | import { 9 | classes as domClasses 10 | } from 'min-dom'; 11 | 12 | import { translate } from 'diagram-js/lib/util/SvgTransformUtil'; 13 | 14 | import { createLine, updateLine } from 'diagram-js/lib/util/RenderUtil'; 15 | 16 | import { isConnection, isRoot } from '../../util/NodeSequencerUtil'; 17 | 18 | import { getMid } from '../../util/GeometryUtil'; 19 | 20 | const LOW_PRIORITY = 500; 21 | 22 | // TODO: fix buggy implementation 23 | class NodeSequencerMovePreview { 24 | constructor( 25 | eventBus, 26 | canvas, 27 | previewSupport, 28 | elementRegistry, 29 | nodeSequencerRules, 30 | nodeSequencerConnectionCropping 31 | ) { 32 | this._nodeSequencerConnectionCropping = nodeSequencerConnectionCropping; 33 | 34 | this.connectionPreviews = []; 35 | 36 | const movePreviewLayer = this.movePreviewLayer = canvas.getLayer('move-preview', -100); 37 | 38 | eventBus.on('shape.move.start', LOW_PRIORITY, ({ context }) => { 39 | if (!context.dragGroup) { 40 | const dragGroup = svgCreate('g'); 41 | 42 | domClasses(dragGroup).add('nodeSequencer-move-group'); 43 | 44 | svgAppend(movePreviewLayer, dragGroup); 45 | 46 | context.dragGroup = dragGroup; 47 | } 48 | 49 | const { shapes } = context; 50 | 51 | shapes.forEach(shape => { 52 | previewSupport.addDragger(shape, context.dragGroup); 53 | }); 54 | 55 | context.nonMovingShapes = context.nonMovingShapes = elementRegistry.filter(s => { 56 | return !shapes.includes(s) && 57 | !isRoot(s) && 58 | !isConnection(s); 59 | }); 60 | }); 61 | 62 | eventBus.on('shape.move.move', LOW_PRIORITY, event => { 63 | const { context, dx, dy } = event; 64 | 65 | const { shapes, nonMovingShapes, dragGroup } = context; 66 | 67 | translate(dragGroup, dx, dy); 68 | 69 | // shapes.forEach(shape => { 70 | // nonMovingShapes.forEach(nonMovingShape => { 71 | 72 | // const movedShape = { 73 | // id: shape.id, 74 | // type: shape.type, 75 | // x: shape.x + dx, 76 | // y: shape.y + dy, 77 | // width: shape.width, 78 | // height: shape.height 79 | // }; 80 | 81 | // // check for possible connections 82 | // if (nodeSequencerRules.canConnect(movedShape, nonMovingShape) 83 | // && !this.hasPreview(movedShape, nonMovingShape)) { 84 | // this.addConnectionPreview(movedShape, nonMovingShape); 85 | // } 86 | // }); 87 | // }); 88 | 89 | // this.connectionPreviews.forEach(connectionPreview => { 90 | // const { gfx, movingShape, nonMovingShape } = connectionPreview; 91 | 92 | // const movingShapeWithDelta = { 93 | // id: movingShape.id, 94 | // type: movingShape.type, 95 | // x: movingShape.x + dx, 96 | // y: movingShape.y + dy, 97 | // width: movingShape.width, 98 | // height: movingShape.height 99 | // }; 100 | 101 | // if (!nodeSequencerRules.canConnect(movingShapeWithDelta, nonMovingShape)) { 102 | 103 | // // remove 104 | // this.removeConnectionPreview(connectionPreview); 105 | // } else { 106 | 107 | // // update 108 | // this.updateConnectionPreview(connectionPreview, dx, dy); 109 | // } 110 | // }); 111 | }); 112 | 113 | eventBus.on([ 'shape.move.cleanup' ], LOW_PRIORITY, ({ context }) => { 114 | svgClear(movePreviewLayer); 115 | 116 | this.connectionPreviews = []; 117 | }); 118 | } 119 | 120 | addConnectionPreview(movingShape, nonMovingShape) { 121 | const movingShapeMid = getMid(movingShape), 122 | nonMovingShapeMid = getMid(nonMovingShape); 123 | 124 | const croppedWaypoints = 125 | this._nodeSequencerConnectionCropping.getCroppedWaypoints(movingShapeMid, nonMovingShapeMid); 126 | 127 | const attrs = { 128 | stroke: 'red' 129 | }; 130 | 131 | const connectionPreview = createLine(croppedWaypoints, attrs); 132 | 133 | svgAppend(this.movePreviewLayer, connectionPreview); 134 | 135 | this.connectionPreviews.push({ 136 | gfx: connectionPreview, 137 | movingShape, 138 | nonMovingShape 139 | }); 140 | } 141 | 142 | updateConnectionPreview(connectionPreview, dx, dy) { 143 | const { gfx, movingShape, nonMovingShape } = connectionPreview; 144 | 145 | const movingShapeMid = getMid(movingShape), 146 | nonMovingShapeMid = getMid(nonMovingShape); 147 | 148 | movingShapeMid.x += dx; 149 | movingShapeMid.y += dy; 150 | 151 | const croppedWaypoints = 152 | this._nodeSequencerConnectionCropping.getCroppedWaypoints(movingShapeMid, nonMovingShapeMid); 153 | 154 | updateLine(gfx, croppedWaypoints); 155 | } 156 | 157 | removeConnectionPreview(connectionPreview) { 158 | const { gfx } = connectionPreview; 159 | 160 | svgRemove(gfx); 161 | 162 | this.connectionPreviews = 163 | this.connectionPreviews.filter(p => p !== connectionPreview); 164 | } 165 | 166 | // TODO: fix, why does object to object comparism not work? 167 | hasPreview(a, b) { 168 | let hasPreview = false; 169 | 170 | this.connectionPreviews.forEach(({ movingShape, nonMovingShape }) => { 171 | if ((movingShape.id === a.id && nonMovingShape.id === b.id) || 172 | (movingShape.id === b.id && nonMovingShape.id === a.id)) { 173 | hasPreview = true; 174 | } 175 | }); 176 | 177 | return hasPreview; 178 | } 179 | } 180 | 181 | NodeSequencerMovePreview.$inject = [ 182 | 'eventBus', 183 | 'canvas', 184 | 'previewSupport', 185 | 'elementRegistry', 186 | 'nodeSequencerRules', 187 | 'nodeSequencerConnectionCropping' 188 | ]; 189 | 190 | export default NodeSequencerMovePreview; 191 | -------------------------------------------------------------------------------- /src/features/move-preview/index.js: -------------------------------------------------------------------------------- 1 | import NodeSequencerMovePreview from './NodeSequencerMovePreview'; 2 | 3 | export default { 4 | __init__: [ 'nodeSequencerMovePreview' ], 5 | nodeSequencerMovePreview: [ 'type', NodeSequencerMovePreview ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/ordering/NodeSequencerOrderingProvider.js: -------------------------------------------------------------------------------- 1 | import OrderingProvider from 'diagram-js/lib/features/ordering/OrderingProvider'; 2 | 3 | import { find } from 'lodash-es'; 4 | import { findIndex } from 'lodash-es'; 5 | 6 | import { isConnection } from '../../util/NodeSequencerUtil'; 7 | 8 | class NodeSequencerOrderingProvider extends OrderingProvider { 9 | constructor(eventBus) { 10 | super(eventBus); 11 | 12 | this.getOrdering = (element, newParent) => { 13 | if (isConnection(element)) { 14 | return { 15 | index: 0, 16 | newParent 17 | }; 18 | } else { 19 | const index = newParent.children.length - 1; 20 | return { 21 | index, 22 | newParent 23 | }; 24 | } 25 | } 26 | } 27 | } 28 | 29 | NodeSequencerOrderingProvider.$inject = [ 'eventBus' ]; 30 | 31 | export default NodeSequencerOrderingProvider; 32 | -------------------------------------------------------------------------------- /src/features/ordering/index.js: -------------------------------------------------------------------------------- 1 | import NodeSequencerOrderingProvider from './NodeSequencerOrderingProvider'; 2 | 3 | export default { 4 | __init__: [ 'nodeSequencerOrderingProvider' ], 5 | nodeSequencerOrderingProvider: [ 'type', NodeSequencerOrderingProvider ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/overridden/Outline.js: -------------------------------------------------------------------------------- 1 | import { getBBox } from 'diagram-js/lib/util/Elements'; 2 | 3 | import { 4 | append as svgAppend, 5 | attr as svgAttr, 6 | create as svgCreate 7 | } from 'tiny-svg'; 8 | 9 | import { 10 | query as domQuery 11 | } from 'min-dom'; 12 | 13 | const LOW_PRIORITY = 500; 14 | 15 | 16 | /** 17 | * @class 18 | * 19 | * A plugin that adds an outline to shapes and connections that may be activated and styled 20 | * via CSS classes. 21 | * 22 | * @param {EventBus} eventBus 23 | * @param {ElementRegistry} elementRegistry 24 | */ 25 | function Outline(eventBus, elementRegistry) { 26 | this.offset = 6; 27 | 28 | var self = this; 29 | 30 | function createOutline(gfx, bounds) { 31 | var outline = svgCreate('circle'); 32 | 33 | outline.classList.add('djs-outline'); 34 | 35 | svgAttr(outline, Object.assign({ 36 | cx: 10, 37 | cy: 10, 38 | r: 20 39 | }, { 40 | style: { 41 | fill: 'none' 42 | } 43 | })); 44 | 45 | svgAppend(gfx, outline); 46 | 47 | return outline; 48 | } 49 | 50 | // A low priortity is necessary, because outlines of labels have to be updated 51 | // after the label bounds have been updated in the renderer. 52 | eventBus.on([ 'shape.added', 'shape.changed' ], LOW_PRIORITY, function(event) { 53 | var element = event.element, 54 | gfx = event.gfx; 55 | 56 | var outline = domQuery('.djs-outline', gfx); 57 | 58 | if (!outline) { 59 | outline = createOutline(gfx, element); 60 | } 61 | 62 | self.updateShapeOutline(outline, element); 63 | }); 64 | 65 | eventBus.on([ 'connection.added', 'connection.changed' ], function(event) { 66 | var element = event.element, 67 | gfx = event.gfx; 68 | 69 | var outline = domQuery('.djs-outline', gfx); 70 | 71 | if (!outline) { 72 | outline = createOutline(gfx, element); 73 | } 74 | 75 | self.updateConnectionOutline(outline, element); 76 | }); 77 | } 78 | 79 | 80 | /** 81 | * Updates the outline of a shape respecting the dimension of the 82 | * element and an outline offset. 83 | * 84 | * @param {SVGElement} outline 85 | * @param {djs.model.Base} element 86 | */ 87 | Outline.prototype.updateShapeOutline = function(outline, element) { 88 | 89 | svgAttr(outline, { 90 | x: -this.offset, 91 | y: -this.offset, 92 | width: element.width + this.offset * 2, 93 | height: element.height + this.offset * 2 94 | }); 95 | 96 | }; 97 | 98 | 99 | /** 100 | * Updates the outline of a connection respecting the bounding box of 101 | * the connection and an outline offset. 102 | * 103 | * @param {SVGElement} outline 104 | * @param {djs.model.Base} element 105 | */ 106 | Outline.prototype.updateConnectionOutline = function(outline, connection) { 107 | 108 | var bbox = getBBox(connection); 109 | 110 | svgAttr(outline, { 111 | x: bbox.x - this.offset, 112 | y: bbox.y - this.offset, 113 | width: bbox.width + this.offset * 2, 114 | height: bbox.height + this.offset * 2 115 | }); 116 | 117 | }; 118 | 119 | 120 | Outline.$inject = ['eventBus', 'elementRegistry']; 121 | 122 | export default Outline; 123 | -------------------------------------------------------------------------------- /src/features/overridden/index.js: -------------------------------------------------------------------------------- 1 | import Outline from './Outline'; 2 | 3 | export default { 4 | __init__: [ 'outline' ], 5 | outline: [ 'type', Outline ] 6 | }; -------------------------------------------------------------------------------- /src/features/palette/NodeSequencerPalette.js: -------------------------------------------------------------------------------- 1 | class NodeSequencerPalette { 2 | constructor(palette, create, nodeSequencerElementFactory, lassoTool) { 3 | this._create = create; 4 | this._nodeSequencerElementFactory = nodeSequencerElementFactory; 5 | this._lassoTool = lassoTool; 6 | 7 | palette.registerProvider(this); 8 | } 9 | 10 | getPaletteEntries(element) { 11 | 12 | const actions = {}, 13 | create = this._create, 14 | nodeSequencerElementFactory = this._nodeSequencerElementFactory, 15 | lassoTool = this._lassoTool; 16 | 17 | const createAction = (type, group, className, title, options) => { 18 | 19 | const createShape = event => { 20 | const shape = nodeSequencerElementFactory.create(type); 21 | 22 | create.start(event, shape); 23 | } 24 | 25 | const shortType = type.replace(/^nodeSequencer\:/, ''); 26 | 27 | return { 28 | group: group, 29 | className: className, 30 | title: title || 'Create ' + shortType, 31 | action: { 32 | dragstart: createShape, 33 | click: createShape 34 | } 35 | }; 36 | } 37 | 38 | Object.assign(actions, { 39 | 'nodeSequencer-emitter': createAction( 40 | 'emitter', 'nodeSequencer', 'icon-node-sequencer-emitter' 41 | ), 42 | 'nodeSequencer-listener': createAction( 43 | 'listener', 'nodeSequencer', 'icon-node-sequencer-listener' 44 | ) 45 | }); 46 | 47 | return actions; 48 | } 49 | } 50 | 51 | NodeSequencerPalette.$inject = [ 'palette', 'create', 'nodeSequencerElementFactory', 'lassoTool' ]; 52 | 53 | export default NodeSequencerPalette; 54 | -------------------------------------------------------------------------------- /src/features/palette/index.js: -------------------------------------------------------------------------------- 1 | import NodeSequencerPalette from './NodeSequencerPalette'; 2 | 3 | export default { 4 | __init__: [ 'nodeSequencerPalette' ], 5 | nodeSequencerPalette: [ 'type', NodeSequencerPalette ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/radial-menu/RadialMenu.js: -------------------------------------------------------------------------------- 1 | import { 2 | domify, 3 | classes as domClasses, 4 | event as domEvent, 5 | } from 'min-dom'; 6 | 7 | import { isEmitter, isListener } from '../../util/NodeSequencerUtil'; 8 | 9 | function calculateStep(numberOfEntries) { 10 | return 2 * Math.PI / numberOfEntries; 11 | } 12 | 13 | function getLabel(soundId) { 14 | switch (soundId) { 15 | case 'kick': 16 | return 'K'; 17 | case 'clap': 18 | return 'C'; 19 | case 'snare': 20 | return 'S'; 21 | case 'closedhat': 22 | return 'CH'; 23 | case 'openhat': 24 | return 'OH'; 25 | case 'tom': 26 | return 'T'; 27 | } 28 | } 29 | 30 | class RadialMenu { 31 | constructor(commandStack, nodeSequencerConfig, eventBus, modeling, overlays, sounds) { 32 | this._commandStack = commandStack; 33 | this._nodeSequencerConfig = nodeSequencerConfig; 34 | this._eventBus = eventBus; 35 | this._modeling = modeling; 36 | this._overlays = overlays; 37 | this._sounds = sounds; 38 | 39 | this.overlay = undefined; 40 | this.element = undefined; 41 | 42 | eventBus.on('selection.changed', ({ newSelection }) => { 43 | if (this.overlay) { 44 | overlays.remove(this.overlay); 45 | 46 | this.overlay = undefined; 47 | } 48 | 49 | // return if no single element selected 50 | if (newSelection.filter(e => isEmitter(e) || isListener(e)).length !== 1) { 51 | this.element = undefined; 52 | 53 | return; 54 | }; 55 | 56 | const element = newSelection[0]; 57 | 58 | // return if not listener 59 | if (!isEmitter(element) && !isListener(element)) { 60 | this.element = undefined; 61 | 62 | return; 63 | }; 64 | 65 | this.element = element; 66 | 67 | this.updateOverlay(element) 68 | }); 69 | } 70 | 71 | updateOverlay(element) { 72 | if (this.overlay) { 73 | this._overlays.remove(this.overlay); 74 | 75 | this.overlay = undefined; 76 | } 77 | 78 | const html = this.getOverlay(element); 79 | 80 | // why is -1.5 necessary? 81 | this.overlay = this._overlays.add(element, 'menu', { 82 | position: { 83 | top: this._nodeSequencerConfig.shapeSize / 2 - 1.5, 84 | left: this._nodeSequencerConfig.shapeSize / 2 - 1.5 85 | }, 86 | html 87 | }); 88 | } 89 | 90 | getOverlay(element) { 91 | const container = document.createElement('div'); 92 | 93 | domClasses(container).add('radial-menu'); 94 | 95 | if (isEmitter(element)) { 96 | const entryDescriptors = [{ 97 | onClick: () => { 98 | this._modeling.removeElements([ this.element ]); 99 | }, 100 | addClasses: [ 'entry-remove' ], 101 | icon: this._nodeSequencerConfig.icons.remove 102 | }]; 103 | 104 | this._nodeSequencerConfig.timeSignatures.forEach(timeSignature => { 105 | const addClasses = [ `entry-${timeSignature.id}` ]; 106 | 107 | if (this.element.timeSignature === timeSignature.id) { 108 | addClasses.push('active'); 109 | } 110 | 111 | entryDescriptors.push({ 112 | onClick: () => { 113 | if (this.element.timeSignature === timeSignature.id) { 114 | return; 115 | } 116 | 117 | this._commandStack.execute('nodeSequencer.changeProperties', { 118 | element: this.element, 119 | properties: { 120 | timeSignature: timeSignature.id 121 | } 122 | }); 123 | 124 | this._eventBus.fire('element.changed', { 125 | element: this.element 126 | }); 127 | 128 | this.updateOverlay(this.element); 129 | }, 130 | addClasses, 131 | label: timeSignature.label 132 | }); 133 | }); 134 | 135 | const entries = this.getEntries(entryDescriptors); 136 | 137 | this.appendEntries(container, entries); 138 | } else 139 | 140 | if (isListener(element)) { 141 | const entryDescriptors = [{ 142 | onClick: () => { 143 | this._modeling.removeElements([ this.element ]); 144 | }, 145 | addClasses: [ 'entry-remove', ], 146 | icon: this._nodeSequencerConfig.icons.remove 147 | }]; 148 | 149 | const soundKit = this._sounds.soundKit; 150 | 151 | this._nodeSequencerConfig.soundKits[soundKit].sounds.forEach(sound => { 152 | const addClasses = [ `entry-${sound.id}` ]; 153 | 154 | if (this.element.sound === sound.id) { 155 | addClasses.push('active'); 156 | } 157 | 158 | entryDescriptors.push({ 159 | onClick: () => { 160 | if (this.element.sound === sound.id) { 161 | return; 162 | } 163 | 164 | this._commandStack.execute('nodeSequencer.changeProperties', { 165 | element: this.element, 166 | properties: { 167 | sound: sound.id 168 | } 169 | }); 170 | 171 | this._eventBus.fire('element.changed', { 172 | element: this.element 173 | }); 174 | 175 | this.updateOverlay(this.element); 176 | }, 177 | addClasses, 178 | label: getLabel(sound.id) 179 | }); 180 | }); 181 | 182 | const entries = this.getEntries(entryDescriptors); 183 | 184 | this.appendEntries(container, entries); 185 | } 186 | 187 | return container; 188 | } 189 | 190 | getEntries(entryDescriptors) { 191 | const { shapeSize } = this._nodeSequencerConfig; 192 | 193 | const entries = []; 194 | 195 | const step = calculateStep(entryDescriptors.length); 196 | 197 | entryDescriptors.forEach((descriptor, i) => { 198 | const entry = document.createElement('div'); 199 | 200 | domClasses(entry).add('entry'); 201 | 202 | const top = Math.cos(i * step) * shapeSize * 2; 203 | const left = Math.sin(i * step) * shapeSize * 2; 204 | 205 | Object.assign(entry.style, { 206 | top: (top - shapeSize / 2) + 'px', 207 | left: (left - shapeSize / 2) + 'px', 208 | width: shapeSize + 'px', 209 | height: shapeSize + 'px' 210 | }); 211 | 212 | if (descriptor.onClick) { 213 | domEvent.bind(entry, 'click', descriptor.onClick); 214 | } 215 | 216 | if (descriptor.addClasses) { 217 | descriptor.addClasses.forEach(cls => { 218 | domClasses(entry).add(cls); 219 | }) 220 | } 221 | 222 | if (descriptor.icon) { 223 | const icon = domify(descriptor.icon); 224 | 225 | entry.appendChild(icon); 226 | } 227 | 228 | if (descriptor.label) { 229 | const span = document.createElement('span'); 230 | 231 | span.textContent = descriptor.label; 232 | 233 | entry.appendChild(span); 234 | } 235 | 236 | entries.push(entry); 237 | }); 238 | 239 | return entries; 240 | } 241 | 242 | appendEntries(container, entries) { 243 | entries.forEach(entry => { 244 | container.appendChild(entry); 245 | }); 246 | } 247 | } 248 | 249 | RadialMenu.$inject = [ 250 | 'commandStack', 251 | 'nodeSequencerConfig', 252 | 'eventBus', 253 | 'modeling', 254 | 'overlays', 255 | 'sounds' 256 | ]; 257 | 258 | export default RadialMenu; -------------------------------------------------------------------------------- /src/features/radial-menu/index.js: -------------------------------------------------------------------------------- 1 | import RadialMenu from './RadialMenu'; 2 | 3 | export default { 4 | __init__: [ 'radialMenu' ], 5 | radialMenu: [ 'type', RadialMenu ] 6 | }; -------------------------------------------------------------------------------- /src/features/rules/NodeSequencerRules.js: -------------------------------------------------------------------------------- 1 | import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider'; 2 | 3 | import { isEmitter, isListener, isConnection } from '../../util/NodeSequencerUtil'; 4 | import { getDistance } from '../../util/GeometryUtil'; 5 | 6 | const HIGH_PRIORITY = 1500; 7 | 8 | class NodeSequencerRules extends RuleProvider { 9 | constructor(eventBus, nodeSequencerConfig) { 10 | super(eventBus); 11 | 12 | this._nodeSequencerConfig = nodeSequencerConfig; 13 | 14 | const canCreate = target => { 15 | if (!target) { 16 | return true; 17 | } 18 | 19 | return !isEmitter(target) && 20 | !isListener(target) && 21 | !isConnection(target); 22 | } 23 | 24 | this.addRule('elements.move', HIGH_PRIORITY, context => { 25 | const target = context.target, 26 | shapes = context.shapes; 27 | 28 | let canMove = true; 29 | 30 | shapes.forEach(shape => { 31 | if (!canCreate(target)) { 32 | canMove = false; 33 | } 34 | }); 35 | 36 | return canMove; 37 | }); 38 | 39 | this.addRule('shape.create', HIGH_PRIORITY, context => { 40 | const target = context.target, 41 | shape = context.shape; 42 | 43 | return canCreate(target); 44 | }); 45 | 46 | this.addRule('connection.create', HIGH_PRIORITY, context => { 47 | const source = context.source, 48 | target = context.target; 49 | 50 | return this.canConnect(source, target); 51 | }); 52 | } 53 | 54 | canConnect(source, target) { 55 | return source.type !== target.type && 56 | getDistance(source, target) < this._nodeSequencerConfig.maxDistance; 57 | } 58 | } 59 | 60 | NodeSequencerRules.$inject = [ 'eventBus', 'nodeSequencerConfig' ]; 61 | 62 | export default NodeSequencerRules; 63 | -------------------------------------------------------------------------------- /src/features/rules/index.js: -------------------------------------------------------------------------------- 1 | import NodeSequencerRules from './NodeSequencerRules'; 2 | 3 | export default { 4 | __init__: [ 'nodeSequencerRules' ], 5 | nodeSequencerRules: [ 'type', NodeSequencerRules ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/save-midi/SaveMidi.js: -------------------------------------------------------------------------------- 1 | import Midi from 'jsmidgen'; 2 | import FileSaver from 'file-saver'; 3 | 4 | import { 5 | domify, 6 | classes as domClasses, 7 | event as domEvent, 8 | } from 'min-dom'; 9 | 10 | import { isRoot } from '../../util/NodeSequencerUtil'; 11 | import { getSequenceFromSequences } from '../../util/SequenceUtil'; 12 | 13 | const NOTES = [ 'c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b' ]; 14 | const SEMI_TONES = 12; 15 | const MEASURES = 16; 16 | 17 | function getNoteFromIndex(index) { 18 | const noteIndex = index % SEMI_TONES; 19 | const octaveIndex = Math.floor(index / SEMI_TONES) + 3; 20 | 21 | return NOTES[noteIndex] + octaveIndex; 22 | } 23 | 24 | class SaveMidi { 25 | constructor(eventBus, canvas, audio) { 26 | this._audio = audio; 27 | } 28 | 29 | saveMidi() { 30 | const file = new Midi.File(); 31 | var track = new Midi.Track(); 32 | file.addTrack(track); 33 | 34 | const allPhrases = this._audio.getAllPhrases(); 35 | 36 | const sequences = []; 37 | 38 | Object.values(allPhrases).forEach(phrase => { 39 | const sequence = getSequenceFromSequences(phrase); 40 | 41 | sequences.push(sequence); 42 | }); 43 | 44 | for (let i = 0; i < MEASURES; i++) { 45 | const chord = []; 46 | 47 | sequences.forEach((sequence, sequenceIndex) => { 48 | if (sequence[i]) { 49 | chord.push(getNoteFromIndex(sequenceIndex)); 50 | } 51 | }); 52 | 53 | if (chord.length) { 54 | track.addChord(0, chord, 32); 55 | } else { 56 | track.noteOff(0, '', 32); 57 | } 58 | } 59 | 60 | FileSaver.saveAs(new Blob([ 61 | new Uint8Array([].map.call(file.toBytes(), c => { 62 | return c.charCodeAt(0); 63 | })).buffer 64 | ], { 65 | type: 'application/x-midi' 66 | }) , 'nodeSequencer.mid' ); 67 | } 68 | } 69 | 70 | SaveMidi.$inject = [ 'eventBus', 'canvas', 'audio' ]; 71 | 72 | export default SaveMidi; -------------------------------------------------------------------------------- /src/features/save-midi/index.js: -------------------------------------------------------------------------------- 1 | import SaveMidi from './SaveMidi'; 2 | 3 | export default { 4 | __init__: [ 'saveMidi' ], 5 | saveMidi: [ 'type', SaveMidi ] 6 | }; -------------------------------------------------------------------------------- /src/features/sequences/Sequences.js: -------------------------------------------------------------------------------- 1 | import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; 2 | 3 | import { isEmitter, isListener } from '../../util/NodeSequencerUtil'; 4 | import { getSequence } from '../../util/SequenceUtil'; 5 | import { getDistance } from '../../util/GeometryUtil'; 6 | 7 | class Sequences extends CommandInterceptor { 8 | constructor(audio, nodeSequencerConfig, eventBus) { 9 | super(eventBus); 10 | 11 | const { maxDistance, offsetDistance } = nodeSequencerConfig; 12 | 13 | // connection create 14 | this.postExecute('connection.create', event => { 15 | const { source, target } = event.context; 16 | 17 | const distance = getDistance(source, target); 18 | 19 | const sequence = getSequence(distance, maxDistance, offsetDistance, source.timeSignature); 20 | 21 | audio.addSequence(sequence, source, target); 22 | }); 23 | 24 | // connection update 25 | this.postExecute('connection.layout', event => { 26 | const context = event.context, 27 | connection = context.connection, 28 | source = connection.source, 29 | target = connection.target; 30 | 31 | const distance = getDistance(source, target); 32 | 33 | const sequence = getSequence(distance, maxDistance, offsetDistance, source.timeSignature); 34 | 35 | audio.updateSequence(sequence, source, target); 36 | }); 37 | 38 | // connection delete 39 | this.postExecute('connection.delete', event => { 40 | const { source, target } = event.context; 41 | 42 | audio.removeSequence(source, target); 43 | }); 44 | } 45 | } 46 | 47 | Sequences.$inject = [ 'audio', 'nodeSequencerConfig', 'eventBus' ]; 48 | 49 | export default Sequences; 50 | -------------------------------------------------------------------------------- /src/features/sequences/index.js: -------------------------------------------------------------------------------- 1 | import Sequences from './Sequences' 2 | 3 | export default { 4 | __init__: [ 'sequences' ], 5 | sequences: [ 'type', Sequences ] 6 | }; 7 | -------------------------------------------------------------------------------- /src/features/tempo-control/TempoControl.js: -------------------------------------------------------------------------------- 1 | import { 2 | domify, 3 | classes as domClasses, 4 | event as domEvent, 5 | query as domQuery 6 | } from 'min-dom'; 7 | 8 | import { isRoot } from '../../util/NodeSequencerUtil'; 9 | 10 | class TempoControl { 11 | constructor(canvas, eventBus, nodeSequencerConfig, nodeSequencerModeling) { 12 | this._canvas = canvas; 13 | this._eventBus = eventBus; 14 | this._nodeSequencerConfig = nodeSequencerConfig; 15 | this._nodeSequencerModeling = nodeSequencerModeling; 16 | 17 | this.rootElement = undefined; 18 | this.oldTempo = undefined; 19 | 20 | this.init(); 21 | 22 | eventBus.on([ 'nodeSequencer.create.start', 'nodeSequencer.load.start' ], () => { 23 | this.unbindListeners(); 24 | }); 25 | 26 | eventBus.on([ 'nodeSequencer.create.end', 'nodeSequencer.load.end' ], () => { 27 | this.rangeInput.value = nodeSequencerConfig.initialTempo; 28 | this.value.textContent = nodeSequencerConfig.initialTempo; 29 | 30 | this.bindListeners(); 31 | }); 32 | 33 | eventBus.on([ 'commandStack.nodeSequencer.changeProperties.executed', 'commandStack.nodeSequencer.changeProperties.reverted' ], ({ context }) => { 34 | const { element } = context; 35 | 36 | if (isRoot(element)) { 37 | this.rangeInput.value = element.tempo; 38 | this.value.textContent = element.tempo; 39 | } 40 | }); 41 | } 42 | 43 | init() { 44 | const { minTempo, maxTempo, initialTempo } = this._nodeSequencerConfig; 45 | 46 | const container = domify(` 47 |
48 | 49 | ${initialTempo} BPM 50 |
51 | `); 52 | 53 | domEvent.bind(container, 'mousedown', e => e.stopPropagation()); 54 | 55 | this.rangeInput = domQuery('input', container); 56 | this.value = domQuery('.value', container); 57 | 58 | this._canvas.getContainer().appendChild(container); 59 | } 60 | 61 | bindListeners() { 62 | this.rootElement = this._canvas.getRootElement(); 63 | 64 | domEvent.bind(this.rangeInput, 'input', this.handleRangeInput.bind(this)); 65 | domEvent.bind(this.rangeInput, 'change', this.handleRangeChange.bind(this)); 66 | } 67 | 68 | unbindListeners() { 69 | this.rootElement = undefined; 70 | 71 | domEvent.unbind(this.rangeInput, 'input', this.handleRangeInput.bind(this)); 72 | domEvent.unbind(this.rangeInput, 'change', this.handleRangeChange.bind(this)); 73 | } 74 | 75 | handleRangeInput() { 76 | if (this.rootElement) { 77 | 78 | // save old tempo for correct command execution 79 | if (!this.oldTempo) { 80 | this.oldTempo = this.rootElement.tempo; 81 | } 82 | 83 | this.rootElement.tempo = this.rangeInput.value; 84 | 85 | this._eventBus.fire('nodeSequencer.tempoControl.input', { 86 | tempo: this.rangeInput.value 87 | }); 88 | 89 | this.value.textContent = this.rangeInput.value; 90 | } 91 | } 92 | 93 | handleRangeChange() { 94 | if (this.rootElement) { 95 | 96 | // reset to old tempo for correct undo/redo 97 | this.rootElement.tempo = this.oldTempo; 98 | 99 | this._nodeSequencerModeling.changeProperties(this.rootElement, { 100 | tempo: this.rangeInput.value 101 | }); 102 | 103 | this.oldTempo = undefined; 104 | } 105 | } 106 | } 107 | 108 | TempoControl.$inject = [ 'canvas', 'eventBus', 'nodeSequencerConfig', 'nodeSequencerModeling' ]; 109 | 110 | export default TempoControl; -------------------------------------------------------------------------------- /src/features/tempo-control/index.js: -------------------------------------------------------------------------------- 1 | import TempoControl from './TempoControl'; 2 | 3 | export default { 4 | __init__: [ 'tempoControl' ], 5 | tempoControl: [ 'type', TempoControl ] 6 | }; -------------------------------------------------------------------------------- /src/util/GeometryUtil.js: -------------------------------------------------------------------------------- 1 | import { pointDistance } from 'diagram-js/lib/util/Geometry'; 2 | 3 | const round = Math.round; 4 | 5 | export function getMid(element) { 6 | return { 7 | x: round(element.x + element.width / 2), 8 | y: round(element.y + element.height / 2) 9 | }; 10 | } 11 | 12 | export function getDistance(a, b) { 13 | return pointDistance(getMid(a), getMid(b)); 14 | } 15 | 16 | export function getVectorFromPoints(a, b) { 17 | return new Vector(b.x - a.x, b.y - a.y); 18 | } 19 | 20 | export class Vector { 21 | constructor(x, y) { 22 | this.x = x; 23 | this.y = y; 24 | } 25 | 26 | normalize() { 27 | const magnitude = this.magnitude(); 28 | 29 | this.x = this.x / magnitude; 30 | this.y = this.y / magnitude; 31 | 32 | return this; 33 | } 34 | 35 | magnitude() { 36 | return Math.sqrt(this.x * this.x + this.y * this.y); 37 | } 38 | 39 | scale(factor) { 40 | this.x = this.x * factor; 41 | this.y = this.y * factor; 42 | 43 | return this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/util/NodeSequencerUtil.js: -------------------------------------------------------------------------------- 1 | export function isEmitter(element) { 2 | return element.type === 'nodeSequencer:Emitter'; 3 | } 4 | 5 | export function isListener(element) { 6 | return element.type === 'nodeSequencer:Listener'; 7 | } 8 | 9 | export function isConnection(element) { 10 | return element.type === 'nodeSequencer:Connection'; 11 | } 12 | 13 | export function isRoot(element) { 14 | return !element.parent; 15 | } 16 | -------------------------------------------------------------------------------- /src/util/SequenceUtil.js: -------------------------------------------------------------------------------- 1 | const MEASURES = 16; 2 | 3 | function zeros(length) { 4 | let array = []; 5 | 6 | for(let i = 0; i < length; i++) { 7 | array = [ ...array, 0 ]; 8 | } 9 | 10 | return array; 11 | } 12 | 13 | export function getStepIndex(distance, maxDistance, offsetDistance, timeSignature) { 14 | let stepIndex = Math.floor(MEASURES / (maxDistance - offsetDistance) * distance); 15 | 16 | stepIndex = Math.min(stepIndex, 15); 17 | 18 | // time signature 1/2 -> length = 8 19 | // time signature 1/4 -> length = 4 20 | // time signature 1/8 -> length = 2 21 | // time signature 1/16 -> length = 1 22 | const length = MEASURES / timeSignature; 23 | 24 | // index 10, time signature 1/2 -> index = Math.floor(10 / 8) * 8 -> 8 25 | // index 10, time signature 1/8 -> index = Math.floor(10 / 2) * 2 -> 10 26 | return Math.floor(stepIndex / length) * length; 27 | } 28 | 29 | export function getSequence(distance, maxDistance, offsetDistance, timeSignature) { 30 | const sequence = zeros(MEASURES); 31 | 32 | const stepIndex = getStepIndex(distance, maxDistance, offsetDistance, timeSignature); 33 | 34 | sequence[stepIndex] = 1; 35 | 36 | return sequence; 37 | } 38 | 39 | export function getSequenceFromSequences(sequences) { 40 | const sequence = zeros(MEASURES); 41 | 42 | Object.values(sequences).forEach(value => { 43 | 44 | value.forEach((step, index) => { 45 | sequence[index] = step ? step : sequence[index]; 46 | }); 47 | 48 | }); 49 | 50 | return sequence; 51 | } 52 | -------------------------------------------------------------------------------- /src/util/TweenUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tween a value linear. 3 | * 4 | * @param {Number} startValue - start value. 5 | * @param {Number} endValue - end value. 6 | * @param {Number} currentTime - current time (starts at 0). 7 | * @param {Number} duration - duration of tween. 8 | */ 9 | export function tweenLinear(startValue, endValue, currentTime, duration) { 10 | 11 | if (startValue === endValue || duration === 0) { 12 | return startValue; 13 | } 14 | 15 | const changeValue = endValue - startValue; 16 | 17 | return changeValue * currentTime / duration + startValue; 18 | } 19 | 20 | export function tweenEased(startValue, endValue, currentTime, duration) { 21 | 22 | if (startValue === endValue || duration === 0) { 23 | return startValue; 24 | } 25 | 26 | const changeValue = endValue - startValue; 27 | 28 | currentTime /= duration / 2; 29 | 30 | if (currentTime < 1) { 31 | return changeValue / 2 * currentTime * currentTime + startValue; 32 | } 33 | 34 | currentTime--; 35 | 36 | const value = -changeValue / 2 * (currentTime * (currentTime - 2) - 1) + startValue; 37 | 38 | return value; 39 | } 40 | 41 | export function tweenPoint(a, b, currentTime, duration, method = 'linear') { 42 | const tweenFunction = method === 'linear' ? tweenLinear : tweenEased; 43 | 44 | return { 45 | x: tweenFunction(a.x, b.x, currentTime, duration), 46 | y: tweenFunction(a.y, b.y, currentTime, duration) 47 | } 48 | } 49 | --------------------------------------------------------------------------------