]> gitweb.michael.orlitzky.com - charm-bypass.git/blob - index.html.in
index.html.in: use less vertical (white)space for keyframes
[charm-bypass.git] / index.html.in
1 <!doctype html>
2 <html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <link rel="icon"
7 type="image/svg+xml"
8 href="data:image/svg+xml;base64,@FAVICON@" />
9
10 <title>
11 CharmBypass: it's transit equity y'all
12 </title>
13
14 <style>
15 fieldset {
16 margin-top: 1em;
17 padding-top: 1em;
18 }
19
20 input[type=submit] {
21 margin-top: 1em;
22 margin-bottom: 1em;
23 }
24
25 legend {
26 font-weight: bold;
27 }
28
29 svg {
30 /* Set the height to 100% of the screen, which we'll keep; and the
31 * initial position to (0,0), which we're going to change
32 * every time the window is resized, because the exact amount
33 * that we have to slide the SVG to the left to get the ticket
34 * into the center will change. */
35 position: fixed;
36 top: 0;
37 left: 0;
38 height: 100%;
39
40 /* Hide everything by default. We only show it once the user has
41 * submitted the menu form */
42 display: none;
43 }
44
45 /* The blinking fade in/out animation for the ticket date and time */
46 @keyframes blink {
47 25% { opacity: 0.5; }
48 50% { opacity: 0; }
49 75% { opacity: 0.5; }
50 }
51
52 #tickettime, #ticketdate {
53 /* 300 two-second blinks is ten minutes */
54 animation: blink 2s linear 300;
55 }
56
57 /* Define, load, and specify the custom font we use for the ticket
58 * date, time, and service name. */
59 @font-face {
60 font-family: "CharmBypass Regular";
61 src:
62 url("data:font/woff2;base64,@CBPREGULAR@") format("woff2")
63 }
64
65 #origindest, #servicename, #tickettime, #ticketdate, #codetext, #zone {
66 font-family: "CharmBypass Regular", sans-serif;
67 }
68
69 @font-face {
70 font-family: "CharmBypass Bold";
71 src:
72 url("data:font/woff2;base64,@CBPBOLD@") format("woff2")
73 }
74
75 #serviceid {
76 font-family: "CharmBypass Bold", sans-serif;
77 }
78
79 /************************/
80 /* Scrolling animations */
81 /************************/
82
83 /* Bus */
84 @keyframes busroll {
85 from { transform: translateX(0%); }
86 to { transform: translateX(-100%); }
87 }
88
89 #bus {
90 animation: busroll 23s linear infinite;
91 }
92
93
94 /* Tram */
95 @keyframes tramroll {
96 from { transform: translateX(0%); }
97 to { transform: translateX(100%); }
98 }
99
100 #tram {
101 animation: tramroll 17s linear infinite;
102 }
103
104
105 /* Tram */
106 @keyframes trainroll {
107 from { transform: translateX(0%); }
108 to { transform: translateX(100%); }
109 }
110
111 #train {
112 animation: trainroll 11s linear infinite;
113 }
114
115
116 /* Clouds */
117 @keyframes cloudsfloat {
118 from { transform: translateX(0%); }
119 to { transform: translateX(-50%); }
120 }
121
122 #clouds {
123 animation: cloudsfloat 40s linear infinite;
124 }
125
126 @keyframes cloudscopyfloat {
127 from { transform: translateX(0%); }
128 to { transform: translateX(-50%); }
129 }
130
131 #cloudscopy {
132 animation: cloudscopyfloat 40s linear infinite;
133 }
134
135
136 /* Trees */
137 @keyframes treespass {
138 from { transform: translateX(0%); }
139 to { transform: translateX(-50%); }
140 }
141
142 #trees {
143 /* The trees move a little faster than the clouds */
144 animation: treespass 30s linear infinite;
145 }
146
147 @keyframes treescopypass {
148 from { transform: translateX(0%); }
149 to { transform: translateX(-50%); }
150 }
151
152 #treescopy {
153 /* The trees move a little faster than the clouds */
154 animation: treescopypass 30s linear infinite;
155 }
156
157
158 /* City skyline */
159 @keyframes cityscroll {
160 from { transform: translateX(0%); }
161 to { transform: translateX(-50%); }
162 }
163
164 #city {
165 /* The city moves faster than the clouds but slower
166 * than the trees */
167 animation: cityscroll 35s linear infinite;
168 }
169
170 @keyframes citycopyscroll {
171 from { transform: translateX(0%); }
172 to { transform: translateX(-50%); }
173 }
174
175 #citycopy {
176 /* The city moves faster than the clouds but slower
177 * than the trees */
178 animation: citycopyscroll 35s linear infinite;
179 }
180 </style>
181 </head>
182
183 <body>
184 <div id="menu">
185 <h1>CharmBypass</h1>
186 <h2>it's transit equity y'all</h2>
187
188 <p>
189 <em>Hint:</em> If you think the driver or fare inspector will
190 actually check it, the daily security code is the same on a
191 $2.00 BaltimoreLink ticket as it is on a $9.00 MARC Train
192 ticket. It's also the same on your friend's phone, or for
193 anyone else in Maryland that day.
194 </p>
195
196 <form>
197 <fieldset>
198 <legend>Local Bus, Light Rail, or Metro</legend>
199 <div>
200 <label for="code1">
201 Daily security code (optional):
202 </label>
203 <input id="code1"
204 name="code"
205 type="text"
206 size="2"
207 minlength="2"
208 maxlength="2"
209 pattern="[a-zA-Z0-9]*" />
210 </div>
211 <input type="hidden" name="servicename" value="BaltimoreLink" />
212 <input type="submit" name="go" value="Generate Ticket" />
213 </fieldset>
214 </form>
215 <form>
216 <fieldset>
217 <legend>Commuter Bus</legend>
218 <input type="hidden" name="serviceid" value="R" />
219 <input type="hidden" name="servicename" value="Commuter Bus" />
220
221 <div>
222 <label for="code2">
223 Daily security code (optional):
224 </label>
225 <input id="code2"
226 name="code"
227 type="text"
228 size="2"
229 minlength="2"
230 maxlength="2"
231 pattern="[a-zA-Z0-9]*" />
232 </div>
233
234 <p>Zone:</p>
235 <div>
236 <input required type="radio" id="zone1" name="zone" value="Zone 1" />
237 <label for="zone1">Zone 1</label>
238 </div>
239 <div>
240 <input required type="radio" id="zone2" name="zone" value="Zone 2" />
241 <label for="zone2">Zone 2</label>
242 </div>
243 <div>
244 <input required type="radio" id="zone3" name="zone" value="Zone 3" />
245 <label for="zone3">Zone 3</label>
246 </div>
247 <div>
248 <input required type="radio" id="zone4" name="zone" value="Zone 4" />
249 <label for="zone4">Zone 4</label>
250 </div>
251 <div>
252 <input required type="radio" id="zone5" name="zone" value="Zone 5" />
253 <label for="zone5">Zone 5</label>
254 </div>
255
256 <input type="submit" name="go" value="Generate Ticket" />
257 </fieldset>
258 </form>
259 <form>
260 <fieldset>
261 <legend>MARC Train</legend>
262
263 <div>
264 <label for="code3">
265 Daily security code (optional):
266 </label>
267 <input id="code3"
268 name="code"
269 type="text"
270 size="2"
271 minlength="2"
272 maxlength="2"
273 pattern="[a-zA-Z0-9]*" />
274 </div>
275
276 <p>Origin:</p>
277 <div>
278 <input required type="radio" id="origin1" name="origin" value="BAL" />
279 <label for="origin1">Baltimore/Penn (Penn Line)</label>
280 </div>
281 <div>
282 <input required type="radio" id="origin2" name="origin" value="BCA" />
283 <label for="origin2">Baltimore/Camden (Camden Line)</label>
284 </div>
285 <div>
286 <input required type="radio" id="origin3" name="origin" value="BWE" />
287 <label for="origin3">Bowie State (Penn Line)</label>
288 </div>
289 <div>
290 <input required type="radio" id="origin4" name="origin" value="BWI" />
291 <label for="origin4">BWI Airport (Penn Line)</label>
292 </div>
293 <div>
294 <input required type="radio" id="origin5" name="origin" value="CPK" />
295 <label for="origin5">College Park (Camden Line)</label>
296 </div>
297 <div>
298 <input required type="radio" id="origin6" name="origin" value="SEB" />
299 <label for="origin6">Seabrook (Penn Line)</label>
300 </div>
301 <div>
302 <input required type="radio" id="origin7" name="origin" value="WAS" />
303 <label for="origin7">Washington D.C.</label>
304 </div>
305 <div>
306 <input required type="radio" id="origin8" name="origin" value="WBL" />
307 <label for="origin8">West Baltimore (Penn Line)</label>
308 </div>
309
310 <p>Destination:</p>
311 <div>
312 <input required type="radio" id="destination1" name="destination" value="BAL" />
313 <label for="destination1">Baltimore/Penn (Penn Line)</label>
314 </div>
315 <div>
316 <input required type="radio" id="destination2" name="destination" value="BCA" />
317 <label for="destination2">Baltimore/Camden (Camden Line)</label>
318 </div>
319 <div>
320 <input required type="radio" id="destination3" name="destination" value="BWE" />
321 <label for="destination3">Bowie State (Penn Line)</label>
322 </div>
323 <div>
324 <input required type="radio" id="destination4" name="destination" value="BWI" />
325 <label for="destination4">BWI Airport (Penn Line)</label>
326 </div>
327 <div>
328 <input required type="radio" id="destination5" name="destination" value="CPK" />
329 <label for="destination5">College Park (Camden Line)</label>
330 </div>
331 <div>
332 <input required type="radio" id="destination6" name="destination" value="SEB" />
333 <label for="destination6">Seabrook (Penn Line)</label>
334 </div>
335 <div>
336 <input required type="radio" id="destination7" name="destination" value="WAS" />
337 <label for="destination7">Washington D.C.</label>
338 </div>
339 <div>
340 <input required type="radio" id="destination8" name="destination" value="WBL" />
341 <label for="destination8">West Baltimore (Penn Line)</label>
342 </div>
343
344 <input type="hidden" name="serviceid" value="R" />
345 <input type="hidden" name="servicename" value="MARC Train" />
346 <input type="submit" name="go" value="Generate Ticket" />
347 </fieldset>
348 </form>
349 </div>
350
351 @SVGDATA@
352
353 <script>
354
355 /***********************************************/
356 /* First, center the ticket within the browser */
357 /***********************************************/
358
359 function center_ticket() {
360 /* We're relying on the SVG being the full height of the
361 * viewport already, and on the aspect ratio being
362 * preserved. First, find the center of the ticket. */
363 const r = document.getElementById("ticket").getBoundingClientRect();
364 const c = r.left + (r.width / 2);
365
366 /* That's the center-line of the ticket. We want to move it to
367 * the center-line of the viewport. */
368 const vc = document.documentElement.clientWidth / 2;
369
370 /* This is how much we need to translate the SVG */
371 const hdelta = vc - c;
372
373 /* But before we can set the absolute left-coordinate of the
374 * SVG, we need to know where it is now. Note: without the
375 * "px" this doesn't default to pixels like CSS does. */
376 const svg = document.querySelector("svg");
377 svg.style.left = (svg.getBoundingClientRect().left + hdelta) + "px";
378 }
379
380
381 /******************************/
382 /* Set the service identifier */
383 /******************************/
384 function set_service_id() {
385 const sid = document.getElementById("serviceid");
386
387 /* Get the "serviceid" from the querystring if it's there */
388 const params = new URLSearchParams(document.location.search);
389 if (params.get("serviceid")) {
390 sid.textContent = params.get("serviceid");
391 }
392
393 /* Otherwise, leave it at "F" */
394 }
395
396 /************************/
397 /* Set the service name */
398 /************************/
399 function set_service_name() {
400 const sid = document.getElementById("servicename");
401
402 /* Get the "servicename" from the querystring if it's there */
403 const params = new URLSearchParams(document.location.search);
404 if (params.get("servicename")) {
405 sid.textContent = params.get("servicename");
406 }
407
408 /* Otherwise, leave it at "BaltimoreLink" */
409 }
410
411
412 /************************/
413 /* Set the service zone */
414 /************************/
415 function set_service_zone(event, zone) {
416 /* We can take the zone as a parameter too; this allows us to
417 * use this function for the (computed) MARC Train zone and
418 * not just the querystring Commuter Bus zone. The extra
419 * "event" parameter is there for the event listener, which
420 * would otherwise stuff an onload event into the zone
421 * parameter. "Thankfully" javascript lets us call a
422 * two-argument function with one argument and thereby abuse
423 * the event handler for this. */
424 const z = document.getElementById("zone");
425 const params = new URLSearchParams(document.location.search);
426
427 if (zone) {
428 z.textContent = zone;
429 z.style.display = "block"; /* It's hidden by default */
430 }
431 else if (params.get("zone")) {
432 /* Get the "zone" from the querystring if it's there */
433 z.textContent = params.get("zone");
434 z.style.display = "block"; /* It's hidden by default */
435 }
436
437 /* Otherwise, leave it blank (and hidden) */
438 }
439
440 /***********************************************************/
441 /* Resize the ticket background based on the service name */
442 /***********************************************************/
443
444 function resize_ticket() {
445 /* Get the "servicename" from the querystring if it's there */
446 const params = new URLSearchParams(document.location.search);
447 const tbg = document.getElementById("ticketbg");
448 const t = document.getElementById("ticket");
449 const sn = document.getElementById("servicename");
450
451 if (params.get("servicename") == "Commuter Bus") {
452 /* The top of the background is initially at y=246.859, and
453 * we scale it by a factor of 1.12 to y=276.482 for a change
454 * of 29.623. So after we scale it, we translate it upwards
455 * by that amount to put it back where it started. */
456 tbg.setAttribute("transform", "translate(0 -29.623) scale(1 1.12)");
457
458 /* Now translate the entire ticket up by the magic amount, 1/5
459 * of the size change we made to the background. This ratio
460 * was found by measuring pixels in side-by-side screenshots
461 * of BaltimoreLink and Commuter Bus tickets. */
462 t.setAttribute("transform", "translate(0 -9.33)");
463
464 /* More magic numbers discovered by comparing the two
465 * tickets overlayed in inkscape */
466 sn.setAttribute("transform", "translate(0 64.28)");
467 }
468 else if (params.get("servicename") == "MARC Train") {
469 /* insane tricks are explained above */
470 tbg.setAttribute("transform",
471 "translate(0 -72.378) scale(1 1.2932)");
472 t.setAttribute("transform", "translate(0 -67.17)");
473 sn.setAttribute("transform", "translate(0 131.0)");
474 }
475
476 /* Otherwise, leave it alone. The SVG was designed with the
477 * BaltimoreLink ticket in mind */
478 }
479
480 /****************************************/
481 /* Set and reposition the security code */
482 /****************************************/
483
484 function set_code() {
485 const ct = document.getElementById("codetext");
486
487 /* Get the "code" from the querystring if it's there */
488 const params = new URLSearchParams(document.location.search);
489 if (params.get("code")) {
490 ct.textContent = params.get("code").toUpperCase();
491 }
492 else {
493 /* Otherwise, use a random code */
494 const bucket = ["0","1","2","3","4","5","6","7","8","9",
495 "A","B","C","D","E","F","G","H","I","J",
496 "K","L","M","N","O","P","Q","R","S","T",
497 "U","V","W","X","Y","Z"];
498
499 /* Two random ints between 0 and 35 */
500 const i1 = Math.floor(Math.random() * 36);
501 const i2 = Math.floor(Math.random() * 36);
502 const d1 = bucket[i1];
503 const d2 = bucket[i2];
504 ct.textContent = d1 + d2;
505 }
506 }
507
508
509 function center_code() {
510 /* Center the security code inside its red box */
511 const ct = document.getElementById("codetext");
512 const bg = document.getElementById("codebg");
513
514 /* First, find the center of the red box */
515 const r1 = bg.getBoundingClientRect();
516 const c1 = r1.left + (r1.width / 2);
517
518 /* Now the center of the code text */
519 const r2 = ct.getBoundingClientRect();
520 const c2 = r2.left + (r2.width / 2);
521
522 /* What do we add to c2 to make it equal to c1? */
523 const hdelta = c1 - c2;
524
525 /* We've measured everything so far in "client rect"
526 * coordinates, because that's the only available measurement
527 * we have for the width of the <text> element after futzing
528 * with its contents. But when we reposition that <text>
529 * element, it will be by adjusting its "x" attribute, and
530 * that attribute uses a different coordinate system than the
531 * client rect does. Specifically, "x" refers to an offset
532 * within the SVG's coordinate system, and the client rect
533 * coordinates are pixels on-screen. To convert between the
534 * two, we can take the "width" attribute of the background
535 * element and compare it to the width of the background
536 * element's client rect. Since the size of the background is
537 * fixed, this should give us a multiplier that turns client recr
538 * distances (what we have) into SVG distances (what we want) */
539 const client_to_svg = parseFloat(bg.getAttribute("width"))/r1.width;
540
541 /* Convert hdelta from client rect to SVG coordinates */
542 const svg_hdelta = hdelta * client_to_svg;
543
544 /* Since this <text> element has an "x" attribute it's easier for
545 * us to shift that than it is to mess with the "left" style. */
546 ct.setAttribute("x", parseFloat(ct.getAttribute("x")) + svg_hdelta);
547 }
548
549 /*****************************************/
550 /* Next, set up the ticket date and time */
551 /*****************************************/
552
553 function set_ticket_expiry() {
554 /* There are two parameters, time and date, that we store in one
555 * underlying "date" variable. Default both to an hour and a
556 * half from now. This is what the CharmPass app does for
557 * one-way tickets. */
558 const date = new Date();
559
560 /* BaltimoreLink and MARC Train are valid for an hour and a half */
561 let minutes = 90;
562 const params = new URLSearchParams(document.location.search);
563 if (params.get("servicename") == "Commuter Bus") {
564 /* But commuter bus tickets are only valid for ten minutes */
565 minutes = 10;
566 }
567
568 /* We use the low-level get/setTime to change the number of
569 * milliseconds since the epoch that this date represents
570 * Obviously correct, and avoids all suspicious corner cases
571 * for a few more decades. */
572 date.setTime(date.getTime() + (minutes*60*1000));
573
574 tt = document.getElementById("tickettime");
575 tt.textContent = date.toLocaleTimeString();
576
577 const td = document.getElementById("ticketdate");
578 const dateopts = {
579 day: "2-digit",
580 month: "2-digit",
581 year: "2-digit"
582 };
583 td.textContent = date.toLocaleDateString("en-US", dateopts);
584 }
585
586
587 /*********************************************************/
588 /* Finally, the onclick handler for the night/day switch */
589 /*********************************************************/
590
591 /* We always start in "day" mode */
592 is_day = true;
593
594 function set_day() {
595 sky.style.fill = "#efb02f";
596 }
597
598 function set_night() {
599 sky.style.fill = "#143b66";
600 }
601
602 function swap_colors() {
603 if (is_day) {
604 set_night();
605 is_day = false;
606 }
607 else {
608 set_day();
609 is_day = true;
610 }
611 }
612
613
614 /*******************************************************/
615 /* Compute the MARC "zone" from its origin/destination */
616 /*******************************************************/
617
618 /* Sorted on the first component, then the second */
619 const zone_map = {
620 BAL_BWE: 2,
621 BAL_BWI: 1,
622 BAL_SEB: 3,
623 BAL_WAS: 4,
624 BAL_WBL: 1,
625 BCA_CPK_: 3,
626 BCA_WAS_: 4,
627 BWI_BWE: 1,
628 BWI_WAS: 3,
629 BWI_WBL: 1
630 };
631
632 /* Compute the zone (string) for the given origin/destination pair.
633 * If we don't know it or if you chose in invalid pair (destination
634 * not on the same line as your origin?) then the empty string is
635 * returned. */
636 function compute_marc_zone(src, dest) {
637 /* Forward direction key for zone_map */
638 const fwd = src + "_" + dest;
639
640 /* Reverse direction key for zone_map. The zone_map only
641 * has them listed in one direction, so we check both
642 * directions here. */
643 const rev = dest + "_" + src;
644
645 /* The default. Obviously wrong for when we don't
646 * have the necessary data. */
647 let zone = -1;
648
649 if (zone_map[fwd]) {
650 zone = zone_map[fwd];
651 }
652 else if (zone_map[rev]) {
653 zone = zone_map[rev];
654 }
655
656 /* Convert the number to a string */
657 switch (zone) {
658 case 1:
659 return "One Zone";
660 case 2:
661 return "Two Zone";
662 case 3:
663 return "Three Zone";
664 case 4:
665 return "Four Zone";
666 default:
667 return "";
668 }
669 }
670
671 function set_marc_zone() {
672 const params = new URLSearchParams(document.location.search);
673 if (params.get("origin") && params.get("destination")) {
674 const src = params.get("origin");
675 const dest = params.get("destination");
676 const zone = compute_marc_zone(src, dest);
677
678 set_service_zone(null, zone);
679 }
680 }
681
682
683 /*****************************************************/
684 /* Set the origin and destination for the MARC Train */
685 /*****************************************************/
686
687 function set_marc_origin_destination() {
688 const params = new URLSearchParams(document.location.search);
689 if (!params.get("origin") || !params.get("destination")) {
690 return;
691 }
692
693 const src = params.get("origin");
694 const dest = params.get("destination");
695
696 /* origindest contains both the origin and destination */
697 const origindest = document.getElementById("origindest");
698
699 const origin = document.getElementById("origin");
700 const destination = document.getElementById("destination");
701
702 origin.textContent = params.get("origin");
703 destination.textContent = params.get("destination");
704
705 origindest.style.display = "block"; /* It's hidden by default */
706 }
707
708 /******************************************/
709 /* Display the ticket (and hide the menu) */
710 /******************************************/
711
712 function go() {
713 /* To create our "window" onto the scene, we're going to slide the
714 * SVG off the left-hand side of the screen, and we don't want
715 * scroll bars to appear. */
716 document.body.style.overflow = "hidden";
717 document.body.style.margin = "0";
718 document.body.style.padding = "0";
719
720 const svg = document.querySelector("svg");
721 const menu = document.getElementById("menu");
722 svg.style.display = "initial";
723 menu.style.display = "none";
724 }
725
726 /*****************************************************/
727 /* Add event handlers for all of the functions above */
728 /*****************************************************/
729
730 const params = new URLSearchParams(document.location.search);
731 if (params.get("go")) {
732 /* First unhide the SVG (swap it with the form) */
733 window.addEventListener("load", go);
734
735 /* Center the ticket once when the page has loaded */
736 window.addEventListener("load", center_ticket);
737
738 /* Re-center the ticket when the window is resized */
739 window.addEventListener("resize", center_ticket);
740
741 /* Set the service identifier when the page has loaded */
742 window.addEventListener("load", set_service_id);
743
744 /* Set the service name when the page has loaded */
745 window.addEventListener("load", set_service_name);
746
747 /* Set the service zone when the page has loaded */
748 window.addEventListener("load", set_service_zone);
749
750 /* Resize the ticket background if necessary */
751 window.addEventListener("load", resize_ticket);
752
753 /* Set the security code text when the page has loaded */
754 window.addEventListener("load", set_code);
755
756 /* Center the security code text when the page has loaded; in
757 * particular, after we set the code. */
758 window.addEventListener("load", center_code);
759
760 /* Set the ticket expiration date/time upon page load */
761 window.addEventListener("load", set_ticket_expiry);
762
763 /* Set the MARC Train origin and destination, if applicable */
764 window.addEventListener("load", set_marc_origin_destination);
765
766 /* Set the MARC Train zone, if applicable */
767 window.addEventListener("load", set_marc_zone);
768
769 /* Swap colors when the screen is tapped */
770 document.body.addEventListener("click", swap_colors);
771 }
772
773 </script>
774 </body>
775 </html>