├── .gitignore ├── LICENSE ├── README.md ├── app ├── css │ ├── console.css │ ├── contextmenu.css │ ├── customComponentToolbar.css │ ├── dialog.css │ ├── mainmenu.css │ ├── popup.css │ ├── style.css │ ├── toolbar.css │ └── tutorial.css ├── fonts │ ├── Inconsolata-Regular.ttf │ ├── LobsterTwo-Regular.ttf │ ├── MaterialIcons-Regular.ttf │ ├── Monospace.ttf │ ├── Righteous-Regular.ttf │ ├── Ubuntu-Regular.ttf │ └── UbuntuMono-Regular.ttf ├── img │ ├── favicon.png │ └── tutorial │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ └── 7.png ├── index.html ├── js │ ├── audio.js │ ├── canvas.js │ ├── chat.js │ ├── clipboard.js │ ├── componentInfo.js │ ├── componentUpdates.js │ ├── components.js │ ├── console.js │ ├── console2.js │ ├── contextmenu.js │ ├── contextmenu2.js │ ├── customComponentToolbar.js │ ├── debug.js │ ├── dialogs.js │ ├── editDialogs.js │ ├── hover.js │ ├── keys.js │ ├── localStorage.js │ ├── localStorage2.js │ ├── mainmenu.js │ ├── notifications.js │ ├── savedCustomComponents.js │ ├── saves.js │ ├── socket.js │ ├── startup.js │ ├── stringifier.js │ ├── tips.js │ ├── toolbar.js │ ├── tutorial.js │ ├── undo.js │ ├── userList.js │ ├── variables.js │ └── waypoints.js └── main.js ├── boolr.desktop ├── build ├── icon.icns ├── icon.ico └── icon.png ├── data └── customcomponents.json ├── package.json └── saves └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | GNU General Public License v 3.0 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BOOLR 2 | A digital logic simulator 3 | Download BOOLR: http://boolr.me 4 | 5 | #### Running in development 6 | 7 | Either npm or yarn can be used to fetch Electron as dependency and run scripts. 8 | 9 | ```bash 10 | # Fetch dependencies 11 | npm install 12 | 13 | # Run in development 14 | npm start 15 | ``` -------------------------------------------------------------------------------- /app/css/console.css: -------------------------------------------------------------------------------- 1 | .console { 2 | display: none; 3 | position: fixed; 4 | bottom: 0; 5 | left: 0; 6 | height: 460px; 7 | width: 360px; 8 | background: #111; 9 | box-shadow: 0 0 20px rgba(0,0,0,.5); 10 | border-radius: 0 5px 0 0; 11 | padding: 20px; 12 | z-index: 8; 13 | 14 | opacity: 0; 15 | transform: scale(.9) translateX(-63px) translateY(150px); 16 | transition: opacity .2s, transform .2s, width .2s, height .2s; 17 | } 18 | 19 | .console .container { 20 | height: 95%; 21 | width: 100%; 22 | overflow-y: auto; 23 | } 24 | 25 | .console .container .focused-input .arrow { 26 | color: #ddd; 27 | position: relative; 28 | top: 7px; 29 | } 30 | 31 | .console .container .focused-input input { 32 | background: transparent; 33 | border: none; 34 | padding: 10px 10px 10px 2px; 35 | font-family: "Ubuntu Mono", Monospaced; 36 | font-size: 18px; 37 | color: #ddd; 38 | outline: none; 39 | width: 90%; 40 | } 41 | 42 | .console .container .focused-input input::selection { 43 | background: rgba(255,255,255,.5); 44 | color: #111; 45 | } 46 | 47 | .console .container .input, .console .container .output { 48 | padding: 2px 5px; 49 | font-family: "Ubuntu Mono", Monospaced; 50 | font-size: 18px; 51 | color: #ddd; 52 | } 53 | 54 | .console .container .output { 55 | color: #888; 56 | } 57 | 58 | .console .container .input:before, .console .container .output:before { 59 | content: "keyboard_arrow_right"; 60 | font-family: "Material-icons"; 61 | font-size: 24px; 62 | color: #888; 63 | position: relative; 64 | top: 7px; 65 | left: -5px; 66 | } 67 | 68 | .console .container .output:before { 69 | content: "keyboard_arrow_left"; 70 | } 71 | 72 | .console .container .log { 73 | background: rgba(255,255,255,.1); 74 | color: #ddd; 75 | font-family: "Ubuntu", Monospaced; 76 | font-size: 15px; 77 | padding: 10px 10px 10px 15px; 78 | margin-top: 5px; 79 | min-height: 24px; 80 | line-height: 25px; 81 | } 82 | 83 | .console .container .error { 84 | background: rgba(255,0,0,.1); 85 | color: red; 86 | font-family: "Ubuntu", Monospaced; 87 | font-size: 15px; 88 | padding: 10px 10px 10px 15px; 89 | margin-top: 5px; 90 | min-height: 24px; 91 | line-height: 12px; 92 | } 93 | 94 | .console .container .error:before { 95 | content: "error"; 96 | font-family: "Material-icons"; 97 | font-size: 20px; 98 | color: red; 99 | position: relative; 100 | top: 5px; 101 | left: -5px; 102 | } 103 | 104 | .console .toolbar { 105 | position: absolute; 106 | bottom: 10px; 107 | left: 0; 108 | width: 100%; 109 | text-align: center; 110 | } 111 | 112 | .console .toolbar button { 113 | background: transparent; 114 | border: none; 115 | color: #888; 116 | margin: 0 15px; 117 | outline: none; 118 | cursor: pointer; 119 | transition: color .2s; 120 | } 121 | 122 | .console .toolbar button:hover { 123 | color: #ddd; 124 | } 125 | 126 | -------------------------------------------------------------------------------- /app/css/contextmenu.css: -------------------------------------------------------------------------------- 1 | #contextMenu { 2 | position: fixed; 3 | background: #111; 4 | list-style: none; 5 | padding: 0; 6 | margin: 0; 7 | box-shadow: 0 0 10px rgba(0,0,0,.25); 8 | outline: none; 9 | border-radius: 5px; 10 | z-index: 5; 11 | 12 | opacity: 0; 13 | transition: opacity .2s; 14 | } 15 | 16 | #contextMenu li { 17 | font-family: "Ubuntu"; 18 | font-size: 15px; 19 | color: #fff; 20 | padding: 8px; 21 | cursor: pointer; 22 | white-space: nowrap; 23 | } 24 | 25 | #contextMenu li.disabled { 26 | pointer-events: none; 27 | opacity: .5; 28 | } 29 | 30 | #contextMenu li .material-icons { 31 | color: #888; 32 | margin-right: 10px; 33 | vertical-align: -5px; 34 | white-space: nowrap; 35 | 36 | transition: color .2s; 37 | } 38 | 39 | #contextMenu li .key { 40 | white-space: nowrap; 41 | color: #888; 42 | padding-left: 10px; 43 | float: right; 44 | line-height: 28px; 45 | } 46 | 47 | #contextMenu li:hover { 48 | background: rgba(255,255,255,.1); 49 | } 50 | 51 | #contextMenu li:hover .material-icons { 52 | color: #fff; 53 | } -------------------------------------------------------------------------------- /app/css/customComponentToolbar.css: -------------------------------------------------------------------------------- 1 | #customComponentToolbar { 2 | display: none; 3 | position: fixed; 4 | left: 50%; 5 | top: -50px; 6 | margin-left: -175px; 7 | width: 350px; 8 | height: 50px; 9 | background: #111; 10 | color: #444; 11 | box-shadow: 0 0 20px rgba(0,0,0,.5); 12 | border-radius: 0 0 5px 5px; 13 | text-align: center; 14 | 15 | transition: top .2s; 16 | } 17 | 18 | #customComponentToolbar .close { 19 | position: absolute; 20 | bottom: 6px; 21 | left: 0; 22 | opacity: 1; 23 | color: #ddd; 24 | } 25 | 26 | #customComponentToolbar .edit { 27 | position: absolute; 28 | right: 0; 29 | background: transparent; 30 | border: none; 31 | font-size: 26px; 32 | color: #ddd; 33 | line-height: 30px; 34 | margin: 10px 0; 35 | outline: none; 36 | cursor: pointer; 37 | 38 | transition: opacity .2s, transform .2s; 39 | } 40 | 41 | #customComponentToolbar .edit:hover { 42 | opacity: .7; 43 | } 44 | 45 | #customComponentToolbar #name { 46 | position: relative; 47 | top: 15px; 48 | font-family: "Ubuntu", Arial; 49 | font-size: 15px; 50 | color: #ddd; 51 | } 52 | 53 | #customComponentToolbar .menu { 54 | position: absolute; 55 | top: 50px; 56 | right: 0; 57 | margin: 0; 58 | padding: 0; 59 | background: #111; 60 | border-radius: 5px; 61 | color: #ddd; 62 | list-style: none; 63 | box-shadow: 0 0 20px rgba(0,0,0,.25); 64 | 65 | display: none; 66 | opacity: 0; 67 | transform: scale(.5) translateX(80px) translateY(-40px); 68 | transition: opacity .2s, transform .2s; 69 | } 70 | 71 | #customComponentToolbar .menu li { 72 | padding: 8px; 73 | font-family: "Ubuntu", Arial; 74 | font-size: 15px; 75 | text-align: left; 76 | cursor: pointer; 77 | } 78 | 79 | #customComponentToolbar .menu li:hover { 80 | background: rgba(255,255,255,.1); 81 | } 82 | 83 | #customComponentToolbar .menu li .material-icons { 84 | color: #888; 85 | margin-right: 10px; 86 | vertical-align: -5px; 87 | white-space: nowrap; 88 | 89 | transition: color .2s; 90 | } -------------------------------------------------------------------------------- /app/css/dialog.css: -------------------------------------------------------------------------------- 1 | #over { 2 | display: none; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | height: 100%; 7 | width: 100%; 8 | background: #888; 9 | opacity: 0; 10 | transition: opacity .5s; 11 | z-index: 200; 12 | } 13 | 14 | #dialog { 15 | display: none; 16 | position: fixed; 17 | top: 16%; 18 | left: 50%; 19 | height: auto; 20 | width: 400px; 21 | margin-left: -200px; 22 | padding-bottom: 100px; 23 | background: #111; 24 | border-radius: 5px; 25 | text-align: center; 26 | font-family: Ubuntu; 27 | color: #ddd; 28 | font-size: 15px; 29 | z-index: 1000; 30 | outline: none; 31 | 32 | top: 100%; 33 | opacity: 0; 34 | transition: opacity .2s, transform .2s, top .2s, height 1s; 35 | } 36 | 37 | #dialog h1 { 38 | font-family: Ubuntu; 39 | font-size: 26px; 40 | font-weight: 400; 41 | margin: 0; 42 | padding: 20px 0; 43 | } 44 | 45 | #dialog .container { 46 | padding: 0 20px; 47 | } 48 | 49 | #dialog input { 50 | font-family: Monospaced; 51 | font-size: 15px; 52 | background: rgba(255,255,255,0); 53 | box-shadow: 0 2px 0 0 rgba(255,255,255,.1); 54 | border: none; 55 | outline: none; 56 | margin: 8px; 57 | padding: 2px; 58 | color: #ddd; 59 | } 60 | 61 | #dialog input.error { 62 | box-shadow: 0 2px 0 0 #500; 63 | color: red; 64 | } 65 | 66 | #dialog textarea { 67 | font-family: Monospaced; 68 | font-size: 15px; 69 | background: rgba(255,255,255,0); 70 | box-shadow: 0 2px 0 0 rgba(255,255,255,.1); 71 | border: none; 72 | outline: none; 73 | margin: 8 8 0 8px; 74 | padding: 2px; 75 | color: #ddd; 76 | } 77 | 78 | #dialog textarea.error { 79 | box-shadow: 0 2px 0 0 #500; 80 | color: red; 81 | } 82 | 83 | #dialog select { 84 | font-family: Monospaced; 85 | font-size: 15px; 86 | background: rgba(255,255,255,0); 87 | box-shadow: 0 2px 0 0 rgba(255,255,255,.1); 88 | border: none; 89 | outline: none; 90 | margin: 8 8 0 8px; 91 | padding: 2px; 92 | color: #ddd; 93 | width: 20%; 94 | } 95 | 96 | #dialog .errormsg { 97 | opacity: 0; 98 | transition: opacity .5s; 99 | color: #f00; 100 | } 101 | 102 | #dialog .container button { 103 | background: rgba(255,255,255,.2); 104 | border: none; 105 | font-family: Ubuntu; 106 | font-size: 15px; 107 | color: #fff; 108 | padding: 10px; 109 | margin: 10px; 110 | min-width: 100px; 111 | outline: none; 112 | cursor: pointer; 113 | float: left; 114 | } 115 | 116 | #dialog ul { 117 | margin-left: 10%; 118 | } 119 | 120 | #dialog li { 121 | font-family: "Ubuntu"; 122 | font-size: 15px; 123 | color: #fff; 124 | cursor: pointer; 125 | text-align: left; 126 | padding: 12px; 127 | width: 75%; 128 | margin: 0 auto; 129 | } 130 | 131 | #dialog li:hover { 132 | background: rgba(255,255,255,.1); 133 | } 134 | 135 | #dialog li .material-icons { 136 | position: relative; 137 | top: -3px; 138 | float: right; 139 | cursor: pointer; 140 | margin-left: 20px; 141 | color: #ddd; 142 | } 143 | 144 | #dialog .container .truthtable { 145 | margin: 20px auto; 146 | border-collapse: collapse; 147 | } 148 | 149 | #dialog .container .truthtable th { 150 | font-family: "Ubuntu", Arial; 151 | font-weight: 400; 152 | color: #111; 153 | background: #ddd; 154 | border-collapse: collapse; 155 | border: 2px solid #ddd; 156 | padding: 8px 16px; 157 | } 158 | 159 | #dialog .container .truthtable td { 160 | border: 2px solid #ddd; 161 | text-align: center; 162 | padding: 8px 16px; 163 | border-collapse: collapse; 164 | font-size: 18px; 165 | color: #ddd; 166 | } 167 | 168 | #dialog .options { 169 | position: absolute; 170 | bottom: 0; 171 | width: 100%; 172 | padding: 20px 0; 173 | } 174 | 175 | #dialog .options button { 176 | position: relative; 177 | top: 0; 178 | background: rgba(255,255,255,.2); 179 | box-shadow: 0 3px 0 0 rgba(255,255,255,.1); 180 | border: none; 181 | font-family: Ubuntu; 182 | font-size: 15px; 183 | color: #fff; 184 | padding: 10px; 185 | margin: 10px; 186 | min-width: 100px; 187 | outline: none; 188 | cursor: pointer; 189 | 190 | transition: top .1s, box-shadow .1s, background .1s; 191 | } 192 | 193 | #dialog .options button:hover { 194 | background: rgba(255,255,255,.3); 195 | box-shadow: 0 3px 0 0 rgba(255,255,255,.2); 196 | } 197 | 198 | #dialog .options button:active { 199 | box-shadow: 0 0 0 0 #111; 200 | top: 3px; 201 | } 202 | -------------------------------------------------------------------------------- /app/css/mainmenu.css: -------------------------------------------------------------------------------- 1 | .main-menu { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | height: 100%; 6 | padding-bottom: 1000px; 7 | width: 100%; 8 | z-index: 100; 9 | text-align: center; 10 | background: #111; 11 | overflow-y: auto; 12 | 13 | opacity: 1; 14 | transform: translateY(0px); 15 | transition: opacity 1s, transform .5s; 16 | } 17 | 18 | .main-menu > h1 { 19 | display: inline-block; 20 | font-family: "Righteous"; 21 | font-size: 160px; 22 | font-weight: 400; 23 | color: #ddd; 24 | margin: 40px 0; 25 | 26 | position: relative; 27 | top: 0; 28 | transition: top .5s; 29 | } 30 | 31 | .main-menu .version { 32 | display: inline-block; 33 | font-family: "Ubuntu", Arial; 34 | font-size: 18px; 35 | color: #ddd; 36 | } 37 | 38 | .main-menu .loading { 39 | position: fixed; 40 | top: 25%; 41 | font-family: "Ubuntu", Arial; 42 | font-size: 30px; 43 | color : #888; 44 | width: 100%; 45 | text-align: center; 46 | } 47 | 48 | .main-menu > button { 49 | position: relative; 50 | display: block; 51 | background: transparent; 52 | 53 | border: 0px solid #111; 54 | margin: 10px auto; 55 | padding: 30px; 56 | width: 400px; 57 | font-family: Ubuntu; 58 | font-size: 24px; 59 | color: #ddd; 60 | outline: none; 61 | cursor: pointer; 62 | 63 | opacity: 0; 64 | transform: translateX(-50px); 65 | top: 0; 66 | transition: opacity 1s, transform .75s, top .5s, background .2s, color .2s; 67 | } 68 | 69 | .main-menu > button:hover { 70 | transform: translateX(5px); 71 | } 72 | 73 | .main-menu > button .material-icons { 74 | position: relative; 75 | top: -8px; 76 | float: left; 77 | font-size: 40px; 78 | color: #888; 79 | 80 | transform: translateX(30px); 81 | transition: transform .75s, color .5s; 82 | } 83 | 84 | .main-menu > button:hover .material-icons { 85 | color: #ddd; 86 | } 87 | 88 | .main-menu > button .material-icons:after { 89 | content: ""; 90 | position: relative; 91 | left: 40px; 92 | top: 0px; 93 | border-left: 3px solid #888; 94 | color: #888; 95 | } 96 | 97 | .main-menu .sub { 98 | position: fixed; 99 | display: none; 100 | width: 100%; 101 | background: #ddd; 102 | padding-bottom: 50px; 103 | z-index: 1; 104 | font-family: "Ubuntu", Arial; 105 | 106 | opacity: 0; 107 | transform: translateY(-50px); 108 | transition: height .5s, opacity .5s, transform .5s; 109 | } 110 | 111 | .main-menu .sub:before { 112 | content: ''; 113 | position: relative; 114 | border-style: solid; 115 | border-width: 0 15px 15px; 116 | border-color: #ddd transparent; 117 | display: block; 118 | width: 0; 119 | z-index: 3; 120 | top: -15px; 121 | left: 50%; 122 | margin-left: -15px; 123 | } 124 | 125 | .main-menu .sub h1 { 126 | font-family: "Ubuntu", Arial; 127 | font-weight: 400; 128 | margin: 30px; 129 | } 130 | 131 | .main-menu .sub button { 132 | background: #111; 133 | border: none; 134 | font-family: "Ubuntu", Arial; 135 | font-size: 16px; 136 | color: #ddd; 137 | padding: 12px 25px; 138 | margin: 10px; 139 | border-radius: 5px; 140 | cursor: pointer; 141 | outline: none; 142 | } 143 | 144 | .main-menu .sub button:hover { 145 | background: #333; 146 | } 147 | 148 | .main-menu .new-board input { 149 | display: block; 150 | margin: 20px auto; 151 | background: transparent; 152 | border: none; 153 | border-bottom: 2px solid rgba(0,0,0,.1); 154 | padding: 10px; 155 | width: 200px; 156 | font-family: "Ubuntu", Arial; 157 | font-size: 18px; 158 | outline: none; 159 | text-align: center; 160 | } 161 | 162 | .main-menu .new-board #filename { 163 | background: rgba(0,0,0,.1); 164 | color: #111; 165 | padding: 10px; 166 | font-size: 15px; 167 | border-radius: 5px; 168 | display: inline-block; 169 | line-height: 0px; 170 | height: 25px; 171 | 172 | opacity: 1; 173 | transition: opacity .2s; 174 | } 175 | 176 | .main-menu .new-board #filename:before { 177 | content: ''; 178 | position: relative; 179 | border-style: solid; 180 | border-width: 0 15px 15px; 181 | border-color: rgba(0,0,0,.1) transparent; 182 | display: block; 183 | width: 0; 184 | z-index: 3; 185 | top: -25px; 186 | left: 50%; 187 | margin-left: -15px; 188 | } 189 | 190 | .main-menu .new-board input::selection { 191 | background: #111; 192 | color: #ddd; 193 | } 194 | 195 | .main-menu .open-board ul { 196 | width: 640px; 197 | list-style: none; 198 | padding: 0; 199 | margin: 20px auto; 200 | max-height: 200px; 201 | overflow-y: auto; 202 | } 203 | 204 | .main-menu .open-board ul::-webkit-scrollbar { 205 | width: 5px; 206 | height: 5px; 207 | } 208 | 209 | .main-menu .open-board ul::-webkit-scrollbar-track { 210 | background: rgba(0,0,0,.1); 211 | } 212 | 213 | .main-menu .open-board ul::-webkit-scrollbar-thumb { 214 | background: rgba(0,0,0,.3); 215 | border-radius: 5px; 216 | } 217 | 218 | .main-menu .open-board li { 219 | font-family: "Ubuntu", Arial; 220 | font-size: 18px; 221 | padding: 20px; 222 | text-align: left; 223 | 224 | transition: background .2s, color .2s; 225 | } 226 | 227 | .main-menu .open-board li .material-icons { 228 | float: right; 229 | margin-left: 30px; 230 | cursor: pointer; 231 | } 232 | 233 | .main-menu .open-board li:hover { 234 | background: rgba(0,0,0,.1); 235 | } 236 | 237 | .main-menu .open-board li span { 238 | position: relative; 239 | top: 2px; 240 | padding-left: 20px; 241 | color: #555; 242 | font-size: 15px; 243 | float: right; 244 | } 245 | 246 | .main-menu .settings #settings { 247 | margin: 0 auto; 248 | width: 500px; 249 | } 250 | 251 | .main-menu .settings #settings li { 252 | text-align: left; 253 | } 254 | 255 | .main-menu .settings #settings .slider { 256 | background: #888; 257 | } 258 | 259 | .main-menu .settings #settings .slider:before { 260 | position: absolute; 261 | content: ""; 262 | height: 14px; 263 | width: 14px; 264 | left: 4px; 265 | bottom: 4px; 266 | background: #ddd; 267 | opacity: .5; 268 | -webkit-transition: .2s; 269 | transition: .2s; 270 | } 271 | 272 | 273 | .main-menu .settings #settings input:checked + .slider { 274 | background: #111; 275 | } 276 | 277 | .main-menu .settings #settings button { 278 | padding: 10px; 279 | background: #500; 280 | margin-left: -250px; 281 | } 282 | -------------------------------------------------------------------------------- /app/css/popup.css: -------------------------------------------------------------------------------- 1 | /* Overlay */ 2 | #overlay { 3 | display: none; 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | background: rgba(0,0,0,.25); 10 | opacity: 0; 11 | transition: opacity .2s; 12 | z-index: 1; 13 | } 14 | 15 | /* Pop-ups */ 16 | .popup { 17 | display: none; 18 | position: fixed; 19 | top: 10%; 20 | left: 50%; 21 | width: 400px; 22 | margin-left: -200px; 23 | text-align: center; 24 | background: #ddd; 25 | box-shadow: 0 0 20px rgba(0,0,0,.2); 26 | font-family: "Ubuntu"; 27 | padding: 10px; 28 | border-radius: 5px; 29 | 30 | opacity: 0; 31 | transform: scale(.95); 32 | transition: transform .2s, opacity .2s; 33 | z-index: 101; 34 | } 35 | 36 | .popup h1 { 37 | font-family: "Ubuntu"; 38 | font-size: 25px; 39 | font-weight: 400; 40 | padding-bottom: 20px; 41 | } 42 | 43 | .popup button { 44 | display: inline-block; 45 | margin: 20px auto; 46 | background: #ddd; 47 | border: none; 48 | padding: 10px; 49 | min-width: 100px; 50 | font-family: "Ubuntu"; 51 | font-size: 16px; 52 | outline: none; 53 | cursor: pointer; 54 | } 55 | 56 | .popup button:hover { 57 | background: #ccc; 58 | } 59 | 60 | .popup ul { 61 | font-family: "Ubuntu"; 62 | font-size: 16px; 63 | text-align: left; 64 | } 65 | .popup li { 66 | padding: 2px; 67 | } 68 | 69 | .popup .material-icons { 70 | color: #333; 71 | vertical-align: -5px; 72 | } 73 | 74 | #settings ul { 75 | list-style: none; 76 | padding: 0; 77 | } 78 | #settings li { 79 | padding: 10px; 80 | } 81 | #settings li button { 82 | margin: 0; 83 | font-weight: 400; 84 | background: #822; 85 | } 86 | 87 | .popup input:not([type=color]) { 88 | width: 90%; 89 | padding: 10px; 90 | background: #ccc; 91 | border: 1px solid #bbb; 92 | outline: none; 93 | font-family: "Ubuntu"; 94 | font-size: 18px; 95 | color: #333; 96 | } 97 | 98 | .popup input[type=color] { 99 | -webkit-appearance: none; 100 | border: none; 101 | width: 70px; 102 | height: 50px; 103 | outline: none; 104 | } 105 | 106 | .popup input[type=number] { 107 | width: 100px; 108 | float: right; 109 | } 110 | 111 | .popup input[type=color]::-webkit-color-swatch-wrapper { 112 | padding: 0; 113 | } 114 | .popup input[type=color]::-webkit-color-swatch { 115 | border: none; 116 | } 117 | 118 | .popup textarea { 119 | width: 93%; 120 | height: 300px; 121 | resize: none; 122 | outline: none; 123 | background: #111; 124 | padding: 10px; 125 | color: #888; 126 | font-family: "Inconsolata", Consolas; 127 | font-size: 18px; 128 | } 129 | 130 | .popup table { 131 | border-collapse: collapse; 132 | width: 90%; 133 | margin: 0 auto; 134 | } 135 | 136 | .popup table, .popup td, .popup th { 137 | border: 2px solid #bbb; 138 | font-family: "Roboto", Arial; 139 | font-size: 14px; 140 | text-align: center; 141 | padding: 10px; 142 | } 143 | 144 | .popup th { 145 | background: #bbb; 146 | font-weight: 800; 147 | } 148 | 149 | .popup span { 150 | text-align: left; 151 | color: #444444; 152 | } 153 | 154 | 155 | #confirm, #prompt { z-index: 102 } 156 | 157 | /* Options slider */ 158 | .switch { 159 | position: relative; 160 | display: inline-block; 161 | width: 40px; 162 | height: 22px; 163 | float: right; 164 | margin-right: 16px; 165 | } 166 | 167 | .switch input {display:none;} 168 | 169 | .slider { 170 | position: absolute; 171 | cursor: pointer; 172 | top: 0; 173 | left: 0; 174 | right: 0; 175 | bottom: 0; 176 | background-color: #ccc; 177 | -webkit-transition: .4s; 178 | transition: .4s; 179 | } 180 | 181 | .slider:before { 182 | position: absolute; 183 | content: ""; 184 | height: 14px; 185 | width: 14px; 186 | left: 4px; 187 | bottom: 4px; 188 | background: #000; 189 | opacity: .5; 190 | -webkit-transition: .2s; 191 | transition: .2s; 192 | } 193 | 194 | input:checked + .slider { 195 | background-color: rgba(0,0,0,.25); 196 | } 197 | 198 | input:checked + .slider:before { 199 | -webkit-transform: translateX(18px); 200 | -ms-transform: translateX(18px); 201 | transform: translateX(18px); 202 | } 203 | 204 | #openproject { 205 | width: 500px; 206 | margin-left: -250px; 207 | } 208 | 209 | #openproject #local, #openproject #server { 210 | margin: 2px auto; 211 | background: rgba(0,0,0,.1); 212 | width: 500px; 213 | height: 250px; 214 | } 215 | 216 | #openproject #local_btn, #openproject #server_btn { 217 | background: rgba(0,0,0,.05); 218 | border: none; 219 | padding: 10px; 220 | margin: -2px; 221 | width: 100px; 222 | font-family: Ubuntu; 223 | font-size: 18px; 224 | outline: none; 225 | cursor: pointer; 226 | border-radius: 5px 5px 0 0; 227 | } 228 | 229 | #openproject button.selected { 230 | background: rgba(0,0,0,.1) !important; 231 | } 232 | 233 | #openproject #local #upload_file { 234 | position: absolute; 235 | left: 50%; 236 | width: 380px; 237 | height: 200px; 238 | margin: 20px auto 0 -190px; 239 | border: 3px dashed rgba(0,0,0,.5); 240 | outline-offset: -10px; 241 | } 242 | 243 | #openproject #local #upload_file .material-icons { 244 | font-size: 70px; 245 | color: rgba(0,0,0,.5); 246 | position: relative; 247 | top: 40px; 248 | } 249 | 250 | #openproject #local #upload_file p { 251 | font-family: "Ubuntu"; 252 | font-size: 18px; 253 | margin: 0; 254 | position: relative; 255 | top: 50px; 256 | } 257 | 258 | #openproject #local #upload_file b:hover { 259 | text-decoration: underline; 260 | cursor: pointer; 261 | } 262 | 263 | #openproject #server input { 264 | font-family: "Inconsolata", Consolas; 265 | font-size: 18px; 266 | width: 300px; 267 | margin: 10px; 268 | padding: 10px; 269 | outline: none; 270 | border: none; 271 | background: rgba(255,255,255,.8); 272 | } 273 | 274 | #openproject #server button { 275 | font-family: "Ubuntu"; 276 | font-size: 18px; 277 | margin: 10px; 278 | padding: 10px; 279 | outline: none; 280 | border: none; 281 | background: rgba(0,0,0,.2); 282 | } 283 | 284 | #openproject #server ul { 285 | list-style: none; 286 | padding: 0; 287 | margin: 0 auto; 288 | max-height: 190px; 289 | overflow-y: auto; 290 | } 291 | 292 | #openproject #server li { 293 | padding: 10px; 294 | font-family: "Ubuntu"; 295 | font-size: 16px; 296 | text-align: left; 297 | } 298 | 299 | #openproject #server li:nth-child(odd) { 300 | background: rgba(0,0,0,.1); 301 | } 302 | 303 | #openproject #server li button { 304 | float: right; 305 | font-size: 16px; 306 | padding: 5px; 307 | margin: -5px; 308 | background: #888; 309 | } -------------------------------------------------------------------------------- /app/css/style.css: -------------------------------------------------------------------------------- 1 | /* todo: backup fonts */ 2 | 3 | body { 4 | background: #111; 5 | margin: 0; 6 | overflow: hidden; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | } 12 | 13 | canvas { 14 | position: fixed; 15 | top: 0; 16 | left: 0; 17 | cursor: crosshair; 18 | outline: none; 19 | } 20 | 21 | /* Custom scroll bars */ 22 | ::-webkit-scrollbar { 23 | width: 5px; 24 | height: 5px; 25 | } 26 | 27 | ::-webkit-scrollbar-track { 28 | background: rgba(255,255,255,.1); 29 | } 30 | 31 | ::-webkit-scrollbar-thumb { 32 | background: rgba(255,255,255,.3); 33 | border-radius: 5px; 34 | } 35 | 36 | /* Icons */ 37 | 38 | @font-face { 39 | font-family: "Material-icons"; 40 | src: url(../fonts/MaterialIcons-Regular.ttf); 41 | } 42 | .material-icons { 43 | font-family: "Material-icons"; 44 | font-style: normal; 45 | font-size: 24px; 46 | } 47 | 48 | /* Fonts */ 49 | @font-face { 50 | font-family: "Righteous"; 51 | src: url(../fonts/Righteous-Regular.ttf); 52 | } 53 | 54 | @font-face { 55 | font-family: "Inconsolata"; 56 | src: url(../fonts/Inconsolata-Regular.ttf); 57 | } 58 | 59 | @font-face { 60 | font-family: "Ubuntu"; 61 | src: url(../fonts/Ubuntu-Regular.ttf); 62 | } 63 | 64 | @font-face { 65 | font-family: "Ubuntu Mono"; 66 | src: url(../fonts/UbuntuMono-Regular.ttf); 67 | } 68 | 69 | @font-face { 70 | font-family: "Monospaced"; 71 | src: url(../fonts/Monospace.ttf); 72 | } 73 | 74 | @font-face { 75 | font-family: "Fancy"; 76 | src: url(../fonts/LobsterTwo-Regular.ttf); 77 | } 78 | 79 | /* Chat */ 80 | #chat { 81 | position: absolute; 82 | left: 0; 83 | bottom: 6px; 84 | width: 230px; 85 | background: #111; 86 | box-shadow: 0 0 20px rgba(0,0,0,.25); 87 | border: none; 88 | padding: 16px; 89 | outline: none; 90 | border-radius: 0 5px 5px 0; 91 | font-family: "Ubuntu"; 92 | font-size: 15px; 93 | color: #aaa; 94 | 95 | transform: translateX(-230px); 96 | transition: transform .2s; 97 | } 98 | 99 | /* Notifications */ 100 | #notifications { 101 | position: absolute; 102 | bottom: 80px; 103 | left: 0; 104 | list-style: none; 105 | padding: 0; 106 | pointer-events: none; 107 | 108 | max-height: 100%; 109 | overflow-y: auto; 110 | 111 | transform: translateY(80px); 112 | transition: transform .2s; 113 | } 114 | 115 | #notifications li { 116 | background: #ddd; 117 | font-family: "Ubuntu", Arial; 118 | font-size: 15px; 119 | color: #111; 120 | padding: 16px; 121 | margin-top: 10px; 122 | width: 200px; 123 | box-shadow: 0 0 20px rgba(0,0,0,.25); 124 | border-radius: 0 5px 5px 0; 125 | opacity: 1; 126 | position: relative; 127 | left: -200px; 128 | transition: opacity 1s, left .1s; 129 | word-break: break-all; 130 | } 131 | 132 | #notifications li.error { 133 | background: #822; 134 | color: #ddd; 135 | } 136 | 137 | .brbtns { 138 | position: fixed; 139 | bottom: 6px; 140 | right: 6px; 141 | } 142 | 143 | .brbtn { 144 | background: #111; 145 | box-shadow: 0 0 20px rgba(0,0,0,.25); 146 | border: none; 147 | color: #ddd; 148 | padding: 8px; 149 | border-radius: 5px; 150 | z-index: 9; 151 | cursor: pointer; 152 | outline: none; 153 | vertical-align:top; 154 | 155 | transition: background .2s; 156 | } 157 | 158 | .brbtn:hover { 159 | background: #333; 160 | } 161 | 162 | #menuopen .container { 163 | pointer-events: none; 164 | } 165 | 166 | #version { 167 | font-family: "Ubuntu", Arial; 168 | font-size: 15px; 169 | -padding-left: 30px; 170 | height: 33px; 171 | } 172 | 173 | #version .material-icons { 174 | float: left; 175 | position: relative; 176 | bottom: 3px; 177 | right: 3px; 178 | } 179 | 180 | #pause { 181 | 182 | } 183 | 184 | #menuopen { 185 | right: 190px; 186 | padding: 4.5px; 187 | } 188 | 189 | #menuopen .container { 190 | transform: rotateZ(0deg); 191 | transition: transform .2s; 192 | } 193 | 194 | #pause { 195 | right: 150px; 196 | padding: 4.5px; 197 | } 198 | 199 | /* Menu */ 200 | 201 | #menu { 202 | position: fixed; 203 | right: 6px; 204 | background: #111; 205 | box-shadow: 0 0 20px rgba(0,0,0,.25); 206 | color: #ddd; 207 | border-radius: 5px; 208 | font-family: "Ubuntu", Arial; 209 | font-size: 15px; 210 | list-style: none; 211 | padding: 0; 212 | z-index: 7; 213 | 214 | bottom: -500px; 215 | opacity: 0; 216 | transition: opacity .2s, bottom .2s; 217 | } 218 | 219 | #menu li { 220 | padding: 10px; 221 | line-height: 25px; 222 | height: 25px; 223 | cursor: pointer; 224 | transition: background .2s; 225 | } 226 | 227 | #menu li:hover { 228 | background: rgba(255,255,255,.1); 229 | } 230 | 231 | #menu li span { 232 | color: #888; 233 | float: right; 234 | padding-left: 20px; 235 | } 236 | 237 | #menu li .material-icons { 238 | color: #888; 239 | position: relative; 240 | top: 0px; 241 | margin-right: 20px; 242 | float: left; 243 | } 244 | 245 | /* Debug info */ 246 | 247 | #debugInfo { 248 | display: none; 249 | position: fixed; 250 | top: 0; 251 | right: 0; 252 | background: #111; 253 | opacity: .95; 254 | font-family: "Ubuntu", Arial; 255 | font-size: 14px; 256 | -font-weight: 600; 257 | color: #ddd; 258 | padding: 10px; 259 | box-shadow: 0 0 10px rgba(0,0,0,.25); 260 | border-radius: 0 0 0 5px; 261 | pointer-events: none; 262 | z-index: 6; 263 | } 264 | 265 | #debugInfo span { 266 | margin-left: 30px; 267 | float: right; 268 | } 269 | 270 | /* User list */ 271 | #userList { 272 | display: none; 273 | position: fixed; 274 | top: 0; 275 | left: 0; 276 | background: #ddd; 277 | opacity: .95; 278 | font-family: "Ubuntu"; 279 | font-size: 15px; 280 | -font-weight: 600; 281 | color: #111; 282 | padding: 10px; 283 | box-shadow: 0 0 10px rgba(0,0,0,.25); 284 | pointer-events: none; 285 | } 286 | 287 | /* Component info */ 288 | #componentInfo { 289 | display: none; 290 | position: fixed; 291 | top: 100px; 292 | left: 100px; 293 | background: #ddd; 294 | font-family: "Ubuntu"; 295 | font-size: 15px; 296 | color: #333; 297 | padding: 5px; 298 | box-shadow: 0 0 20px rgba(0,0,0,.25); 299 | border-radius: 5px; 300 | text-align: center; 301 | 302 | transform: translateY(20px); 303 | opacity: 0; 304 | transition: transform .2s, opacity .2s; 305 | } 306 | 307 | #componentInfo:after { 308 | content: ''; 309 | position: fixed; 310 | border-style: solid; 311 | border-width: 15px 15px 0; 312 | border-color: #ddd transparent; 313 | display: block; 314 | width: 0; 315 | z-index: 1; 316 | bottom: -15px; 317 | left: 50%; 318 | margin-left: -15px; 319 | } 320 | 321 | #componentInfo h1 { 322 | font-size: 15px; 323 | font-weight: 600; 324 | color: #111; 325 | margin: 5px; 326 | text-align: center; 327 | } 328 | 329 | #componentInfo p { 330 | margin: 2px; 331 | } 332 | 333 | #componentInfo span { 334 | float: right; 335 | margin-left: 10px; 336 | opacity: .75; 337 | } 338 | 339 | /* Open board */ 340 | #openboard #dropfile { 341 | width: 100%; 342 | height: 140px; 343 | background: rgba(255,255,255,.1); 344 | border: 3px dashed rgba(255,255,255,.33); 345 | font-size: 24px; 346 | color: #888; 347 | padding-top: 60px; 348 | margin-top: 20px; 349 | 350 | transition: background .5s; 351 | } 352 | 353 | #openboard #dropfile:hover { 354 | color: #ddd; 355 | } 356 | 357 | /* Settings */ 358 | 359 | #settings ul { 360 | list-style: none; 361 | margin: 0; 362 | } 363 | 364 | #settings li { 365 | padding: 8px; 366 | } 367 | 368 | #settings .switch { 369 | position: relative; 370 | display: inline-block; 371 | width: 40px; 372 | height: 22px; 373 | float: right; 374 | margin-right: 16px; 375 | } 376 | 377 | #settings .switch input {display:none;} 378 | 379 | #settings .slider { 380 | position: absolute; 381 | cursor: pointer; 382 | top: 0; 383 | left: 0; 384 | right: 0; 385 | bottom: 0; 386 | background: #555; 387 | -webkit-transition: .4s; 388 | transition: .4s; 389 | } 390 | 391 | #settings .slider:before { 392 | position: absolute; 393 | content: ""; 394 | height: 14px; 395 | width: 14px; 396 | left: 4px; 397 | bottom: 4px; 398 | background: #000; 399 | opacity: .5; 400 | -webkit-transition: .2s; 401 | transition: .2s; 402 | } 403 | 404 | #settings input:checked + .slider { 405 | background: #ddd; 406 | } 407 | 408 | #settings input:checked + .slider:before { 409 | -webkit-transform: translateX(18px); 410 | -ms-transform: translateX(18px); 411 | transform: translateX(18px); 412 | } 413 | 414 | /* Waypoints menu */ 415 | #waypointsMenu { 416 | position: fixed; 417 | background: #111; 418 | list-style: none; 419 | padding: 0; 420 | padding-top: 10px; 421 | text-align: center; 422 | margin: 0; 423 | box-shadow: 0 0 10px rgba(0,0,0,.25); 424 | outline: none; 425 | max-height: 300px; 426 | overflow: auto; 427 | border-radius: 5px; 428 | font-family: "Ubuntu"; 429 | font-size: 15px; 430 | color: #ddd; 431 | 432 | opacity: 0; 433 | transition: opacity .2s; 434 | } 435 | 436 | #waypointsMenu li { 437 | border-bottom: 1px solid #333; 438 | padding: 14px; 439 | cursor: pointer; 440 | white-space: nowrap; 441 | text-align: left; 442 | } 443 | 444 | #waypointsMenu li:last-child { 445 | border: none; 446 | } 447 | 448 | #waypointsMenu li .remove { 449 | float: right; 450 | font-size: 18px; 451 | font-weight: 600; 452 | margin-left: 10px; 453 | transition: color .2s; 454 | color: #888; 455 | } 456 | 457 | #waypointsMenu li .remove:hover { 458 | color: #822; 459 | } 460 | 461 | #waypointsMenu li:hover { 462 | background: rgba(255,255,255,.1); 463 | } 464 | 465 | /* Tips */ 466 | 467 | .tip { 468 | display: none; 469 | position: fixed; 470 | background: #ddd; 471 | box-shadow: 0 0 20px rgba(0,0,0,.25); 472 | border-radius: 5px; 473 | font-family: "Ubuntu"; 474 | max-width: 250px; 475 | padding: 10px; 476 | opacity: 0; 477 | transition: opacity 1s; 478 | pointer-events: none; 479 | } 480 | 481 | /* Value balloon */ 482 | #hoverBalloon { 483 | display: none; 484 | position: fixed; 485 | left: 0; 486 | background: #111; 487 | font-family: "Ubuntu"; 488 | font-size: 14px; 489 | color: #888; 490 | padding: 8px; 491 | box-shadow: 0 0 20px rgba(0,0,0,.25); 492 | border-radius: 5px; 493 | text-align: center; 494 | line-height: 16px; 495 | pointer-events: none; 496 | overflow: hidden; 497 | white-space: nowrap; 498 | 499 | opacity: 0; 500 | transform: translateY(30px); 501 | transition: opacity .2s, transform .2s; 502 | } 503 | 504 | #hoverBalloon:after { 505 | content: ''; 506 | position: fixed; 507 | border-style: solid; 508 | border-width: 15px 15px 0; 509 | border-color: #111 transparent; 510 | display: block; 511 | width: 0; 512 | z-index: 1; 513 | bottom: -15px; 514 | left: 50%; 515 | margin-left: -15px; 516 | } 517 | 518 | #hoverBalloon h1 { 519 | color: #aaa; 520 | font-size: 16px; 521 | font-weight: 400; 522 | margin: 5px; 523 | } 524 | 525 | #spectateIndicator { 526 | position: fixed; 527 | top: 0; 528 | left: 50%; 529 | font-family: "Ubuntu"; 530 | font-size: 16px; 531 | display: none; 532 | } 533 | 534 | /* Loading screen */ 535 | #loading { 536 | display: none; 537 | position: fixed; 538 | top: 0; 539 | left: 0; 540 | height: 100%; 541 | width: 100%; 542 | background: rgba(16,16,16,.5); 543 | text-align: center; 544 | padding-top: 20%; 545 | font-family: "Roboto Condensed"; 546 | font-size: 30px; 547 | color: #fff; 548 | } 549 | 550 | /* Close buttons */ 551 | .close { 552 | position: absolute; 553 | top: 10px; 554 | right: 5px; 555 | background: transparent; 556 | color: #888; 557 | border: none; 558 | opacity: .5; 559 | outline: none; 560 | 561 | transform: rotateZ(0deg) scale(1); 562 | transition: opacity .2s, transform .2s; 563 | } 564 | 565 | .close:hover { 566 | font-weight: 800; 567 | transform: rotateZ(90deg) scale(1.2); 568 | opacity: .7; 569 | } 570 | 571 | .close:active { 572 | transform: rotateZ(90deg) scale(.5); 573 | } -------------------------------------------------------------------------------- /app/css/toolbar.css: -------------------------------------------------------------------------------- 1 | #toolbar { 2 | position: fixed; 3 | left: 50%; 4 | bottom: 0; 5 | margin-left: -175px; 6 | width: 350px; 7 | height: 50px; 8 | background: #111; 9 | color: #555; 10 | box-shadow: 0 0 20px rgba(0,0,0,.5); 11 | border-radius: 5px 5px 0 0; 12 | font-size: 0; 13 | opacity: 1; 14 | transition: opacity .5s; 15 | } 16 | 17 | #toolbar .slot { 18 | height: 50px; 19 | width: 50px; 20 | font-family: "Ubuntu"; 21 | font-size: 16px; 22 | font-weight: 600; 23 | text-align: center; 24 | padding-top: 18px; 25 | margin: 0; 26 | display: inline-block; 27 | cursor: pointer; 28 | border-radius: 5px; 29 | vertical-align: middle; 30 | 31 | transition: color .2s; 32 | } 33 | 34 | #toolbar .slot:hover { 35 | background: #222; 36 | color: #ddd; 37 | } 38 | 39 | #toolbar .slot .material-icons { 40 | font-weight: 400; 41 | font-size: 24px; 42 | line-height: 18px; 43 | margin: 0; 44 | } 45 | 46 | #toolbar #toast { 47 | position: fixed; 48 | left: 50%; 49 | bottom: 60px; 50 | background: #ddd; 51 | -box-shadow: 0 0 10px rgba(0,0,0,.25); 52 | border-radius: 50px; 53 | color: #111; 54 | padding: 10px 25px; 55 | font-family: "Ubuntu"; 56 | font-size: 15px; 57 | text-align: center; 58 | opacity: 0; 59 | transition: opacity .5s; 60 | white-space: nowrap; 61 | pointer-events: none; 62 | } 63 | 64 | #toolbartip { 65 | display: none; 66 | position: fixed; 67 | bottom: 85px; 68 | left: 0; 69 | max-width: 100px; 70 | background: #111; 71 | font-family: "Ubuntu"; 72 | font-size: 15px; 73 | color: #ddd; 74 | padding: 10px; 75 | box-shadow: 0 0 20px rgba(0,0,0,.25); 76 | border-radius: 5px; 77 | text-align: center; 78 | pointer-events: none; 79 | 80 | opacity: 0; 81 | transform: translateY(30px); 82 | transition: transform .2s, opacity .2s; 83 | } 84 | 85 | #toolbartip:after { 86 | content: ''; 87 | position: fixed; 88 | border-style: solid; 89 | border-width: 15px 15px 0; 90 | border-color: #111 transparent; 91 | display: block; 92 | width: 0; 93 | z-index: 1; 94 | bottom: -15px; 95 | left: 50%; 96 | margin-left: -15px; 97 | } 98 | 99 | #toolbar .material-icons { 100 | font-size: 20px; 101 | margin-right: 5px; 102 | } 103 | 104 | #toolbar button { 105 | cursor: pointer; 106 | pointer-events: auto; 107 | background: transparent; 108 | border: none; 109 | border-left: 1px solid rgba(0,0,0,.5); 110 | margin: 0 0 0 10px; 111 | padding: 0 0 0 10px; 112 | outline: none; 113 | } 114 | 115 | #toolbar button .material-icons { 116 | margin-right: 5px; 117 | } 118 | 119 | #toolbar #list { 120 | position: absolute; 121 | left: 0; 122 | bottom: 50px; 123 | padding: 0; 124 | margin: 0; 125 | font-family: "Ubuntu"; 126 | font-size: 15px; 127 | color: #ddd; 128 | list-style: none; 129 | background: #111; 130 | box-shadow: 0 0 20px rgba(0,0,0,.5); 131 | border-radius: 5px; 132 | outline: none; 133 | max-height: 300px; 134 | width: 150px; 135 | overflow-y: auto; 136 | 137 | opacity: 0; 138 | transform: scale(.5) translateX(-63px) translateY(150px); 139 | transition: opacity .2s, transform .2s; 140 | } 141 | 142 | #toolbar #list::-webkit-scrollbar-track { 143 | background: rgba(255,255,255,.1); 144 | } 145 | 146 | #toolbar #list::-webkit-scrollbar-thumb { 147 | background: rgba(255,255,255,.3); 148 | border-radius: 5px; 149 | } 150 | 151 | #toolbar #list li { 152 | padding: 10px; 153 | border-radius: 5px; 154 | cursor: pointer; 155 | } 156 | 157 | #toolbar #list li:hover, #toolbar #list li:active { 158 | background: #ddd; 159 | } -------------------------------------------------------------------------------- /app/css/tutorial.css: -------------------------------------------------------------------------------- 1 | .tutorial { 2 | position: fixed; 3 | top: 0; 4 | left: -500px; 5 | height: 100%; 6 | width: 500px; 7 | background: #ddd; 8 | box-shadow: 0 0 20px rgba(0,0,0,.25); 9 | font-family: "Ubuntu", Arial; 10 | font-size: 15px; 11 | color: #111; 12 | text-align: center; 13 | overflow: hidden; 14 | 15 | opacity: 0; 16 | transition: opacity .2s, left .2s; 17 | } 18 | 19 | .tutorial h1 { 20 | font-weight: 400; 21 | } 22 | 23 | .tutorial .sections { 24 | padding: 20px; 25 | height: 100%; 26 | } 27 | 28 | .tutorial .sections div { 29 | position: absolute; 30 | width: 450px; 31 | transform: translateX(500px); 32 | opacity: 0; 33 | transition: transform .2s, opacity .5s; 34 | word-wrap: normal; 35 | } 36 | 37 | .tutorial .sections div p { 38 | line-height: 24px; 39 | margin: 8px; 40 | } 41 | 42 | .tutorial .sections div a { 43 | color: #888; 44 | text-decoration: underline; 45 | cursor: pointer; 46 | } 47 | 48 | .tutorial .sections div img { 49 | width: 250px; 50 | opacity: .7; 51 | border-radius: 5px; 52 | } 53 | 54 | .tutorial .sections div code { 55 | font-family: "Ubuntu Mono", Monospaced; 56 | } 57 | 58 | .tutorial .sections .material-icons { 59 | font-size: 50px; 60 | margin: 20px; 61 | } 62 | 63 | .tutorial .options { 64 | position: absolute; 65 | bottom: 0; 66 | width: 100%; 67 | text-align: center; 68 | } 69 | 70 | .tutorial span { 71 | position: relative; 72 | bottom: 20px; 73 | } 74 | 75 | .tutorial .previous, .tutorial .next { 76 | background: transparent; 77 | border: none; 78 | color: #888; 79 | font-size: 50px; 80 | outline: none; 81 | cursor: pointer; 82 | margin: 20px 50px; 83 | } 84 | 85 | .tutorial .previous:disabled,.tutorial .next:disabled { 86 | opacity: .5; 87 | } 88 | 89 | .tutorial .close { 90 | color: #111; 91 | font-size: 24px; 92 | margin: 5px; 93 | } -------------------------------------------------------------------------------- /app/fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /app/fonts/LobsterTwo-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/fonts/LobsterTwo-Regular.ttf -------------------------------------------------------------------------------- /app/fonts/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/fonts/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /app/fonts/Monospace.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/fonts/Monospace.ttf -------------------------------------------------------------------------------- /app/fonts/Righteous-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/fonts/Righteous-Regular.ttf -------------------------------------------------------------------------------- /app/fonts/Ubuntu-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/fonts/Ubuntu-Regular.ttf -------------------------------------------------------------------------------- /app/fonts/UbuntuMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/fonts/UbuntuMono-Regular.ttf -------------------------------------------------------------------------------- /app/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/img/favicon.png -------------------------------------------------------------------------------- /app/img/tutorial/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/img/tutorial/0.png -------------------------------------------------------------------------------- /app/img/tutorial/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/img/tutorial/1.png -------------------------------------------------------------------------------- /app/img/tutorial/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/img/tutorial/2.png -------------------------------------------------------------------------------- /app/img/tutorial/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/img/tutorial/3.png -------------------------------------------------------------------------------- /app/img/tutorial/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/img/tutorial/4.png -------------------------------------------------------------------------------- /app/img/tutorial/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/img/tutorial/5.png -------------------------------------------------------------------------------- /app/img/tutorial/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/img/tutorial/6.png -------------------------------------------------------------------------------- /app/img/tutorial/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/app/img/tutorial/7.png -------------------------------------------------------------------------------- /app/js/audio.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let audioCtx; 4 | try { 5 | audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 6 | } catch(e) { 7 | console.error("Web Audio API is not supported."); 8 | } 9 | 10 | function beep(frequency = 440, duration = 500) { 11 | let oscillator = audioCtx.createOscillator(); 12 | oscillator.type = "sine"; 13 | oscillator.frequency.value = frequency; 14 | oscillator.connect(audioCtx.destination); 15 | oscillator.start(0); 16 | setTimeout(() => oscillator.stop(0), duration); 17 | } -------------------------------------------------------------------------------- /app/js/chat.js: -------------------------------------------------------------------------------- 1 | const chat = document.getElementById("chat"); 2 | chat.hidden = true; 3 | 4 | chat.show = function() { 5 | chat.hidden = false; 6 | 7 | this.style.transform = "translateY(0px)"; 8 | notifications.style.transform = "translateY(0px)"; 9 | notifications.style.pointerEvents = "auto"; 10 | 11 | for(let i = 0; i < notifications.children.length; ++i) { 12 | notifications.children[i].style.display = "block"; 13 | notifications.children[i].style.opacity = 1; 14 | } 15 | notifications.scrollTop = notifications.scrollHeight - notifications.clientHeight; 16 | } 17 | 18 | chat.onblur = function() { 19 | //this.hide(); 20 | } 21 | 22 | chat.hide = function() { 23 | chat.hidden = true; 24 | 25 | this.style.transform = "translateX(-230px)"; 26 | notifications.style.transform = "translateY(80px)"; 27 | notifications.style.pointerEvents = "none"; 28 | 29 | for(let i = 0; i < notifications.children.length; ++i) { 30 | if(!notifications.children[i].display) { 31 | notifications.children[i].style.display = "none"; 32 | notifications.children[i].style.opacity = 0; 33 | } 34 | } 35 | 36 | c.focus(); 37 | } 38 | 39 | chat.onkeydown = function(e) { 40 | if(e.which == 13) { 41 | if(socket) { 42 | socket.send(JSON.stringify({ 43 | type: "chat", data: this.value 44 | })); 45 | this.value = ""; 46 | setTimeout(() => this.focus()); 47 | e.preventDefault(); 48 | return false; 49 | } 50 | } else if(e.which == 27) { 51 | this.hide(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/js/clipboard.js: -------------------------------------------------------------------------------- 1 | let clipboard = {}; 2 | clipboard.components = []; 3 | clipboard.wires = []; 4 | 5 | clipboard.copy = function(components = [], wires = [], selection) { 6 | const clone = cloneSelection(components,wires); 7 | clipboard.components = clone.components; 8 | clipboard.wires = clone.wires; 9 | if(selection) { 10 | clipboard.selection = Object.assign({},selection); 11 | } else { 12 | delete clipboard.selection; 13 | } 14 | } 15 | 16 | clipboard.paste = function(x, y, undoable = false) { 17 | if(this.selection) { 18 | const dx = Math.round(x - this.selection.x) || 0; 19 | const dy = Math.round(y - this.selection.y) || 0; 20 | 21 | const clone = cloneSelection(this.components,this.wires,dx,dy); 22 | addSelection( 23 | clone.components, 24 | clone.wires, 25 | this.selection, 26 | x, y, 27 | true 28 | ) 29 | // components.push(...clone.components); 30 | // wires.push(...clone.wires); 31 | // 32 | // selecting = Object.assign({},this.selection); 33 | // selecting.x = x; 34 | // selecting.y = y; 35 | // selecting.components = [...clone.components]; 36 | // selecting.wires = [...clone.wires]; 37 | // 38 | // contextMenu.show( 39 | // selecting.x + selecting.w, 40 | // selecting.y + selecting.h 41 | // ); 42 | } else if(this.components.length > 0) { 43 | const clone = cloneComponent(this.components[0]) 44 | clone.pos.x = x; 45 | clone.pos.y = y; 46 | components.push(clone); 47 | } 48 | } 49 | 50 | // clipboard.paste = function(x,y) { 51 | // if(this.components.length == 0 && this.wires.length == 0) { 52 | // return; 53 | // } 54 | // 55 | // let components = [...this.components]; 56 | // let wires = [...this.wires]; 57 | // const connections = this.connections; 58 | // 59 | // let added = []; 60 | // if(clipboard.selection) { 61 | // const dx = Math.round(x - this.selection.x) || 0; 62 | // const dy = Math.round(y - this.selection.y) || 0; 63 | // 64 | // // for(let i = clipboard.components.length - 1; i >= 0; --i) { 65 | // // const pos = clipboard.components[i].pos; 66 | // // 67 | // // clipboard.components[i] = clone(clipboard.components[i]); 68 | // // 69 | // // if(Array.isArray(pos)) { 70 | // // for(let j = 0, len2 = pos.length; j < len2; ++j) { 71 | // // clipboard.components[i].pos.push({ 72 | // // x: Math.round(pos[j].x + dx), 73 | // // y: Math.round(pos[j].y + dy) 74 | // // }); 75 | // // } 76 | // // 77 | // // added.unshift(clipboard.components[i]); 78 | // // } 79 | // // else { 80 | // // clipboard.components[i].pos.x = Math.round(pos.x + dx); 81 | // // clipboard.components[i].pos.y = Math.round(pos.y + dy); 82 | // // added.push(clipboard.components[i]); 83 | // // } 84 | // // } 85 | // 86 | // components = components.map(component => cloneComponent(component,dx,dy)); 87 | // wires = wires.map(wire => cloneWire(wire,dx,dy)); 88 | // 89 | // for(let i = 0; i < connections.length; ++i) { 90 | // const from = components[connections[i][0]]; 91 | // const to = components[connections[i][1]]; 92 | // const wire = wires[connections[i][4]]; 93 | // 94 | // const fromPort = from.output[connections[i][2]]; 95 | // const toPort = to.input[connections[i][3]]; 96 | // 97 | // wire.from = fromPort; 98 | // wire.to = toPort; 99 | // 100 | // connect(fromPort,toPort,wire); 101 | // } 102 | // 103 | // if(this.selection) { 104 | // setTimeout(() => { 105 | // selecting = Object.assign({}, this.selection); 106 | // selecting.x = Math.round(x); 107 | // selecting.y = Math.round(y); 108 | // 109 | // selecting.components = components; 110 | // selecting.wires = wires; 111 | // 112 | // contextMenu.show( 113 | // (selecting.x + selecting.w - offset.x) * zoom, 114 | // (-(selecting.y + selecting.h) + offset.y) * zoom 115 | // ); 116 | // 117 | // action("addSelection",[components,wires],true); 118 | // }); 119 | // } 120 | // } 121 | // else { 122 | // const component = cloneComponent(components[0]); 123 | // component.pos.x = x; 124 | // component.pos.y = y; 125 | // action("add",component,true); 126 | // } 127 | // } 128 | -------------------------------------------------------------------------------- /app/js/componentInfo.js: -------------------------------------------------------------------------------- 1 | const componentInfo = document.getElementById("componentInfo"); 2 | componentInfo.expanded = false; 3 | 4 | componentInfo.show = function(component,pos) { 5 | this.innerHTML = `

${ component.name }

`; 6 | this.innerHTML += `${ component.constructor.name }
`; 7 | this.innerHTML += `x: ${ component.pos.x }, y: ${ component.pos.y }
`; 8 | 9 | if(!this.expanded) { 10 | this.innerHTML += 11 | "Press tab for more details"; 12 | } else { 13 | for(let i in component) { 14 | this.innerHTML += `${i}: ${component[i]}
`; 15 | } 16 | } 17 | 18 | this.style.top = pos.y - this.clientHeight - zoom / 2; 19 | this.style.left = pos.x - this.clientWidth / 2; 20 | 21 | setTimeout(() => { 22 | this.style.display = "block"; 23 | setTimeout(() => { 24 | this.style.transform = "translateY(0px)"; 25 | this.style.opacity = 1; 26 | }, 100); 27 | }); 28 | } 29 | componentInfo.hide = function() { 30 | componentInfo.expanded = false; 31 | 32 | this.style.transform = "translateY(20px)"; 33 | this.style.opacity = 0; 34 | setTimeout(() => this.style.display = "none",100); 35 | }; 36 | -------------------------------------------------------------------------------- /app/js/componentUpdates.js: -------------------------------------------------------------------------------- 1 | var updateQueue = []; 2 | 3 | let lastTick = new Date; 4 | let ticksPerSecond = 0; 5 | 6 | let pauseSimulation = false; 7 | let updates; 8 | 9 | 10 | function tick() { 11 | const start = new Date; 12 | updates = 0; 13 | 14 | while(updateQueue.length > 0 && updates < 10000 && !pauseSimulation) { 15 | updateQueue.splice(0,1)[0](); 16 | ++updates; 17 | } 18 | 19 | ticksPerSecond = 1000 / (new Date - lastTick); 20 | lastTick = new Date; 21 | } 22 | 23 | setInterval(tick); 24 | 25 | 26 | // 500ms : 1:39 27 | // 250ms : 56 28 | -------------------------------------------------------------------------------- /app/js/console.js: -------------------------------------------------------------------------------- 1 | // This is the rewritten version of console2.js. This file isn't used anymore 2 | 3 | const Console = document.querySelector("#console"); 4 | Console.input = document.querySelector("#console #input"); 5 | Console.messages = document.querySelector("#console #messages"); 6 | 7 | let variables = {}; 8 | 9 | Console.show = function() { 10 | this.style.display = "block"; 11 | this.style.left = 0; 12 | } 13 | 14 | Console.hide = function() { 15 | this.style.display = "none"; 16 | Console.input.blur(); 17 | c.focus(); 18 | } 19 | 20 | Console.message = function(msg) { 21 | Console.messages.innerHTML += "
" + msg + "
"; 22 | } 23 | 24 | Console.error = function(msg) { 25 | Console.messages.innerHTML += "
ERROR: " + msg + "
"; 26 | } 27 | 28 | Console.input.onkeydown = function(e) { 29 | if(e.which == 13) { 30 | let input = this.textContent; 31 | Console.input.textContent = ""; 32 | Console.messages.innerHTML += "
" + input + "
"; 33 | 34 | input = input.split(/ +/g); 35 | const command = input[0]; 36 | const args = input.slice(1); 37 | 38 | switch(command) { 39 | case "help": 40 | Console.message( 41 | "Commands
" + 42 | "help: get list of all commands
" + 43 | "center: move to center of the map
" + 44 | "goto [x] [y]: move to a point on the map
" + 45 | "find [name]: find and move to component
" + 46 | "connect [name] [name]: connect two components
" 47 | ); 48 | break; 49 | case "center": 50 | scroll(-offset.x,-offset.y); 51 | Console.messages.innerHTML += "
Moved to center
"; 52 | break; 53 | case "goto": 54 | let dx = 0; 55 | if(args[0] && (args[0][0] == "+" || args[0][0] == "-") && args[0].length > 1) dx = +args[0]; 56 | else if(!isNaN(args[0])) dx = +args[0] - offset.x; 57 | 58 | let dy = 0; 59 | if(args[1] && (args[1][0] == "+" || args[1][0] == "-") && args[1].length > 1) dy = +args[1]; 60 | else if(!isNaN(args[1])) dy = +args[1] - offset.y; 61 | 62 | Console.messages.innerHTML += "
Moved to " + Math.round(offset.x + dx) + " " + Math.round(offset.y + dy) + "
"; 63 | scroll(dx,dy); 64 | break; 65 | case "find": 66 | let component = components.find(n => n.name == args[0]); 67 | if(component) { 68 | Console.messages.innerHTML += "
Component '" + args[0] + "' found at " + component.pos.x + " " + component.pos.y + "
"; 69 | } else Console.messages.innerHTML += "
No component called '" + args[0] + "' found
"; 70 | break; 71 | case "connect": 72 | let from = components.find(n => n.name == args[0]); 73 | let to = components.find(n => n.name == args[1]); 74 | if(!from && !to) { 75 | Console.messages.innerHTML += "
No components called '" + args[0] + "' and '" + args[1] + "' found
"; 76 | } else if(!from) { 77 | Console.messages.innerHTML += "
No component called '" + args[0] + "' found
"; 78 | } else if(!to) { 79 | Console.messages.innerHTML += "
No component called '" + args[1] + "' found
"; 80 | } else { 81 | let wire = new Wire(); 82 | wire.from = from; 83 | wire.to = to; 84 | from.connect(to,wire); 85 | Console.messages.innerHTML += "
Connected '" + args[0] + "' with '" + args[1] + "'
"; 86 | } 87 | break; 88 | case "list": 89 | let list = ""; 90 | for(let i in socket.users) { 91 | list += `${i}
`; 92 | } 93 | Console.messages.innerHTML += 94 | "
" + list + "
"; 95 | break; 96 | case "set": 97 | if((args[0].match(/[a-zA-Z'`´_-]+/g) || []) != args[0]) { 98 | Console.error(args[0] + ": not a valid variable name"); 99 | } else if(!variables[args[0]]) { 100 | variables[args[0]] = { 101 | value: isNaN(args[1]) ? args[1] : +args[1], 102 | updates: [] 103 | }; 104 | Console.messages.innerHTML += "
" + args[1] + "
"; 105 | } else { 106 | const variable = variables[args[0]]; 107 | variable.value = isNaN(args[1]) ? args[1] : +args[1]; 108 | 109 | for(let i = 0; i < variable.updates.length; ++i) { 110 | variable.updates[i](); 111 | } 112 | 113 | Console.messages.innerHTML += "
" + args[1] + "
"; 114 | } 115 | break; 116 | case "get": 117 | const value = variables[args[0]].value; 118 | if(value == undefined) { 119 | Console.error("Variable " + args[0] + " not found"); 120 | } else { 121 | Console.messages.innerHTML += "
" + value + "
"; 122 | } 123 | break; 124 | default: 125 | Console.error("Command not found: \"" + command + "\""); 126 | break; 127 | } 128 | setTimeout(() => Console.input.focus()); 129 | return false; 130 | } else if(e.which == 27) { 131 | Console.hide(); 132 | } 133 | } -------------------------------------------------------------------------------- /app/js/console2.js: -------------------------------------------------------------------------------- 1 | const boolrConsole = document.querySelector(".console"); 2 | const container = boolrConsole.querySelector(".container"); 3 | const focusedInput = container.querySelector(".focused-input"); 4 | focusedInput.input = focusedInput.querySelector("input"); 5 | 6 | boolrConsole.history = []; 7 | let historyIndex = 0; 8 | 9 | boolrConsole.show = function() { 10 | this.style.display = "block"; 11 | setTimeout(() => { 12 | this.style.opacity = 1; 13 | this.style.transform = "scale(1)"; 14 | focusedInput.input.focus(); 15 | }, 10); 16 | } 17 | 18 | boolrConsole.hide = function() { 19 | this.style.opacity = 0; 20 | this.style.transform = "scale(.9) translateX(-63px) translateY(150px)"; 21 | setTimeout(() => { 22 | this.style.display = "none"; 23 | c.focus(); 24 | }, 200); 25 | } 26 | 27 | boolrConsole.toggleFullscreen = function() { 28 | this.fullscreen = !this.fullscreen; 29 | 30 | this.style.height = this.fullscreen ? innerHeight - 40 : "460px"; 31 | this.style.width = this.fullscreen ? innerWidth - 40 : "360px"; 32 | 33 | focusedInput.input.focus(); 34 | } 35 | 36 | boolrConsole.clear = function() { 37 | container.innerHTML = ""; 38 | container.appendChild(focusedInput); 39 | focusedInput.input.focus(); 40 | } 41 | 42 | boolrConsole.log = function(msg) { 43 | const log = document.createElement("div"); 44 | log.className = "log"; 45 | log.innerHTML = msg; 46 | container.insertBefore(log, focusedInput); 47 | container.scrollTop = container.scrollHeight; 48 | } 49 | 50 | boolrConsole.error = function(msg) { 51 | const log = document.createElement("div"); 52 | log.className = "error"; 53 | log.innerHTML = msg; 54 | container.insertBefore(log, focusedInput); 55 | container.scrollTop = container.scrollHeight; 56 | } 57 | 58 | boolrConsole.onkeydown = function(e) { 59 | if(e.which == 13) { // Enter key 60 | const input = document.createElement("div"); 61 | input.className = "input"; 62 | input.innerHTML = focusedInput.input.value; 63 | container.insertBefore(input, focusedInput); 64 | 65 | boolrConsole.history.push(focusedInput.input.value); 66 | 67 | try { 68 | const output = document.createElement("div"); 69 | output.className = "output"; 70 | output.innerHTML = inputHandler(focusedInput.input.value) || ""; 71 | container.insertBefore(output, focusedInput); 72 | } catch(e) { 73 | this.error(e); 74 | } 75 | 76 | focusedInput.input.value = ""; 77 | container.scrollTop = container.scrollHeight; 78 | historyIndex = 0; 79 | } else if(e.which == 38) { 80 | if(historyIndex > -boolrConsole.history.length) { 81 | focusedInput.input.value = boolrConsole.history.slice(--historyIndex)[0]; 82 | } 83 | 84 | // Move caret to the end 85 | setTimeout(() => focusedInput.input.value = focusedInput.input.value); 86 | } else if(e.which == 40) { 87 | if(historyIndex == -1) { 88 | historyIndex = 0; 89 | focusedInput.input.value = ""; 90 | } else if(historyIndex < -1) { 91 | focusedInput.input.value = boolrConsole.history.slice(++historyIndex)[0]; 92 | } 93 | 94 | // Move caret to the end 95 | setTimeout(() => focusedInput.input.value = focusedInput.input.value); 96 | } else if(e.which == 27) { 97 | this.hide(); 98 | } else if(e.which == 76 && e.ctrlKey) { 99 | this.clear(); 100 | 101 | // Prevents default address bar focus 102 | return false; 103 | } else if(e.which == 9) { 104 | if(focusedInput.input.value == "" || focusedInput.input.value.includes(" ")) return false; 105 | 106 | let found = []; 107 | for(let i = 0; i < commands.length; ++i) { 108 | if(commands[i].match(new RegExp("^" + focusedInput.input.value))) { 109 | found.push(commands[i]); 110 | } 111 | } 112 | focusedInput.input.value = found[0] + " "; 113 | 114 | // Move caret to the end 115 | setTimeout(() => focusedInput.input.value = focusedInput.input.value); 116 | 117 | // Prevent default action 118 | return false; 119 | } 120 | } 121 | 122 | const commands = ["set","get","variables","remove","edit","findComponent","pause"]; 123 | 124 | boolrConsole.help = function() { 125 | this.log("set [name] [value]: sets a variable"); 126 | this.log("get [name]: returns value of a variable"); 127 | this.log("variables: return list of all variables"); 128 | this.log("remove ([name] | [id] | [x] [y]): removes component"); 129 | this.log("edit ([name] | [id] | [x] [y]): edits property of component"); 130 | this.log("findComponent ([name] | [id] | [x] [y]): finds component"); 131 | this.log("start: starts simulation"); 132 | this.log("pause: pauses simulation"); 133 | } 134 | 135 | function inputHandler(input) { 136 | input = input.split(" "); 137 | const command = input[0]; 138 | const args = input.slice(1); 139 | 140 | switch(command) { 141 | case "set": 142 | return setVariable(args[0],args[1]); 143 | break; 144 | case "get": 145 | return getVariable(args[0]); 146 | break; 147 | case "variables": 148 | for(let i in variables) { 149 | boolrConsole.log(i + ": " + variables[i]); 150 | } 151 | break; 152 | case "remove": 153 | if(args.length == 1 && isNaN(args[0])) { 154 | var component = findComponentByName(args[0]); 155 | if(!component) return "Component " + args[0] + " not found"; 156 | } else if(args.length == 1) { 157 | var component = findComponentByID(+args[0]); 158 | if(!component) return "Component with ID " + args[0] + " not found"; 159 | } else if(args.length > 1) { 160 | var x = args[0] == "~" ? mouse.grid.x : +args[0]; 161 | var y = args[1] == "~" ? mouse.grid.y : +args[1]; 162 | var component = findComponentByPos(x, y); 163 | if(!component) return "No component found at " + x + "," + y; 164 | } 165 | removeComponent(component,true); 166 | return "Removed component " + component.name; 167 | break; 168 | case "edit": 169 | if((!isNaN(args[0]) || args[0] == "~") && (!isNaN(args[1]) || args[1] == "~")) { 170 | var x = args[0] == "~" ? mouse.grid.x : +args[0]; 171 | var y = args[1] == "~" ? mouse.grid.y : +args[1]; 172 | var component = findComponentByPos(x, y); 173 | if(!component) return "No component found at " + x + "," + y; 174 | 175 | edit(component,args[2],args[3],true); 176 | } else if(!isNaN(args[0])) { 177 | var component = findComponentByID(+args[0]); 178 | if(!component) return "Component with ID " + args[0] + " not found"; 179 | 180 | edit(component,args[1],args[2],true); 181 | } else if(args.length > 1) { 182 | var component = findComponentByName(args[0]); 183 | if(!component) return "Component " + args[0] + " not found"; 184 | 185 | edit(component,args[1],args[2],true); 186 | } 187 | break; 188 | case "findComponent": 189 | if(args.length == 1 && isNaN(args[0])) { 190 | var component = findComponentByName(args[0]); 191 | if(!component) return "Component " + args[0] + " not found"; 192 | else return "Component " + args[0] + " found at " + component.pos.x + "," + component.pos.y; 193 | } else if(args.length == 1) { 194 | var component = findComponentByID(+args[0]); 195 | if(!component) return "Component with ID " + args[0] + " not found"; 196 | else return "Component with ID " + args[0] + " found at " + component.pos.x + "," + component.pos.y; 197 | } else if(args.length > 1) { 198 | var x = args[0] == "~" ? mouse.grid.x : +args[0]; 199 | var y = args[1] == "~" ? mouse.grid.y : +args[1]; 200 | var component = findComponentByPos(x, y); 201 | if(!component) return "No component found at " + x + "," + y; 202 | else { 203 | return "Component " + component.name + " found at " + component.pos.x + "," + component.pos.y; 204 | } 205 | } 206 | break; 207 | case "start": 208 | pauseSimulation = false; 209 | document.querySelector("#pause").innerHTML = "play_arrow"; 210 | return "Simulation started"; 211 | break; 212 | case "pause": 213 | pauseSimulation = true; 214 | document.querySelector("#pause").innerHTML = "pause"; 215 | return "Simulation paused"; 216 | break; 217 | case "help": 218 | boolrConsole.help(); 219 | break; 220 | case "?": 221 | boolrConsole.help(); 222 | break; 223 | case "openDevTools": 224 | require('electron').remote.getCurrentWindow().webContents.openDevTools(); 225 | return "Opened Developer Tools"; 226 | break; 227 | default: 228 | throw "Command not found: " + command; 229 | break; 230 | } 231 | } 232 | 233 | -------------------------------------------------------------------------------- /app/js/contextmenu.js: -------------------------------------------------------------------------------- 1 | // This is the rewritten version of contextmenu2.js. This file isn't used anymore 2 | 3 | const contextMenu = document.getElementById("contextMenu"); 4 | contextMenu.pos = {}; 5 | 6 | contextMenu.show = function(pos) { 7 | if(dragging || connecting) return false; 8 | this.pos = { 9 | x: (pos.x + 1) / zoom + offset.x, 10 | y: (-pos.y - 1) / zoom + offset.y 11 | } 12 | 13 | this.innerHTML = ""; 14 | if(selecting) { 15 | this.appendChild(contextOptions["copy"]); 16 | if(selecting.components && selecting.components.length > 2) this.appendChild(contextOptions["componentize"]); 17 | this.appendChild(contextOptions["remove all"]); 18 | } else { 19 | const component = findComponentByPos(Math.round(pos.x / zoom + offset.x),Math.round(-pos.y / zoom + offset.y)); 20 | if(component) { 21 | if(component.constructor == Wire) { 22 | this.appendChild(contextOptions["edit color"]); 23 | } else { 24 | component.hasOwnProperty("name") && this.appendChild(contextOptions["edit name"]); 25 | component.hasOwnProperty("delay") && this.appendChild(contextOptions["edit delay"]); 26 | this.appendChild(contextOptions["rotate"]); 27 | this.appendChild(contextOptions["copy"]); 28 | this.appendChild(contextOptions["view connections"]); 29 | } 30 | 31 | this.appendChild(contextOptions["set waypoint"]); 32 | contextOptions["set waypoint"].innerHTML = `my_locationSet waypoint @${component.name} (S)`; 33 | 34 | this.appendChild(contextOptions["remove"]); 35 | } else { 36 | this.appendChild(contextOptions["paste"]); 37 | 38 | this.appendChild(contextOptions["set waypoint"]); 39 | contextOptions["set waypoint"].innerHTML = `my_locationSet waypoint @${Math.round(contextMenu.pos.x)},${Math.round(contextMenu.pos.y)} (S)`; 40 | 41 | this.appendChild(contextOptions["goto waypoint"]); 42 | contextOptions["goto waypoint"].innerHTML = 'redoJump to waypoint (W)'; 43 | if(waypoints.length == 0) contextOptions["goto waypoint"].className = "disabled"; 44 | else { 45 | contextOptions["goto waypoint"].className = ""; 46 | contextOptions["goto waypoint"].innerHTML += 'navigate_next'; 47 | } 48 | } 49 | } 50 | 51 | this.style.display = "block"; 52 | setTimeout(() => contextMenu.style.opacity = 1, 1); 53 | 54 | if(pos.x > c.width - this.clientWidth) this.pos.x = (c.width - this.clientWidth) / zoom + offset.x; 55 | if(pos.y > c.height - this.clientHeight) this.pos.y = -(c.height - this.clientHeight) / zoom + offset.y; 56 | } 57 | 58 | contextMenu.hide = function() { 59 | this.style.opacity = 0; 60 | setTimeout(() => contextMenu.style.display = "none", 200); 61 | 62 | waypointsMenu.hide(); 63 | } 64 | 65 | /* Menu options */ 66 | const contextOptions = {}; 67 | 68 | // Edit name 69 | contextOptions["edit name"] = document.createElement("li"); 70 | contextOptions["edit name"].innerHTML = 'mode_editEdit name (E)'; 71 | contextOptions["edit name"].onclick = () => { 72 | const component = findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y)); 73 | if(component && component.hasOwnProperty("name")) { 74 | popup.prompt.show( 75 | "Edit name", 76 | "Enter a name for this component:", 77 | name => name && name.length < 18 && action("edit",[component,"name",n => name],true) 78 | ); 79 | } 80 | } 81 | 82 | // Edit wire color 83 | contextOptions["edit color"] = document.createElement("li"); 84 | contextOptions["edit color"].innerHTML = 'color_lensEdit color (E)'; 85 | contextOptions["edit color"].onclick = () => { 86 | const component = findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y)); 87 | if(component && component.color_off) { 88 | popup.color_picker.show( 89 | color => color 90 | && (color.match(/\#((\d|[a-f]){6}|(\d|[a-f]){3})/g) || [])[0] == color 91 | && !edit(component,"color_off",color) && action("edit",[component,"color_on",lighter(color,50)],true) 92 | ); 93 | } 94 | } 95 | 96 | // Edit delay 97 | contextOptions["edit delay"] = document.createElement("li"); 98 | contextOptions["edit delay"].innerHTML = 'timerEdit delay'; 99 | contextOptions["edit delay"].onclick = () => { 100 | const component = findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y)); 101 | if(component && component.hasOwnProperty("delay")) { 102 | popup.prompt.show( 103 | "Edit delay", 104 | "Enter the new delay in ms", 105 | delay => { 106 | edit(component,"delay",+delay); 107 | } 108 | ); 109 | } 110 | } 111 | 112 | // Rotate 113 | contextOptions["rotate"] = document.createElement("li"); 114 | contextOptions["rotate"].innerHTML = 'rotate_leftRotate (R)'; 115 | contextOptions["rotate"].onclick = () => { 116 | const component = findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y)); 117 | const t = component.height; 118 | component.height = component.width; 119 | component.width = t; 120 | } 121 | 122 | // 123 | // // Clone 124 | // contextOptions["clone"] = document.createElement("li"); 125 | // contextOptions["clone"].innerHTML = 'content_copyClone (CTRL+D+Drag)'; 126 | // contextOptions["clone"].onclick = () => { 127 | // findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y)) && clone(findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y))); 128 | // } 129 | 130 | // Copy 131 | contextOptions["copy"] = document.createElement("li"); 132 | contextOptions["copy"].innerHTML = 'content_copyCopy to clipbord (Ctrl+C)'; 133 | contextOptions["copy"].onclick = () => { 134 | // clipbord = Object.assign({},selecting); 135 | // clipbord.components = stringify(selecting.components); 136 | 137 | if(selecting) { 138 | clipbord.copy(selecting.components, selecting); 139 | } else if(findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y))) { 140 | clipbord.copy([findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y))]); 141 | } 142 | } 143 | 144 | // Paste 145 | contextOptions["paste"] = document.createElement("li"); 146 | contextOptions["paste"].innerHTML = 'content_pastePaste (Ctrl+V)'; 147 | contextOptions["paste"].onclick = function() { 148 | //parse(clipbord.components,-(clipbord.x - contextMenu.pos.x),-(clipbord.y - contextMenu.pos.y),true); 149 | clipbord.paste(contextMenu.pos.x,contextMenu.pos.y); 150 | } 151 | 152 | // Remove component 153 | contextOptions["remove component"] = document.createElement("li"); 154 | contextOptions["remove component"].innerHTML = 'deleteRemove (Del)'; 155 | contextOptions["remove component"].onclick = () => { 156 | if(findComponentByPos()) { 157 | action( 158 | "remove", 159 | findComponentByPos(), 160 | true 161 | ); 162 | } 163 | } 164 | 165 | // Remove wire 166 | contextOptions["remove wire"] = document.createElement("li"); 167 | contextOptions["remove wire"].innerHTML = 'deleteRemove (Del)'; 168 | contextOptions["remove wire"].onclick = () => { 169 | if(findWireByPos()) { 170 | action( 171 | "disconnect", 172 | findWireByPos(), 173 | true 174 | ); 175 | } 176 | } 177 | 178 | // Delete 179 | contextOptions["remove component"] = document.createElement("li"); 180 | contextOptions["remove component"].innerHTML = 'deleteRemove (Del)'; 181 | contextOptions["remove component"].onclick = () => { 182 | if(findComponentByPos()) { 183 | action( 184 | "remove", 185 | findComponentByPos(), 186 | true 187 | ); 188 | } 189 | } 190 | 191 | // Delete All 192 | contextOptions["remove all"] = document.createElement("li"); 193 | contextOptions["remove all"].innerHTML = 'deleteRemove (Del)'; 194 | contextOptions["remove all"].onclick = () => { 195 | action( 196 | "removeSelection", 197 | [...selecting.components], 198 | true 199 | ); 200 | }; 201 | 202 | // Input/Output 203 | contextOptions["view connections"] = document.createElement("li"); 204 | contextOptions["view connections"].innerHTML = 'compare_arrowsView connections'; 205 | contextOptions["view connections"].onclick = () => { 206 | popup.connections.show( 207 | findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y)) 208 | ); 209 | }; 210 | 211 | // Componentize 212 | contextOptions["componentize"] = document.createElement("li"); 213 | contextOptions["componentize"].innerHTML = 'settings_input_componentComponentize'; 214 | contextOptions["componentize"].onclick = () => { 215 | const component = new Custom( 216 | selecting.components, 217 | { x: Math.round(selecting.x + selecting.w / 2), 218 | y: Math.round(selecting.y + selecting.h / 2) 219 | }); 220 | 221 | add(component); 222 | }; 223 | 224 | // Set waypoint 225 | contextOptions["set waypoint"] = document.createElement("li"); 226 | contextOptions["set waypoint"].innerHTML = 'my_locationSet waypoint (S)'; 227 | contextOptions["set waypoint"].onclick = () => { 228 | setWaypoint( 229 | Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y), 230 | findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y)) && findComponentByPos(Math.round(contextMenu.pos.x),Math.round(contextMenu.pos.y)).name 231 | ); 232 | }; 233 | 234 | // Go to waypoint 235 | contextOptions["goto waypoint"] = document.createElement("li"); 236 | 237 | contextOptions["goto waypoint"].onmouseenter = () => { 238 | waypointsMenu.show({ 239 | x: contextMenu.pos.x + contextMenu.clientWidth / zoom, 240 | y: -contextOptions["goto waypoint"].getBoundingClientRect().top / zoom + offset.y 241 | }); 242 | 243 | for(let i in contextOptions) { 244 | i != "goto waypoint" && (contextOptions[i].onmouseover = function() { 245 | waypointsMenu.hide(); 246 | for(let i in contextOptions) i != "goto waypoint" && (contextOptions[i].onmouseover = undefined); 247 | }); 248 | } 249 | } 250 | 251 | 252 | 253 | contextMenu.onclick = function() { this.style.display = "none"; selecting = null; c.focus() }; 254 | 255 | contextMenu.onkeydown = function(e) { 256 | switch(e.which) { 257 | case 27: 258 | this.hide(); 259 | break; 260 | case 46: // Delete 261 | if(selecting) contextOptions["remove all"].onclick(); 262 | else contextOptions["remove"].onclick(); 263 | 264 | this.style.display = "none"; 265 | selecting = null; 266 | c.focus(); 267 | break; 268 | case 67: // C 269 | if(e.ctrlKey) contextOptions["copy"].onclick(); 270 | break; 271 | } 272 | } 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /app/js/contextmenu2.js: -------------------------------------------------------------------------------- 1 | const contextMenu = document.getElementById("contextMenu"); 2 | 3 | contextMenu.show = function( 4 | x = mouse.screen.x / zoom + offset.x, 5 | y = -mouse.screen.y / zoom + offset.y 6 | ) { 7 | if(dragging || connecting) return; 8 | 9 | this.style.width = "auto"; 10 | 11 | this.style.display = "block"; 12 | this.x = x; 13 | this.y = y; 14 | 15 | // Add context options 16 | this.innerHTML = ""; 17 | for(let i = 0; i < this.options.length; ++i) { 18 | if(this.options[i].show()) { 19 | contextMenu.appendChild(this.options[i]); 20 | } 21 | } 22 | 23 | // Show the context menu on the screen 24 | // this.style.display = "block"; 25 | // this.x = Math.min( 26 | // x, 27 | // c.width / zoom + offset.x - (this.clientWidth + 1) / zoom 28 | // ); 29 | // this.y = Math.max( 30 | // y - 1 / zoom, 31 | // -c.height / zoom + offset.y + this.clientHeight / zoom 32 | // ); 33 | 34 | setTimeout(() => { 35 | contextMenu.style.opacity = 1; 36 | this.style.width = this.clientWidth + 1; 37 | },10); 38 | } 39 | 40 | contextMenu.hide = function() { 41 | this.style.opacity = 0; 42 | setTimeout( 43 | () => contextMenu.style.display = "none", 44 | 200 45 | ); 46 | 47 | waypointsMenu.hide(); 48 | } 49 | 50 | contextMenu.onclick = function() { 51 | this.style.display = "none"; 52 | selecting = null; 53 | c.focus(); 54 | } 55 | 56 | contextMenu.getPos = () => [Math.round(contextMenu.x),Math.round(contextMenu.y)]; 57 | 58 | contextMenu.options = []; 59 | function createContextMenuOption(text,icon,key,onclick,show) { 60 | const option = document.createElement("li"); 61 | option.innerHTML = 62 | `${icon}` + 63 | `${text}` + 64 | `${key}`; 65 | option.onclick = onclick; 66 | option.show = show; 67 | contextMenu.options.push(option); 68 | } 69 | 70 | createContextMenuOption( 71 | "Copy", 72 | "content_copy", 73 | "Ctrl+C", 74 | function() { 75 | if(selecting) { 76 | clipboard.copy( 77 | selecting.components, 78 | selecting.wires, 79 | selecting 80 | ); 81 | } else if(findComponentByPos(...contextMenu.getPos())) { 82 | clipboard.copy( 83 | [findComponentByPos(...contextMenu.getPos())] 84 | ); 85 | } 86 | }, 87 | function() { 88 | return findComponentByPos() || selecting; 89 | } 90 | ); 91 | 92 | createContextMenuOption( 93 | "Paste", 94 | "content_paste", 95 | "Ctrl+V", 96 | function() { 97 | clipboard.paste(...contextMenu.getPos()); 98 | }, 99 | function() { 100 | if(clipboard.components.length < 1) this.className += " disabled"; 101 | else this.className = ""; 102 | 103 | return !findComponentByPos() && !findPortByPos() && !findWireByPos() && !selecting; 104 | } 105 | ); 106 | 107 | createContextMenuOption( 108 | "Merge wires", 109 | "merge_type", 110 | "Q", 111 | function() { 112 | 113 | }, 114 | function() { 115 | return findAllWiresByPos(...contextMenu.getPos()).length > 1 && !selecting; 116 | } 117 | ); 118 | 119 | createContextMenuOption( 120 | "Edit", 121 | "mode_edit", 122 | "E", 123 | function() { 124 | const component = findComponentByPos(...contextMenu.getPos()); 125 | dialog.editComponent(component); 126 | }, 127 | function() { 128 | const component = findComponentByPos(...contextMenu.getPos()); 129 | return component; 130 | } 131 | ); 132 | 133 | createContextMenuOption( 134 | "Edit", 135 | "mode_edit", 136 | "E", 137 | function() { 138 | const port = findPortByPos(...contextMenu.getPos()); 139 | dialog.editPort(port); 140 | }, 141 | function() { 142 | const port = findPortByPos(...contextMenu.getPos()); 143 | return port; 144 | } 145 | ); 146 | 147 | createContextMenuOption( 148 | "Edit color", 149 | "color_lens", 150 | "E", 151 | function() { 152 | const el = findComponentByPos(...contextMenu.getPos()) || findWireByPos(...contextMenu.getPos()); 153 | dialog.colorPicker( 154 | color => el.color = color 155 | ) 156 | }, 157 | function() { 158 | const wire = findWireByPos(...contextMenu.getPos()); 159 | const component = findComponentByPos(...contextMenu.getPos()); 160 | return (wire || (component && component.color)) && !selecting; 161 | } 162 | ); 163 | 164 | createContextMenuOption( 165 | "Edit color", 166 | "color_lens", 167 | "E", 168 | function() { 169 | const components = selecting.components; 170 | const wires = findWiresInSelection(); 171 | dialog.colorPicker( 172 | color => { 173 | for(let i = 0; i < wires.length; ++i) { 174 | wires[i].color = color; 175 | } 176 | 177 | for(let i = 0; i < components.length; ++i) { 178 | if(components[i].color) { 179 | components[i].color = color; 180 | } 181 | } 182 | } 183 | ) 184 | }, 185 | function() { 186 | return selecting && selecting.components && 187 | (findWiresInSelection().length > 0 || selecting.components.find(a => a.color)); 188 | } 189 | ); 190 | 191 | createContextMenuOption( 192 | "Open", 193 | "open_in_new", 194 | "Shift+O", 195 | function() { 196 | const component = findComponentByPos(...contextMenu.getPos()); 197 | component.open(); 198 | }, 199 | function() { 200 | const component = findComponentByPos(...contextMenu.getPos()); 201 | return component && component.constructor == Custom && !selecting; 202 | } 203 | ); 204 | 205 | createContextMenuOption( 206 | "Save component", 207 | "file_download", 208 | "Shift+R", 209 | function() { 210 | const component = findComponentByPos(...contextMenu.getPos()); 211 | saveCustomComponent(component); 212 | }, 213 | function() { 214 | const component = findComponentByPos(...contextMenu.getPos()); 215 | return component && component.constructor == Custom && !selecting; 216 | } 217 | ); 218 | 219 | 220 | createContextMenuOption( 221 | "Rotate", 222 | "rotate_right", 223 | "R", 224 | function() { 225 | const component = findComponentByPos(...contextMenu.getPos()); 226 | component.rotate(); 227 | }, 228 | function() { 229 | const component = findComponentByPos(...contextMenu.getPos()); 230 | return component && component.rotate && !selecting; 231 | } 232 | ); 233 | 234 | createContextMenuOption( 235 | "View connections", 236 | "compare_arrows", 237 | "", 238 | function() { 239 | const component = findComponentByPos(...contextMenu.getPos()); 240 | dialog.connections(component); 241 | }, 242 | function() { 243 | const component = findComponentByPos(...contextMenu.getPos()); 244 | return component && !selecting; 245 | } 246 | ); 247 | 248 | createContextMenuOption( 249 | "Set waypoint", 250 | "my_location", 251 | "Shift+S", 252 | function() { 253 | const component = findComponentByPos(...contextMenu.getPos()); 254 | setWaypoint( 255 | ...contextMenu.getPos(), 256 | component && component.name 257 | ); 258 | }, 259 | function() { 260 | return !selecting; 261 | } 262 | ); 263 | 264 | createContextMenuOption( 265 | "Go to waypoint", 266 | "redo", 267 | "Shift+W", 268 | function() { 269 | waypointsMenu.show(); 270 | }, 271 | function() { 272 | this.className = ""; 273 | if(waypoints.length < 1) this.className = "disabled"; 274 | return !selecting; 275 | } 276 | ); 277 | 278 | createContextMenuOption( 279 | "Componentize", 280 | "memory", 281 | "Shift+C", 282 | function() { 283 | componentize( 284 | selecting.components, 285 | selecting.wires, 286 | selecting, 287 | Math.round(selecting.x + selecting.w / 2), 288 | Math.round(selecting.y + selecting.h / 2), 289 | true 290 | ); 291 | }, 292 | function() { 293 | return selecting && selecting.components; 294 | } 295 | ); 296 | 297 | createContextMenuOption( 298 | "Remove", 299 | "delete", 300 | "Delete", 301 | function() { 302 | const component = findComponentByPos(...contextMenu.getPos()); 303 | removeComponent(component); 304 | }, 305 | function() { 306 | const component = findComponentByPos(...contextMenu.getPos()); 307 | return component && !selecting; 308 | } 309 | ); 310 | 311 | createContextMenuOption( 312 | "Remove", 313 | "delete", 314 | "Delete", 315 | function() { 316 | removeSelection(selecting.components,selecting.wires); 317 | }, 318 | function() { 319 | return selecting; 320 | } 321 | ); 322 | 323 | createContextMenuOption( 324 | "Remove connection", 325 | "delete", 326 | "Delete", 327 | function() { 328 | const wire = findWireByPos(...contextMenu.getPos()); 329 | removeWire(wire); 330 | }, 331 | function() { 332 | return findWireByPos(...contextMenu.getPos()) && !selecting; 333 | } 334 | ); 335 | 336 | -------------------------------------------------------------------------------- /app/js/customComponentToolbar.js: -------------------------------------------------------------------------------- 1 | const customComponentToolbar = document.getElementById("customComponentToolbar"); 2 | 3 | customComponentToolbar.querySelector(".close").onmouseup = function() { 4 | const component = path.splice(-1)[0].component; 5 | 6 | const data = path.slice(-1)[0]; 7 | components = data.components; 8 | wires = data.wires; 9 | undoStack = data.undoStack; 10 | redoStack = data.redoStack; 11 | offset = data.offset; 12 | zoom = zoomAnimation = data.zoom; 13 | 14 | component.create(); 15 | 16 | customComponentToolbar.querySelector("#name").innerHTML = path.slice(1).map(a => a.name).join(" > "); 17 | 18 | customComponentToolbar.menu.style.opacity = 0; 19 | customComponentToolbar.menu.style.transform = "scale(.5) translateX(80px) translateY(-40px)"; 20 | setTimeout(() => customComponentToolbar.menu.style.display = "none", 200); 21 | customComponentToolbar.querySelector(".edit").style.transform = "rotateZ(0deg)"; 22 | 23 | if(path.length < 2) customComponentToolbar.hide(); 24 | 25 | c.focus(); 26 | } 27 | 28 | customComponentToolbar.menu = customComponentToolbar.querySelector(".menu"); 29 | customComponentToolbar.menu.onmousedown = function() { 30 | this.style.opacity = 0; 31 | this.style.transform = "scale(.5) translateX(80px) translateY(-40px)"; 32 | setTimeout(() => this.style.display = "none", 200); 33 | customComponentToolbar.querySelector(".edit").style.transform = "rotateZ(0deg)"; 34 | } 35 | 36 | customComponentToolbar.querySelector(".edit").onmouseup = function() { 37 | if(customComponentToolbar.menu.style.display != "block") { 38 | customComponentToolbar.menu.style.display = "block"; 39 | setTimeout(() => { 40 | customComponentToolbar.menu.style.opacity = 1; 41 | customComponentToolbar.menu.style.transform = "scale(1)"; 42 | }, 10); 43 | this.style.transform = "rotateZ(180deg)"; 44 | } else { 45 | customComponentToolbar.menu.style.opacity = 0; 46 | customComponentToolbar.menu.style.transform = "scale(.5) translateX(80px) translateY(-40px)"; 47 | setTimeout(() => customComponentToolbar.menu.style.display = "none", 200); 48 | this.style.transform = "rotateZ(0deg)"; 49 | } 50 | } 51 | 52 | customComponentToolbar.show = function() { 53 | const component = path.slice(-1)[0]; 54 | 55 | this.style.display = "block"; 56 | this.querySelector("#name").innerHTML = path.slice(1).map(a => a.name).join(" > "); 57 | 58 | setTimeout(() => { 59 | this.style.top = 0; 60 | }, 10); 61 | } 62 | 63 | customComponentToolbar.hide = function() { 64 | this.style.top = -50; 65 | setTimeout(() => { 66 | this.style.display = "none"; 67 | }, 200); 68 | } 69 | -------------------------------------------------------------------------------- /app/js/debug.js: -------------------------------------------------------------------------------- 1 | const debugInfo = document.getElementById("debugInfo"); 2 | debugInfo.addLine = function(name,val) { 3 | let value = document.createElement("span"); 4 | switch(typeof val) { 5 | case "number": 6 | value.style.color = "#888"; 7 | break; 8 | case "string": 9 | value.style.color = "#888"; 10 | value.style.fontStyle = "italic"; 11 | break; 12 | case "boolean": 13 | if(val) value.style.color = "#080"; 14 | else value.style.color = "#800"; 15 | value.style.fontWeight = 800; 16 | break; 17 | } 18 | value.innerHTML = val; 19 | 20 | this.innerHTML += name + ": " + value.outerHTML + "
"; 21 | } 22 | 23 | function updateDebugInfo() { 24 | if(settings.showDebugInfo) { 25 | debugInfo.style.display = "block"; 26 | debugInfo.innerText = ""; 27 | 28 | debugInfo.addLine("Framerate", Math.round(framerate)); 29 | debugInfo.addLine("Screen res", c.width + "*" + c.height); 30 | debugInfo.addLine("Canvas focus", c == document.activeElement); 31 | debugInfo.addLine("Mouse screen x", Math.round(mouse.screen.x)); 32 | debugInfo.addLine("Mouse screen y", Math.round(mouse.screen.y)); 33 | debugInfo.addLine("Mouse grid x", Math.round(mouse.grid.x)); 34 | debugInfo.addLine("Mouse grid y", Math.round(mouse.grid.y)); 35 | debugInfo.addLine("Offset x", Math.round(offset.x * 10) / 10); 36 | debugInfo.addLine("Offset y", Math.round(offset.y * 10) / 10); 37 | debugInfo.addLine("Zoom", Math.round(zoom * 10) / 10); 38 | debugInfo.addLine("Dragging", !!dragging); 39 | debugInfo.addLine("Selecting", !!selecting); 40 | if(selecting) { 41 | debugInfo.addLine("Selection size", Math.round(selecting.w) + "*" + Math.round(selecting.h)); 42 | } 43 | if(selecting && selecting.components) { 44 | debugInfo.addLine("Selected components", selecting.components.length); 45 | } 46 | debugInfo.addLine("Connecting", !!connecting); 47 | debugInfo.addLine("Components", components.length); 48 | debugInfo.addLine("Wires", wires.length); 49 | debugInfo.addLine("Selected", Selected.name); 50 | debugInfo.addLine("undoStack", undoStack.length); 51 | debugInfo.addLine("redoStack", redoStack.length); 52 | debugInfo.addLine("Ticks/sec", Math.round(ticksPerSecond)); 53 | debugInfo.addLine("Updates", updates); 54 | if(socket) { 55 | debugInfo.innerHTML += "
"; 56 | debugInfo.addLine("Socket", !!socket); 57 | debugInfo.addLine("port", socket.url.match(/\d+/g).slice(-1)[0]); 58 | debugInfo.addLine("readyState", socket.readyState); 59 | } else { 60 | debugInfo.addLine("Socket", false); 61 | } 62 | debugInfo.innerHTML += "
Hold F3 to hide this"; 63 | } else { 64 | debugInfo.style.display = "none"; 65 | } 66 | } -------------------------------------------------------------------------------- /app/js/dialogs.js: -------------------------------------------------------------------------------- 1 | const overlay = document.getElementById("over"); 2 | const dialog = document.getElementById("dialog"); 3 | dialog.name = document.querySelector("#dialog h1"); 4 | dialog.container = document.querySelector("#dialog .container"); 5 | dialog.options = document.querySelector("#dialog .options"); 6 | 7 | dialog.show = function() { 8 | this.container.innerHTML = ""; 9 | this.options.innerHTML = ""; 10 | hoverBalloon.style.display = "none"; 11 | 12 | overlay.style.display = "block"; 13 | overlay.style.pointerEvents = "auto"; 14 | setTimeout(() => overlay.style.opacity = .8, 10); 15 | 16 | dialog.style.display = "block"; 17 | setTimeout(() => { 18 | dialog.focus(); 19 | dialog.style.opacity = 1; 20 | dialog.style.transform = "scale(1)"; 21 | dialog.style.top = "16%"; 22 | },10); 23 | } 24 | 25 | dialog.hide = function() { 26 | overlay.style.opacity = 0; 27 | overlay.style.pointerEvents = "none"; 28 | setTimeout(() => { 29 | if(overlay.style.opacity == "0") { 30 | overlay.style.display = "none"; 31 | } 32 | }, 500); 33 | 34 | dialog.style.opacity = 0; 35 | dialog.style.top = "100%"; 36 | setTimeout(() => { 37 | if(dialog.style.opacity == "0") { 38 | dialog.style.display = "none"; 39 | } 40 | }, 200); 41 | 42 | c.focus(); 43 | } 44 | 45 | dialog.addOption = function(text,onclick) { 46 | const button = document.createElement("button"); 47 | button.innerHTML = text; 48 | button.onmousedown = onclick; 49 | button.onmouseup = dialog.hide; 50 | dialog.options.appendChild(button); 51 | } 52 | 53 | dialog.onkeydown = function(e) { 54 | if(e.which == 13) { // Enter 55 | dialog.options.children[dialog.options.children.length - 1].onmousedown(); 56 | dialog.options.children[dialog.options.children.length - 1].onmouseup(); 57 | } else if(e.which == 27) { // Esc. 58 | this.hide(); 59 | } 60 | } 61 | 62 | dialog.welcome = function(component) { 63 | dialog.show(); 64 | dialog.name.innerHTML = "Welcome"; 65 | 66 | dialog.container.innerHTML += "memory"; 67 | dialog.container.innerHTML += "

Welcome to BOOLR !

"; 68 | dialog.container.innerHTML += "

If i'm right this is the first time you are using BOOLR.

"; 69 | dialog.container.innerHTML += "

Need a tutorial to learn how everything works?

"; 70 | dialog.addOption("Yes please!", () => tutorial.show()); 71 | dialog.addOption("No, just start"); 72 | } 73 | 74 | dialog.createBoard = function() { 75 | dialog.show(); 76 | dialog.name.innerHTML = "Create save file"; 77 | 78 | dialog.container.innerHTML += "save"; 79 | dialog.container.innerHTML += "

This board doesn't have a save file yet. Want to create one?

"; 80 | 81 | dialog.container.appendChild(document.createTextNode("Name: ")); 82 | const name = document.createElement("input"); 83 | dialog.container.appendChild(name); 84 | setTimeout(() => name.focus(),10); 85 | 86 | dialog.addOption("Cancel"); 87 | dialog.addOption("OK", () => { 88 | createSaveFile(name.value); 89 | }); 90 | } 91 | 92 | dialog.editBoard = function(save) { 93 | dialog.show(); 94 | dialog.name.innerHTML = "Edit board"; 95 | 96 | dialog.container.appendChild(document.createTextNode("Board name: ")); 97 | const boardName = document.createElement("input"); 98 | boardName.value = save.name; 99 | dialog.container.appendChild(boardName); 100 | setTimeout(() => boardName.focus(),10); 101 | dialog.container.appendChild(document.createElement("br")); 102 | 103 | dialog.container.appendChild(document.createTextNode("File name: ")); 104 | const fileName = document.createElement("input"); 105 | fileName.value = save.fileName.slice(0,save.fileName.indexOf(".board")); 106 | dialog.container.appendChild(fileName); 107 | dialog.container.appendChild(document.createTextNode(".board")); 108 | 109 | dialog.addOption("Cancel"); 110 | dialog.addOption("OK", () => { 111 | if(boardName.value != save.name && boardName.value.length > 0 && boardName.value.length < 100) { 112 | save.name = boardName.value; 113 | 114 | const content = JSON.parse(fs.readFileSync(savesFolder + save.fileName)); 115 | content.name = boardName.value; 116 | fs.writeFile( 117 | savesFolder + save.fileName, 118 | JSON.stringify(content), 119 | "utf-8" 120 | ); 121 | } 122 | 123 | if(fileName.value + ".board" != save.fileName) { 124 | const newFileName = createFileName(fileName.value); 125 | fs.rename( 126 | savesFolder + save.fileName, 127 | savesFolder + newFileName 128 | ); 129 | save.fileName = newFileName; 130 | } 131 | 132 | openBoardMenu.onopen(); 133 | }); 134 | } 135 | 136 | dialog.update = function(component) { 137 | dialog.show(); 138 | dialog.name.innerHTML = "Update " + VERSION; 139 | 140 | dialog.container.innerHTML += "update"; 141 | dialog.container.innerHTML += "

What's new:

"; 142 | dialog.container.innerHTML += 143 | "
    " + 144 | "
  • Tutorial
  • " + 145 | "
  • New main menu
  • " + 146 | "
  • New edit menu
  • " + 147 | "
"; 148 | dialog.addOption("Close"); 149 | } 150 | 151 | dialog.openBoard = function() { 152 | dialog.show(); 153 | dialog.name.innerHTML = "Open board"; 154 | 155 | dialog.container.innerHTML += "insert_drive_file"; 156 | 157 | const openboard = document.getElementById("openboard").cloneNode(true); 158 | openboard.style.display = "block"; 159 | dialog.container.appendChild(openboard); 160 | 161 | dialog.addOption("Cancel"); 162 | } 163 | 164 | dialog.connectToServer = function() { 165 | dialog.show(); 166 | dialog.name.innerHTML = "Connect to server"; 167 | 168 | dialog.container.innerHTML += "dns"; 169 | 170 | dialog.container.innerHTML += "

There are no public servers available.

"; 171 | 172 | dialog.container.appendChild(document.createTextNode("Server URL: ")); 173 | const url = document.createElement("input"); 174 | dialog.container.appendChild(url); 175 | setTimeout(() => url.focus(),10); 176 | dialog.container.appendChild(document.createElement("br")); 177 | 178 | const msg = document.createElement("p"); 179 | msg.show = function(text) { 180 | this.innerHTML = text; 181 | this.style.opacity = 1; 182 | } 183 | dialog.container.appendChild(msg); 184 | 185 | dialog.addOption("Cancel"); 186 | dialog.addOption("Connect", function() { 187 | msg.show("Connecting..."); 188 | connectToSocket(url.value, connected => { 189 | if(connected) { 190 | dialog.hide() 191 | } else { 192 | msg.className = "errormsg"; 193 | msg.show("Could not connect to '" + url.value + "'"); 194 | } 195 | }); 196 | this.onmouseup = () => undefined; 197 | }); 198 | } 199 | 200 | dialog.connections = function(component) { 201 | dialog.show(); 202 | dialog.name.innerHTML = "Connections"; 203 | 204 | const input = document.createElement("ul"); 205 | for(let i = 0; i < component.input.length; ++i) { 206 | const wire = component.input[i].connection; 207 | wire && (function getInputs(wire) { 208 | if(wire.from) { 209 | const li = document.createElement("li"); 210 | li.innerHTML = wire.from.component.name; 211 | input.appendChild(li); 212 | } 213 | for(let i = 0; i < wire.input.length; ++i) { 214 | getInputs(wire.input[i]); 215 | } 216 | })(wire); 217 | } 218 | 219 | const output = document.createElement("ul"); 220 | for(let i = 0; i < component.output.length; ++i) { 221 | const wire = component.output[i].connection; 222 | wire && (function getOutputs(wire) { 223 | if(wire.to) { 224 | const li = document.createElement("li"); 225 | li.innerHTML = wire.to.component.name; 226 | output.appendChild(li); 227 | } 228 | for(let i = 0; i < wire.output.length; ++i) { 229 | getOutputs(wire.output[i]); 230 | } 231 | })(wire); 232 | } 233 | 234 | const connections = input.children.length + output.children.length; 235 | dialog.container.innerHTML += `${component.name} has ${connections} connection${connections == 1 ? "" : "s"}.

`; 236 | 237 | if(input.children.length > 0) { 238 | dialog.container.innerHTML += `${input.children.length} connection${input.children.length == 1 ? "" : "s"} from:`; 239 | dialog.container.appendChild(input); 240 | } 241 | 242 | if(output.children.length > 0) { 243 | dialog.container.innerHTML += `${output.children.length} connection${output.children.length == 1 ? "" : "s"} to:`; 244 | dialog.container.appendChild(output); 245 | } 246 | 247 | dialog.addOption("Close"); 248 | } 249 | 250 | dialog.truthTable = function(type) { 251 | dialog.show(); 252 | dialog.name.innerHTML = type.name + " gate"; 253 | 254 | const component = new type(); 255 | 256 | dialog.container.innerHTML += "" + component.icon.text + "
"; 257 | dialog.container.innerHTML += "

Truth table:

"; 258 | 259 | // Create truth table 260 | const table = document.createElement("table"); 261 | table.className = "truthtable"; 262 | dialog.container.appendChild(table); 263 | 264 | const length = Math.pow(2,component.input.length); 265 | 266 | const tr = document.createElement("tr"); 267 | const inputTh = document.createElement("th"); 268 | inputTh.innerHTML = "Input"; 269 | inputTh.colSpan = component.input.length; 270 | tr.appendChild(inputTh); 271 | const outputTh = document.createElement("th"); 272 | outputTh.innerHTML = "Output"; 273 | outputTh.colSpan = component.output.length; 274 | tr.appendChild(outputTh); 275 | table.appendChild(tr); 276 | 277 | 278 | for(let i = 0; i < length; ++i) { 279 | const tr = document.createElement("tr"); 280 | const input = ("0".repeat(component.input.length) + i.toString(2)).slice(-component.input.length); 281 | for(let i = 0; i < input.length; ++i) { 282 | component.input[i].value = +input[i]; 283 | } 284 | component.update(); 285 | 286 | for(let i = 0; i < input.length; ++i) { 287 | const td = document.createElement("td"); 288 | td.innerHTML = input[i]; 289 | tr.appendChild(td); 290 | } 291 | for(let i = 0; i < component.output.length; ++i) { 292 | const td = document.createElement("td"); 293 | td.innerHTML = component.output[i].value; 294 | tr.appendChild(td); 295 | } 296 | 297 | table.appendChild(tr); 298 | } 299 | 300 | dialog.addOption("Close"); 301 | } 302 | 303 | dialog.settings = function(component) { 304 | dialog.show(); 305 | dialog.name.innerHTML = "Settings"; 306 | 307 | dialog.container.innerHTML += "settings"; 308 | 309 | const settingsList = document.getElementById("settings").cloneNode(true); 310 | settingsList.style.display = "block"; 311 | dialog.container.appendChild(settingsList); 312 | 313 | const scrollAnimationOption = settingsList.querySelector(".option.scrollAnimation"); 314 | scrollAnimationOption.checked = settings.scrollAnimation; 315 | 316 | const zoomAnimationOption = settingsList.querySelector(".option.zoomAnimation"); 317 | zoomAnimationOption.checked = settings.zoomAnimation; 318 | 319 | const showDebugInfoOption = settingsList.querySelector(".option.showDebugInfo"); 320 | showDebugInfoOption.checked = settings.showDebugInfo; 321 | 322 | const showComponentUpdatesOption = settingsList.querySelector(".option.showComponentUpdates"); 323 | showComponentUpdatesOption.checked = settings.showComponentUpdates; 324 | 325 | settingsList.querySelector("#settings #reset").onclick = () => dialog.confirm( 326 | 'Are you sure you want to clear all local stored data?', 327 | () => { 328 | delete localStorage.pwsData; 329 | window.onbeforeunload = undefined; 330 | location.reload() 331 | } 332 | ); 333 | 334 | dialog.addOption("Cancel"); 335 | dialog.addOption("OK", () => { 336 | settings.scrollAnimation = scrollAnimationOption.checked; 337 | settings.zoomAnimation = zoomAnimationOption.checked; 338 | settings.showDebugInfo = showDebugInfoOption.checked; 339 | settings.showComponentUpdates = showComponentUpdatesOption.checked; 340 | }); 341 | } 342 | 343 | dialog.confirm = function(text,callback) { 344 | dialog.show(); 345 | dialog.name.innerHTML = "Confirm"; 346 | dialog.container.innerHTML += "?"; 347 | dialog.container.innerHTML += "

" + text + "

"; 348 | 349 | dialog.addOption("Cancel"); 350 | dialog.addOption("OK", callback); 351 | } 352 | 353 | dialog.warning = function(text) { 354 | dialog.show(); 355 | dialog.name.innerHTML = "Warning"; 356 | dialog.container.innerHTML += "warning"; 357 | dialog.container.innerHTML += "

" + text + "

"; 358 | 359 | dialog.addOption("OK"); 360 | } 361 | 362 | dialog.localStorageError = function() { 363 | dialog.show(); 364 | dialog.name.innerHTML = "localStorage not available"; 365 | dialog.container.innerHTML += "warning"; 366 | dialog.container.innerHTML += "

Your browser doesn't allow this application to store data locally. " + 367 | "BOOLR uses localStorage to store clipbord data, settings, etc. " + 368 | "Either you have disabled localStorage in your browser's settings or your browser is too old.

"; 369 | 370 | dialog.addOption("OK"); 371 | } 372 | 373 | // dialog.edit = function(component) { 374 | // if(!component) return; 375 | // dialog.show(); 376 | // dialog.name.innerHTML = "Edit"; 377 | // 378 | // const properties = ["name",...Object.keys(component.properties)]; 379 | // const inputs = []; 380 | // 381 | // // Name 382 | // const name = document.createElement("input"); 383 | // inputs.push(name); 384 | // name.value = component.name; 385 | // 386 | // dialog.container.appendChild(document.createTextNode("Name:")); 387 | // dialog.container.appendChild(name); 388 | // dialog.container.appendChild(document.createElement("br")); 389 | // 390 | // for(let i in component.properties) { 391 | // const input = document.createElement("input"); 392 | // inputs.push(input); 393 | // input.value = component.properties[i]; 394 | // 395 | // dialog.container.appendChild(document.createTextNode(i.slice(0,1).toUpperCase() + i.slice(1) + ":")); 396 | // dialog.container.appendChild(input); 397 | // 398 | // if(i == "duration" || i == "delay") { 399 | // dialog.container.appendChild(document.createTextNode("ms")); 400 | // } else if(i == "frequency") { 401 | // dialog.container.appendChild(document.createTextNode("Hz")); 402 | // } 403 | // dialog.container.appendChild(document.createElement("br")); 404 | // } 405 | // 406 | // dialog.addOption("Cancel"); 407 | // dialog.addOption("OK", () => { 408 | // for(let i in component.properties) { 409 | // component.properties[i] = inputs[Object.keys(component.properties).indexOf(i) + 1].value; 410 | // } 411 | // }); 412 | // } 413 | 414 | dialog.editName = function(component) { 415 | if(!component) return; 416 | dialog.show(); 417 | dialog.name.innerHTML = "Edit name"; 418 | dialog.container.innerHTML += `

Enter a new name for component ${component.name}

`; 419 | const input = document.createElement("input"); 420 | dialog.container.appendChild(input); 421 | setTimeout(() => input.focus(),10); 422 | 423 | dialog.addOption("Cancel"); 424 | dialog.addOption("OK", () => { 425 | if(input.value.length > 0 && input.value.length < 16) { 426 | edit(component,"name",input.value,true); 427 | } 428 | }); 429 | } 430 | 431 | dialog.colorPicker = function(callback = a => a) { 432 | dialog.show(); 433 | dialog.name.innerHTML = "Color Picker"; 434 | dialog.container.innerHTML += "color_lens"; 435 | dialog.container.innerHTML += `

Pick a color:

`; 436 | 437 | const el = document.createElement("div"); 438 | el.style.width = 70; 439 | el.style.height = 50; 440 | el.style.display = "inline-block"; 441 | el.style.margin = 10; 442 | el.onclick = function() { 443 | callback(this.style.background.match(/\d+/g).map(n => +n)); 444 | dialog.hide() 445 | } 446 | 447 | const colors = [ 448 | "#f33","#37f","#5b5","#ff5", 449 | "#f90","#60f","#0fc","#f0f", 450 | "#222","#555","#888","#ddd"]; 451 | for(let i = 0; i < colors.length; ++i) { 452 | const color = el.cloneNode(); 453 | color.style.background = colors[i]; 454 | color.onclick = el.onclick; 455 | dialog.container.appendChild(color); 456 | } 457 | 458 | dialog.addOption("Cancel"); 459 | } 460 | 461 | dialog.editPort = function(port) { 462 | if(!port) return; 463 | dialog.show(); 464 | dialog.name.innerHTML = "Edit port " + (port.name || ""); 465 | 466 | dialog.container.appendChild( 467 | document.createTextNode("Name: ") 468 | ); 469 | const name = document.createElement("input"); 470 | dialog.container.appendChild(name); 471 | name.value = port.name || ""; 472 | setTimeout(() => name.focus(),10); 473 | dialog.container.appendChild(document.createElement("br")); 474 | 475 | 476 | const from = document.createElement("p"); 477 | from.innerHTML = "From: " + port.component.name; 478 | dialog.container.appendChild(from); 479 | 480 | const portType = document.createElement("p"); 481 | portType.innerHTML = "Port type: " + port.type; 482 | dialog.container.appendChild(portType); 483 | 484 | const portId = document.createElement("p"); 485 | portId.innerHTML = "ID: " + port.id; 486 | dialog.container.appendChild(portId); 487 | 488 | const position = document.createElement("p"); 489 | position.innerHTML = "Position: " + port.pos; 490 | dialog.container.appendChild(position); 491 | 492 | const deleteConnection = document.createElement("button"); 493 | deleteConnection.innerHTML = "Delete connection"; 494 | deleteConnection.style.background = "#600"; 495 | deleteConnection.onclick = () => { 496 | removeWire(port.connection); 497 | } 498 | dialog.container.appendChild(deleteConnection); 499 | 500 | dialog.container.appendChild(document.createElement("br")); 501 | 502 | dialog.addOption("Cancel"); 503 | dialog.addOption("OK", () => { 504 | if(name.value.length > 0 && name.value.length < 20) port.name = name.value; 505 | }); 506 | } 507 | 508 | dialog.editCustom = function(component) { 509 | if(!component) return; 510 | dialog.show(); 511 | dialog.name.innerHTML = "Edit " + component.name; 512 | 513 | dialog.container.appendChild( 514 | document.createTextNode("Name: ") 515 | ); 516 | const name = document.createElement("input"); 517 | dialog.container.appendChild(name); 518 | name.value = component.name; 519 | 520 | dialog.container.appendChild(document.createElement("br")); 521 | 522 | dialog.container.appendChild( 523 | document.createTextNode("Description (optional): ") 524 | ); 525 | const description = document.createElement("input"); 526 | dialog.container.appendChild(description); 527 | description.value = component.properties.description; 528 | 529 | dialog.container.appendChild(document.createElement("br")); 530 | 531 | dialog.container.appendChild( 532 | document.createTextNode("Width: ") 533 | ); 534 | const width = document.createElement("input"); 535 | dialog.container.appendChild(width); 536 | width.value = component.width; 537 | 538 | dialog.container.appendChild(document.createElement("br")); 539 | 540 | dialog.container.appendChild( 541 | document.createTextNode("Height: ") 542 | ); 543 | const height = document.createElement("input"); 544 | dialog.container.appendChild(height); 545 | height.value = component.height; 546 | 547 | 548 | dialog.addOption("Cancel"); 549 | dialog.addOption("OK", () => { 550 | if(name.value.length > 0 && name.value.length < 20) component.name = name.value; 551 | if(description.value.length > 0) component.properties.description = description.value; 552 | if(+width.value > 1) component.width = +width.value; 553 | if(+height.value > 1) component.height = +height.value; 554 | }); 555 | } 556 | 557 | dialog.savedCustomComponents = function() { 558 | dialog.show(); 559 | dialog.name.innerHTML = "Saved components"; 560 | 561 | const list = document.createElement("ul"); 562 | list.style.listStyle = "none"; 563 | list.style.margin = 0; 564 | list.style.padding = 0; 565 | 566 | for(let i = 0; i < savedCustomComponents.length; ++i) { 567 | const component = savedCustomComponents[i]; 568 | 569 | const li = document.createElement("li"); 570 | li.component = component; 571 | li.innerHTML = component.name; 572 | li.onclick = function() { 573 | select( 574 | class { 575 | constructor() { 576 | return cloneComponent( 577 | component, 578 | mouse.grid.x - component.pos.x, 579 | mouse.grid.y - component.pos.y 580 | ) 581 | } 582 | } 583 | ); 584 | dialog.hide(); 585 | } 586 | 587 | // Remove board button 588 | const removeBtn = document.createElement("i"); 589 | removeBtn.className = "material-icons"; 590 | removeBtn.title = "Remove component"; 591 | removeBtn.innerHTML = "delete"; 592 | removeBtn.onclick = function(e) { 593 | const index = savedCustomComponents.indexOf(component); 594 | index > -1 && savedCustomComponents.splice(index,1); 595 | 596 | dialog.savedCustomComponents(); 597 | e.stopPropagation(); 598 | } 599 | li.appendChild(removeBtn); 600 | 601 | list.appendChild(li); 602 | } 603 | 604 | if(!savedCustomComponents || savedCustomComponents.length == 0) { 605 | dialog.container.innerHTML = "

You have no saved custom components.

"; 606 | } else { 607 | dialog.container.appendChild(list); 608 | } 609 | 610 | dialog.addOption("Close"); 611 | } 612 | 613 | dialog.save = function(text) { 614 | dialog.show(); 615 | dialog.name.innerHTML = "Save board"; 616 | dialog.container.innerHTML += "save"; 617 | dialog.container.innerHTML += "

This board will be saved as a .board file

"; 618 | dialog.container.innerHTML += "Name: "; 619 | 620 | let input = document.createElement("input"); 621 | input.setAttribute("placeholder","BOOLR-Save-" + new Date().toLocaleString()); 622 | dialog.container.appendChild(input); 623 | setTimeout(() => input.focus()); 624 | 625 | dialog.container.innerHTML += ".board"; 626 | input = document.querySelector("#dialog input"); 627 | 628 | dialog.addOption("Cancel"); 629 | dialog.addOption("OK", () => { 630 | saveBoard(input.value); 631 | }); 632 | } 633 | -------------------------------------------------------------------------------- /app/js/editDialogs.js: -------------------------------------------------------------------------------- 1 | // I'm sorry for the mess 2 | 3 | (function() { 4 | function createInput( 5 | component, 6 | property, 7 | value, 8 | valid, 9 | errormsg, 10 | apply) { 11 | const input = document.createElement("input"); 12 | input.value = value; 13 | 14 | input.valid = valid; 15 | input.errormsg = errormsg; 16 | input.apply = apply; 17 | 18 | dialog.container.appendChild(document.createTextNode(property.slice(0,1).toUpperCase() + property.slice(1) + ":")); 19 | dialog.container.appendChild(input); 20 | dialog.container.appendChild(document.createElement("br")); 21 | return input; 22 | } 23 | 24 | function createTextArea( 25 | component, 26 | property, 27 | value, 28 | valid, 29 | errormsg, 30 | apply) { 31 | const input = document.createElement("textarea"); 32 | input.value = value; 33 | 34 | input.valid = valid; 35 | input.errormsg = errormsg; 36 | input.apply = apply; 37 | 38 | dialog.container.appendChild(document.createTextNode(property.slice(0,1).toUpperCase() + property.slice(1) + ":")); 39 | dialog.container.appendChild(input); 40 | dialog.container.appendChild(document.createElement("br")); 41 | return input; 42 | } 43 | 44 | function createSelect( 45 | component, 46 | property, 47 | value, 48 | options, 49 | apply) { 50 | const input = document.createElement("select"); 51 | for (let i = 0; i < options.length; i++) { 52 | const option = document.createElement("option"); 53 | option.value = options[i].value; 54 | if (option.value === value) { 55 | option.selected = true; 56 | } 57 | option.appendChild(document.createTextNode(options[i].text)); 58 | input.appendChild(option); 59 | } 60 | input.valid = () => true; 61 | input.errormsg = ""; 62 | input.apply = apply; 63 | 64 | dialog.container.appendChild(document.createTextNode(property.slice(0,1).toUpperCase() + property.slice(1) + ":")); 65 | dialog.container.appendChild(input); 66 | dialog.container.appendChild(document.createElement("br")); 67 | return input; 68 | } 69 | 70 | dialog.editComponent = function(component) { 71 | dialog.show(); 72 | dialog.name.innerHTML = "Edit component"; 73 | dialog.container.innerHTML += "

edit

"; 74 | 75 | const name = createInput( 76 | component, "name", component.name, 77 | name => name.length > 0 && name.length < 12, 78 | "Enter a name between 0 and 12 characters", 79 | function() { 80 | edit(component,"name",this.value,true); 81 | } 82 | ); 83 | setTimeout(() => name.focus(), 10); 84 | 85 | const pos = createInput( 86 | component, "pos", component.pos.x + "," + component.pos.y, 87 | pos => (pos.match(/-?\d+\s*\,\s*-?\d+/g) || [])[0] == pos, 88 | "Enter a value for x and y separated by a comma", 89 | function() { 90 | const pos = this.value.split(",").map(n => +n); 91 | component.pos.x = pos[0]; 92 | component.pos.t = pos[1]; 93 | } 94 | ); 95 | const width = createInput( 96 | component, "width", component.width, 97 | width => width > 0 && 2 * (+width + component.height) >= component.input.length + component.output.length, 98 | "The component must be wider for the ports to fit", 99 | function() { 100 | changeSize(component,+this.value,undefined,true); 101 | } 102 | ); 103 | const height = createInput( 104 | component, "height", component.height, 105 | height => { 106 | height = parseVariableInput(height); 107 | if(isNaN(height)) return false; 108 | return height > 0 && 2 * (+height + component.width) >= component.input.length + component.output.length 109 | }, 110 | "The component must be higher for the ports to fit", 111 | function() { 112 | changeSize(component,undefined, parseVariableInput(+this.value), true); 113 | } 114 | ); 115 | 116 | const inputs = [name,pos,width,height]; 117 | 118 | // Additional properties: 119 | 120 | if(component.properties.hasOwnProperty("delay")) { 121 | inputs.push( 122 | createInput( 123 | component.properties, "delay", component.properties.delay || "", 124 | delay => !isNaN(parseVariableInput(delay)), 125 | "Enter a positive delay time in milliseconds", 126 | function() { 127 | component.properties.delay = parseVariableInput(this.value); 128 | createVariableReference(this.value,component,["properties","delay"]); 129 | } 130 | ) 131 | ); 132 | dialog.container.removeChild(dialog.container.children[dialog.container.children.length - 1]); 133 | dialog.container.appendChild(document.createTextNode("ms")); 134 | dialog.container.appendChild(document.createElement("br")); 135 | } 136 | 137 | if(component.properties.hasOwnProperty("frequency")) { 138 | inputs.push( 139 | createInput( 140 | component.properties, "frequency", component.properties.frequency, 141 | frequency => +frequency > 0, 142 | "Enter a positive frequency value in Hz", 143 | function() { 144 | component.properties.frequency = +this.value; 145 | } 146 | ) 147 | ); 148 | dialog.container.removeChild(dialog.container.children[dialog.container.children.length - 1]); 149 | dialog.container.appendChild(document.createTextNode("Hz")); 150 | dialog.container.appendChild(document.createElement("br")); 151 | } 152 | 153 | if(component.properties.hasOwnProperty("duration")) { 154 | inputs.push( 155 | createInput( 156 | component.properties, "duration", component.properties.duration, 157 | frequency => +frequency > 0, 158 | "Enter a positive duration time in ms", 159 | function() { 160 | component.properties.duration = +this.value; 161 | } 162 | ) 163 | ); 164 | dialog.container.removeChild(dialog.container.children[dialog.container.children.length - 1]); 165 | dialog.container.appendChild(document.createTextNode("ms")); 166 | dialog.container.appendChild(document.createElement("br")); 167 | } 168 | 169 | if(component.properties.hasOwnProperty("data")) { 170 | inputs.push( 171 | createTextArea( 172 | component.properties, "data", component.properties.data, 173 | () => true, 174 | "Enter hex-encoded data", 175 | function() { 176 | component.properties.data = this.value; 177 | const dataWidth = component.properties.dataWidth; 178 | const contents = this.value.replace(/\s/g, '').toUpperCase(); 179 | let data = Array(Math.pow(2, component.properties.addressWidth)).fill(0); 180 | for (let i = 0; i < data.length; i++) { 181 | const start = i * dataWidth / 4; 182 | const end = start + dataWidth / 4; 183 | const content = contents.slice(start, end); 184 | data[i] = parseInt(content, 16); 185 | } 186 | component.properties.rom = data; 187 | } 188 | ) 189 | ); 190 | dialog.container.removeChild(dialog.container.children[dialog.container.children.length - 1]); 191 | dialog.container.appendChild(document.createElement("br")); 192 | } 193 | 194 | const errormsg = document.createElement("p"); 195 | errormsg.className = "errormsg"; 196 | errormsg.innerHTML = "."; 197 | errormsg.hide = null; 198 | errormsg.show = function(text) { 199 | clearTimeout(this.hide); 200 | this.innerHTML = text; 201 | this.style.opacity = 1; 202 | this.hide = setTimeout(() => this.style.opacity = 0, 2500); 203 | } 204 | dialog.container.appendChild(errormsg); 205 | 206 | dialog.addOption("Cancel"); 207 | dialog.addOption("OK", function() { 208 | for(let i = 0; i < inputs.length; ++i) { 209 | const input = inputs[i]; 210 | input.className = ""; 211 | 212 | if(!input.valid(input.value)) { 213 | input.className = "error"; 214 | errormsg.show(input.errormsg); 215 | 216 | this.onmouseup = () => this.onmouseup = dialog.hide; 217 | return; 218 | } 219 | } 220 | 221 | for(let i = 0; i < inputs.length; ++i) { 222 | inputs[i].apply(); 223 | } 224 | }); 225 | } 226 | 227 | dialog.editPort = function(port) { 228 | dialog.show(); 229 | dialog.name.innerHTML = "Edit port"; 230 | dialog.container.innerHTML += "

edit

"; 231 | 232 | const name = createInput( 233 | port, "name", port.name || "", 234 | name => name.length < 12, 235 | "Enter a name between 0 and 12 characters", 236 | function() { 237 | edit(port,"name",this.value); 238 | } 239 | ); 240 | setTimeout(() => name.focus(), 10); 241 | 242 | const side = createInput( 243 | port.pos, "side", port.pos.side, 244 | side => +side >= 0 && +side <= 3, 245 | "Enter the number of a side, a number between 0 and 3", 246 | function() { 247 | movePort(port,+this.value,port.pos.pos); 248 | } 249 | ); 250 | 251 | const pos = createInput( 252 | port.pos, "pos", port.pos.pos, 253 | pos => side.valid(side.value) && +pos >= 0 && +pos < (+side.value % 2 == 0 ? port.component.width: port.component.height) && !findPortByComponent(port.component,+side.value,+pos), 254 | "Enter a (free) position for the port, a number between 0 and the width/height of the component", 255 | function() { 256 | movePort(port,port.pos.side,+this.value); 257 | } 258 | ); 259 | 260 | const inputs = [name,side,pos]; 261 | 262 | const errormsg = document.createElement("p"); 263 | errormsg.className = "errormsg"; 264 | errormsg.innerHTML = "."; 265 | errormsg.hide = null; 266 | errormsg.show = function(text) { 267 | clearTimeout(this.hide); 268 | this.innerHTML = text; 269 | this.style.opacity = 1; 270 | this.hide = setTimeout(() => this.style.opacity = 0, 2500); 271 | } 272 | dialog.container.appendChild(errormsg); 273 | 274 | dialog.addOption("Cancel"); 275 | dialog.addOption("OK", function() { 276 | for(let i = 0; i < inputs.length; ++i) { 277 | const input = inputs[i]; 278 | input.className = ""; 279 | 280 | if(!input.valid(input.value)) { 281 | input.className = "error"; 282 | errormsg.show(input.errormsg); 283 | 284 | this.onmouseup = () => this.onmouseup = dialog.hide; 285 | return; 286 | } 287 | } 288 | 289 | for(let i = 0; i < inputs.length; ++i) { 290 | inputs[i].apply(); 291 | } 292 | }); 293 | } 294 | 295 | dialog.editDelay = function(component,callback) { 296 | if(!component) return; 297 | dialog.show(); 298 | dialog.name.innerHTML = "Edit delay"; 299 | dialog.container.innerHTML += "access_time"; 300 | dialog.container.innerHTML += `

Enter a delay time in ms for component ${component.name}

`; 301 | 302 | 303 | const input = createInput( 304 | component.properties, "delay", component.properties.delay || "", 305 | delay => !isNaN(parseVariableInput(delay)), 306 | "Enter a positive delay time in milliseconds", 307 | function() { 308 | component.properties.delay = parseVariableInput(this.value); 309 | createVariableReference(this.value,component,["properties","delay"]); 310 | } 311 | ); 312 | setTimeout(() => input.focus(),10); 313 | dialog.container.removeChild(dialog.container.children[dialog.container.children.length - 1]); 314 | dialog.container.appendChild(document.createTextNode("ms")); 315 | 316 | const errormsg = document.createElement("p"); 317 | errormsg.className = "errormsg"; 318 | errormsg.innerHTML = "."; 319 | errormsg.hide = null; 320 | errormsg.show = function(text) { 321 | clearTimeout(this.hide); 322 | this.innerHTML = text; 323 | this.style.opacity = 1; 324 | this.hide = setTimeout(() => this.style.opacity = 0, 2500); 325 | } 326 | dialog.container.appendChild(errormsg); 327 | 328 | dialog.addOption("Cancel", function() { 329 | if(!component.properties.delay) { 330 | component.properties.delay = 1000; 331 | callback && callback(); 332 | } 333 | }); 334 | dialog.addOption("OK", function() { 335 | if(input.valid(input.value)) { 336 | input.apply(); 337 | callback && callback(); 338 | } else { 339 | input.className = "error"; 340 | errormsg.show(input.errormsg); 341 | this.onmouseup = () => this.onmouseup = dialog.hide; 342 | } 343 | }); 344 | } 345 | dialog.editRom = function(component,callback) { 346 | if(!component) return; 347 | dialog.show(); 348 | dialog.name.innerHTML = "Edit ROM"; 349 | dialog.container.innerHTML += "memory"; 350 | dialog.container.innerHTML += `

Enter a hex-encoded data for component ${component.name}

`; 351 | 352 | 353 | const addressWidthInput = createInput( 354 | component.properties, "addressWidth", component.properties.addressWidth || "4", 355 | addressWidth => !isNaN(parseVariableInput(addressWidth)), 356 | "Address width in bits", 357 | function() { 358 | component.properties.addressWidth = parseVariableInput(this.value); 359 | component.height = 360 | Math.max( 361 | component.properties.addressWidth, 362 | component.properties.dataWidth); 363 | component.input = []; 364 | for(let i = 0; i < component.properties.addressWidth; ++i) { 365 | component.addInputPort({ side: 3, pos: i }); 366 | } 367 | createVariableReference(this.value,component,["properties","addressWidth"]); 368 | } 369 | ); 370 | const dataWidthInput = createSelect( 371 | component.properties, "dataWidth", component.properties.dataWidth || 4, 372 | [{"value": 4, "text": "4"}, 373 | {"value": 8, "text": "8"}, 374 | {"value": 16, "text": "16"}, 375 | {"value": 32, "text": "32"}], 376 | function() { 377 | component.properties.dataWidth = +this.value; 378 | component.height = 379 | Math.max( 380 | component.properties.addressWidth, 381 | component.properties.dataWidth); 382 | component.output = []; 383 | for(let i = 0; i < component.properties.dataWidth; ++i) { 384 | component.addOutputPort({ side: 1, pos: i }); 385 | } 386 | } 387 | ); 388 | const dataInput = createTextArea( 389 | component.properties, "data", component.properties.data || "", 390 | // TODO better validation? 391 | () => true, 392 | "Enter hex-encoded data", 393 | function() { 394 | // Keep original data 395 | component.properties.data = this.value; 396 | // Sanatize and store parsed data as an array of numbers 397 | const contents = this.value.replace(/\s/g, '').toUpperCase(); 398 | const dataWidth = component.properties.dataWidth; 399 | let data = Array(Math.pow(2, component.properties.addressWidth)).fill(0); 400 | for (let i = 0; i < data.length; i++) { 401 | const start = i * dataWidth / 4; 402 | const end = start + dataWidth / 4; 403 | const content = contents.slice(start, end); 404 | data[i] = parseInt(content, 16); 405 | } 406 | component.properties.rom = data; 407 | createVariableReference(this.value,component,["properties","rom"]); 408 | } 409 | ); 410 | setTimeout(() => addressWidthInput.focus(),10); 411 | dialog.container.removeChild(dialog.container.children[dialog.container.children.length - 1]); 412 | 413 | const errormsg = document.createElement("p"); 414 | errormsg.className = "errormsg"; 415 | errormsg.innerHTML = "."; 416 | errormsg.hide = null; 417 | errormsg.show = function(text) { 418 | clearTimeout(this.hide); 419 | this.innerHTML = text; 420 | this.style.opacity = 1; 421 | this.hide = setTimeout(() => this.style.opacity = 0, 2500); 422 | } 423 | dialog.container.appendChild(errormsg); 424 | 425 | dialog.addOption("Cancel", function() { 426 | if(!component.properties.addressWidth && !component.properties.data) { 427 | component.properties.addressWidth = 0; 428 | component.properties.data = ""; 429 | callback && callback(); 430 | } 431 | }); 432 | dialog.addOption("OK", function() { 433 | if(addressWidthInput.valid(addressWidthInput.value) && 434 | dataInput.valid(dataInput.value)) { 435 | addressWidthInput.apply(); 436 | dataWidthInput.apply(); 437 | dataInput.apply(); 438 | callback && callback(); 439 | } else { 440 | input.className = "error"; 441 | errormsg.show(addressWidthInput.errormsg); 442 | errormsg.show(dataInput.errormsg); 443 | this.onmouseup = () => this.onmouseup = dialog.hide; 444 | } 445 | }); 446 | } 447 | })(); 448 | -------------------------------------------------------------------------------- /app/js/hover.js: -------------------------------------------------------------------------------- 1 | // Component hover balloon 2 | 3 | let hoverTime = 0; 4 | setInterval(function() { 5 | const component = findComponentByPos(); 6 | 7 | if(mouse.hover && mouse.hover == component) { 8 | ++hoverTime; 9 | hoverTime > 6 && hoverBalloon.show(component); 10 | } else if(component) { 11 | hoverBalloon.hide(); 12 | 13 | mouse.hover = component; 14 | } else { 15 | hoverBalloon.hide(); 16 | } 17 | }, 100); 18 | 19 | 20 | // TODO: moet worden bijgewerkt! 21 | const hoverBalloon = document.getElementById("hoverBalloon"); 22 | hoverBalloon.show = function(component) { 23 | if(this.display) return; 24 | this.display = true; 25 | 26 | if(component.constructor == Wire) { 27 | this.innerHTML = "

Wire

"; 28 | this.innerHTML += "From: " + component.from.name + "
"; 29 | this.innerHTML += "To: " + component.to.name + "
"; 30 | this.innerHTML += "Value: " + component.value + "
"; 31 | } else { 32 | this.innerHTML = "

" + component.name + "

"; 33 | this.innerHTML += Math.round(component.pos.x) + "," + Math.round(component.pos.y) + "
"; 34 | this.innerHTML += "ID: " + component.id + "
"; 35 | this.innerHTML += "Placed by: " + (component.placedBy ? component.placedBy : "you") + "
"; 36 | if(component.hasOwnProperty("value")) this.innerHTML += "Value: " + component.value + "
"; 37 | } 38 | 39 | this.style.display = "block"; 40 | setTimeout(() => { 41 | this.style.opacity = 1; 42 | this.style.transform = "translateY(20px)"; 43 | },10); 44 | } 45 | 46 | hoverBalloon.hide = function() { 47 | hoverTime = 0; 48 | 49 | if(!this.display) return; 50 | this.display = false; 51 | this.style.opacity = 0; 52 | this.style.transform = "translateY(30px)"; 53 | setTimeout(() => !hoverBalloon.display && (hoverBalloon.style.display = "none"), 200); 54 | } 55 | 56 | // Toolbar hover balloon 57 | 58 | for(let i = 0; i < document.getElementsByClassName("slot").length; ++i) { 59 | document.getElementsByClassName("slot")[i].onmouseenter = function(e) { 60 | this.hover = true; 61 | const toolbartip = document.getElementById("toolbartip"); 62 | 63 | if(toolbartip.style.display == "block") { 64 | toolbartip.innerHTML = this.getAttribute("tooltip"); 65 | if(i > 0 && i < 5) { 66 | toolbartip.innerHTML += "
Right click for details"; 67 | } 68 | toolbartip.style.left = this.getBoundingClientRect().left + this.clientWidth / 2 - toolbartip.clientWidth / 2; 69 | } else { 70 | toolbartip.style.display = "block"; 71 | toolbartip.innerHTML = this.getAttribute("tooltip") 72 | if(i > 0 && i < 5) { 73 | toolbartip.innerHTML += "
Right click for details"; 74 | } 75 | setTimeout(() => { 76 | toolbartip.style.opacity = 1; 77 | toolbartip.style.transform = "translateY(20px)"; 78 | }, 1); 79 | toolbartip.style.left = this.getBoundingClientRect().left + this.clientWidth / 2 - toolbartip.clientWidth / 2; 80 | setTimeout(() => toolbartip.style.transition = "transform .2s, opacity .2s, left .2s", 100); 81 | } 82 | } 83 | 84 | document.getElementsByClassName("slot")[i].onmouseleave = function(e) { 85 | this.hover = false; 86 | const toolbartip = document.getElementById("toolbartip"); 87 | 88 | setTimeout(() => { 89 | if(this.hover) return; 90 | 91 | let removeTooltip = true; 92 | for(let j = 0; j < document.getElementsByClassName("slot").length; ++j) { 93 | if(document.getElementsByClassName("slot")[j].hover) removeTooltip = false; 94 | } 95 | 96 | if(removeTooltip) { 97 | toolbartip.style.opacity = 0; 98 | toolbartip.style.transform = "translateY(30px)"; 99 | toolbartip.style.transition = "transform .2s, opacity .2s"; 100 | setTimeout(() => toolbartip.style.display = "none", 200); 101 | } 102 | },200); 103 | } 104 | } 105 | toolbar.onmousedown = function() { 106 | document.getElementById("toolbartip").style.display = "none"; 107 | } 108 | 109 | // Credits hover balloon 110 | for(let i = 0; i < document.querySelectorAll("#credits button").length; ++i) { 111 | document.querySelectorAll("#credits button")[i].onmouseenter = function(e) { 112 | this.hover = true; 113 | const creditstip = document.getElementById("creditstip"); 114 | 115 | if(creditstip.style.display == "block") { 116 | creditstip.innerHTML = this.getAttribute("tooltip"); 117 | creditstip.style.left = this.getBoundingClientRect().left + this.clientWidth / 2 - creditstip.clientWidth / 2; 118 | } else { 119 | setTimeout(() => { 120 | if(this.hover) { 121 | creditstip.style.display = "block"; 122 | creditstip.innerHTML = this.getAttribute("tooltip"); 123 | setTimeout(() => { 124 | creditstip.style.opacity = 1; 125 | creditstip.style.transform = "translateY(20px)"; 126 | }, 1); 127 | creditstip.style.left = this.getBoundingClientRect().left + this.clientWidth / 2 - creditstip.clientWidth / 2; 128 | setTimeout(() => creditstip.style.transition = "transform .2s, opacity .2s, left .2s", 100); 129 | } 130 | }, 200); 131 | } 132 | } 133 | 134 | document.querySelectorAll("#credits button")[i].onmouseleave = function(e) { 135 | this.hover = false; 136 | const creditstip = document.getElementById("creditstip"); 137 | 138 | setTimeout(() => { 139 | let removeTooltip = true; 140 | for(let j = 0; j < document.querySelectorAll("#credits button").length; ++j) { 141 | if(document.querySelectorAll("#credits button")[j].hover) removeTooltip = false; 142 | } 143 | 144 | if(removeTooltip) { 145 | creditstip.style.opacity = 0; 146 | creditstip.style.transform = "translateY(30px)"; 147 | creditstip.style.transition = "transform .2s, opacity .2s"; 148 | setTimeout(() => creditstip.style.display = "none", 200); 149 | } 150 | }, 200); 151 | } 152 | } -------------------------------------------------------------------------------- /app/js/keys.js: -------------------------------------------------------------------------------- 1 | let keys = {}; 2 | 3 | // Canvas key bindings 4 | c.onkeydown = function(e) { 5 | if(!keys[e.which]) keys[e.which] = new Date; 6 | switch(e.which) { 7 | case 37: // Arrow left 8 | scroll(-5,0); 9 | break; 10 | case 38: // Arrow up 11 | scroll(0,5); 12 | break; 13 | case 39: // Arrow right 14 | scroll(5,0); 15 | break; 16 | case 40: // Arrow down 17 | scroll(0,-5); 18 | break; 19 | case 36: // Home 20 | scroll(-offset.x,-offset.y); 21 | break; 22 | case 46: // Delete 23 | if(selecting && selecting.components) { 24 | removeSelection(selecting.components,selecting.wires,true); 25 | selecting = null; 26 | contextMenu.hide(); 27 | } else { 28 | let found; 29 | if(found = findComponentByPos()) { 30 | removeComponent(found,true); 31 | } else if(found = findWireByPos()) { 32 | removeWire(found,true); 33 | } 34 | } 35 | break; 36 | case 33: // Page Up 37 | changeZoom(zoom / 2); 38 | break; 39 | case 34: // Page Down 40 | changeZoom(zoom / -2); 41 | break; 42 | case 13: // Enter 43 | break; 44 | case 27: // Escape 45 | document.getElementById("list").style.display = "none"; 46 | contextMenu.hide(); 47 | waypointsMenu.hide(); 48 | selecting = null; 49 | break; 50 | case 49: // 1 51 | document.getElementsByClassName("slot")[0].onmousedown({which:1}); 52 | break; 53 | case 50: // 2 54 | document.getElementsByClassName("slot")[1].onmousedown({which:1}); 55 | break; 56 | case 51: // 3 57 | document.getElementsByClassName("slot")[2].onmousedown({which:1}); 58 | break; 59 | case 52: // 4 60 | document.getElementsByClassName("slot")[3].onmousedown({which:1}); 61 | break; 62 | case 53: // 5 63 | document.getElementsByClassName("slot")[4].onmousedown({which:1}); 64 | break; 65 | case 54: // 6 66 | document.getElementsByClassName("slot")[5].onmousedown({which:1}); 67 | break; 68 | case 55: // 7 69 | document.getElementsByClassName("slot")[6].onmousedown({which:1}); 70 | break; 71 | case 56: // 8 72 | break; 73 | case 57: // 9 74 | break; 75 | case 58: // 0 76 | break; 77 | case 67: // C 78 | if(e.ctrlKey) { 79 | if(selecting) { 80 | clipboard.copy(selecting.components,selecting.wires,selecting); 81 | } else if(findComponentByPos()) { 82 | clipboard.copy([findComponentByPos()]); 83 | } 84 | } else if(e.shiftKey && selecting && selecting.components) { 85 | componentize( 86 | components, 87 | wires, 88 | selecting, 89 | Math.round(selecting.x + selecting.w / 2), 90 | Math.round(selecting.y + selecting.h / 2), 91 | true 92 | ); 93 | 94 | selecting = null; 95 | contextMenu.hide(); 96 | } 97 | break; 98 | case 69: // E: 99 | var found; 100 | if(found = findPortByPos()) { 101 | dialog.editPort(found); 102 | } else if(found = findWireByPos()) { 103 | const wire = found; 104 | dialog.colorPicker( 105 | color => { 106 | wire.color = color 107 | } 108 | ) 109 | } else if(found = findComponentByPos()) { 110 | dialog.editComponent(found); 111 | } 112 | return false; 113 | break; 114 | case 79: // O 115 | if(e.ctrlKey) { 116 | mainMenu.show(); 117 | setTimeout(clearBoard,1000); 118 | openBoardMenu.show(); 119 | } else if(e.shiftKey) { 120 | const component = findComponentByPos(); 121 | component && component.open && component.open(); 122 | } 123 | return false; 124 | break; 125 | case 80: // P 126 | if(e.ctrlKey) { 127 | pauseSimulation = !pauseSimulation; 128 | document.querySelector("#pause").innerHTML = pauseSimulation ? "play_arrow" : "pause"; 129 | pauseSimulation && toolbar.message("Paused simulation"); 130 | !pauseSimulation && toolbar.message("Started simulation"); 131 | } 132 | break; 133 | case 82: // R 134 | if(e.shiftKey) { 135 | const component = findComponentByPos(); 136 | if(component && component.constructor == Custom) { 137 | saveCustomComponent(component); 138 | } 139 | } else { 140 | const component = findComponentByPos(); 141 | component && component.rotate && component.rotate(); 142 | } 143 | break; 144 | case 83: // S 145 | if(e.ctrlKey && e.shiftKey) { 146 | dialog.settings(); 147 | } else if(e.ctrlKey) { 148 | save(true); 149 | } else if(e.shiftKey) { 150 | waypointsMenu.hide(); 151 | 152 | const component = findComponentByPos(); 153 | setWaypoint( 154 | mouse.grid.x,mouse.grid.y, 155 | component && component.name 156 | ); 157 | } 158 | break; 159 | case 84: // T 160 | if(e.shiftKey) { 161 | boolrConsole.show(); 162 | } else { 163 | chat.show(); 164 | chat.focus(); 165 | } 166 | return false; 167 | break; 168 | case 86: // V 169 | if(e.ctrlKey) { 170 | clipboard.paste(mouse.grid.x,mouse.grid.y); 171 | } 172 | break; 173 | case 87: // W 174 | if(e.shiftKey) { 175 | waypointsMenu.show(); 176 | } 177 | // gotoWaypoint(waypoints.length - 1); 178 | break; 179 | case 89: // Y 180 | if(e.ctrlKey) { 181 | redo(); 182 | } 183 | break; 184 | case 90: // Z 185 | if(e.ctrlKey) { 186 | if(e.shiftKey) redo(); 187 | else undo(); 188 | } 189 | break; 190 | case 9: // Tab 191 | var component = findComponentByPos(mouse.grid.x, mouse.grid.y); 192 | if(component && component.constructor != Wire) { 193 | select(component.constructor); 194 | } 195 | keys[9] = true; 196 | return false; 197 | break; 198 | case 112: 199 | tutorial.toggle(); 200 | break; 201 | case 114: // F3 202 | if(keys[114] instanceof Date && new Date - keys[114] > 50) { 203 | settings.showDebugInfo = !settings.showDebugInfo; 204 | keys[114] = true; 205 | } 206 | return false; 207 | break; 208 | case 93: // Context menu 209 | contextMenu.show(); 210 | break; 211 | } 212 | 213 | if(e.ctrlKey) return false; 214 | } 215 | 216 | c.onkeyup = function(e) { keys[e.which] = false } 217 | 218 | c.onblur = function() { 219 | for(let i in keys) keys[i] = false; 220 | } 221 | 222 | // Window key bindings 223 | window.onkeydown = function(e) { 224 | if(e.which >= 96 && e.which <= 105) { 225 | 226 | } else if(e.which == 27) { 227 | menu.hide(); 228 | } 229 | } 230 | 231 | window.onkeyup = function(e) { 232 | if(e.which >= 96 && e.which <= 105) { 233 | 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /app/js/localStorage.js: -------------------------------------------------------------------------------- 1 | // This is the rewritten version of localstorage2.js. This file isn't used anymore 2 | 3 | function stringify(data) { 4 | let stringified = []; 5 | 6 | if(data.components) { 7 | let components = [...data.components]; 8 | let connections; 9 | for(let i = 0, len = components.length; i < len; ++i) { 10 | const component = components[i]; 11 | components[i] = []; 12 | 13 | // Constructor 14 | components[i].push(component.constructor.name); 15 | 16 | // Params 17 | let params = Object.assign({}, component); 18 | delete params.input; 19 | delete params.output; 20 | delete params.from; 21 | delete params.to; 22 | delete params.blinking; 23 | components[i].push(params); 24 | } 25 | stringified.push(components); 26 | 27 | if(data.connections) { 28 | connections = [...data.connections]; 29 | } else { 30 | connections = []; 31 | const components = [...data.components]; 32 | for(let i = 0, len = components.length; i < len; ++i) { 33 | if(components[i].constructor == Wire) { 34 | if(components.includes(components[i].from) && 35 | components.includes(components[i].to)) { 36 | connections.push([ 37 | components.indexOf(components[i].from), 38 | components.indexOf(components[i].to), 39 | components.indexOf(components[i]) 40 | ]); 41 | } else { 42 | components.splice(i,1); 43 | --i; --len; 44 | } 45 | } 46 | } 47 | } 48 | stringified.push(connections); 49 | } 50 | 51 | if(data.selection) { 52 | const selection = Object.assign({},data.selection); 53 | delete selection.components; 54 | stringified.push(selection); 55 | } 56 | 57 | return JSON.stringify(stringified); 58 | } 59 | 60 | function parse(data,clip) { 61 | data = JSON.parse(data); 62 | if(!data[0] && !data[1]) return; 63 | 64 | 65 | let parsed = []; 66 | for(let i = 0, len = data[0].length; i < len; ++i) { 67 | let component = eval(`new ${data[0][i][0]}`); 68 | let properties = typeof data[0][i][1] == "string" ? JSON.parse(data[0][i][1]) : data[0][i][1]; 69 | Object.assign( 70 | component, 71 | properties 72 | ); 73 | 74 | parsed.push(component); 75 | } 76 | 77 | if(clip) { 78 | clipbord.components = parsed; 79 | clipbord.connections = data[1]; 80 | data[2] && (clipbord.selection = data[2]); 81 | } else { 82 | const connections = data[1]; 83 | for(let i = 0, len = connections.length; i < len; ++i) { 84 | connect( 85 | parsed[connections[i][0]], 86 | parsed[connections[i][1]], 87 | parsed[connections[i][2]], 88 | false, 89 | false 90 | ); 91 | } 92 | } 93 | return parsed; 94 | } 95 | 96 | function download(name, string) { 97 | const a = document.createElement("a"); 98 | const data = "data:text/json;charset=utf-8," + encodeURIComponent(string); 99 | a.setAttribute('href', data); 100 | if(name) a.setAttribute('download', name + ".dat"); 101 | else a.setAttribute('download', "BOOLR-Save-" + new Date().toLocaleString() + ".dat"); 102 | a.click(); 103 | } 104 | -------------------------------------------------------------------------------- /app/js/localStorage2.js: -------------------------------------------------------------------------------- 1 | function localStorageAvailable() { 2 | try { 3 | localStorage.setItem("",""); 4 | localStorage.removeItem(""); 5 | return true; 6 | } catch(e) { 7 | return false; 8 | } 9 | } 10 | 11 | 12 | function setLocalStorage() { 13 | if(!localStorageAvailable()) { 14 | dialog.localStorageError(); 15 | return; 16 | } 17 | 18 | const data = {}; 19 | 20 | let tipsData = {}; 21 | for(let i in tips) { 22 | tipsData[i] = !!tips[i].disabled; 23 | } 24 | 25 | data.version = VERSION; 26 | if(clipboard.components.length || clipboard.wires.length || clipboard.selection) { 27 | data.clipboard = stringify( 28 | clipboard.components, 29 | clipboard.wires, 30 | clipboard.selection 31 | ); 32 | } 33 | data.settings = settings; 34 | data.tips = tipsData; 35 | 36 | //data.savedCustomComponents = stringify(savedCustomComponents); 37 | 38 | try { 39 | localStorage.pwsData = JSON.stringify(data); 40 | } catch(e) { 41 | console.warn("Could not set localStorage data"); 42 | } 43 | } 44 | 45 | function getLocalStorage() { 46 | if(!localStorageAvailable()) { 47 | dialog.localStorageError(); 48 | return; 49 | } 50 | 51 | let data = localStorage.pwsData; 52 | 53 | if(!localStorage.pwsData) { 54 | return; 55 | } 56 | 57 | try { 58 | data = JSON.parse(data); 59 | } catch(e) { 60 | console.warn("Could not parse localStorage data " + e); 61 | return; 62 | } 63 | 64 | if(!data.version || data.version != VERSION) { 65 | dialog.update(); 66 | } 67 | 68 | if(data.clipboard) { 69 | try { 70 | const parsed = parse(data.clipboard); 71 | clipboard.copy( 72 | parsed.components, 73 | parsed.wires, 74 | parsed.selection 75 | ); 76 | } catch(e) { 77 | console.warn("Could not parse clipboard data from localStorage " + e); 78 | } 79 | } 80 | 81 | // if(data.savedCustomComponents) { 82 | // savedCustomComponents = parse(data.savedCustomComponents).components; 83 | // } 84 | 85 | if(data.settings) { 86 | settings = data.settings; 87 | } 88 | 89 | if(data.tips) { 90 | for(let tip in data.tips) { 91 | tips[tip].disabled = data.tips[tip]; 92 | } 93 | } 94 | } 95 | 96 | const constructors = { 97 | Input,Output,NOT,AND,OR,XOR, 98 | Button,Constant,Delay,Clock,Debug, 99 | Beep,Counter,LED,Display, 100 | Custom, TimerStart, TimerEnd, 101 | ROM 102 | }; 103 | 104 | /* 105 | Stringifies board 106 | @param {array} components 107 | @param {array} [wires] 108 | @param {array} [selection] 109 | @return {string} 110 | */ 111 | function stringify(components = [], wires = [], selection) { 112 | let stringified = [ 113 | [], // Component data 114 | [] // Wire data 115 | ]; 116 | 117 | for(let i = 0; i < components.length; ++i) { 118 | const component = components[i]; 119 | 120 | const constructor = component.constructor.name; 121 | const data = {}; 122 | 123 | data.id = component.id; 124 | data.name = component.name; 125 | data.pos = component.pos; 126 | 127 | data.width = component.width; 128 | data.height = component.height; 129 | 130 | data.rotation = component.rotation; 131 | 132 | data.color = component.color; 133 | 134 | data.properties = component.properties; 135 | 136 | if(component.value) data.value = component.value; 137 | 138 | data.input = []; 139 | for(let i = 0; i < component.input.length; ++i) { 140 | data.input[i] = { 141 | id: component.input[i].id, 142 | name: component.input[i].name, 143 | pos: Object.assign({},component.input[i].pos), 144 | value: component.input[i].value 145 | } 146 | } 147 | 148 | data.output = []; 149 | for(let i = 0; i < component.output.length; ++i) { 150 | data.output[i] = { 151 | id: component.output[i].id, 152 | name: component.output[i].name, 153 | pos: Object.assign({},component.output[i].pos), 154 | value: component.output[i].value 155 | } 156 | } 157 | 158 | if(constructor == "Custom") { 159 | data.componentData = JSON.parse(stringify(component.components,component.wires)); 160 | } 161 | 162 | stringified[0].push([constructor,data]); 163 | } 164 | 165 | for(let i = 0; i < wires.length; ++i) { 166 | const wire = wires[i]; 167 | 168 | const fromIndex = components.indexOf(wire.from && wire.from.component); 169 | const fromPortIndex = wire.from && wire.from.component && wire.from.component.output.indexOf(wire.from); 170 | const toIndex = components.indexOf(wire.to && wire.to.component); 171 | const toPortIndex = wire.to && wire.to.component && wire.to.component.input.indexOf(wire.to); 172 | 173 | const input = []; 174 | for(let i = 0; i < wire.input.length; ++i) { 175 | input.push(wires.indexOf(wire.input[i])); 176 | } 177 | 178 | const output = []; 179 | for(let i = 0; i < wire.output.length; ++i) { 180 | output.push(wires.indexOf(wire.output[i])); 181 | } 182 | 183 | stringified[1].push([ 184 | fromIndex, 185 | fromPortIndex, 186 | toIndex, 187 | toPortIndex, 188 | input, 189 | output, 190 | wire.id, 191 | wire.value, 192 | wire.pos, 193 | wire.intersections, 194 | wire.color 195 | ]); 196 | } 197 | 198 | if(selection) { 199 | stringified[2] = [ 200 | Math.round(selection.x), 201 | Math.round(selection.y), 202 | Math.round(selection.w), 203 | Math.round(selection.h) 204 | ]; 205 | } 206 | 207 | try { 208 | return JSON.stringify(stringified); 209 | } catch(e) { 210 | throw new Error("Unable to stringify data"); 211 | } 212 | } 213 | 214 | /* 215 | Creates board from string 216 | @param {string} [data] 217 | @return {string} 218 | */ 219 | function parse(data) { 220 | if(typeof data == "string") { 221 | try { 222 | data = JSON.parse(data); 223 | } catch(e) { 224 | throw new Error("Board data not valid"); 225 | } 226 | } 227 | 228 | if(!Array.isArray(data)) throw new Error("Board data not valid"); 229 | 230 | const components = data[0] || []; 231 | const wires = data[1] || []; 232 | let selection = data[2]; 233 | 234 | for(let i = 0; i < components.length; ++i) { 235 | const constructor = components[i][0]; 236 | if(!constructors[constructor]) { 237 | components.splice(i,1); 238 | --i; 239 | continue; 240 | } 241 | 242 | let data = components[i][1]; 243 | if(typeof data == "string") { 244 | try { 245 | data = JSON.parse(data); 246 | } catch(e) { 247 | throw new Error("Board data not valid"); 248 | } 249 | } 250 | 251 | const component = new constructors[constructor](); 252 | 253 | if(constructor == "Custom") { 254 | const parsed = parse(JSON.stringify(data.componentData)); 255 | component.components = parsed.components; 256 | component.wires = parsed.wires; 257 | delete component.componentData; 258 | component.create(); 259 | } 260 | 261 | const input = data.input; 262 | for(let i = 0; i < component.input.length; ++i) { 263 | component.input[i].name = input[i].name; 264 | component.input[i].value = input[i].value; 265 | component.input[i].pos = input[i].pos; 266 | } 267 | delete data.input; 268 | 269 | const output = data.output; 270 | for(let i = 0; i < component.output.length; ++i) { 271 | component.output[i].name = output[i].name; 272 | component.output[i].value = output[i].value; 273 | component.output[i].pos = output[i].pos; 274 | } 275 | delete data.output; 276 | 277 | Object.assign(component,data); 278 | component.pos = Object.assign({},data.pos); 279 | 280 | components[i] = component; 281 | } 282 | 283 | for(let i = 0; i < wires.length; ++i) { 284 | if(wires[i].length == 11) { 285 | const pos = wires[i][8]; 286 | const intersections = wires[i][9]; 287 | let color = wires[i][10]; 288 | 289 | // If color is not in array format ([r,g,b]), convert 290 | if(typeof color == "string") { 291 | if(color[0] == "#" && color.length == 4) { 292 | color = color.match(/\w/g).map(n => parseInt(n.repeat(2),16)); 293 | } else if(color[0] == "#" && color.length == 7) { 294 | color = color.match(/\w{2}/g).map(n => parseInt(n,16)); 295 | } else if(color[0] == "r") { 296 | color = color.match(/\d+/g).map(n => +n); 297 | } else { 298 | color = [136,136,136]; 299 | } 300 | } 301 | 302 | const wire = new Wire( 303 | pos, intersections, color 304 | ); 305 | 306 | wire.id = wires[i][6]; 307 | 308 | wire.from = [wires[i][0],wires[i][1]]; // This is getting parsed later 309 | wire.to = [wires[i][2],wires[i][3]]; // This one too 310 | 311 | wire.input = wires[i][4]; 312 | wire.output = wires[i][5]; 313 | 314 | wire.value = wires[i][7]; 315 | 316 | wires[i] = wire; 317 | } else { 318 | const pos = wires[i][7]; 319 | const intersections = wires[i][8]; 320 | let color = wires[i][9]; 321 | 322 | // If color is not in array format ([r,g,b]), convert 323 | if(typeof color == "string") { 324 | if(color[0] == "#" && color.length == 4) { 325 | color = color.match(/\w/g).map(n => parseInt(n.repeat(2),16)); 326 | } else if(color[0] == "#" && color.length == 7) { 327 | color = color.match(/\w{2}/g).map(n => parseInt(n,16)); 328 | } else if(color[0] == "r") { 329 | color = color.match(/\d+/g).map(n => +n); 330 | } else { 331 | color = [136,136,136]; 332 | } 333 | } 334 | 335 | const wire = new Wire( 336 | pos, intersections, color 337 | ); 338 | 339 | wire.from = [wires[i][0],wires[i][1]]; // This is getting parsed later 340 | wire.to = [wires[i][2],wires[i][3]]; // This one too 341 | 342 | wire.input = wires[i][4]; 343 | wire.output = wires[i][5]; 344 | 345 | wire.value = wires[i][6]; 346 | 347 | wires[i] = wire; 348 | } 349 | } 350 | 351 | // Create connections 352 | for(let i = 0; i < wires.length; ++i) { 353 | const wire = wires[i]; 354 | 355 | const from = components[wire.from[0]]; 356 | let fromPort; 357 | if(from && from.output) fromPort = from.output[wire.from[1]]; 358 | 359 | const to = components[wire.to[0]]; 360 | let toPort; 361 | if(to && to.input) toPort = to.input[wire.to[1]]; 362 | 363 | wire.from = fromPort; 364 | wire.to = toPort; 365 | 366 | for(let i = 0; i < wire.input.length; ++i) { 367 | wire.input[i] = wires[wire.input[i]]; 368 | } 369 | 370 | for(let i = 0; i < wire.output.length; ++i) { 371 | wire.output[i] = wires[wire.output[i]]; 372 | } 373 | 374 | if(wire.to) { 375 | wire.to.connection = wire; 376 | } 377 | 378 | if(wire.from) { 379 | wire.from.connection = wire; 380 | } 381 | } 382 | 383 | if(selection) { 384 | selection = { 385 | x: selection[0], 386 | y: selection[1], 387 | w: selection[2], 388 | h: selection[3], 389 | animate: { 390 | w: selection[2], 391 | h: selection[3] 392 | }, 393 | dashOffset: 0 394 | } 395 | } 396 | 397 | return { 398 | components, 399 | wires, 400 | selection 401 | } 402 | } 403 | 404 | function saveBoard( 405 | name, 406 | components = window.components, 407 | wires = window.wires 408 | ) { 409 | // let data = stringify(components_,wires_); 410 | // 411 | // document.title = "BOOLR | " + name; 412 | // 413 | // // Export data as .board file 414 | // const a = document.createElement("a"); 415 | // data = "data:text/json;charset=utf-8," + encodeURIComponent(data); 416 | // a.setAttribute('href', data); 417 | // a.setAttribute('download', name + ".board"); 418 | // a.click(); 419 | 420 | name = name || "BOOLR-save-" + new Date().toLocaleString(); 421 | 422 | const data = stringify(components,wires); 423 | const csvData = new Blob([data], { type: "text/csv" }); 424 | const csvUrl = URL.createObjectURL(csvData); 425 | 426 | const a = document.createElement("a"); 427 | a.href = csvUrl; 428 | a.target = "_blank"; 429 | a.download = name + ".board"; 430 | a.click(); 431 | } 432 | 433 | function openFile() { 434 | const input = document.createElement("input"); 435 | input.type = "file"; 436 | input.accept = ".board"; 437 | input.click(); 438 | input.onchange = e => readFile(e.target); 439 | } 440 | 441 | function readFile(input) { 442 | const name = input.files[0] && input.files[0].name.replace(".board",""); 443 | document.title = "BOOLR | " + name; 444 | 445 | const reader = new FileReader; 446 | reader.onload = function() { 447 | const data = reader.result; 448 | // try { 449 | // TODO: dit is lelijk! 450 | const parsed = parse(data); 451 | const clone = cloneSelection(parsed.components || [],parsed.wires || []); 452 | 453 | components = []; 454 | wires = []; 455 | redoStack = []; 456 | undoStack = []; 457 | 458 | addSelection( 459 | clone.components, 460 | clone.wires 461 | ); 462 | // } catch(e) { 463 | // throw new Error("Error reading save file"); 464 | // } 465 | } 466 | 467 | reader.readAsText(input.files[0]); 468 | dialog.hide(); 469 | } 470 | -------------------------------------------------------------------------------- /app/js/mainmenu.js: -------------------------------------------------------------------------------- 1 | const mainMenu = document.querySelector(".main-menu"); 2 | 3 | mainMenu.show = function() { 4 | openedSaveFile && save(); 5 | this.style.display = "block"; 6 | 7 | setTimeout(() => { 8 | this.style.opacity = 1; 9 | },10); 10 | 11 | this.querySelector("h1").style.top = 0; 12 | 13 | const buttons = this.querySelectorAll(".main-menu > button"); 14 | for(let i of buttons) { 15 | i.style.top = 0; 16 | i.style.opacity = 1; 17 | i.style.transform = "translateX(0px)"; 18 | 19 | i.querySelector(".material-icons").style.transform = "translateX(0px)"; 20 | } 21 | 22 | setTimeout(() => loading.style.display = "none"); 23 | setTimeout(clearBoard,1000); 24 | } 25 | 26 | mainMenu.hide = function() { 27 | for(let i of sub) i.hide(); 28 | 29 | const buttons = this.querySelectorAll("button"); 30 | for(let i of buttons) { 31 | i.style.top = "100%"; 32 | } 33 | 34 | this.querySelector("h1").style.top = "-100%"; 35 | 36 | this.style.opacity = 0; 37 | 38 | setTimeout(() => { 39 | this.style.display = "none"; 40 | c.focus(); 41 | 42 | if(!localStorage.pwsData) { 43 | dialog.welcome(); 44 | } 45 | }, 500); 46 | } 47 | 48 | // Loading indicator 49 | const loading = document.querySelector(".main-menu .loading"); 50 | 51 | // Sub menu's 52 | const sub = document.querySelectorAll(".main-menu .sub"); 53 | 54 | // Apply show and hide methods to sub menu's 55 | for(let i of sub) { 56 | i.show = function() { 57 | i.onopen && i.onopen(); 58 | 59 | this.style.display = "block"; 60 | const height = Math.min(innerHeight - this.getBoundingClientRect().bottom, 0) - 100; 61 | 62 | setTimeout(() => { 63 | this.style.opacity = 1; 64 | this.style.transform = "translateY(0px)"; 65 | mainMenu.style.transform = `translateY(${height}px)`; 66 | },10); 67 | 68 | setTimeout(() => this.querySelector("input") && this.querySelector("input") .focus(), 10); 69 | 70 | for(let j of sub) i != j && j.hide(); 71 | } 72 | 73 | i.hide = function() { 74 | this.style.opacity = 0; 75 | this.style.transform = "translateY(-50px)"; 76 | mainMenu.style.transform = "translateY(0px)"; 77 | setTimeout(() => this.style.display = "none", 500); 78 | 79 | i.onclose && i.onclose(); 80 | } 81 | 82 | i.toggle = function() { 83 | if(this.style.display != "block") this.show(); 84 | else this.hide(); 85 | } 86 | 87 | i.onkeydown = function(e) { 88 | if(e.which == 13) { 89 | const buttons = this.querySelectorAll("button"); 90 | buttons[buttons.length - 1] && buttons[buttons.length - 1].click(); 91 | } else if(e.which == 27) { 92 | this.hide(); 93 | } 94 | } 95 | } 96 | 97 | document.body.onkeydown = e => { 98 | if(e.which == 27) { 99 | for(let i of sub) i.hide(); 100 | } 101 | } 102 | 103 | const newBoardMenu = document.querySelector(".main-menu .new-board"); 104 | const openBoardMenu = document.querySelector(".main-menu .open-board"); 105 | const settingsMenu = document.querySelector(".main-menu .settings"); 106 | 107 | newBoardMenu.onopen = function() { 108 | this.querySelector("#boardname").value = ""; 109 | this.querySelector("#filename").innerHTML = "This board will be saved as new-board.board"; 110 | this.querySelector("#filename").style.opacity = 1; 111 | 112 | setTimeout(() => this.querySelector("#boardname").focus(), 10); 113 | } 114 | 115 | openBoardMenu.onopen = function() { 116 | const list = document.querySelector(".open-board ul"); 117 | 118 | list.innerHTML = ""; 119 | readSaveFiles(); 120 | 121 | if(saves.length < 1) { 122 | const li = document.createElement("li"); 123 | li.innerHTML = "You have no saved boards"; 124 | li.style.textAlign = "center"; 125 | li.style.color = "#888"; 126 | list.appendChild(li); 127 | return; 128 | } 129 | 130 | for(let save of saves) { 131 | const li = document.createElement("li"); 132 | li.save = save; 133 | 134 | li.appendChild(document.createTextNode(`${save.name}`)); 135 | 136 | // Remove board button 137 | const removeBtn = document.createElement("i"); 138 | removeBtn.className = "material-icons"; 139 | removeBtn.title = "Remove board"; 140 | removeBtn.innerHTML = "delete"; 141 | removeBtn.onclick = function(e) { 142 | dialog.confirm( 143 | "Are you sure you want to delete " + this.parentNode.save.name + "?", 144 | () => { 145 | fs.unlink(savesFolder + save.fileName, (err) => console.log(err)); 146 | const index = saves.indexOf(save); 147 | if(index > -1) saves.splice(index,1); 148 | openBoardMenu.onopen(); 149 | } 150 | ); 151 | e.stopPropagation(); 152 | } 153 | li.appendChild(removeBtn); 154 | 155 | // Edit board button 156 | const editBtn = document.createElement("i"); 157 | editBtn.className = "material-icons"; 158 | editBtn.title = "Edit board"; 159 | editBtn.innerHTML = "edit"; 160 | editBtn.onclick = function(e) { 161 | dialog.editBoard(save); 162 | e.stopPropagation(); 163 | } 164 | li.appendChild(editBtn); 165 | 166 | // Convert file size 167 | const i = Math.floor(Math.log(save.fileSize) / Math.log(1024)); 168 | const size = (save.fileSize / Math.pow(1024,i)).toFixed(2) * 1 + " " + ["bytes","KB","MB","GB","TB"][i]; 169 | 170 | const sizeSpan = document.createElement("span"); 171 | sizeSpan.innerHTML = `${size}`; 172 | li.appendChild(sizeSpan); 173 | 174 | const fileNameSpan = document.createElement("span"); 175 | fileNameSpan.innerHTML = `${save.fileName}`; 176 | li.appendChild(fileNameSpan); 177 | 178 | li.onclick = () => { 179 | openSaveFile(save); 180 | openBoardMenu.hide(); 181 | mainMenu.hide(); 182 | } 183 | 184 | list.appendChild(li); 185 | } 186 | } 187 | 188 | settingsMenu.onopen = function() { 189 | this.querySelector("#settings") && this.removeChild(this.querySelector("#settings")); 190 | 191 | const settingsList = document.getElementById("settings").cloneNode(true); 192 | settingsList.style.display = "block"; 193 | this.insertBefore(settingsList, this.querySelector("br")); 194 | 195 | const scrollAnimationOption = settingsList.querySelector(".option.scrollAnimation"); 196 | scrollAnimationOption.checked = settings.scrollAnimation; 197 | 198 | const zoomAnimationOption = settingsList.querySelector(".option.zoomAnimation"); 199 | zoomAnimationOption.checked = settings.zoomAnimation; 200 | 201 | const showDebugInfoOption = settingsList.querySelector(".option.showDebugInfo"); 202 | showDebugInfoOption.checked = settings.showDebugInfo; 203 | 204 | const showComponentUpdatesOption = settingsList.querySelector(".option.showComponentUpdates"); 205 | showComponentUpdatesOption.checked = settings.showComponentUpdates; 206 | 207 | settingsList.querySelector("#settings #reset").onclick = () => dialog.confirm( 208 | 'Are you sure you want to clear all local stored data?', 209 | () => { 210 | delete localStorage.pwsData; 211 | window.onbeforeunload = undefined; 212 | location.reload() 213 | } 214 | ); 215 | 216 | this.apply = () => { 217 | settings.scrollAnimation = scrollAnimationOption.checked; 218 | settings.zoomAnimation = zoomAnimationOption.checked; 219 | settings.showDebugInfo = showDebugInfoOption.checked; 220 | settings.showComponentUpdates = showComponentUpdatesOption.checked; 221 | } 222 | } 223 | 224 | -------------------------------------------------------------------------------- /app/js/notifications.js: -------------------------------------------------------------------------------- 1 | // if(window.Notification) { 2 | // if(Notification.permission != "granted") { 3 | // Notification.requestPermission(); 4 | // } 5 | // } 6 | 7 | 8 | const notifications = document.getElementById("notifications"); 9 | 10 | notifications.push = function(msg,type) { 11 | notifications.style.maxHeight = c.height - 80 - userList.clientHeight - 40; 12 | 13 | let notification = document.createElement("li"); 14 | 15 | if(type == "error") { 16 | notification.className = "error"; 17 | notification.innerHTML = "ERROR: "; 18 | } 19 | notification.innerHTML += msg; 20 | notifications.appendChild(notification); 21 | 22 | if(!chat.hidden) { 23 | notifications.scrollTop = notifications.scrollHeight - notifications.clientHeight; 24 | } 25 | 26 | setTimeout(() => notification.style.left = "0px"); 27 | 28 | notification.hide = function() { 29 | this.display = false; 30 | if(chat.hidden) { 31 | this.style.opacity = 0; 32 | setTimeout(() => { 33 | this.style.display = "none"; 34 | }, 200); 35 | } 36 | } 37 | 38 | setTimeout(() => notification.hide(),5000); 39 | 40 | const length = notifications.children.length; 41 | for(let i = 0; i < length - 4; ++i) { 42 | if(notifications.children[i]) { 43 | setTimeout(() => notifications.children[i].hide(), 1000); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/js/savedCustomComponents.js: -------------------------------------------------------------------------------- 1 | let savedCustomComponents = []; 2 | 3 | function saveCustomComponent(component) { 4 | const clone = cloneComponent(component); 5 | clone.name = component.name; 6 | savedCustomComponents.push(clone); 7 | toolbar.message("Saved component " + component.name); 8 | } 9 | 10 | function saveCustomComponents() { 11 | const stringified = stringify(savedCustomComponents); 12 | fs.writeFileSync( 13 | __dirname + "/../data/customcomponents.json", 14 | stringified, 15 | "utf-8" 16 | ); 17 | } 18 | 19 | function getCustomComponents() { 20 | const data = fs.readFileSync( 21 | __dirname + "/../data/customcomponents.json", 22 | "utf-8" 23 | ); 24 | 25 | savedCustomComponents = parse(data).components; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /app/js/saves.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const savesFolder = __dirname + "/../saves/"; 3 | 4 | let saves = []; 5 | 6 | let openedSaveFile; 7 | 8 | // Read save files from "saves" folder 9 | function readSaveFiles() { 10 | const updatedSaves = []; 11 | const files = fs.readdirSync(savesFolder).filter(file => /\.board$/.test(file)); 12 | 13 | files.forEach(file => { 14 | const found = saves.find(save => save.fileName == file); 15 | if(found) return updatedSaves.push(found); 16 | 17 | function getName(file) { 18 | try { 19 | return JSON.parse(fs.readFileSync(savesFolder + file, "utf-8")).name 20 | } catch(e) { 21 | return false; 22 | } 23 | } 24 | 25 | updatedSaves.push({ 26 | name: getName(file) || file, 27 | fileSize: fs.statSync(savesFolder + file).size, 28 | fileName: file, 29 | location: savesFolder + file 30 | }); 31 | }); 32 | 33 | saves = updatedSaves; 34 | } 35 | 36 | function clearBoard() { 37 | setLocalStorage(); 38 | 39 | openedSaveFile = undefined; 40 | 41 | zoom = zoomAnimation = 100; 42 | offset = {x: 0, y: 0}; 43 | 44 | variables = {}; 45 | variableReferences = {}; 46 | 47 | path = []; 48 | 49 | chat.hide(); 50 | boolrConsole.hide(); 51 | contextMenu.hide(); 52 | waypointsMenu.hide(); 53 | hoverBalloon.hide(); 54 | customComponentToolbar.hide(); 55 | notifications.innerHTML = ""; 56 | 57 | components = []; 58 | wires = []; 59 | 60 | redoStack = []; 61 | undoStack = []; 62 | 63 | setTimeout(() => newBoardMenu.onopen()); 64 | } 65 | 66 | function openSaveFile(save) { 67 | const saveFile = JSON.parse(fs.readFileSync(savesFolder + save.fileName, "utf-8")); 68 | if(!Array.isArray(saveFile)) { 69 | clearBoard(); 70 | 71 | offset.x = saveFile.offset.x || 0; 72 | offset.y = saveFile.offset.y || 0; 73 | zoom = zoomAnimation = saveFile.zoom || 100; 74 | 75 | variables = saveFile.variables || {}; 76 | variableReferences = saveFile.variableReferences || {}; 77 | 78 | const parsed = parse(saveFile.data); 79 | 80 | addSelection( 81 | parsed.components, 82 | parsed.wires 83 | ); 84 | } else { 85 | clearBoard(); 86 | 87 | const parsed = parse(saveFile); 88 | console.log(parsed); 89 | addSelection( 90 | parsed.components, 91 | parsed.wires 92 | ); 93 | } 94 | 95 | openedSaveFile = save; 96 | document.title = save.name + " - BOOLR"; 97 | } 98 | 99 | function createFileName(name) { 100 | name = name.replace(".board", ""); 101 | name = name.replace(/[^a-z0-9|.]+/gi, '-').toLowerCase() || "new-board"; 102 | 103 | let i = 0; 104 | while(fs.readdirSync(savesFolder).includes(name + (i > 0 ? ` (${i})`: '') + ".board")) ++i; 105 | if(i > 0) name += " (" + i + ")"; 106 | 107 | name += ".board"; 108 | 109 | return name; 110 | } 111 | 112 | function createSaveFile(name) { 113 | if(!name || name.length == 0) name = "New board"; 114 | 115 | // Create safe file name 116 | const filename = createFileName(name); 117 | 118 | const save = {}; 119 | save.name = name; 120 | 121 | save.offset = offset; 122 | save.zoom = zoom; 123 | 124 | save.variables = variables; 125 | save.variableReferences = variableReferences; 126 | 127 | save.data = stringify(components,wires); 128 | 129 | fs.writeFileSync( 130 | savesFolder + filename, 131 | JSON.stringify(save), 132 | "utf-8" 133 | ); 134 | 135 | saves.push({ 136 | name, 137 | fileSize: fs.statSync(savesFolder + filename).size, 138 | fileName: filename, 139 | location: savesFolder + filename 140 | }); 141 | 142 | openedSaveFile = saves.slice(-1)[0]; 143 | document.title = save.name + " - BOOLR"; 144 | } 145 | 146 | function save(msg = false) { 147 | toolbar.message("Saving..."); 148 | setTimeout(() => { 149 | if(!components || components.length == 0) return; 150 | if(!openedSaveFile) return dialog.createBoard(); 151 | 152 | const save = {}; 153 | save.name = openedSaveFile.name; 154 | 155 | save.offset = offset; 156 | save.zoom = zoom; 157 | 158 | save.variables = variables; 159 | save.variableReferences = variableReferences; 160 | 161 | save.data = stringify(components,wires); 162 | 163 | fs.writeFile( 164 | savesFolder + openedSaveFile.fileName, 165 | JSON.stringify(save), 166 | "utf-8", 167 | (err,data) => err && console.log(err) 168 | ); 169 | 170 | if(msg) { 171 | toolbar.message("Saved changes to " + openedSaveFile.fileName); 172 | } 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /app/js/socket.js: -------------------------------------------------------------------------------- 1 | var socket; 2 | 3 | function connectToSocket(url, callback) { 4 | try { 5 | socket = new WebSocket(url); 6 | } catch(e) { 7 | callback && callback(false); 8 | return false; 9 | } 10 | 11 | socket.onopen = function() { 12 | components = []; 13 | notifications.push("Connected to " + url); 14 | 15 | callback && callback(true); 16 | } 17 | 18 | socket.onclose = function() { 19 | notifications.push("Connection closed", "error"); 20 | socket = null; 21 | 22 | callback && callback(false); 23 | } 24 | 25 | socket.onerror = function(err) { 26 | notifications.push("Connection error: " + err, "error"); 27 | socket = null; 28 | 29 | callback && callback(false); 30 | } 31 | 32 | socket.onmessage = function(e) { 33 | const msg = JSON.parse(e.data); 34 | 35 | const data = JSON.parse(msg.data); 36 | switch(msg.type) { 37 | case "board": 38 | var parsed = parse(data); 39 | 40 | components = []; 41 | wires = []; 42 | redoStack = []; 43 | undoStack = []; 44 | 45 | addSelection( 46 | parsed.components, 47 | parsed.wires, 48 | undefined,undefined,undefined, 49 | false, 50 | false 51 | ); 52 | break; 53 | case "add": 54 | try { 55 | var parsed = parse(msg.data); 56 | components.push(...parsed.components); 57 | wires.push(...parsed.wires); 58 | } catch(e) { 59 | console.warn("Could not parse data from server " + e); 60 | } 61 | break; 62 | case "remove": 63 | var component = findComponentByID(data[0]); 64 | removeComponent(component,false,false); 65 | 66 | for(let i = 0; i < data[1].length; ++i) { 67 | var wire = findWireByID(data[1][i]); 68 | removeWire(wire); 69 | } 70 | 71 | break; 72 | case "connect": 73 | const wireData = data[0][1][0]; 74 | var wire = findWireByID(wireData[6]); 75 | if(!wire) { 76 | wire = parse(data[0]).wires[0]; 77 | if(wire) wires.push(wire); 78 | } 79 | 80 | var from = components[data[1]].output[data[2]]; 81 | var to = components[data[3]].input[data[4]]; 82 | connect(from,to,wire,false,false); 83 | break; 84 | case "move": 85 | var component = findComponentByID(+data[0]); 86 | moveComponent( 87 | component, 88 | +data[1], 89 | +data[2], 90 | false, 91 | false 92 | ); 93 | break; 94 | case "mousedown": 95 | var component = findComponentByID(+msg.data); 96 | component.onmousedown && component.onmousedown(false); 97 | break; 98 | case "mouseup": 99 | var component = findComponentByID(+msg.data); 100 | component.onmouseup && component.onmouseup(false); 101 | break; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /app/js/startup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | setTimeout(() => { 4 | // parse localstorage 5 | getLocalStorage(localStorage.pwsData); 6 | getCustomComponents(); 7 | 8 | readSaveFiles(); 9 | updateDebugInfo(); 10 | setInterval(updateDebugInfo, 500); 11 | draw(); 12 | 13 | loading.style.display = "none"; 14 | 15 | const buttons = mainMenu.querySelectorAll(".main-menu > button"); 16 | for(let i of buttons) { 17 | i.style.top = 0; 18 | i.style.opacity = 1; 19 | i.style.transform = "translateX(0px)"; 20 | 21 | i.querySelector(".material-icons").style.transform = "translateX(0px)"; 22 | } 23 | }); -------------------------------------------------------------------------------- /app/js/stringifier.js: -------------------------------------------------------------------------------- 1 | function stringify_old(area = components) { 2 | let result = { 3 | components: [], 4 | connections: [] 5 | } 6 | 7 | for(let i of area) { 8 | let component = [ 9 | i.constructor.name 10 | ]; 11 | 12 | // Params 13 | component[1] = Object.assign({}, i); 14 | delete component[1].input; 15 | delete component[1].output; 16 | delete component[1].from; 17 | delete component[1].to; 18 | delete component[1].blinking; 19 | 20 | // Connections 21 | if(i.input) { 22 | for(let input of i.input) { 23 | if(area.indexOf(input) == -1 || area.indexOf(input.from) == -1) { 24 | if(area.indexOf(input) != -1) area.splice(area.indexOf(input),1); 25 | continue; 26 | } 27 | 28 | result.connections.push([ 29 | area.indexOf(input.from), // From 30 | area.indexOf(input.to), // To 31 | area.indexOf(input) // Wire 32 | ]); 33 | } 34 | } 35 | 36 | if(i.output) { 37 | for(let output of i.output) { 38 | if(area.indexOf(output.to) == -1) area.splice(area.indexOf(output.to), 1); 39 | } 40 | } 41 | 42 | result.components.push(component); 43 | } 44 | 45 | return JSON.stringify(result); 46 | } 47 | 48 | function parse_old(string,dx,dy,select) { 49 | let result = []; 50 | string = JSON.parse(string); 51 | for(let i of string.components.reverse()) { 52 | let component = eval("new " + i[0]); 53 | Object.assign(component,i[1]); 54 | 55 | if(component.constructor == Wire) { 56 | if(component.pos[0].x % 1 == 0 && component.pos[0].y % 1 == 0 57 | && component.pos.slice(-1)[0].x % 1 == 0 && component.pos.slice(-1)[0].y % 1 == 0) { 58 | const dx1 = component.pos[1].x - component.pos[0].x; 59 | const dy1 = component.pos[1].y - component.pos[0].y; 60 | const dx2 = component.pos.slice(-2)[0].x - component.pos.slice(-1)[0].x; 61 | const dy2 = component.pos.slice(-2)[0].y - component.pos.slice(-1)[0].y; 62 | component.pos[0].x += dx1 / 2; 63 | component.pos[0].y += dy1 / 2; 64 | component.pos.slice(-1)[0].x += dx2 / 2; 65 | component.pos.slice(-1)[0].y += dy2 / 2; 66 | } 67 | } 68 | 69 | if(dx && dy) { 70 | if(Array.isArray(component.pos)) { 71 | for(let pos of component.pos) { 72 | pos.x = Math.round(pos.x + dx); 73 | pos.y = Math.round(pos.y + dy); 74 | } 75 | } else { 76 | component.pos.x = Math.round(component.pos.x + dx); 77 | component.pos.y = Math.round(component.pos.y + dy); 78 | } 79 | } 80 | component.name = component.constructor.name + "#" + (components.filter(n => n.constructor == component.constructor).length); 81 | component.constructor == Wire ? components.unshift(component) : components.push(component); 82 | result.unshift(component); 83 | } 84 | 85 | for(let i = string.connections.length - 1; i >= 0; --i) { 86 | const connection = string.connections[i]; 87 | 88 | const from = result[connection[0]]; 89 | const to = result[connection[1]]; 90 | const wire = result[connection[2]]; 91 | 92 | wire.from = from; 93 | wire.to = to; 94 | connect(from,to,wire); 95 | } 96 | 97 | if(select) { 98 | selecting = Object.assign({}, clipbord); 99 | selecting.x = Math.round(contextMenu.pos.x); 100 | selecting.y = Math.round(contextMenu.pos.y); 101 | selecting.components = result; 102 | contextMenu.show({ x: (selecting.x + selecting.w + offset.x) * zoom, y: (-(selecting.y + selecting.h) + offset.y) * zoom }); 103 | } 104 | } -------------------------------------------------------------------------------- /app/js/tips.js: -------------------------------------------------------------------------------- 1 | const tips = { 2 | dragging: document.querySelector(".tip#dragging"), 3 | connecting_ctrl: document.querySelector(".tip#connecting_ctrl"), 4 | connecting_shift: document.querySelector(".tip#connecting_shift"), 5 | waypoints: document.querySelector(".tip#waypoints") 6 | } 7 | 8 | for(let i in tips) { 9 | tips[i].show = function() { 10 | if(this.style.display == "block") return; 11 | this.style.display = "block"; 12 | setTimeout(() => { 13 | this.style.opacity = .9; 14 | 15 | (function animate() { 16 | tips[i].style.left = 17 | Math.max( 18 | Math.min( 19 | mouse.screen.x - tips[i].clientWidth / 2, 20 | c.width - tips[i].clientWidth), 21 | 0 22 | ); 23 | tips[i].style.top = 24 | Math.max( 25 | mouse.screen.y - tips[i].clientHeight - 20, 26 | 0 27 | ); 28 | 29 | if(tips[i].style.display == "block") requestAnimationFrame(animate); 30 | })(); 31 | 32 | this.disabled = true; 33 | }); 34 | 35 | setTimeout(() => { 36 | this.hide(); 37 | }, 5000); 38 | } 39 | 40 | tips[i].hide = function() { 41 | if(this.style.display == "none") return; 42 | this.style.opacity = 0; 43 | setTimeout(() => { 44 | this.style.display = "none"; 45 | }, 500); 46 | } 47 | } 48 | 49 | setInterval(function() { 50 | if(!tips.dragging.disabled && dragging) { 51 | tips.dragging.show(); 52 | } 53 | 54 | if(connecting) { 55 | if(!tips.connecting_ctrl.disabled && 56 | mouse.screen.x < 100 || mouse.screen.x > c.width - 100 || 57 | mouse.screen.y < 100 || mouse.screen.y > c.height - 100) { 58 | tips.connecting_ctrl.show(); 59 | } else if(!tips.connecting_shift.disabled) { 60 | const x = connecting.pos.slice(-8).map(n => n.x); 61 | const y = connecting.pos.slice(-8).map(n => n.y); 62 | if(x.join("") == x[0].toString().repeat(8) || y.join("") == y[0].toString().repeat(8)) { 63 | tips.connecting_shift.show(); 64 | } 65 | } 66 | } 67 | 68 | if(!tips.waypoints.disabled && Math.random() < .01) { 69 | tips.waypoints.show(); 70 | } 71 | }, 500); -------------------------------------------------------------------------------- /app/js/toolbar.js: -------------------------------------------------------------------------------- 1 | function select(Component) { 2 | Selected = Component; 3 | toolbar.message(`Selected ${Component.name} ${"gate"}`); 4 | document.getElementById("list").style.display = "none"; 5 | } 6 | 7 | const toolbar = document.getElementById("toolbar"); 8 | let hideToolbarMessage; 9 | toolbar.message = function(msg,type) { 10 | clearTimeout(hideToolbarMessage); 11 | 12 | const toast = document.getElementById("toast"); 13 | toast.style.display = "block"; 14 | toast.innerHTML = msg; 15 | if(type == "warning") { 16 | toast.innerHTML = "warning" + toast.innerHTML; 17 | } else if(type == "action") { 18 | toast.innerHTML += ""; 19 | } 20 | 21 | toast.style.marginLeft = -toast.clientWidth / 2 + "px"; 22 | toast.style.opacity = 1; 23 | hideToolbarMessage = setTimeout(() => { 24 | toast.style.opacity = 0; 25 | },3000); 26 | } 27 | 28 | // Input/Output list 29 | const list = document.getElementById("list"); 30 | list.show = function() { 31 | list.style.display = "block"; 32 | setTimeout(() => { 33 | list.style.opacity = 1; 34 | list.style.transform = "scale(1)"; 35 | },1); 36 | } 37 | list.hide = function() { 38 | list.style.opacity = 0; 39 | list.style.transform = "scale(.5) translateX(-63px) translateY(150px)"; 40 | c.focus(); 41 | setTimeout(() => list.style.display = "none",200); 42 | } 43 | 44 | document.getElementsByClassName("slot")[0].onmousedown = function() { 45 | document.getElementById("toolbartip").style.display = "none"; 46 | if(list.style.display == "none") list.show(); 47 | else list.hide(); 48 | } 49 | document.getElementsByClassName("slot")[0].onmouseup = function() { 50 | document.getElementsByClassName("slot")[0].focus(); 51 | } 52 | 53 | document.getElementById("list").onblur = function() { 54 | list.hide(); 55 | } 56 | 57 | const listItems = document.getElementById("list").children; 58 | for(let i = 0; i < listItems.length; ++i) { 59 | listItems[i].onmouseenter = function() { this.style.background = "#222" }; 60 | listItems[i].onmouseleave = function() { this.style.background = "#111" }; 61 | listItems[i].onmouseup = function() { this.onclick() }; 62 | } 63 | -------------------------------------------------------------------------------- /app/js/tutorial.js: -------------------------------------------------------------------------------- 1 | const tutorial = document.querySelector(".tutorial"); 2 | 3 | tutorial.sections = tutorial.querySelectorAll(".sections div"); 4 | tutorial.sectionIndex = 0; 5 | 6 | tutorial.querySelector(".index").innerHTML = (tutorial.sectionIndex + 1) + "/" + tutorial.sections.length; 7 | 8 | tutorial.nextBtn = tutorial.querySelector(".next"); 9 | tutorial.nextBtn.onclick = function() { 10 | const previousSection = tutorial.sections[tutorial.sectionIndex]; 11 | previousSection.style.opacity = 0; 12 | previousSection.style.transform = "translateX(-500px)"; 13 | 14 | ++tutorial.sectionIndex; 15 | 16 | const nextSection = tutorial.sections[tutorial.sectionIndex] 17 | nextSection.style.opacity = 1; 18 | nextSection.style.transform = "translateX(0px)"; 19 | 20 | tutorial.querySelector(".index").innerHTML = (tutorial.sectionIndex + 1) + "/" + tutorial.sections.length; 21 | 22 | tutorial.previousBtn.disabled = false; 23 | if(tutorial.sectionIndex >= tutorial.sections.length - 1) { 24 | this.disabled = true; 25 | } 26 | } 27 | 28 | tutorial.previousBtn = tutorial.querySelector(".previous"); 29 | tutorial.previousBtn.onclick = function() { 30 | const previousSection = tutorial.sections[tutorial.sectionIndex]; 31 | previousSection.style.opacity = 0; 32 | previousSection.style.transform = "translateX(500px)"; 33 | 34 | --tutorial.sectionIndex; 35 | 36 | const nextSection = tutorial.sections[tutorial.sectionIndex] 37 | nextSection.style.opacity = 1; 38 | nextSection.style.transform = "translateX(0px)"; 39 | 40 | tutorial.querySelector(".index").innerHTML = (tutorial.sectionIndex + 1) + "/" + tutorial.sections.length; 41 | 42 | tutorial.nextBtn.disabled = false; 43 | if(tutorial.sectionIndex < 1) { 44 | this.disabled = true; 45 | } 46 | } 47 | 48 | tutorial.show = function() { 49 | this.style.display = "block"; 50 | setTimeout(() => { 51 | this.style.opacity = 1; 52 | this.style.left = 0; 53 | }, 10); 54 | } 55 | 56 | tutorial.hide = function() { 57 | this.style.opacity = 0; 58 | this.style.left = "-30%"; 59 | setTimeout(() => { 60 | this.style.display = "none"; 61 | 62 | for(let i of this.sections) { 63 | i.style.opacity = 0; 64 | i.style.transform = "translateX(500px)"; 65 | } 66 | 67 | const nextSection = this.sections[0]; 68 | nextSection.style.opacity = 1; 69 | nextSection.style.transform = "translateX(0px)"; 70 | 71 | this.sectionIndex = 0; 72 | tutorial.previousBtn.disabled = true; 73 | tutorial.querySelector(".index").innerHTML = (tutorial.sectionIndex + 1) + "/" + tutorial.sections.length; 74 | }, 200); 75 | } 76 | 77 | tutorial.toggle = function() { 78 | if(this.style.display != "block") { 79 | this.show(); 80 | } else { 81 | this.hide(); 82 | } 83 | } -------------------------------------------------------------------------------- /app/js/undo.js: -------------------------------------------------------------------------------- 1 | let undoStack = []; 2 | let redoStack = []; 3 | 4 | const redoCaller = {}; 5 | 6 | function undo( 7 | action = undoStack.splice(-1)[0] 8 | ) { 9 | if(action) { 10 | action(); 11 | } 12 | } 13 | 14 | function redo( 15 | action = redoStack.splice(-1)[0] 16 | ) { 17 | if(action) { 18 | action(); 19 | } 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/js/userList.js: -------------------------------------------------------------------------------- 1 | const userList = document.getElementById("userList"); 2 | 3 | userList.show = function() { 4 | this.style.display = "block"; 5 | this.innerHTML = ""; 6 | 7 | for(let i in socket.users) { 8 | this.innerHTML += 9 | i + ": " + 10 | (socket.users[i].online ? "online".fontcolor("#050") : "offline".fontcolor("#500")).bold() + 11 | "
"; 12 | } 13 | 14 | this.innerHTML += "Spectators: " + (socket.spectators ? socket.spectators : 0); 15 | } -------------------------------------------------------------------------------- /app/js/variables.js: -------------------------------------------------------------------------------- 1 | let variables = {}; 2 | let variableReferences = {}; 3 | 4 | function setVariable(name,value) { 5 | name = (name.match(/[a-zA-Z'`´_-]+/g) || []) == name ? name : null; 6 | if(!name) throw "Invalid variable name"; 7 | if(!value) throw "No value given for variable " + name; 8 | 9 | variables[name] = +value || value; 10 | 11 | if(variableReferences[name]) { 12 | for(let i = 0; i < variableReferences[name].length; ++i) { 13 | const reference = variableReferences[name][i]; 14 | let obj = findComponentByID(reference.id) || findWireByID(reference.id) || findPortByID(reference.id); 15 | if(!obj) { 16 | variableReferences[name].splice(i, 1); 17 | continue; 18 | } 19 | 20 | for(let j = 0; j < reference.property.length - 1; ++j) { 21 | obj = obj[reference.property[j]]; 22 | } 23 | 24 | if(!obj) { 25 | variableReferences[name].splice(i, 1); 26 | continue; 27 | } 28 | 29 | obj[reference.property.slice(-1)[0]] = parseVariableInput(reference.str); 30 | console.log(obj,obj[reference.property.slice(-1)[0]]); 31 | } 32 | } 33 | 34 | return value; 35 | } 36 | 37 | function getVariable(name) { 38 | name = (name.match(/[a-zA-Z'`´_-]+/g) || []) == name ? name : null; 39 | if(!name) throw "Invalid variable name"; 40 | return variables[name]; 41 | } 42 | 43 | function parseVariableInput(str) { 44 | str = str + ""; 45 | const vars = str.match(/[a-zA-Z'`´_-]+/g) || []; 46 | str = str.replace( 47 | /[a-zA-Z'`´_-]+/g, 48 | "variables['$&']" 49 | ); 50 | 51 | const value = eval(str); 52 | if(isNaN(value)) return; 53 | 54 | return value; 55 | } 56 | 57 | function createVariableReference(str,component,property) { 58 | const vars = str.match(/[a-zA-Z'`´_-]+/g) || []; 59 | for(let i = 0; i < vars.length; ++i) { 60 | if(!variableReferences[vars[i]]) variableReferences[vars[i]] = []; 61 | variableReferences[vars[i]].push({ 62 | id: component.id, 63 | property, 64 | str 65 | }); 66 | } 67 | } -------------------------------------------------------------------------------- /app/js/waypoints.js: -------------------------------------------------------------------------------- 1 | let waypoints = []; 2 | 3 | function setWaypoint(x,y,label = `Waypoint#${waypoints.length}`) { 4 | waypoints.push({ 5 | x,y, 6 | label 7 | }); 8 | toolbar.message(`Waypoint ${label} set at ${x},${y}`); 9 | } 10 | 11 | function gotoWaypoint(index) { 12 | if(!waypoints[index]) return; 13 | scroll(waypoints[index].x - mouse.grid.x, waypoints[index].y - mouse.grid.y); 14 | toolbar.message(`Jumped to waypoint#${index} at ${waypoints[index].x}, ${waypoints[index].y}`); 15 | } 16 | 17 | const waypointsMenu = document.getElementById("waypointsMenu"); 18 | 19 | waypointsMenu.show = function( 20 | x = mouse.screen.x / zoom + offset.x, 21 | y = -mouse.screen.y / zoom + offset.y 22 | ) { 23 | if(waypoints.length == 0) { 24 | toolbar.message("You have no waypoints set. Press s to add a waypoint") 25 | return; 26 | } 27 | else if(waypoints.length == 1 && contextMenu.style.display == "none") { gotoWaypoint(0); return } 28 | 29 | this.x = x; 30 | this.y = y; 31 | 32 | this.innerHTML = "Jump to..."; 33 | for(let i = 0; i < waypoints.length; ++i) { 34 | const li = document.createElement("li"); 35 | li.onclick = () => { 36 | gotoWaypoint(i); 37 | this.hide(); 38 | contextMenu.hide(); 39 | c.focus(); 40 | } 41 | 42 | li.innerHTML = waypoints[i].label; 43 | li.innerHTML += `\t|\t${waypoints[i].x},${waypoints[i].y}`; 44 | 45 | const deleteBtn = document.createElement("i"); 46 | deleteBtn.className = "material-icons remove"; 47 | deleteBtn.innerHTML = "close"; 48 | deleteBtn.onclick = e => { 49 | e.stopPropagation(); 50 | if(waypoints.length <= 1) { this.style.display = "none"; this.style.opacity = 0; c.focus(); } 51 | 52 | const index = Array.from(this.children).indexOf(deleteBtn.parentNode) - 1; 53 | toolbar.message(`Removed waypoint ${waypoints[index].label}`) 54 | waypointsMenu.removeChild(deleteBtn.parentNode); 55 | waypoints.splice(index,1); 56 | } 57 | li.appendChild(deleteBtn); 58 | 59 | this.appendChild(li); 60 | } 61 | 62 | this.style.display = "block"; 63 | setTimeout(() => this.style.opacity = .95, 1); 64 | } 65 | 66 | waypointsMenu.hide = function() { 67 | this.style.opacity = 0; 68 | setTimeout(() => this.style.display = "none", 200); 69 | } -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron'); 2 | const path = require('path'); 3 | const url = require('url'); 4 | 5 | //app.disableHardwareAcceleration(); 6 | app.commandLine.appendSwitch('ignore-gpu-blacklist'); 7 | 8 | let window; 9 | 10 | function createWindow () { 11 | window = new BrowserWindow({ 12 | width: 1200, 13 | height: 800, 14 | icon: __dirname + '../build/icon.png', 15 | show: false 16 | }); 17 | 18 | window.loadURL(url.format({ 19 | pathname: path.join(__dirname, 'index.html'), 20 | protocol: 'file:', 21 | slashes: true 22 | })); 23 | 24 | window.once('ready-to-show', () => { 25 | window.maximize(); 26 | window.show(); 27 | }); 28 | 29 | window.on('closed', () => { 30 | window = null 31 | }); 32 | } 33 | 34 | app.on('ready', createWindow); 35 | 36 | app.on('window-all-closed', () => { 37 | if (process.platform !== 'darwin') { 38 | app.quit() 39 | } 40 | }); 41 | 42 | app.on('activate', () => { 43 | if (window === null) { 44 | createWindow() 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /boolr.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Name=BOOLR 6 | Icon=icon.png 7 | Path=/home/ggbrw/Documents/PWS/BOOLR 8 | Exec=/usr/local/lib/node_modules/electron/dist/electron . 9 | StartupNotify=false 10 | StartupWMClass=BOOLR 11 | OnlyShowIn=Unity; 12 | X-UnityGenerated=true 13 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/build/icon.png -------------------------------------------------------------------------------- /data/customcomponents.json: -------------------------------------------------------------------------------- 1 | [[],[]] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BOOLR", 3 | "description": "A digital logic simulator", 4 | "version": "0.1.301", 5 | "scripts": { 6 | "start": "electron ." 7 | }, 8 | "author": { 9 | "name": "Gees Brouwer", 10 | "email": "gees@ggbrw.nl", 11 | "url": "http://ggbrw.nl" 12 | }, 13 | "main": "app/main.js", 14 | "dependencies": { 15 | "electron": "^1.7.9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /saves/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GGBRW/BOOLR/1f8c866d8bc6f9e99e6551de51f98aeb1e94948b/saves/.gitkeep --------------------------------------------------------------------------------