├── .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_location Set 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_location Set waypoint @${Math.round(contextMenu.pos.x)},${Math.round(contextMenu.pos.y)} (S) `;
40 |
41 | this.appendChild(contextOptions["goto waypoint"]);
42 | contextOptions["goto waypoint"].innerHTML = 'redo Jump 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_edit Edit 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_lens Edit 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 = 'timer Edit 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_left Rotate (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_copy Clone (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_copy Copy 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_paste Paste (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 = 'delete Remove (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 = 'delete Remove (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 = 'delete Remove (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 = 'delete Remove (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_arrows View 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_component Componentize ';
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_location Set 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 += "undo Undo ";
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
--------------------------------------------------------------------------------