├── .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 | ![Script Visualizer Screenshot](https://raw.githubusercontent.com/setpixel/script-visualizer/master/media/screenshot1.png) 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 |
15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 | 43 | 46 | 49 | 52 | 55 | 58 | 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('
s
') 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 | 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 | --------------------------------------------------------------------------------