├── .gitignore
├── README.md
├── media
├── screenshot1.png
├── screenshot2.png
└── screenshot3.png
├── package.json
└── src
├── css
├── main.css
└── welcome.css
├── fonts
├── blenderpro
│ ├── blenderpro-bold-webfont.eot
│ ├── blenderpro-bold-webfont.ttf
│ ├── blenderpro-bold-webfont.woff
│ ├── blenderpro-book-webfont.eot
│ ├── blenderpro-book-webfont.ttf
│ ├── blenderpro-book-webfont.woff
│ ├── blenderpro-heavy-webfont.eot
│ ├── blenderpro-heavy-webfont.ttf
│ ├── blenderpro-heavy-webfont.woff
│ ├── blenderpro-medium-webfont.eot
│ ├── blenderpro-medium-webfont.ttf
│ ├── blenderpro-medium-webfont.woff
│ ├── blenderpro-thin-webfont.eot
│ ├── blenderpro-thin-webfont.ttf
│ └── blenderpro-thin-webfont.woff
├── courierprime
│ ├── courier-prime-sans.ttf
│ └── courier-prime.ttf
├── pcbvector
│ └── PCBVector-Regular.otf
└── proximanova
│ ├── ProximaNova-Bold-webfont.woff
│ ├── ProximaNova-Bold.ttf
│ ├── ProximaNova-Light-webfont.woff
│ ├── ProximaNova-Light.ttf
│ ├── ProximaNova-Reg-webfont.woff
│ ├── ProximaNova-Reg.ttf
│ ├── ProximaNova-Sbold.ttf
│ ├── ProximaNova-Thin.ttf
│ └── proximanova-black-webfont.woff
├── img
├── button-loop.svg
├── button-loop1.svg
├── button-next.svg
├── button-nextscene.svg
├── button-pause.svg
├── button-play.svg
├── button-prev.svg
├── button-prevscene.svg
├── button-speaker-on.svg
├── button-speaker.svg
├── fileicon.png
├── logoicon.png
└── outliner-display-icon.icns
├── index.html
├── js
├── app.js
├── fountain-data-parser.js
├── main.js
├── menu.js
├── outlineWindow.js
├── prefs.js
├── vendor
│ └── fountain.js
├── welcome.js
└── welcomeWindow.js
└── welcome.html
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dist
2 | dist/
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # node-waf configuration
27 | .lock-wscript
28 |
29 | # Compiled binary addons (http://nodejs.org/api/addons.html)
30 | build/Release
31 |
32 | # Dependency directories
33 | node_modules
34 | jspm_packages
35 |
36 | # Optional npm cache directory
37 | .npm
38 |
39 | # Optional REPL history
40 | .node_repl_history
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Script Visualizer
2 |
3 | Script Visualizer simply displays your screenplay so you can experience it like a movie.
4 |
5 | 
6 |
7 | It automatically goes through the script and calculates the timing for each scene, so when you play it back, it will be very similar to actual shot video. Additionally, Script Visualizer can read the script back to you as you edit your script.
8 |
9 | Script Visualizer is just a viewer. You write your screenplay, Script Visualizer provides a time-paced way to view it.
10 |
11 | ### Why use this?
12 |
13 | Ultimately, the content of 120 pages of feature script are hard to keep in your head. This visualizer allows you just another way to look at your script from an objective perspective and perhaps allow you to see something in your script you might have otherwise missed. It's also a nice tool to use as you're writing to double check pacing, feeling, language, etc. It's also an enjoyable way to read a script!
14 |
15 | Simply open a screenplay in PDF or Fountain format.
16 |
17 | ## Installation
18 |
19 | ### Download for OS X here: https://github.com/setpixel/script-visualizer/releases/download/0.0.1/Script.Visualizer.zip
20 |
21 | or
22 |
23 | ### Install from source:
24 |
25 | ```
26 | # Clone this repository
27 | git clone https://github.com/setpixel/script-visualizer
28 | # Go into the repository
29 | cd script-visualizer
30 | # Install dependencies
31 | npm install
32 | # Run the app
33 | npm start
34 | ```
35 |
36 | ## How to use
37 |
38 | 1. Load script
39 | 2. Press play
40 |
41 | It's very simple. You can also choose whether you want it to speak or not.
42 |
43 | ### The user interface
44 |
45 | On the left is a list of every scene in order. You can click any scene to go right to it. The colors represent the unique scene. If you have different scenes in the same location, they will always be the same color.
46 |
47 | On the right, is the content that will be played back, and the navigation below.
48 |
49 | In the navigation, there are 2 timelines. On the top, is the current scene you are on. It will display each piece of dialogue or action over the time for that particular scene. Top character's dialogue will appear in neon colors.
50 |
51 | ### Use it while writing
52 |
53 | When you make changes to the script, SV detects and will update automatically. SV will also advance automatically to the scene you last edited and automatically play it if you want. This means that you can have SV automatically read to you every time you press command+S in your script editor.
54 |
55 | ## Feedback Please!
56 |
57 | If you notice any bugs or have feedback, please open a github issue!
58 |
59 | Thanks!
60 |
--------------------------------------------------------------------------------
/media/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/media/screenshot1.png
--------------------------------------------------------------------------------
/media/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/media/screenshot2.png
--------------------------------------------------------------------------------
/media/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/media/screenshot3.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "script-visualizer",
3 | "productName": "Script Visualizer",
4 | "version": "0.0.1",
5 | "description": "A simple way to visualize fountain screenplays.",
6 | "main": "src/js/main.js",
7 | "scripts": {
8 | "start": "electron .",
9 | "buildosx": "electron-packager . --out=dist --overwrite --platform=darwin --arch=x64 --icon=src/img/outliner-display-icon.icns"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/setpixel/script-visualizer.git"
14 | },
15 | "keywords": [
16 | "fountain",
17 | "screenplay",
18 | "visualizer"
19 | ],
20 | "author": "Charles Forman",
21 | "license": "ISC",
22 | "devDependencies": {
23 | "electron-prebuilt": "^1.4.5"
24 | },
25 | "dependencies": {
26 | "color-js": "^1.0.3",
27 | "jquery": "^3.1.0",
28 | "moment": "^2.14.1",
29 | "pdf2json": "^1.1.7"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/css/main.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'blenderpro';
3 | src: url('../fonts/blenderpro/blenderpro-thin-webfont.woff') format('woff');
4 | /* Pretty Modern Browsers */
5 | font-weight: 400; }
6 |
7 | @font-face {
8 | font-family: 'proximanova';
9 | src: url('../fonts/proximanova/ProximaNova-Reg-webfont.woff') format('woff');
10 | /* Pretty Modern Browsers */
11 | font-weight: 400; }
12 |
13 | @font-face {
14 | font-family: 'proximanova';
15 | src: url('../fonts/proximanova/ProximaNova-Light-webfont.woff') format('woff');
16 | /* Pretty Modern Browsers */
17 | font-weight: 300; }
18 |
19 | @font-face {
20 | font-family: 'proximanova';
21 | src: url('../fonts/proximanova/ProximaNova-Bold-webfont.woff') format('woff');
22 | /* Pretty Modern Browsers */
23 | font-weight: 700; }
24 |
25 | @font-face {
26 | font-family: 'courierprime';
27 | src: url('../fonts/courierprime/courier-prime.ttf');
28 | /* Pretty Modern Browsers */
29 | font-weight: 200; }
30 |
31 | @font-face {
32 | font-family: 'courierprimesans';
33 | src: url('../fonts/courierprime/courier-prime-sans.ttf');
34 | /* Pretty Modern Browsers */
35 | font-weight: 200; }
36 |
37 | html {
38 | height: 100%;
39 | }
40 |
41 | body {
42 | display: flex;
43 | min-height: 100vh;
44 | min-width: 100vw;
45 | flex-direction: row;
46 | margin: 0;
47 | padding: 0;
48 | color: white;
49 | background: black;
50 | background-size: cover;
51 | font-family: 'proximanova';
52 | font-weight: 300;
53 | user-select: none;
54 | -webkit-user-select: none;
55 | cursor:default;
56 | -webkit-app-region: drag;
57 | }
58 |
59 | p, h1 {
60 | margin: 0;
61 | }
62 |
63 | #outline {
64 | height: 100vh;
65 | background: rgba(0,0,0,1);
66 | min-width: 200px;
67 | width: 200px;
68 | overflow-y: scroll;
69 | -webkit-app-region: no-drag;
70 |
71 | }
72 |
73 | #outline div {
74 | display: block;
75 | background: grey;
76 | border-top: 1px solid rgba(255,255,255,0.1);
77 | border-bottom: 1px solid rgba(0,0,0,0.1);
78 | padding: 5px;
79 | }
80 |
81 | #outline .node {
82 | opacity: 0.7;
83 | }
84 | #outline .node:hover {
85 | opacity: 1;
86 | }
87 |
88 | #outline-spacer {
89 | background: black !important;
90 | border: 0 !important;
91 | height: 25px;
92 | }
93 |
94 | #outline-gradient {
95 | position: fixed;
96 | top: 0px;
97 | width: 200px;
98 | background: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%,rgba(0,0,0,0) 100%) !important;
99 | border: 0 !important;
100 | height: 70px;
101 | z-index: 999;
102 | padding: 0 !important;
103 | pointer-events: none;
104 | -webkit-app-region: drag;
105 | }
106 |
107 |
108 | #outline .title {
109 | background: black;
110 | font-size: 22px;
111 | font-weight: 700;
112 | border: 0;
113 | }
114 |
115 | #outline .title .title-text {
116 | display: block;
117 | }
118 |
119 | #outline .title .author {
120 | font-size: 12px;
121 | opacity: 0.4;
122 | font-weight: 300;
123 | display: block;
124 |
125 | }
126 |
127 | #outline .section {
128 | background: black;
129 | font-size: 14px;
130 | font-weight: 700;
131 | border: 0;
132 | color: rgba(255,255,255,0.6);
133 | }
134 |
135 | #outline .scene {
136 | padding-top: 9px;
137 | padding-bottom: 9px;
138 | }
139 |
140 | #outline .scene .slugline {
141 | font-size: 10px;
142 | display: block;
143 | color: rgba(255,255,255,1);
144 | opacity: 0.6
145 | }
146 |
147 | #content {
148 | background: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0.3) 64%, rgba(0,0,0,0.6) 100%);
149 | padding: 50px;
150 | flex: 1 auto;
151 | display: flex;
152 | flex-direction: column;
153 | justify-content: flex-end;
154 | text-shadow: rgb(0,0,0) 0 1px 0;
155 | }
156 |
157 | #content .script-content {
158 | font-family: 'courierprime','Courier New';
159 | font-weight: 300;
160 | font-size: 23px;
161 | line-height: 35px;
162 | min-height: 250px;
163 | margin-bottom: 40px;
164 | display: flex;
165 | flex-direction: column;
166 | align-items: top;
167 | }
168 |
169 | @media only screen and (min-width:1200px){
170 |
171 |
172 | #content .script-content {
173 | align-items: center;
174 | }
175 |
176 |
177 | #content .script-content .content-line {
178 | width: 900px;
179 | }
180 |
181 | #content .script-content .slugline {
182 | width: 900px;
183 | }
184 |
185 |
186 |
187 | }
188 |
189 |
190 |
191 | #content .script-content .slugline {
192 | opacity: 0.8;
193 | font-size: 18px;
194 | margin-bottom: 20px;
195 | }
196 |
197 | #content .script-content .credit {
198 | opacity: 0.8;
199 | font-size: 18px;
200 | margin-bottom: 20px;
201 | line-height: 10px;
202 | }
203 |
204 |
205 | #controls {
206 | background-color: rgba(255,255,255,0.2);
207 | border-radius: 7px;
208 | height: 180px;
209 | display: flex;
210 | flex-direction: column;
211 | -webkit-app-region: no-drag;
212 | }
213 |
214 | #controls #scene-timeline {
215 | flex: 1;
216 | display: flex;
217 | flex-direction: row;
218 | align-items: center;
219 | align-content: center;
220 | justify-content: center;
221 |
222 | }
223 |
224 |
225 | #controls #scene-timeline #scene-timeline-content {
226 | flex: 1;
227 | display: flex;
228 | flex-direction: row;
229 | height: 100%;
230 | padding-top: 50px;
231 |
232 | }
233 |
234 | #controls #scene-timeline #scene-timeline-content div {
235 | background: rgba(255,255,255,0.2);
236 | display: block;
237 | font-size: 1px;
238 | height: 7px;
239 | border-radius: 5px;
240 | position: relative;
241 | box-shadow: 0 1px 0 rgba(0,0,0,0.3);
242 | }
243 |
244 | #controls #scene-timeline #scene-timeline-content div.action {
245 | background: rgba(255,255,255,0.4);
246 | }
247 |
248 | #controls #scene-timeline #scene-timeline-content div.character {
249 | background: rgba(255,255,255,0.6);
250 | }
251 |
252 | #controls #scene-timeline #scene-timeline-content div.primary {
253 | background: rgba(100,255,255,0.6);
254 | }
255 | #controls #scene-timeline #scene-timeline-content div.secondary {
256 | background: rgba(255,100,255,0.6);
257 | }
258 | #controls #scene-timeline #scene-timeline-content div.thirdary {
259 | background: rgba(255,255,100,0.6);
260 | }
261 | #controls #scene-timeline #scene-timeline-content div.fourthary {
262 | background: rgba(100,255,100,0.6);
263 | }
264 |
265 | #controls #scene-timeline #scene-timeline-content div.parenthetical {
266 | background: rgba(255,255,255,0.3);
267 | }
268 |
269 |
270 |
271 | #controls #scene-timeline #scene-timeline-content span {
272 | font-size: 10px;
273 | display: block;
274 | position: absolute;
275 | top: -12px;
276 | left: 4px;
277 | opacity: 0.5;
278 | width: calc(100% - 8px);
279 | overflow: hidden;
280 | white-space: nowrap;
281 | }
282 |
283 | #controls #scene-timeline #scene-timeline-content .character span {
284 | width: 100px;
285 | opacity: 0.7;
286 | }
287 |
288 | #controls #movie-timeline {
289 | height: 10px;
290 | display: flex;
291 | flex-direction: row;
292 | align-items: center;
293 | align-content: center;
294 | justify-content: center;
295 |
296 | }
297 |
298 | #controls #movie-timeline #movie-timeline-content {
299 | flex: 1;
300 | display: flex;
301 | flex-direction: row;
302 | height: 100%;
303 |
304 | }
305 |
306 | #controls #movie-timeline #movie-timeline-content div {
307 | background: rgba(255,255,255,0.2);
308 | display: block;
309 | font-size: 1px;
310 | height: 100%;
311 | box-shadow: 0 1px 0 rgba(0,0,0,0.3);
312 | }
313 |
314 | #controls #movie-timeline #movie-timeline-content div:nth-child(even) {
315 | background: rgba(255,255,255,0.4);
316 | }
317 |
318 | #controls #movie-timeline #movie-timeline-content div:nth-child(2) {
319 | border-radius: 5px 0 0 5px;
320 | }
321 |
322 | #controls #movie-timeline #movie-timeline-content div:last-child {
323 | border-radius: 0 5px 5px 0;
324 | }
325 |
326 | #controls div.marker-holder {
327 | position: absolute;
328 | background-color: red !important;
329 | width: 0px;
330 | height: 0px !important;
331 | }
332 |
333 | #controls #movie-timeline .marker {
334 | position: absolute;
335 | background-color: rgba(255,255,255,0.7) !important;
336 | width: 5px;
337 | height: 20px !important;
338 | top: -5px;
339 | left: 0px;
340 | z-index: 999;
341 | border-radius: 5px !important;
342 | box-shadow: 0 1px 3px rgba(0,0,0,0.7);
343 | transition: left 0.3s;
344 |
345 | }
346 |
347 | #controls #scene-timeline .marker {
348 | position: absolute;
349 | background-color: rgba(255,255,255,0.7) !important;
350 | width: 5px;
351 | height: 70px !important;
352 | top: -5px;
353 | left: 0px;
354 | z-index: 999;
355 | border-radius: 5px !important;
356 | box-shadow: 0 1px 3px rgba(0,0,0,0.7);
357 | transition: left 0.3s;
358 | }
359 |
360 |
361 |
362 | #controls .left-block, #controls .right-block {
363 | width: 40px;
364 | padding: 0 10px;
365 | font-size: 12px;
366 | display: flex;
367 | align-items: center;
368 | align-content: center;
369 | justify-content: center;
370 | opacity: 0.5;
371 | }
372 |
373 | #controls #playback {
374 | display: flex;
375 | flex-direction: row;
376 | align-items: center;
377 | align-content: center;
378 | justify-content: center;
379 | padding: 5px 10px;
380 | height: 50px;
381 | }
382 |
383 | #controls #playback #left-stats {
384 | font-size: 10px;
385 | width: 200px;
386 | opacity: 0.5;
387 | left: 15px;
388 | position: relative;
389 | }
390 |
391 | #controls #playback #right-stats {
392 | font-size: 10px;
393 | width: 200px;
394 | opacity: 0.5;
395 | text-align: right;
396 | right: 15px;
397 | position: relative;
398 | }
399 |
400 | #controls #playback #icons {
401 | flex: 1;
402 | display: flex;
403 | flex-direction: row;
404 | align-content: center;
405 | justify-content: center;
406 |
407 | }
408 |
409 | #controls #playback svg {
410 | fill: rgba(255,255,255,0.5);
411 | width: 25px;
412 | height: 40px;
413 | margin: 0 10px;
414 | filter: drop-shadow( 0 1px 1px rgba(0,0,0,0.4) );
415 | }
416 |
417 | #controls #playback svg:hover {
418 | fill: rgba(255,255,255,0.8);
419 | }
420 |
421 | #content img {
422 | width: 100%;
423 | margin-bottom: 100px;
424 | }
425 |
426 |
--------------------------------------------------------------------------------
/src/css/welcome.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'blenderpro';
3 | src: url('../fonts/blenderpro/blenderpro-thin-webfont.woff') format('woff');
4 | /* Pretty Modern Browsers */
5 | font-weight: 400; }
6 |
7 | @font-face {
8 | font-family: 'proximanova';
9 | src: url('../fonts/proximanova/ProximaNova-Reg-webfont.woff') format('woff');
10 | /* Pretty Modern Browsers */
11 | font-weight: 400; }
12 |
13 | @font-face {
14 | font-family: 'proximanova';
15 | src: url('../fonts/proximanova/ProximaNova-Light-webfont.woff') format('woff');
16 | /* Pretty Modern Browsers */
17 | font-weight: 300; }
18 |
19 | @font-face {
20 | font-family: 'proximanova';
21 | src: url('../fonts/proximanova/ProximaNova-Bold-webfont.woff') format('woff');
22 | /* Pretty Modern Browsers */
23 | font-weight: 700; }
24 |
25 | @font-face {
26 | font-family: 'proximanova';
27 | src: url('../fonts/proximanova/proximanova-black-webfont.woff') format('woff');
28 | /* Pretty Modern Browsers */
29 | font-weight: 900; }
30 |
31 | @font-face {
32 | font-family: 'courierprime';
33 | src: url('../fonts/courierprime/courier-prime.ttf');
34 | /* Pretty Modern Browsers */
35 | font-weight: 200; }
36 |
37 | @font-face {
38 | font-family: 'courierprimesans';
39 | src: url('../fonts/courierprime/courier-prime-sans.ttf');
40 | /* Pretty Modern Browsers */
41 | font-weight: 200; }
42 |
43 | body {
44 | display: flex;
45 | flex-direction: row;
46 | margin: 0;
47 | padding: 0;
48 | color: white;
49 | background: black;
50 | background-size: cover;
51 | font-family: 'proximanova';
52 | font-weight: 300;
53 | overflow: hidden;
54 | }
55 |
56 | body {
57 | background: rgb(229,229,229);
58 | user-select: none;
59 | -webkit-user-select: none;
60 | cursor:default;
61 | }
62 |
63 | iframe {
64 | height: 100vh;
65 | }
66 |
67 | #close-button {
68 | position: absolute;
69 | top: 0px;
70 | left: 0px;
71 | padding: 20px;
72 | opacity: 0.5;
73 | }
74 |
75 | #close-button:hover {
76 | opacity: 0.8;
77 | }
78 |
79 | .title {
80 | margin: 40px 0 0 40px;
81 | display: flex;
82 | flex-direction: row;
83 | color: #3A3A3A;
84 | }
85 |
86 | .logo img {
87 | width: 130px;
88 | }
89 |
90 | .title .text {
91 | padding-left: 25px;
92 | display: flex;
93 | flex-direction: column;
94 | align-items: left;
95 | top: 30px;
96 | position: relative;
97 | }
98 |
99 | .title .text div {
100 | font-size: 15px;
101 | margin: 0;
102 | opacity: 0.5;
103 | }
104 |
105 | .title .text h1 {
106 | font-size: 48px;
107 | margin: -6px 0;
108 | font-weight: 900;
109 | }
110 |
111 | .button {
112 | background: rgb(200,200,200);
113 | padding: 20px 50px;
114 | position: absolute;
115 | margin: 20px;
116 | bottom: 0;
117 | border-radius: 10px;
118 | font-weight: 400;
119 | font-size: 18px;
120 | transition: 300ms background;
121 | }
122 |
123 | .button.blue {
124 | background: #46B9EB;
125 | }
126 |
127 | .button.blue:hover {
128 | background: #59C4EA;
129 | }
130 |
131 | .button.grey:hover {
132 | background: rgb(210,210,210);
133 | }
134 |
135 |
136 | .button.left {
137 | left: 300px;
138 | }
139 |
140 | .button.right {
141 | right: 0;
142 | }
143 |
144 | .recent {
145 | position: relative;
146 | display: block;
147 | font-size: 15px;
148 | opacity: 0.5;
149 | color: #3A3A3A;
150 | margin: 15px 40px 5px 40px;
151 | }
152 |
153 | #recent {
154 | height: 275px;
155 | overflow: scroll;
156 | }
157 |
158 | .recent-item {
159 | background: rgba(100,100,100,0.05);
160 | display: flex;
161 | flex-direction: row;
162 | width: 600px;
163 | padding: 10px 0;
164 | color: rgba(0,0,0,0.4);
165 | font-size: 13px;
166 | }
167 |
168 | .recent-item:nth-child(even) {
169 | background: rgba(100,100,100,0.0);
170 | }
171 |
172 | .recent-item:hover {
173 | background: #46B9EB;
174 | color: rgba(255,255,255,0.6);
175 |
176 | }
177 |
178 | .recent-item:hover h2 {
179 | color: rgba(255,255,255,1);
180 |
181 | }
182 |
183 | .recent-item img {
184 | width: 40px;
185 | height: 40px;
186 | padding-left: 40px;
187 | padding-right: 15px;
188 | position: relative;
189 | top: 3px;
190 |
191 | }
192 |
193 | h2 {
194 | color: rgba(0,0,0,0.8);
195 | margin: 0;
196 | font-weight: 600;
197 | font-size: 23px;
198 |
199 | }
200 |
201 | #welcome {
202 | color: black;
203 | margin: 0 40px;
204 | font-size: 21px;
205 | font-weight: 100;
206 | opacity: 0.8;
207 | }
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-bold-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-bold-webfont.eot
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-bold-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-bold-webfont.ttf
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-bold-webfont.woff
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-book-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-book-webfont.eot
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-book-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-book-webfont.ttf
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-book-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-book-webfont.woff
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-heavy-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-heavy-webfont.eot
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-heavy-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-heavy-webfont.ttf
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-heavy-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-heavy-webfont.woff
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-medium-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-medium-webfont.eot
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-medium-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-medium-webfont.ttf
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-medium-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-medium-webfont.woff
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-thin-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-thin-webfont.eot
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-thin-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-thin-webfont.ttf
--------------------------------------------------------------------------------
/src/fonts/blenderpro/blenderpro-thin-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/blenderpro/blenderpro-thin-webfont.woff
--------------------------------------------------------------------------------
/src/fonts/courierprime/courier-prime-sans.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/courierprime/courier-prime-sans.ttf
--------------------------------------------------------------------------------
/src/fonts/courierprime/courier-prime.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/courierprime/courier-prime.ttf
--------------------------------------------------------------------------------
/src/fonts/pcbvector/PCBVector-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/pcbvector/PCBVector-Regular.otf
--------------------------------------------------------------------------------
/src/fonts/proximanova/ProximaNova-Bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/proximanova/ProximaNova-Bold-webfont.woff
--------------------------------------------------------------------------------
/src/fonts/proximanova/ProximaNova-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/proximanova/ProximaNova-Bold.ttf
--------------------------------------------------------------------------------
/src/fonts/proximanova/ProximaNova-Light-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/proximanova/ProximaNova-Light-webfont.woff
--------------------------------------------------------------------------------
/src/fonts/proximanova/ProximaNova-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/proximanova/ProximaNova-Light.ttf
--------------------------------------------------------------------------------
/src/fonts/proximanova/ProximaNova-Reg-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/proximanova/ProximaNova-Reg-webfont.woff
--------------------------------------------------------------------------------
/src/fonts/proximanova/ProximaNova-Reg.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/proximanova/ProximaNova-Reg.ttf
--------------------------------------------------------------------------------
/src/fonts/proximanova/ProximaNova-Sbold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/proximanova/ProximaNova-Sbold.ttf
--------------------------------------------------------------------------------
/src/fonts/proximanova/ProximaNova-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/proximanova/ProximaNova-Thin.ttf
--------------------------------------------------------------------------------
/src/fonts/proximanova/proximanova-black-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/fonts/proximanova/proximanova-black-webfont.woff
--------------------------------------------------------------------------------
/src/img/button-loop.svg:
--------------------------------------------------------------------------------
1 | button-loop
--------------------------------------------------------------------------------
/src/img/button-loop1.svg:
--------------------------------------------------------------------------------
1 | button-loop1
--------------------------------------------------------------------------------
/src/img/button-next.svg:
--------------------------------------------------------------------------------
1 | button-next-01
--------------------------------------------------------------------------------
/src/img/button-nextscene.svg:
--------------------------------------------------------------------------------
1 | button-nextscene
--------------------------------------------------------------------------------
/src/img/button-pause.svg:
--------------------------------------------------------------------------------
1 | button-pause
--------------------------------------------------------------------------------
/src/img/button-play.svg:
--------------------------------------------------------------------------------
1 | button-play
--------------------------------------------------------------------------------
/src/img/button-prev.svg:
--------------------------------------------------------------------------------
1 | button-previous
--------------------------------------------------------------------------------
/src/img/button-prevscene.svg:
--------------------------------------------------------------------------------
1 | button-prevscene
--------------------------------------------------------------------------------
/src/img/button-speaker-on.svg:
--------------------------------------------------------------------------------
1 | button-speaker-on
--------------------------------------------------------------------------------
/src/img/button-speaker.svg:
--------------------------------------------------------------------------------
1 | button-speaker
--------------------------------------------------------------------------------
/src/img/fileicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/img/fileicon.png
--------------------------------------------------------------------------------
/src/img/logoicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/img/logoicon.png
--------------------------------------------------------------------------------
/src/img/outliner-display-icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderunit/script-visualizer/65abdff58f95cfc7e0b0ddb965745e1942669e9f/src/img/outliner-display-icon.icns
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Outline Display
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
77 |
78 |
79 |
80 |
83 |
--------------------------------------------------------------------------------
/src/js/app.js:
--------------------------------------------------------------------------------
1 | /*
2 | TODO:
3 | menu checks for speaking, auto play on update, loop
4 |
5 | go through and extract more stats out of the script
6 |
7 | draw sections in timelines
8 |
9 | render times
10 |
11 | render stats
12 |
13 | advance marker to correct place
14 |
15 | drag and drop file
16 |
17 | hook up icons
18 |
19 | -=-=-=-
20 |
21 | time of day overlays
22 | scroll to right scene if not visible
23 | welcome screen
24 | stats window
25 | save settings to prefs
26 | */
27 |
28 | var remote = nodeRequire('electron').remote
29 | var ipc = nodeRequire('electron').ipcRenderer
30 | const {shell} = nodeRequire('electron')
31 |
32 | const FountainDataParser = nodeRequire('./js/fountain-data-parser')
33 | const Color = nodeRequire('../node_modules/color-js/color')
34 | const moment = nodeRequire('moment')
35 |
36 | let scriptData
37 | let locations
38 | let characters
39 |
40 | let currentNode = 0
41 | let currentSceneNode = 0
42 | let playbackMode = false
43 | let playheadTimer
44 | let updateTimer
45 | let frameDuration
46 |
47 | let speakingMode = true
48 | let utter = new SpeechSynthesisUtterance()
49 | let startSpeakingTime
50 | let delayTime = 0
51 | let currentSpeaker = ''
52 |
53 | let loopMode = 0
54 | let playWhenUpdate = true
55 |
56 | let totalWordCount
57 |
58 | // var currentNode = 0
59 | // var playbackMode = false
60 | // var playbackType = 0
61 | var frameTimer
62 | // var updateTimer
63 | // var imageTimer
64 |
65 | // var colorList = ["6dcff6", "f69679", "00bff3", "f26c4f", "fff799", "c4df9b", "f49ac1", "8393ca", "82ca9c", "f5989d", "605ca8", "a3d39c", "fbaf5d", "fff568", "3cb878", "fdc689", "5674b9", "8781bd", "7da7d9", "a186be", "acd373", "7accc8", "1cbbb4", "f9ad81", "bd8cbf", "7cc576", "f68e56", "448ccb"];
66 |
67 |
68 | $(document).ready(function() {
69 | // scriptData = remote.getGlobal('sharedObj').scriptData
70 | // locations = remote.getGlobal('sharedObj').locations
71 | // characters = remote.getGlobal('sharedObj').characters
72 | // assignColors()
73 |
74 | // currentNode = 0
75 | // advanceFrame(0)
76 | // //togglePlayback()
77 |
78 | // renderOutline()
79 | // renderTimeline()
80 | // renderSceneTimeline()
81 | // renderFrame()
82 | // controls
83 | reloadDocument()
84 | //renderTimeline()
85 | //advanceFrame(0)
86 | })
87 |
88 | let stopPlaying = () => {
89 | clearTimeout(frameTimer)
90 | playbackMode = false
91 | utter.onend = null
92 | ipc.send('resumeSleep')
93 | clearTimeout(updateTimer)
94 | speechSynthesis.cancel()
95 | }
96 |
97 | let togglePlayback = function() {
98 | playbackMode = !playbackMode
99 | if (playbackMode) {
100 | // prevent from sleeping
101 | ipc.send('preventSleep')
102 |
103 | // begin playing
104 | if (speakingMode) {
105 | playSpeechAdvance(true)
106 | } else {
107 | playAdvance(true)
108 | }
109 | } else {
110 | // stop playing
111 | stopPlaying()
112 | }
113 | }
114 |
115 | let playAdvance = function(first) {
116 | //clearTimeout(playheadTimer)
117 | clearTimeout(frameTimer)
118 | if (!first) {
119 | advanceFrame(1)
120 | }
121 |
122 | frameTimer = setTimeout(playAdvance, frameDuration)
123 | }
124 |
125 | let playSpeechAdvance = function(first) {
126 | //clearTimeout(frameTimer)
127 | clearTimeout(updateTimer)
128 |
129 | if (playbackMode) {
130 | if (!first) {
131 | advanceFrame(1)
132 | } else {
133 | advanceFrame(0)
134 | }
135 |
136 | utter.pitch = 0.65;
137 | utter.rate = 1.1;
138 |
139 | switch (scriptData[currentNode].type) {
140 | case 'title':
141 | let string = []
142 | string.push(scriptData[currentNode].text.toLowerCase().replace(/<\/?[^>]+(>|$)/g, "") + '. ')
143 | if (scriptData[currentNode].credit) {
144 | string.push(scriptData[currentNode].credit + ' ')
145 | }
146 | if (scriptData[currentNode].author) {
147 | string.push(scriptData[currentNode].author + ' ')
148 | }
149 | if (scriptData[currentNode].authors) {
150 | string.push(scriptData[currentNode].authors + ' ')
151 | }
152 |
153 | utter.text = string.join('')
154 | delayTime = 2000
155 | break
156 | case 'section':
157 | utter.text = scriptData[currentNode].text.toLowerCase()
158 | delayTime = 2000
159 | break
160 | case 'scene':
161 | if (currentSceneNode > -1) {
162 | switch (scriptData[currentNode]['script'][currentSceneNode].type) {
163 | case 'scene_padding':
164 | utter.text = ''
165 | playSpeechAdvance()
166 | break
167 | case 'scene_heading':
168 | utter.text = scriptData[currentNode]['script'][currentSceneNode].text.toLowerCase().replace("mr.", "mister").replace("int. ", "interior, ").replace("ext. ", "exterior, ")
169 | currentSpeaker = ''
170 | delayTime = 1000
171 | break
172 | case 'action':
173 | utter.text = scriptData[currentNode]['script'][currentSceneNode].text.replace(/<\/?[^>]+(>|$)/g, "")
174 | currentSpeaker = ''
175 | delayTime = 500
176 | break
177 | case 'parenthetical':
178 | case 'dialogue':
179 | let string = []
180 |
181 | if (scriptData[currentNode].type == 'dialogue') {
182 | delayTime = 1000
183 | } else {
184 | delayTime = 500
185 | }
186 | if (currentSpeaker !== scriptData[currentNode]['script'][currentSceneNode].character) {
187 | str = scriptData[currentNode]['script'][currentSceneNode].character.toLowerCase().replace("mr.", "mister").replace("(o.s.)", ", offscreen, ").replace("(v.o.)", ", voiceover, ").replace("(cont'd)", ", continued, ").replace("(cont’d)", ", continued, ") + ', '
188 | string.push(str)
189 | currentSpeaker = scriptData[currentNode]['script'][currentSceneNode].character
190 | }
191 | string.push(scriptData[currentNode]['script'][currentSceneNode].text.replace(/<\/?[^>]+(>|$)/g, ""))
192 | utter.text = string.join('')
193 | break
194 | case 'transition':
195 | utter.text = scriptData[currentNode]['script'][currentSceneNode].text.replace(/<\/?[^>]+(>|$)/g, "")
196 | break
197 | case 'section':
198 | utter.text = ''
199 | playSpeechAdvance()
200 | break
201 | }
202 | }
203 | break
204 | }
205 |
206 | utter.onend = function(event) {
207 | //console.log(((new Date().getTime())-startSpeakingTime)/utter.text.length)
208 | speechSynthesis.cancel()
209 | if (playbackMode) {
210 | setTimeout(playSpeechAdvance, delayTime)
211 | }
212 | }
213 |
214 | speechSynthesis.speak(utter);
215 | startSpeakingTime = new Date().getTime()
216 | }
217 | }
218 |
219 | let gotoFrame = (nodeNumber) => {
220 | currentNode = Number(nodeNumber)
221 | currentSceneNode = 0
222 | advanceFrame(0)
223 | }
224 |
225 | let advanceFrame = function(direction, keyboard) {
226 | let differentScene = false
227 | switch (scriptData[currentNode]['type']) {
228 | case 'title':
229 | currentNode += direction
230 | break
231 | case 'section':
232 | currentNode += direction
233 | break
234 | case 'scene':
235 | currentSceneNode += direction
236 | if (currentSceneNode < 0) {
237 | currentNode = Math.max(0, currentNode-1)
238 | if (scriptData[currentNode]['script']) {
239 | currentSceneNode = scriptData[currentNode]['script'].length -1
240 | } else {
241 | currentSceneNode = 0
242 | }
243 | differentScene = true
244 | }
245 | if (currentSceneNode > (scriptData[currentNode]['script'].length -1)) {
246 | // if loop scene then repeat
247 | if (loopMode == 2) {
248 | if (speakingMode) {
249 | utter.text = ". . . End of scene."
250 | speechSynthesis.speak(utter);
251 | delayTime = 5000
252 | currentSceneNode = -1
253 | } else {
254 | currentSceneNode = 0
255 | }
256 | } else {
257 | // go to next node
258 | currentNode++
259 | currentSceneNode = 0
260 | }
261 | }
262 | break
263 | }
264 | currentNode = Math.max(0, currentNode)
265 |
266 | let currentSceneTime = 0
267 | let totalSceneTime = 0
268 | let currentMovieTime = 0
269 | let totalMovieTime = 0
270 | let currentSceneNumber = 0
271 | let currentPageNumber = 0
272 | let currentPercentage = 0
273 | let currentSceneDuration = 0
274 | let currentScenePageCount = 0
275 | let currentSceneWordCount = 0
276 | let totalPageCount = 0
277 | let totalSceneCount = 0
278 |
279 | switch (scriptData[currentNode]['type']) {
280 | case 'title':
281 | renderSceneTimeline()
282 | $('body').css('background', 'black')
283 | $("#content .slugline").html('')
284 | let html = []
285 | html.push(scriptData[currentNode].text + ' ')
286 | if (scriptData[currentNode].credit) {
287 | html.push('' + scriptData[currentNode].credit + ' ')
288 | }
289 | if (scriptData[currentNode].author) {
290 | html.push('' + scriptData[currentNode].author + ' ')
291 | }
292 | if (scriptData[currentNode].authors) {
293 | html.push('' + scriptData[currentNode].authors + ' ')
294 | }
295 | $("#content .content-line").html(html.join(''))
296 | frameDuration = scriptData[currentNode].duration
297 | $('#controls #movie-timeline .marker').css('left', 0)
298 |
299 | currentMovieTime = scriptData[currentNode].time
300 | currentPageNumber = scriptData[currentNode].page
301 | break
302 | case 'section':
303 | renderSceneTimeline()
304 | $('body').css('background', 'black')
305 | $("#content .slugline").html('')
306 | $("#content .content-line").html(scriptData[currentNode].text)
307 | frameDuration = scriptData[currentNode].duration
308 | currentMovieTime = scriptData[currentNode].time
309 | currentPageNumber = scriptData[currentNode].page
310 | break
311 | case 'scene':
312 | if (currentSceneNode == 0 || differentScene) {
313 | renderSceneTimeline()
314 | if (scriptData[currentNode].slugline) {
315 | $("#content .slugline").html(scriptData[currentNode].scene_number + ': ' + scriptData[currentNode].slugline)
316 | } else {
317 | $("#content .slugline").html('')
318 | }
319 | $("#content .content-line").html('')
320 | $('body').css('background', getSceneColor(scriptData[currentNode].slugline))
321 |
322 | // draw marker
323 | let percentage = (scriptData[currentNode].time - 2000)/(scriptData[scriptData.length-1].time+scriptData[scriptData.length-1].duration - 2000)
324 | let width = $('#controls #scene-timeline #scene-timeline-content').width()
325 | $('#controls #movie-timeline .marker').css('left', width*percentage)
326 |
327 |
328 |
329 | //console.log("-=-=-=-=- NEW SCENE -=-=-=-=-=-")
330 | }
331 | if (currentSceneNode > -1) {
332 | switch(scriptData[currentNode]['script'][currentSceneNode].type) {
333 | case 'action':
334 | $("#content .content-line").html(scriptData[currentNode]['script'][currentSceneNode].text)
335 | break
336 | case 'dialogue':
337 | case 'parenthetical':
338 | let html = []
339 | html.push(scriptData[currentNode]['script'][currentSceneNode].character + ': ')
340 | html.push(scriptData[currentNode]['script'][currentSceneNode].text)
341 | $("#content .content-line").html(html.join(''))
342 | break
343 | case 'transition':
344 | $("#content .content-line").html(scriptData[currentNode]['script'][currentSceneNode].text)
345 | break
346 | case 'scene_heading':
347 | if (keyboard) {
348 | advanceFrame(direction)
349 | }
350 | break
351 | }
352 |
353 | // draw marker
354 | let percentage = (scriptData[currentNode]['script'][currentSceneNode].time-(scriptData[currentNode].time-2000))/scriptData[currentNode].duration
355 | let width = $('#controls #scene-timeline #scene-timeline-content').width()
356 | $('#controls #scene-timeline .marker').css('left', width*percentage)
357 |
358 | frameDuration = scriptData[currentNode]['script'][currentSceneNode].duration
359 |
360 | }
361 |
362 | currentSceneTime = scriptData[currentNode]['script'][currentSceneNode].time-scriptData[currentNode].time
363 | totalSceneTime = scriptData[currentNode].duration
364 |
365 | currentMovieTime = scriptData[currentNode]['script'][currentSceneNode].time
366 | currentPageNumber = scriptData[currentNode]['script'][currentSceneNode].page
367 |
368 | break
369 | }
370 |
371 | switch (scriptData[scriptData.length-1].type) {
372 | case 'title':
373 | case 'section':
374 | totalPageCount = scriptData[scriptData.length-1].page
375 | totalMovieTime = scriptData[scriptData.length-1].time + scriptData[scriptData.length-1].duration
376 | break
377 | case 'scene':
378 | let lastNode = scriptData[scriptData.length-1]['script'][scriptData[scriptData.length-1]['script'].length-1]
379 | totalPageCount = lastNode.page
380 | totalMovieTime = lastNode.time + lastNode.duration
381 | break
382 | }
383 |
384 | currentSceneNumber = scriptData[currentNode].scene_number || 0
385 | totalSceneCount = scriptData[scriptData.length-1].scene_number
386 |
387 | currentPercentage = Math.round((currentMovieTime/totalMovieTime)*100)
388 |
389 | currentScenePageCount = Math.ceil((totalSceneTime/totalMovieTime)*totalPageCount*10/5)*5/10
390 | currentSceneWordCount = scriptData[currentNode].word_count || 0
391 |
392 | $('#scene-timeline .left-block').html(msToTime(currentSceneTime))
393 | $('#scene-timeline .right-block').html(msToTime(totalSceneTime))
394 |
395 | $('#movie-timeline .left-block').html(msToTime(currentMovieTime))
396 | $('#movie-timeline .right-block').html(msToTime(totalMovieTime))
397 |
398 | $('#playback #left-stats').html('SCENE ' + currentSceneNumber + ', PAGE ' + currentPageNumber + ', ' + currentPercentage + '% ' + msToTime(totalSceneTime) + ' MINS, ' + currentScenePageCount + ' PAGES, ' + currentSceneWordCount + ' WORDS')
399 |
400 | $('#playback #right-stats').html(totalSceneCount + ' SCENES, ' + totalPageCount + ' PAGES ' + totalWordCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' WORDS')
401 |
402 |
403 |
404 |
405 |
406 |
407 | }
408 |
409 | let assignColors = function () {
410 | let angle = 0
411 | for (var node of locations) {
412 | angle += (360/4)+7
413 | c = Color("#00FF00").shiftHue(angle).desaturateByRatio(.1).darkenByRatio(0.65).blend(Color('white'), 0.4).saturateByRatio(.7)
414 | node.push(c.toCSS())
415 | }
416 | }
417 |
418 | let getSceneColor = function (sceneString) {
419 | if (sceneString) {
420 | let location = sceneString.split(' - ')
421 | if (location.length > 1) {
422 | location.pop()
423 | }
424 | location = location.join(' - ')
425 | return (locations.find(function (node) { return node[0] == location })[2])
426 | }
427 | return ('black')
428 | }
429 |
430 | let getCharacterOrder = function (characterString) {
431 | let character = characterString.split('(')[0].split(' AND ')[0].trim()
432 | return characters.indexOf(characters.find(function (node) { return node[0] == character }))
433 | }
434 |
435 | let renderSceneTimeline = () => {
436 | let html = []
437 | let currentCharacter
438 | let characterList = []
439 |
440 | html.push('')
441 | switch (scriptData[currentNode].type) {
442 | case 'title':
443 | case 'section':
444 | html.push('
')
445 | break
446 | case 'scene':
447 | for (var node of scriptData[currentNode]['script'] ) {
448 | switch (node.type) {
449 | case 'scene_padding':
450 | html.push('
')
451 | break
452 | case 'action':
453 | currentCharacter = ''
454 | html.push('' + node.text + '
')
455 | break
456 | case 'dialogue':
457 | case 'parenthetical':
458 | let character = node.character.split('(')[0].split(' AND ')[0].trim()
459 | if (characterList.indexOf(character) == -1) {
460 | characterList.push(character)
461 | }
462 | let index = characterList.indexOf(character)
463 | let importanceIndex = getCharacterOrder(character)
464 |
465 | html.push('')
487 |
488 | if (currentCharacter !== character) {
489 | html.push(character)
490 | }
491 | currentCharacter = character
492 |
493 | html.push('
')
494 |
495 | break
496 | case 'transition':
497 | currentCharacter = ''
498 | html.push('
')
499 | break
500 | }
501 | }
502 | break
503 | }
504 | $('#controls #scene-timeline #scene-timeline-content').html(html.join(''))
505 | }
506 |
507 | let renderTimeline = () => {
508 | let html = []
509 | html.push('')
510 | for (var node of scriptData ) {
511 | if (node.type !== 'section') {
512 | html.push('
')
513 | }
514 | }
515 | $('#controls #movie-timeline #movie-timeline-content').html(html.join(''))
516 | }
517 |
518 | let renderOutline = function() {
519 | let html = []
520 | let angle = 0
521 | let i = 0
522 | html.push('
')
523 | for (var node of scriptData ) {
524 | switch (node.type) {
525 | case 'title':
526 | html.push('' + node.text + ' ')
527 | if (node.author) {
528 | html.push('' + node.author + ' ')
529 | }
530 | if (node.authors) {
531 | html.push('' + node.authors + ' ')
532 | }
533 | html.push('
')
534 | break
535 | case 'section':
536 | html.push('' + node.text + '
')
537 | break
538 | case 'scene':
539 | if (node.slugline) {
540 | html.push('')
541 | }
542 | if (node.slugline) {
543 | html.push('' + node.scene_number + '. ' + node.slugline + ' ')
544 | }
545 | if (node.synopsis) {
546 | html.push('' + node.synopsis + ' ')
547 | }
548 | // time, duration, page, word_count
549 | html.push('
')
550 | break
551 | }
552 | i++
553 | }
554 |
555 | $('#outline').html(html.join(''))
556 |
557 | $("#outline .node").unbind('click').click((e)=>{
558 | stopPlaying()
559 | gotoFrame(e.currentTarget.dataset.node)
560 | })
561 | }
562 |
563 |
564 | let msToTime = (s)=> {
565 | if(!s) s = 0
566 | s = Math.max(0, s)
567 | function addZ(n) {
568 | return (n<10? '0':'') + n;
569 | }
570 | var ms = (s % 1000);
571 | s = (s - ms) / 1000;
572 | var secs = s % 60;
573 | s = (s - secs) / 60;
574 | var mins = s % 60;
575 | var hrs = (s - mins) / 60;
576 | if (hrs) {
577 | return hrs + ':' + addZ(mins) + ':' + addZ(secs);
578 | } else {
579 | return mins + ':' + addZ(secs); //+ '.' + ms.toString().substring(0,1);
580 | }
581 | };
582 |
583 | window.onkeydown = function(e) {
584 | if(e.keyCode == 32 && e.target == document.body) {
585 | togglePlayback()
586 | e.preventDefault()
587 | //return false;
588 | }
589 | }
590 | // $('.status').css('width', '0%')
591 | // var outlineData = remote.getGlobal('sharedObj').outlineData;
592 |
593 | // currentNode = Math.max(currentNode + direction,0);
594 |
595 | // if (outlineData[currentNode].type == 'section') {
596 | // currentNode = Math.max(currentNode + direction,1);
597 | // }
598 |
599 | // remote.getGlobal('sharedObj')['currentNode'] = currentNode
600 |
601 | // var sceneCount = 0;
602 | // var currentScene = 0;
603 | // var currentTime = 0;
604 | // var totalTime = 0;
605 | // var currentSection = '';
606 |
607 |
608 | // for (var i = 0; i < outlineData.length; i++) {
609 | // if (outlineData[i].type !== 'section') {
610 | // sceneCount++
611 | // if (i == currentNode) {
612 | // currentScene = sceneCount;
613 | // }
614 | // if (outlineData[i].timing) {
615 | // totalTime += Number(outlineData[i].timing)
616 | // } else {
617 | // totalTime += 90;
618 | // }
619 | // if (i < currentNode) {
620 | // currentTime = totalTime;
621 | // }
622 |
623 | // } else {
624 | // if (i < currentNode) {
625 | // currentSection = outlineData[i].text;
626 | // }
627 | // }
628 | // }
629 |
630 | // $('body').css('background', 'linear-gradient(#' + colorList[(currentNode+1) % colorList.length] + ', #' + colorList[Math.max(currentNode-1,0) % colorList.length] + ')')
631 |
632 | // $(".scene.current").removeClass('current')
633 | // $(".scene[data-id='" + currentNode + "']").addClass('current');
634 |
635 |
636 | // $('#scenemarker').text(currentSection + ': ' + currentScene + ' / ' + sceneCount + ' ' + msToTime(currentTime*1000) + ' / ' + msToTime(totalTime*1000))
637 |
638 |
639 | // clearTimeout(imageTimer)
640 | // if (remote.getGlobal('sharedObj').outlineData[currentNode].image.length > 0) {
641 | // currentImage = 0
642 | // if (playbackMode) {
643 | // imageInterval = Math.max(remote.getGlobal('sharedObj').outlineData[currentNode].description.length*69+600,2500)/remote.getGlobal('sharedObj').outlineData[currentNode].image.length
644 | // imageTimer = setTimeout(advanceImage, imageInterval)
645 | // }
646 | // $("#posterimage").attr("src",remote.getGlobal('sharedObj').documentPath + "/" + remote.getGlobal('sharedObj').outlineData[currentNode].image[0]);
647 | // $('#posterimage').show()
648 | // } else {
649 | // $('#posterimage').hide()
650 | // }
651 |
652 |
653 | // if (remote.getGlobal('sharedObj').outlineData[currentNode].text) {
654 | // $('#caption').text(remote.getGlobal('sharedObj').outlineData[currentNode].text)
655 | // } else {
656 | // $('#caption').text('')
657 | // }
658 | // if (remote.getGlobal('sharedObj').outlineData[currentNode].description) {
659 | // $('#description').html(remote.getGlobal('sharedObj').outlineData[currentNode].description)
660 | // } else {
661 | // $('#description').text('')
662 | // }
663 | // if (remote.getGlobal('sharedObj').outlineData[currentNode].slugline) {
664 | // $('#slugline').text(remote.getGlobal('sharedObj').outlineData[currentNode].slugline)
665 | // } else {
666 | // $('#slugline').text('')
667 | // }
668 | // if (remote.getGlobal('sharedObj').outlineData[currentNode].timing) {
669 | // $('#timing').text(remote.getGlobal('sharedObj').outlineData[currentNode].timing)
670 | // } else {
671 | // $('#timing').text('')
672 | // }
673 |
674 |
675 |
676 | // }
677 |
678 |
679 |
680 |
681 |
682 | // var playAdvance = function(first) {
683 | // clearTimeout(frameTimer)
684 | // clearTimeout(updateTimer);
685 | // if(!first){
686 | // advanceFrame(1);
687 | // }
688 | // var outlineData = remote.getGlobal('sharedObj').outlineData;
689 |
690 | // if (playbackType < 2) {
691 | // var mult
692 | // if (playbackType == 1) {
693 | // mult = 4;
694 | // } else {
695 | // mult = 1;
696 | // }
697 | // if (outlineData[currentNode].timing) {
698 | // timing = Number(outlineData[currentNode].timing)*1000/mult
699 | // } else {
700 | // timing = 90*1000/mult
701 | // }
702 | // } else {
703 | // timing = 2000;
704 | // }
705 |
706 |
707 | // startSceneTime = new Date().getTime()
708 | // endSceneTime = startSceneTime + timing
709 |
710 | // frameTimer = setTimeout(playAdvance, timing)
711 | // updateTimer = setTimeout(updateTime, 20)
712 | // };
713 |
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 | // var updateTime = function() {
723 | // clearTimeout(updateTimer);
724 |
725 |
726 | // var per = ((new Date().getTime()-startSceneTime)/(endSceneTime-startSceneTime)*100).toFixed(2)
727 |
728 | // $('.status').css('width', String(per) + '%')
729 |
730 |
731 | // if (playbackMode) {
732 | // updateTimer = setTimeout(updateTime, 20)
733 | // } else {
734 | // }
735 | // }
736 |
737 | var reloadDocument = (update) => {
738 |
739 | scriptData = remote.getGlobal('sharedObj').scriptData
740 | locations = remote.getGlobal('sharedObj').locations
741 | characters = remote.getGlobal('sharedObj').characters
742 |
743 | totalWordCount = 0
744 | for (var node of scriptData) {
745 | if (node.word_count) totalWordCount += node.word_count
746 | }
747 |
748 | assignColors()
749 | if (update) {
750 | currentSceneNode = 0
751 | renderOutline()
752 | renderTimeline()
753 | renderSceneTimeline()
754 | togglePlayback()
755 |
756 | } else {
757 | currentNode = 0
758 | currentSceneNode = 0
759 | renderOutline()
760 | renderTimeline()
761 | renderSceneTimeline()
762 | advanceFrame(0)
763 | }
764 | }
765 |
766 | ipc.on('reload', (event, update, updatedScene) => {
767 | stopPlaying()
768 | currentSceneNode = 0
769 | if (updatedScene) {
770 | currentNode = updatedScene
771 | }
772 |
773 | if (update) {
774 | setTimeout(()=>{reloadDocument(true); advanceFrame(0)}, 1000)
775 | } else {
776 | advanceFrame(0)
777 | reloadDocument()
778 | }
779 |
780 | })
781 |
782 | ipc.on('togglePlayback', (event, arg) => {
783 | togglePlayback()
784 | })
785 |
786 |
787 | // document.ondragover = document.ondrop = function(ev) {
788 | // ev.preventDefault()
789 | // }
790 |
791 | // document.body.ondrop = function(ev) {
792 | // console.log(ev.dataTransfer.files[0].path)
793 | // // if image
794 | // // copy to document path
795 | // // save new outline text
796 |
797 | // // if text
798 | // // open the text file
799 | // // save pref for the last opened file
800 |
801 | // ev.preventDefault()
802 | // }
803 |
804 | // function msToTime(s) {
805 | // function addZ(n) {
806 | // return (n<10? '0':'') + n;
807 | // }
808 | // var ms = (s % 1000);
809 | // s = (s - ms) / 1000;
810 | // var secs = s % 60;
811 | // s = (s - secs) / 60;
812 | // var mins = s % 60;
813 | // var hrs = (s - mins) / 60;
814 | // if (hrs) {
815 | // return hrs + ':' + addZ(mins) + ':' + addZ(secs);
816 | // } else {
817 | // return mins + ':' + addZ(secs); //+ '.' + ms.toString().substring(0,1);
818 | // }
819 | // };
820 |
821 |
822 | // from external commands
823 |
824 | ipc.on('goPrevious', (event, arg)=> {
825 | stopPlaying()
826 | advanceFrame(-1, true)
827 | })
828 |
829 | ipc.on('goPreviousScene', (event, arg)=> {
830 | stopPlaying()
831 | if (currentSceneNode > 0) {
832 | currentSceneNode = 0
833 | } else {
834 | currentNode = Math.max(currentNode-1, 0)
835 | currentSceneNode = 0
836 | }
837 | advanceFrame(0, true)
838 | })
839 |
840 | ipc.on('goNext', (event, arg)=> {
841 | stopPlaying()
842 | advanceFrame(1, true)
843 | })
844 |
845 | ipc.on('goNextScene', (event, arg)=> {
846 | stopPlaying()
847 | currentNode++
848 | currentSceneNode = 0
849 | advanceFrame(0, true)
850 | })
851 |
852 | ipc.on('goBeginning', (event, arg)=> {
853 | stopPlaying()
854 | currentNode = 0
855 | currentSceneNode = 0
856 | advanceFrame(0, true)
857 | })
858 |
859 | ipc.on('toggleSpeaking', (event, arg) => {
860 | speakingMode = !speakingMode
861 | if (speakingMode) {
862 | togglePlayback()
863 | } else {
864 | stopPlaying()
865 | }
866 | })
--------------------------------------------------------------------------------
/src/js/fountain-data-parser.js:
--------------------------------------------------------------------------------
1 | let scriptData
2 |
3 | // TODO: get most used words in dialogue
4 |
5 |
6 |
7 | function paginate(tokens) {
8 | let currentPage = 0
9 | let currentLine = 0
10 | let currentCurs = 0
11 |
12 | let reqLine = 0
13 | let inDialogue = false
14 |
15 | for (var token of tokens) {
16 | if (!inDialogue){reqLine = 0}
17 |
18 | switch (token.type) {
19 | case 'scene_heading': reqLine += 3; break;
20 | case 'action': reqLine += linesForText(token.text, 63)+1; break;
21 | case 'dialogue_begin': inDialogue = true; break;
22 | case 'dual_dialogue_begin': inDialogue = true; break;
23 | case 'character': reqLine += 1; break;
24 | case 'parenthetical': reqLine += 1; break;
25 | case 'dialogue': reqLine += linesForText(token.text, 35); break;
26 | case 'dialogue_end': reqLine += 1; inDialogue = false; break;
27 | case 'dual_dialogue_end': reqLine += 1; inDialogue = false; break;
28 | case 'centered': reqLine += 2; break;
29 | case 'transition': reqLine += 2; break;
30 | }
31 |
32 | if (!inDialogue){
33 | if ((currentLine + reqLine) < 55) {
34 | currentLine += reqLine
35 | } else {
36 | currentPage += 1
37 | currentLine = reqLine
38 | switch (token.type) {
39 | case 'scene_heading':
40 | case 'action':
41 | case 'centered':
42 | case 'transition':
43 | case 'dialogue_end':
44 | case 'dual_dialogue_end':
45 | currentLine -= 1
46 | break
47 | }
48 | }
49 | }
50 | token.page = currentPage+1;
51 | }
52 |
53 | let pageCount = currentPage+1;
54 |
55 | //console.log("page count: " + pageCount);
56 | }
57 |
58 | function linesForText(text, charWidth) {
59 | if (!text) return 0
60 | let splitText = text.split(' ')
61 | let line = 0
62 | let currentCurs = 0
63 | for (var word of splitText) {
64 | if (word.indexOf("/>") != -1) {
65 | line++
66 | currentCurs = word.length - 1
67 | } else if (word.indexOf(" 1) {
122 | location.pop()
123 | }
124 | location = location.join(' - ')
125 |
126 | if (locations[location] == undefined) {
127 | locations[location] = 1
128 | } else {
129 | locations[location]++
130 | }
131 | }
132 | }
133 | return values(locations)
134 | }
135 |
136 | function sortedValues(obj) {
137 | let tuples = []
138 | for (var key in obj) tuples.push([key, obj[key]])
139 | tuples.sort((a, b)=>{ return a[1] < b[1] ? 1 : a[1] > b[1] ? -1 : 0 })
140 | return tuples
141 | }
142 |
143 | function values(obj) {
144 | let tuples = []
145 | for (var key in obj) tuples.push([key, obj[key]])
146 | return tuples
147 | }
148 |
149 |
150 | function parseTokens(tokens) {
151 | let script = []
152 | let sceneAtom = {type: 'scene'}
153 | sceneAtom['script'] = []
154 | let currentTime = 0
155 | let currentCharacter
156 | let currentScene = 0
157 | let inDialogue = false
158 |
159 | // stats
160 | let totalWordCount = 0
161 | let sceneWordCount = 0
162 | let startSceneTime = 0
163 |
164 |
165 | // add wordcount per scene, add duration per scene
166 |
167 |
168 | for (var token of tokens) {
169 | switch (token.type) {
170 | case 'boneyard_begin':
171 | console.log('boneyard BEGIN')
172 | break
173 | case 'boneyard_end':
174 | console.log('boneyard END')
175 | break
176 | case 'title':
177 | token['time'] = currentTime
178 | token['duration'] = 2000
179 | token['scene'] = currentScene
180 | currentTime += token['duration']
181 | script.push(token)
182 | break
183 | case 'credit':
184 | case 'author':
185 | case 'authors':
186 | case 'format':
187 | case 'source':
188 | case 'notes':
189 | case 'draft_date':
190 | case 'date':
191 | case 'contact':
192 | case 'copyright':
193 | script[0][token.type] = token.text
194 | break
195 | case 'scene_heading':
196 | if (sceneAtom['script'].length > 0) {
197 | sceneAtom['duration'] = (currentTime - startSceneTime)
198 | sceneAtom['word_count'] = sceneWordCount
199 | script.push(sceneAtom)
200 | }
201 |
202 | startSceneTime = currentTime
203 | sceneWordCount = 0
204 |
205 | sceneAtom = {type: 'scene'}
206 | sceneAtom['script'] = []
207 | currentScene++
208 | let atom = {
209 | time: currentTime,
210 | duration: 2000,
211 | type: 'scene_padding',
212 | scene: currentScene,
213 | page: token.page,
214 | }
215 | currentTime += atom['duration']
216 | sceneAtom['script'].push(atom)
217 | sceneAtom['scene_number'] = currentScene
218 | sceneAtom['slugline'] = token.text
219 | sceneAtom['time'] = currentTime
220 | sceneAtom['page'] = token.page
221 |
222 | token['time'] = currentTime
223 | token['duration'] = 0
224 | token['scene'] = currentScene
225 | currentTime += token['duration']
226 | sceneAtom['script'].push(token)
227 | sceneWordCount += wordCount(token.text)
228 | break
229 | case 'action':
230 | token['time'] = currentTime
231 | token['duration'] = durationOfWords(token.text, 200)+500
232 | token['scene'] = currentScene
233 | currentTime += token['duration']
234 | sceneAtom['script'].push(token)
235 | sceneWordCount += wordCount(token.text)
236 | break
237 |
238 | case 'dialogue_begin': inDialogue = true; break;
239 | case 'dual_dialogue_begin': inDialogue = true; break;
240 | case 'character':
241 | currentCharacter = token.text
242 | sceneWordCount += wordCount(token.text)
243 | break
244 | case 'parenthetical':
245 | token['time'] = currentTime
246 | token['duration'] = durationOfWords(token.text, 300)+1000
247 | token['scene'] = currentScene
248 | token['character'] = currentCharacter
249 | currentTime += token['duration']
250 | sceneAtom['script'].push(token)
251 | sceneWordCount += wordCount(token.text)
252 | break
253 | case 'dialogue':
254 | token['time'] = currentTime
255 | token['duration'] = durationOfWords(token.text, 300)+1000
256 | token['scene'] = currentScene
257 | token['character'] = currentCharacter
258 | currentTime += token['duration']
259 | sceneAtom['script'].push(token)
260 | sceneWordCount += wordCount(token.text)
261 | break
262 | case 'dialogue_end':
263 | case 'dual_dialogue_end':
264 | inDialogue = false
265 | break
266 | case 'centered':
267 | // token['time'] = currentTime
268 | // token['duration'] = 2000
269 | // token['scene'] = currentScene
270 | // currentTime += token['duration']
271 | // sceneAtom['script'].push(token)
272 | break
273 | case 'transition':
274 | token['time'] = currentTime
275 | token['duration'] = 1000
276 | token['scene'] = currentScene
277 | currentTime += token['duration']
278 | sceneAtom['script'].push(token)
279 | sceneWordCount += wordCount(token.text)
280 | break
281 | case 'section':
282 | if (token.depth == 1) {
283 |
284 | if (sceneAtom['script'].length > 0) {
285 | sceneAtom['duration'] = (currentTime - startSceneTime)
286 | sceneAtom['word_count'] = sceneWordCount
287 | script.push(sceneAtom)
288 | sceneAtom = {type: 'scene'}
289 | sceneAtom['script'] = []
290 | }
291 |
292 |
293 | token['time'] = currentTime
294 | token['duration'] = 0
295 | token['scene'] = currentScene
296 | currentTime += token['duration']
297 | script.push(token)
298 | //console.log(token)
299 | } else {
300 | token['time'] = currentTime
301 | token['duration'] = 0
302 | token['scene'] = currentScene
303 | currentTime += token['duration']
304 | sceneAtom['script'].push(token)
305 | }
306 | break
307 | case 'synopsis':
308 | sceneAtom['synopsis'] = token.text
309 | break
310 | case 'note':
311 | //console.log(token)
312 | break
313 |
314 | }
315 |
316 | }
317 | if (sceneAtom['script'].length > 0) {
318 | sceneAtom['duration'] = (currentTime - startSceneTime)
319 | sceneAtom['word_count'] = sceneWordCount
320 | script.push(sceneAtom)
321 | }
322 |
323 | return script
324 |
325 | }
326 |
327 |
328 |
329 | let fountainDataParser = {
330 | parse: (scriptTokens)=> {
331 | scriptData = scriptTokens
332 | paginate(scriptData)
333 |
334 | //console.log(scriptData)
335 | //parseScenes(scriptData)
336 | //getCharacters(scriptData)
337 | return parseTokens(scriptData)
338 | },
339 | getLocations: (scriptTokens)=> {
340 | return getLocations(scriptTokens)
341 | },
342 | getCharacters: (scriptTokens)=> {
343 | return getCharacters(scriptTokens)
344 | }
345 |
346 |
347 | }
348 |
349 | module.exports = fountainDataParser
350 |
--------------------------------------------------------------------------------
/src/js/main.js:
--------------------------------------------------------------------------------
1 | /* TODO:
2 | WELCOME WINDOW
3 | set up carousel for welcome with content:
4 | did you know?
5 | learn more about wonder unit
6 | more software
7 | testimonials
8 | forum
9 | link getting started properly
10 | IDEAS:
11 | MAIN WINDOW
12 | hook up buttons
13 | padding on buttons
14 | open close properly
15 | click scrub
16 | pref for speaking and save
17 | menu for help
18 | getting started
19 | submit feedback
20 | forum
21 | ICON
22 |
23 |
24 | click to load
25 | load file
26 | close welcome
27 | open main
28 | when main window closes, open welcome window
29 |
30 |
31 |
32 | */
33 |
34 | const {app, ipcMain, BrowserWindow} = electron = require('electron')
35 | const {dialog} = require('electron')
36 | const {powerSaveBlocker} = require('electron')
37 | const PDFParser = require("pdf2json");
38 | const fs = require('fs')
39 |
40 | const prefModule = require('./prefs')
41 |
42 | const fountain = require('./vendor/fountain')
43 | const fountainDataParser = require('./fountain-data-parser')
44 |
45 | let mainWindow
46 | let welcomeWindow
47 | let welcomeInprogress
48 |
49 | let statWatcher
50 |
51 | let powerSaveId = 0
52 |
53 | let previousScript
54 |
55 | let prefs = prefModule.getPrefs()
56 |
57 | app.on('ready', ()=> {
58 | // open the welcome window when the app loads up first
59 | openWelcomeWindow()
60 | })
61 |
62 | app.on('activate', ()=> {
63 | if (!mainWindow && !welcomeWindow) openWelcomeWindow()
64 | })
65 |
66 | let openWelcomeWindow = ()=> {
67 | // RESET PREFS - SHOULD BE COMMENTED OUT
68 | // console.log(prefs)
69 | // prefs = {scriptFile: `./outl3ine.txt`}
70 | // prefModule.savePrefs(prefs)
71 | welcomeWindow = new BrowserWindow({width: 900, height: 600, show: false, resizable: false, frame: false})
72 | welcomeWindow.loadURL(`file://${__dirname}/../welcome.html`)
73 | let recentDocumentsCopy
74 | if (prefs.recentDocuments) {
75 | let count = 0
76 | recentDocumentsCopy = prefs.recentDocuments
77 | for (var recentDocument of prefs.recentDocuments) {
78 | try {
79 | fs.accessSync(recentDocument.path, fs.F_OK);
80 | } catch (e) {
81 | // It isn't accessible
82 | console.log('error file no longer exists.')
83 | recentDocumentsCopy.splice(count, 1)
84 | }
85 | count++
86 | }
87 | prefs.recentDocuments = recentDocumentsCopy
88 | }
89 | global.sharedObj = {'prefs': prefs}
90 |
91 | welcomeWindow.once('ready-to-show', () => {
92 | setTimeout(()=>{welcomeWindow.show()}, 300)
93 | })
94 |
95 | welcomeWindow.once('close', () => {
96 | welcomeWindow = null
97 | if (!welcomeInprogress) {
98 | app.quit()
99 | } else {
100 | welcomeInprogress = false
101 | }
102 | })
103 | }
104 |
105 | let openMainWindow = ()=> {
106 | if (welcomeWindow) {
107 | // close welcome window if open
108 | welcomeInprogress = true
109 | welcomeWindow.close()
110 | }
111 |
112 | if (mainWindow) {
113 | mainWindow.webContents.send('reload')
114 | } else {
115 | mainWindow = new BrowserWindow({width: 1300, height: 1000, minWidth: 1024, minHeight: 600, show: false, titleBarStyle: 'hidden-inset', title: 'Script Visualizer'})
116 | mainWindow.loadURL(`file://${__dirname}/../index.html`)
117 | }
118 |
119 | // Emitted when the window is closed.
120 | mainWindow.on('close', function () {
121 | prefModule.savePrefs(prefs)
122 | openWelcomeWindow()
123 | mainWindow = null
124 | })
125 |
126 | mainWindow.once('ready-to-show', () => {
127 | setTimeout(()=>{mainWindow.show()}, 500)
128 | })
129 | }
130 |
131 | function openFile(file) {
132 | if (file) {
133 | fs.unwatchFile(prefs.scriptFile)
134 | prefs.scriptFile = file
135 | prefs.currentScene = 0
136 | loadFile(true)
137 | } else {
138 | // open dialogue
139 | dialog.showOpenDialog({title:"Open Script", filters:[{name: 'Screenplay', extensions: ['fountain', 'pdf']}]}, (filenames)=>{
140 | if (filenames) {
141 | fs.unwatchFile(prefs.scriptFile)
142 | prefs.scriptFile = filenames[0]
143 | prefs.currentScene = 0
144 | loadFile(true)
145 | }
146 | })
147 | }
148 | }
149 |
150 | function loadFile(create, update) {
151 | if (update == true) {
152 | previousScript = global.sharedObj['scriptData']
153 | }
154 |
155 | let filenameParts = prefs.scriptFile.toLowerCase().split('.')
156 | let type = filenameParts[filenameParts.length-1]
157 | if (type == 'pdf') {
158 | let pdfParser = new PDFParser();
159 |
160 | pdfParser.on("pdfParser_dataError", errData => console.error(errData.parserError) );
161 |
162 | pdfParser.on('pdfParser_dataReady', pdfData => {
163 | let pages = pdfData['formImage']['Pages']
164 | let scriptText = ''
165 | let currentX = 0
166 | for (var page of pages) {
167 | let texts = page['Texts']
168 | let currentY = -1
169 | let textCount = 0
170 | for (var text of texts) {
171 | if (textCount == 0) {
172 | if ((text['x'] !== currentX)) {
173 | scriptText += "\n\n"
174 | }
175 | scriptText += decodeURIComponent(text['R'][0]['T'])
176 | currentX = text['x']
177 | } else {
178 | if ((text['y'] - currentY) > 1) {
179 | // new line
180 | scriptText += "\n\n"
181 | } else if (text['y'] == currentY) {
182 | } else if (text['y'] < currentY) {
183 | break
184 | } else {
185 | if ((text['x'] !== currentX) && (text['x'] > 10.17)) {
186 | scriptText += "\n"
187 | }
188 | }
189 | scriptText += decodeURIComponent(text['R'][0]['T'])
190 | currentX = text['x']
191 | }
192 | currentY = text['y']
193 | textCount++
194 | }
195 | }
196 | processFountainData(scriptText, create, update)
197 | pdfParser.destroy()
198 | })
199 |
200 | pdfParser.loadPDF(prefs.scriptFile);
201 |
202 | } else if (type == 'fountain') {
203 | fs.readFile(prefs.scriptFile, 'utf-8', (err,data)=>{
204 | if (err) {
205 | console.log("ERROR: Can't open file.")
206 | openFile()
207 | return
208 | }
209 | processFountainData(data, create, update)
210 | })
211 | }
212 | }
213 |
214 | let processFountainData = (data, create, update) => {
215 |
216 | // parse fountain file
217 | let documentPath = prefs.scriptFile.split('/')
218 | documentPath.pop()
219 | documentPath = documentPath.join('/')
220 |
221 | let scriptData = fountain.parse(data, true)
222 |
223 | let locations = fountainDataParser.getLocations(scriptData.tokens)
224 | let characters = fountainDataParser.getCharacters(scriptData.tokens)
225 | scriptData = fountainDataParser.parse(scriptData.tokens)
226 |
227 | global.sharedObj = {scriptData: scriptData, locations: locations, characters: characters, documentPath: documentPath, currentNode: 1, prefs: prefs}
228 |
229 | if (!prefs.recentDocuments) {
230 | prefs.recentDocuments = []
231 | }
232 |
233 | let currPos = 0
234 |
235 | for (var document of prefs.recentDocuments) {
236 | if (document.path == prefs.scriptFile) {
237 | prefs.recentDocuments.splice(currPos, 1)
238 | break
239 | }
240 | currPos++
241 | }
242 |
243 | let recentDocument = {}
244 | recentDocument.path = prefs.scriptFile
245 |
246 | // generate stats
247 |
248 | let totalWordCount = 0
249 | for (var node of scriptData) {
250 | if (node.word_count) totalWordCount += node.word_count
251 | }
252 | let totalPageCount
253 | let totalMovieTime
254 | switch (scriptData[scriptData.length-1].type) {
255 | case 'title':
256 | case 'section':
257 | totalPageCount = scriptData[scriptData.length-1].page
258 | totalMovieTime = scriptData[scriptData.length-1].time + scriptData[scriptData.length-1].duration
259 | break
260 | case 'scene':
261 | let lastNode = scriptData[scriptData.length-1]['script'][scriptData[scriptData.length-1]['script'].length-1]
262 | totalPageCount = lastNode.page
263 | totalMovieTime = lastNode.time + lastNode.duration
264 | break
265 | }
266 | recentDocument.time = Date.now()
267 | recentDocument.totalWordCount = totalWordCount
268 | recentDocument.totalPageCount = totalPageCount
269 | recentDocument.totalMovieTime = totalMovieTime
270 | prefs.recentDocuments.unshift(recentDocument)
271 | // save
272 | prefModule.savePrefs(prefs)
273 |
274 | if (create) {
275 | fs.watchFile(prefs.scriptFile, {persistent: false}, (e) => {
276 | loadFile(false, true)
277 | })
278 |
279 | openMainWindow()
280 | }
281 |
282 | if (update == true) {
283 | let diffScene = getSceneDifference(previousScript, global.sharedObj['scriptData'])
284 | mainWindow.webContents.send('reload', 1, diffScene)
285 | }
286 | }
287 |
288 | let getSceneDifference = (scriptA, scriptB) => {
289 | let i = 0
290 | for (var node of scriptB) {
291 | if(!scriptA[i]) {
292 | return i
293 | }
294 | if (JSON.stringify(node) !== JSON.stringify(scriptA[i])) {
295 | return i
296 | }
297 | i++
298 | }
299 | return false
300 | }
301 |
302 | // ipcMain.on('showWindow', ()=> {
303 | // mainWindow.show()
304 | // })
305 |
306 | ipcMain.on('openFile', (e, arg)=> {
307 | openFile(arg)
308 | })
309 |
310 | ipcMain.on('preventSleep', ()=> {
311 | powerSaveId = powerSaveBlocker.start('prevent-display-sleep')
312 | })
313 |
314 | ipcMain.on('resumeSleep', ()=> {
315 | powerSaveBlocker.stop(powerSaveId)
316 | })
317 |
318 | /// menu pass through
319 |
320 | ipcMain.on('togglePlayback', (event, arg)=> {
321 | mainWindow.webContents.send('togglePlayback')
322 | })
323 |
324 | ipcMain.on('goBeginning', (event, arg)=> {
325 | mainWindow.webContents.send('goBeginning')
326 | })
327 |
328 | ipcMain.on('goPreviousScene', (event, arg)=> {
329 | mainWindow.webContents.send('goPreviousScene')
330 | })
331 |
332 | ipcMain.on('goPrevious', (event, arg)=> {
333 | mainWindow.webContents.send('goPrevious')
334 | })
335 |
336 | ipcMain.on('goNext', (event, arg)=> {
337 | mainWindow.webContents.send('goNext')
338 | })
339 |
340 | ipcMain.on('goNextScene', (event, arg)=> {
341 | mainWindow.webContents.send('goNextScene')
342 | })
343 |
344 | ipcMain.on('toggleSpeaking', (event, arg)=> {
345 | mainWindow.webContents.send('toggleSpeaking')
346 | })
--------------------------------------------------------------------------------
/src/js/menu.js:
--------------------------------------------------------------------------------
1 | const {Menu} = require('electron').remote
2 | const {ipcRenderer} = require('electron')
3 |
4 | const template = [
5 | {
6 | label: 'File',
7 | submenu: [
8 | {
9 | label: 'Open...',
10 | accelerator: 'CmdOrCtrl+O',
11 | click ( item, focusedWindow, event) {
12 | ipcRenderer.send('openFile')
13 | }
14 | },
15 | {
16 | type: 'separator'
17 | },
18 | {
19 | label: 'Export Treatment...',
20 | click ( item, focusedWindow, event) {
21 | ipcRenderer.send('exportTreatment')
22 | }
23 | },
24 | {
25 | label: 'Export to Fountain Screenplay...',
26 | click ( item, focusedWindow, event) {
27 | ipcRenderer.send('exportFountain')
28 | }
29 | },
30 | {
31 | label: 'Export to Outliner...',
32 | click ( item, focusedWindow, event) {
33 | ipcRenderer.send('exportOutliner')
34 | }
35 | },
36 | {
37 | label: 'Export to CSV file...',
38 | click ( item, focusedWindow, event) {
39 | ipcRenderer.send('exportCSV')
40 | }
41 | },
42 | {
43 | type: 'separator'
44 | },
45 | {
46 | label: 'Export poster to PDF...',
47 | click ( item, focusedWindow, event) {
48 | ipcRenderer.send('exportPoster')
49 | }
50 | },
51 | {
52 | type: 'separator'
53 | },
54 | {
55 | accelerator: 'CmdOrCtrl+P',
56 | label: 'Print current scene worksheet...',
57 | click ( item, focusedWindow, event) {
58 | ipcRenderer.send('printWorksheet')
59 | }
60 | },
61 | {
62 | accelerator: 'CmdOrCtrl+I',
63 | label: 'Import worksheets...',
64 | click ( item, focusedWindow, event) {
65 | ipcRenderer.send('importWorksheets')
66 | }
67 | },
68 | ]
69 | },
70 | {
71 | label: 'Navigation',
72 | submenu: [
73 | {
74 | accelerator: 'Space',
75 | label: 'Toggle playback',
76 | click ( item, focusedWindow, event) {
77 | ipcRenderer.send('togglePlayback')
78 | }
79 | },
80 | {
81 | type: 'separator'
82 | },
83 | {
84 | accelerator: 'Home',
85 | label: 'Go to the beginning',
86 | click ( item, focusedWindow, event) {
87 | ipcRenderer.send('goBeginning')
88 | }
89 | },
90 | {
91 | label: 'Previous scene',
92 | accelerator: 'CmdOrCtrl+Left',
93 | click ( item, focusedWindow, event) {
94 | ipcRenderer.send('goPreviousScene')
95 | }
96 | },
97 | {
98 | label: 'Previous line',
99 | accelerator: 'Left',
100 | click ( item, focusedWindow, event) {
101 | ipcRenderer.send('goPrevious')
102 | }
103 | },
104 | {
105 | label: 'Next line',
106 | accelerator: 'Right',
107 | click ( item, focusedWindow, event) {
108 | ipcRenderer.send('goNext')
109 | }
110 | },
111 | {
112 | label: 'Next scene',
113 | accelerator: 'CmdOrCtrl+Right',
114 | click ( item, focusedWindow, event) {
115 | ipcRenderer.send('goNextScene')
116 | }
117 | },
118 | {
119 | type: 'separator'
120 | },
121 | {
122 | accelerator: 'CmdOrCtrl+S',
123 | label: 'Toggle speaking',
124 | type: 'checkbox',
125 | click ( item, focusedWindow, event) {
126 | ipcRenderer.send('toggleSpeaking')
127 | }
128 | }
129 | ]
130 | },
131 | {
132 | label: 'Edit',
133 | submenu: [
134 | {
135 | role: 'copy'
136 | },
137 | {
138 | role: 'paste'
139 | }
140 | ]
141 | },
142 | {
143 | label: 'View',
144 | submenu: [
145 | {
146 | label: 'Reload',
147 | accelerator: 'CmdOrCtrl+R',
148 | click (item, focusedWindow) {
149 | if (focusedWindow) focusedWindow.reload()
150 | }
151 | },
152 | {
153 | label: 'Toggle Developer Tools',
154 | accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
155 | click (item, focusedWindow) {
156 | if (focusedWindow) focusedWindow.webContents.toggleDevTools()
157 | }
158 | },
159 | {
160 | type: 'separator'
161 | },
162 | {
163 | accelerator: 'CmdOrCtrl+F',
164 | role: 'togglefullscreen'
165 | }
166 | ]
167 | },
168 | {
169 | role: 'window',
170 | submenu: [
171 | {
172 | role: 'minimize'
173 | },
174 | {
175 | role: 'close'
176 | }
177 | ]
178 | },
179 | {
180 | role: 'help',
181 | submenu: [
182 | {
183 | label: 'Learn More',
184 | click () { require('electron').shell.openExternal('http://www.setpixel.com') }
185 | }
186 | ]
187 | }
188 | ]
189 |
190 |
191 |
192 |
193 | if (process.platform === 'darwin') {
194 | const name = require('electron').remote.app.getName()
195 | template.unshift({
196 | label: name,
197 | submenu: [
198 | {
199 | role: 'about'
200 | },
201 | {
202 | type: 'separator'
203 | },
204 | {
205 | role: 'services',
206 | submenu: []
207 | },
208 | {
209 | type: 'separator'
210 | },
211 | {
212 | role: 'hide'
213 | },
214 | {
215 | role: 'hideothers'
216 | },
217 | {
218 | role: 'unhide'
219 | },
220 | {
221 | type: 'separator'
222 | },
223 | {
224 | role: 'quit'
225 | }
226 | ]
227 | })
228 | // // Edit menu.
229 | // template[1].submenu.push(
230 | // {
231 | // type: 'separator'
232 | // },
233 | // {
234 | // label: 'Speech',
235 | // submenu: [
236 | // {
237 | // role: 'startspeaking'
238 | // },
239 | // {
240 | // role: 'stopspeaking'
241 | // }
242 | // ]
243 | // }
244 | // )
245 | // Window menu.
246 | template[5].submenu = [
247 | {
248 | label: 'Close',
249 | accelerator: 'CmdOrCtrl+W',
250 | role: 'close'
251 | },
252 | {
253 | label: 'Minimize',
254 | accelerator: 'CmdOrCtrl+M',
255 | role: 'minimize'
256 | },
257 | {
258 | label: 'Zoom',
259 | role: 'zoom'
260 | },
261 | {
262 | type: 'separator'
263 | },
264 | {
265 | label: 'Bring All to Front',
266 | role: 'front'
267 | }
268 | ]
269 | }
270 |
271 |
272 | const welcomeTemplate = [
273 | {
274 | label: 'File',
275 | submenu: [
276 | {
277 | label: 'Open...',
278 | accelerator: 'CmdOrCtrl+O',
279 | click ( item, focusedWindow, event) {
280 | ipcRenderer.send('openFile')
281 | }
282 | }
283 | ]
284 | },
285 | {
286 | label: 'Edit',
287 | submenu: [
288 | {
289 | role: 'copy'
290 | },
291 | {
292 | role: 'paste'
293 | }
294 | ]
295 | },
296 | {
297 | label: 'View',
298 | submenu: [
299 | {
300 | label: 'Reload',
301 | accelerator: 'CmdOrCtrl+R',
302 | click (item, focusedWindow) {
303 | if (focusedWindow) focusedWindow.reload()
304 | }
305 | },
306 | {
307 | label: 'Toggle Developer Tools',
308 | accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
309 | click (item, focusedWindow) {
310 | if (focusedWindow) focusedWindow.webContents.toggleDevTools()
311 | }
312 | },
313 | {
314 | type: 'separator'
315 | },
316 | {
317 | accelerator: 'CmdOrCtrl+F',
318 | role: 'togglefullscreen'
319 | }
320 | ]
321 | },
322 | {
323 | role: 'window',
324 | submenu: [
325 | {
326 | role: 'minimize'
327 | },
328 | {
329 | role: 'close'
330 | }
331 | ]
332 | },
333 | {
334 | role: 'help',
335 | submenu: [
336 | {
337 | label: 'Learn More',
338 | click () { require('electron').shell.openExternal('http://www.setpixel.com') }
339 | }
340 | ]
341 | }
342 | ]
343 |
344 | const menuInstance = Menu.buildFromTemplate(template)
345 | const welcomeMenuInstance = Menu.buildFromTemplate(welcomeTemplate)
346 |
347 | const menu = {
348 | setWelcomeMenu: function() {
349 | Menu.setApplicationMenu(welcomeMenuInstance)
350 | },
351 | setMenu: function() {
352 | Menu.setApplicationMenu(menuInstance)
353 | }
354 | }
355 |
356 | module.exports = menu
--------------------------------------------------------------------------------
/src/js/outlineWindow.js:
--------------------------------------------------------------------------------
1 | const menu = require('./menu.js')
2 | const {ipcRenderer} = require('electron')
3 | menu.setMenu()
--------------------------------------------------------------------------------
/src/js/prefs.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const {app} = require('electron')
3 |
4 | let prefs
5 |
6 | let prefModule = {
7 | loadPrefs: function () {
8 | let prefFile = app.getPath('userData') + '/pref.json'
9 | try {
10 | prefs = JSON.parse(fs.readFileSync(prefFile))
11 | } catch (e) {
12 | console.log(e)
13 | prefs = {scriptFile: `./outl3ine.txt`}
14 |
15 | try {
16 | fs.writeFileSync(prefFile, JSON.stringify(prefs))
17 | } catch (e) {
18 | console.log(e)
19 | }
20 | }
21 | },
22 | savePrefs: function (prefs) {
23 | let prefFile = app.getPath('userData') + '/pref.json'
24 | fs.writeFileSync(prefFile, JSON.stringify(prefs))
25 | },
26 | getPrefs: function(){return prefs},
27 | }
28 |
29 | prefModule.loadPrefs()
30 |
31 | module.exports = prefModule
--------------------------------------------------------------------------------
/src/js/vendor/fountain.js:
--------------------------------------------------------------------------------
1 | // fountain-js 0.1.10
2 | // http://www.opensource.org/licenses/mit-license.php
3 | // Copyright (c) 2012 Matt Daly
4 |
5 | ;(function() {
6 | 'use strict';
7 |
8 | var regex = {
9 | title_page: /^((?:title|credit|author[s]?|format|source|notes|draft date|date|contact|copyright|Title|Credit|Author[s]?|Format|Source|Notes|Draft date|Draft Date|Date|Contact|Copyright)\:)/gm,
10 |
11 | scene_heading: /^((?:\*{0,3}_?)?(?:(?:int|ext|est|i\/e)[. ]).+)|^(?:\.(?!\.+))(.+)/i,
12 | scene_number: /( *#(.+)# *)/,
13 |
14 | transition: /^((?:FADE (?:TO BLACK|OUT)|CUT TO BLACK)\.|.+ TO\:)|^(?:> *)(.+)/,
15 |
16 | dialogue: /^([A-Z*_]+[0-9A-Z (._\-'’,)]*)(\^?)?(?:\n(?!\n+))([\s\S]+)/,
17 | parenthetical: /^(\(.+\))$/,
18 |
19 | action: /^(.+)/g,
20 | centered: /^(?:> *)(.+)(?: *<)(\n.+)*/g,
21 |
22 | section: /^(#+)(?: *)(.*)/,
23 | synopsis: /^(?:\=(?!\=+) *)(.*)/,
24 |
25 | note: /^(?:\[{2}(?!\[+))(.+)(?:\]{2}(?!\[+))$/,
26 | note_inline: /(?:\[{2}(?!\[+))([\s\S]+?)(?:\]{2}(?!\[+))/g,
27 | //boneyard: /(^\/\*|^\*\/)$/g,
28 | boneyard: /\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm,
29 | //boneyard: /(?:\r\n|\n|^)(?:[^'"])*?(?:'(?:[^\r\n\\']|\\'|[\\]{2})*'|"(?:[^\r\n\\"]|\\"|[\\]{2})*")*?(?:[^'"])*?(\/\*(?:[\s\S]*?)\*\/|\/\/.*)/g,
30 | page_break: /^\={3,}$/,
31 | line_break: /^ {2}$/,
32 |
33 | emphasis: /(_|\*{1,3}|_\*{1,3}|\*{1,3}_)(.+)(_|\*{1,3}|_\*{1,3}|\*{1,3}_)/g,
34 | bold_italic_underline: /(_{1}\*{3}(?=.+\*{3}_{1})|\*{3}_{1}(?=.+_{1}\*{3}))(.+?)(\*{3}_{1}|_{1}\*{3})/g,
35 | bold_underline: /(_{1}\*{2}(?=.+\*{2}_{1})|\*{2}_{1}(?=.+_{1}\*{2}))(.+?)(\*{2}_{1}|_{1}\*{2})/g,
36 | italic_underline: /(?:_{1}\*{1}(?=.+\*{1}_{1})|\*{1}_{1}(?=.+_{1}\*{1}))(.+?)(\*{1}_{1}|_{1}\*{1})/g,
37 | bold_italic: /(\*{3}(?=.+\*{3}))(.+?)(\*{3})/g,
38 | bold: /(\*{2}(?=.+\*{2}))(.+?)(\*{2})/g,
39 | italic: /(\*{1}(?=.+\*{1}))(.+?)(\*{1})/g,
40 | underline: /(_{1}(?=.+_{1}))(.+?)(_{1})/g,
41 |
42 | splitter: /\n{2,}/g,
43 | cleaner: /^\n+|\n+$/,
44 | standardizer: /\r\n|\r/g,
45 | whitespacer: /^\t+|^ {3,}/gm
46 | };
47 |
48 | var lexer = function (script) {
49 | //
50 | return script.replace(regex.boneyard, '').replace(regex.standardizer, '\n')
51 | .replace(regex.cleaner, '')
52 | .replace(regex.whitespacer, '')
53 |
54 | };
55 |
56 | var tokenize = function (script) {
57 | var src = lexer(script).split(regex.splitter)
58 | , i = src.length, line, match, parts, text, meta, x, xlen, dual
59 | , tokens = [];
60 |
61 | while (i--) {
62 | line = src[i];
63 |
64 | // title page
65 | if (regex.title_page.test(line)) {
66 | match = line.replace(regex.title_page, '\n$1').split(regex.splitter).reverse();
67 | for (x = 0, xlen = match.length; x < xlen; x++) {
68 | parts = match[x].replace(regex.cleaner, '').split(/\:\n*/);
69 | tokens.push({ type: parts[0].trim().toLowerCase().replace(' ', '_'), text: parts[1].trim() });
70 | }
71 | continue;
72 | }
73 |
74 | // scene headings
75 | if (match = line.match(regex.scene_heading)) {
76 | text = match[1] || match[2];
77 |
78 | if (text.indexOf(' ') !== text.length - 2) {
79 | if (meta = text.match(regex.scene_number)) {
80 | meta = meta[2];
81 | text = text.replace(regex.scene_number, '');
82 | }
83 | tokens.push({ type: 'scene_heading', text: text, scene_number: meta || undefined });
84 | }
85 | continue;
86 | }
87 |
88 | // centered
89 | if (match = line.match(regex.centered)) {
90 | tokens.push({ type: 'centered', text: match[0].replace(/>| 0) {
116 | tokens.push({ type: regex.parenthetical.test(text) ? 'parenthetical' : 'dialogue', text: text });
117 | }
118 | }
119 |
120 | tokens.push({ type: 'character', text: match[1].trim() });
121 | tokens.push({ type: 'dialogue_begin', dual: match[2] ? 'right' : dual ? 'left' : undefined });
122 |
123 | if (dual) {
124 | tokens.push({ type: 'dual_dialogue_begin' });
125 | }
126 |
127 | dual = match[2] ? true : false;
128 | continue;
129 | }
130 | }
131 |
132 | // section
133 | if (match = line.match(regex.section)) {
134 | tokens.push({ type: 'section', text: match[2], depth: match[1].length });
135 | continue;
136 | }
137 |
138 | // synopsis
139 | if (match = line.match(regex.synopsis)) {
140 | tokens.push({ type: 'synopsis', text: match[1] });
141 | continue;
142 | }
143 |
144 | // notes
145 | if (match = line.match(regex.note)) {
146 | tokens.push({ type: 'note', text: match[1]});
147 | continue;
148 | }
149 |
150 | // boneyard
151 | if (match = line.match(regex.boneyard)) {
152 | // console.log('boneyard')
153 | // console.log( match[0])
154 | //tokens.push({ type: match[0][0] === '/' ? 'boneyard_begin' : 'boneyard_end' });
155 | continue;
156 | }
157 |
158 | // page breaks
159 | if (regex.page_break.test(line)) {
160 | tokens.push({ type: 'page_break' });
161 | continue;
162 | }
163 |
164 | // line breaks
165 | if (regex.line_break.test(line)) {
166 | tokens.push({ type: 'line_break' });
167 | continue;
168 | }
169 |
170 | tokens.push({ type: 'action', text: line });
171 | }
172 |
173 | return tokens;
174 | };
175 |
176 | var inline = {
177 | note: '',
178 |
179 | line_break: ' ',
180 |
181 | bold_italic_underline: '$2 ',
182 | bold_underline: '$2 ',
183 | italic_underline: '$2 ',
184 | bold_italic: '$2 ',
185 | bold: '$2 ',
186 | italic: '$2 ',
187 | underline: '$2 '
188 | };
189 |
190 | inline.lexer = function (s) {
191 | if (!s) {
192 | return;
193 | }
194 |
195 | var styles = [ 'underline', 'italic', 'bold', 'bold_italic', 'italic_underline', 'bold_underline', 'bold_italic_underline' ]
196 | , i = styles.length, style, match;
197 |
198 | s = s.replace(regex.note_inline, inline.note).replace(/\\\*/g, '[star]').replace(/\\_/g, '[underline]').replace(/\n/g, inline.line_break);
199 |
200 | // if (regex.emphasis.test(s)) { // this was causing only every other occurence of an emphasis syntax to be parsed
201 | while (i--) {
202 | style = styles[i];
203 | match = regex[style];
204 |
205 | if (match.test(s)) {
206 | s = s.replace(match, inline[style]);
207 | }
208 | }
209 | // }
210 |
211 | return s.replace(/\[star\]/g, '*').replace(/\[underline\]/g, '_').trim();
212 | };
213 |
214 | var parse = function (script, toks, callback) {
215 | if (callback === undefined && typeof toks === 'function') {
216 | callback = toks;
217 | toks = undefined;
218 | }
219 |
220 | var tokens = tokenize(script)
221 | , i = tokens.length, token
222 | , title, title_page = [], html = [], output;
223 |
224 | while (i--) {
225 | token = tokens[i];
226 | token.text = inline.lexer(token.text);
227 |
228 | switch (token.type) {
229 | case 'title':
230 | if (token.text) {
231 | title = token.text.replace(' ', ' ').replace(/<(?:.|\n)*?>/g, '');
232 | } else {
233 | title = ''
234 | }
235 | break;
236 | }
237 | }
238 |
239 | // output = { title: title, html: { title_page: title_page.join(''), script: html.join('') }, tokens: toks ? tokens.reverse() : undefined };
240 | output = { title: title, tokens: tokens.reverse() };
241 |
242 | if (typeof callback === 'function') {
243 | return callback(output);
244 | }
245 |
246 | return output;
247 | };
248 |
249 | var fountain = function (script, callback) {
250 | return fountain.parse(script, callback);
251 | };
252 |
253 | fountain.parse = function (script, tokens, callback) {
254 | return parse(script, tokens, callback);
255 | };
256 |
257 | if (typeof module !== 'undefined') {
258 | module.exports = fountain;
259 | } else {
260 | this.fountain = fountain;
261 | }
262 | }).call(this);
--------------------------------------------------------------------------------
/src/js/welcome.js:
--------------------------------------------------------------------------------
1 | var remote = nodeRequire('electron').remote
2 | const {shell} = nodeRequire('electron')
3 | const {ipcRenderer} = nodeRequire('electron')
4 |
5 | const moment = nodeRequire('moment')
6 |
7 | $(document).ready(function() {
8 | let recentDocuments = remote.getGlobal('sharedObj').prefs['recentDocuments']
9 | let count = 0
10 | let html = []
11 |
12 | if (recentDocuments) {
13 | for (var recentDocument of recentDocuments) {
14 | html.push(``)
15 | let path = recentDocument.path.split('/')
16 | path = path[path.length-1]
17 | html.push(`
${path} `)
18 | html.push(`${moment(recentDocument.time).fromNow().toUpperCase()} // ${msToTime(recentDocument.totalMovieTime)} / ${recentDocument.totalPageCount} PAGES / ${String(recentDocument.totalWordCount).replace(/\B(?=(\d{3})+(?!\d))/g, ",")} WORDS`)
19 | html.push(' ')
20 | count++
21 | }
22 |
23 | $('#recent').html(html.join(''))
24 | $('.recent').html("RECENT SCRIPTS")
25 |
26 | $('.recent-item').click((e)=>{
27 | console.log(e.currentTarget.dataset.path)
28 | ipcRenderer.send('openFile', e.currentTarget.dataset.path)
29 | })
30 | }
31 |
32 |
33 | $('#close-button').click((e) => {
34 | var window = remote.getCurrentWindow();
35 | window.close();
36 | })
37 |
38 | $('iframe').contents().delegate('a','click',(e)=>{
39 | shell.openExternal(e.currentTarget.href)
40 | e.preventDefault()
41 | })
42 |
43 | $('#open-script').click( ()=> {
44 | ipcRenderer.send('openFile')
45 | })
46 |
47 | $('#getting-started').click( ()=> {
48 | shell.openExternal("https://wonderunit.com")
49 | })
50 |
51 | })
52 |
53 | let msToTime = (s)=> {
54 | if(!s) s = 0
55 | s = Math.max(0, s)
56 | function addZ(n) {
57 | return (n<10? '0':'') + n;
58 | }
59 | var ms = (s % 1000);
60 | s = (s - ms) / 1000;
61 | var secs = s % 60;
62 | s = (s - secs) / 60;
63 | var mins = s % 60;
64 | var hrs = (s - mins) / 60;
65 | if (hrs) {
66 | return hrs + ':' + addZ(mins) + ':' + addZ(secs);
67 | } else {
68 | return mins + ':' + addZ(secs); //+ '.' + ms.toString().substring(0,1);
69 | }
70 | };
--------------------------------------------------------------------------------
/src/js/welcomeWindow.js:
--------------------------------------------------------------------------------
1 | const menu = require('./menu.js')
2 | const {ipcRenderer} = require('electron')
3 | menu.setWelcomeMenu()
--------------------------------------------------------------------------------
/src/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
WONDER UNIT // 2017 Release
25 |
Script Visualizer
26 |
27 |
28 |
29 |
30 |
31 |
32 |
Script Visualizer simply displays your screenplay so you can experience it like a movie.
33 |
34 |
It automatically goes through the script and calculates the timing for each scene, so when you play it back, it will be very similar to actual shot video. Additionally, Script Visualizer can read the script back to you as you edit your script.
35 |
Simply open a screenplay...
36 |
37 |
38 |
39 |
Getting started...
40 |
Open script...
41 |
42 |
43 |
44 |
45 |
46 |
47 |
53 |
54 |
55 |
56 |
59 |
--------------------------------------------------------------------------------