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