4 <meta name=
"viewport" content=
"width=device-width, initial-scale=1" />
12 * Reset styles for the html and body elements, the only two HTML
24 text-decoration: none;
25 vertical-align: baseline;
26 background: transparent;
30 /* To create our "window" onto the scene, we're going to slide the
31 * SVG off the left-hand side of the screen, and we don't want
32 * scroll bars to appear. */
37 /* Set the height to
100% of the screen, which we'll keep; and the
38 * initial position to (
0,
0), which we're going to change
39 * every time the window is resized, because the exact amount
40 * that we have to slide the SVG to the left to get the ticket
41 * into the center will change. */
47 /* Hide everything by default. We only show it once the user has
48 * submitted the menu form */
52 /* The blinking fade in/out animation for the ticket date and time */
65 #tickettime, #ticketdate {
66 /*
300 two-second blinks is ten minutes */
67 animation: blink
2s linear
300;
70 /* Define, load, and specify the custom font we use for the ticket
71 * date, time, and service name. */
73 font-family: "CharmBypass Regular";
75 url("data:font/woff2;base64,@CBPREGULAR@") format("woff2")
78 #servicename, #tickettime, #ticketdate, #codetext {
79 font-family: "CharmBypass Regular", sans-serif;
83 font-family: "CharmBypass Bold";
85 url("data:font/woff2;base64,@CBPBOLD@") format("woff2")
89 font-family: "CharmBypass Bold", sans-serif;
93 /************************/
94 /* Scrolling animations */
95 /************************/
100 transform: translateX(
0%);
103 transform: translateX(-
100%);
108 animation: busroll
23s linear infinite;
113 @keyframes tramroll {
115 transform: translateX(
0%);
118 transform: translateX(
100%);
123 animation: tramroll
17s linear infinite;
128 @keyframes trainroll {
130 transform: translateX(
0%);
133 transform: translateX(
100%);
138 animation: trainroll
11s linear infinite;
143 @keyframes cloudsfloat {
145 transform: translateX(
0%);
148 transform: translateX(-
50%);
153 animation: cloudsfloat
40s linear infinite;
156 @keyframes cloudscopyfloat {
158 transform: translateX(
0%);
161 transform: translateX(-
50%);
166 animation: cloudscopyfloat
40s linear infinite;
171 @keyframes treespass {
173 transform: translateX(
0%);
176 transform: translateX(-
50%);
181 /* The trees move a little faster than the clouds */
182 animation: treespass
30s linear infinite;
185 @keyframes treescopypass {
187 transform: translateX(
0%);
190 transform: translateX(-
50%);
195 /* The trees move a little faster than the clouds */
196 animation: treescopypass
30s linear infinite;
201 @keyframes cityscroll {
203 transform: translateX(
0%);
206 transform: translateX(-
50%);
211 /* The city moves faster than the clouds but slower
213 animation: cityscroll
35s linear infinite;
216 @keyframes citycopyscroll {
218 transform: translateX(
0%);
221 transform: translateX(-
50%);
226 /* The city moves faster than the clouds but slower
228 animation: citycopyscroll
35s linear infinite;
237 <legend>BaltimoreLink (Bus, Light Rail, Metro)
</legend>
238 <input type=
"hidden" name=
"servicename" value=
"BaltimoreLink" />
239 <input type=
"submit" name=
"go" value=
"Get Ticket" />
244 <legend>Commuter Bus
</legend>
245 <input type=
"hidden" name=
"serviceid" value=
"R" />
246 <input type=
"hidden" name=
"servicename" value=
"Commuter Bus" />
248 <input type=
"radio" id=
"zone1" name=
"zone" value=
"Zone 1" />
249 <label for=
"zone1">Zone
1</label>
252 <input type=
"radio" id=
"zone2" name=
"zone" value=
"Zone 2" />
253 <label for=
"zone2">Zone
2</label>
256 <input type=
"radio" id=
"zone3" name=
"zone" value=
"Zone 3" />
257 <label for=
"zone3">Zone
3</label>
260 <input type=
"radio" id=
"zone4" name=
"zone" value=
"Zone 4" />
261 <label for=
"zone4">Zone
4</label>
264 <input type=
"radio" id=
"zone5" name=
"zone" value=
"Zone 5" />
265 <label for=
"zone5">Zone
5</label>
268 <input type=
"submit" name=
"go" value=
"Get Ticket" />
273 <legend>MARC Train
</legend>
277 <input type=
"radio" id=
"origin1" name=
"origin" value=
"BAL" />
278 <label for=
"origin1">Baltimore/Penn (Penn Line)
</label>
281 <input type=
"radio" id=
"origin2" name=
"origin" value=
"BCA" />
282 <label for=
"origin2">Baltimore/Camden (Camden Line)
</label>
285 <input type=
"radio" id=
"origin3" name=
"origin" value=
"BWE" />
286 <label for=
"origin3">Bowie State (Penn Line)
</label>
289 <input type=
"radio" id=
"origin4" name=
"origin" value=
"BWI" />
290 <label for=
"origin4">BWI Airport (Penn Line)
</label>
293 <input type=
"radio" id=
"origin5" name=
"origin" value=
"CPK" />
294 <label for=
"origin5">College Park (Camden Line)
</label>
297 <input type=
"radio" id=
"origin6" name=
"origin" value=
"SEB" />
298 <label for=
"origin6">Seabrook (Penn Line)
</label>
301 <input type=
"radio" id=
"origin7" name=
"origin" value=
"WAS" />
302 <label for=
"origin7">Washington D.C.
</label>
305 <input type=
"radio" id=
"origin8" name=
"origin" value=
"WBL" />
306 <label for=
"origin8">West Baltimore (Penn Line)
</label>
311 <input type=
"radio" id=
"destination1" name=
"destination" value=
"BAL" />
312 <label for=
"destination1">Baltimore/Penn (Penn Line)
</label>
315 <input type=
"radio" id=
"destination2" name=
"destination" value=
"BCA" />
316 <label for=
"destination2">Baltimore/Camden (Camden Line)
</label>
319 <input type=
"radio" id=
"destination3" name=
"destination" value=
"BWE" />
320 <label for=
"destination3">Bowie State (Penn Line)
</label>
323 <input type=
"radio" id=
"destination4" name=
"destination" value=
"BWI" />
324 <label for=
"destination4">BWI Airport (Penn Line)
</label>
327 <input type=
"radio" id=
"destination5" name=
"destination" value=
"CPK" />
328 <label for=
"destination5">College Park (Camden Line)
</label>
331 <input type=
"radio" id=
"destination6" name=
"destination" value=
"SEB" />
332 <label for=
"destination6">Seabrook (Penn Line)
</label>
335 <input type=
"radio" id=
"destination7" name=
"destination" value=
"WAS" />
336 <label for=
"destination7">Washington D.C.
</label>
339 <input type=
"radio" id=
"destination8" name=
"destination" value=
"WBL" />
340 <label for=
"destination8">West Baltimore (Penn Line)
</label>
343 <input type=
"hidden" name=
"serviceid" value=
"R" />
344 <input type=
"hidden" name=
"servicename" value=
"MARC Train" />
345 <input type=
"submit" name=
"go" value=
"Get Ticket" />
354 /***********************************************/
355 /* First, center the ticket within the browser */
356 /***********************************************/
358 function center_ticket() {
359 /* We're relying on the SVG being the full height of the
360 * viewport already, and on the aspect ratio being
361 * preserved. First, find the center of the ticket. */
362 const r = document.getElementById("ticket").getBoundingClientRect();
363 const c = r.left + (r.width /
2);
365 /* That's the center-line of the ticket. We want to move it to
366 * the center-line of the viewport. */
367 const vc = document.documentElement.clientWidth /
2;
369 /* This is how much we need to translate the SVG */
370 const hdelta = vc - c;
372 /* But before we can set the absolute left-coordinate of the
373 * SVG, we need to know where it is now. Note: without the
374 * "px" this doesn't default to pixels like CSS does. */
375 const svg = document.querySelector("svg");
376 svg.style.left = (svg.getBoundingClientRect().left + hdelta) + "px";
380 /******************************/
381 /* Set the service identifier */
382 /******************************/
383 function set_service_id() {
384 const sid = document.getElementById("serviceid");
386 /* Get the "serviceid" from the querystring if it's there */
387 const params = new URLSearchParams(document.location.search);
388 if (params.get("serviceid")) {
389 sid.textContent = params.get("serviceid");
392 /* Otherwise, leave it at "F" */
395 /******************************/
396 /* Set the service name */
397 /******************************/
398 function set_service_name() {
399 const sid = document.getElementById("servicename");
401 /* Get the "servicename" from the querystring if it's there */
402 const params = new URLSearchParams(document.location.search);
403 if (params.get("servicename")) {
404 sid.textContent = params.get("servicename");
407 /* Otherwise, leave it at "BaltimoreLink" */
410 /****************************************/
411 /* Set and reposition the security code */
412 /****************************************/
414 function set_code() {
415 const ct = document.getElementById("codetext");
417 /* Get the "code" from the querystring if it's there */
418 const params = new URLSearchParams(document.location.search);
419 if (params.get("code")) {
420 ct.textContent = params.get("code");
423 /* Otherwise, use a random code */
424 const bucket = ["
0","
1","
2","
3","
4","
5","
6","
7","
8","
9",
425 "A","B","C","D","E","F","G","H","I","J",
426 "K","L","M","N","O","P","Q","R","S","T",
427 "U","V","W","X","Y","Z"];
429 /* Two random ints between
0 and
35 */
430 const i1 = Math.floor(Math.random() *
36);
431 const i2 = Math.floor(Math.random() *
36);
432 const d1 = bucket[i1];
433 const d2 = bucket[i2];
434 ct.textContent = d1 + d2;
439 function center_code() {
440 /* Center the security code inside its red box */
441 const ct = document.getElementById("codetext");
442 const bg = document.getElementById("codebg");
444 /* First, find the center of the red box */
445 const r1 = bg.getBoundingClientRect();
446 const c1 = r1.left + (r1.width /
2);
448 /* Now the center of the code text */
449 const r2 = ct.getBoundingClientRect();
450 const c2 = r2.left + (r2.width /
2);
452 /* What do we add to c2 to make it equal to c1? */
453 const hdelta = c1 - c2;
455 /* We've measured everything so far in "client rect"
456 * coordinates, because that's the only available measurement
457 * we have for the width of the
<text> element after futzing
458 * with its contents. But when we reposition that
<text>
459 * element, it will be by adjusting its "x" attribute, and
460 * that attribute uses a different coordinate system than the
461 * client rect does. Specifically, "x" refers to an offset
462 * within the SVG's coordinate system, and the client rect
463 * coordinates are pixels on-screen. To convert between the
464 * two, we can take the "width" attribute of the background
465 * element and compare it to the width of the background
466 * element's client rect. Since the size of the background is
467 * fixed, this should give us a multiplier that turns client recr
468 * distances (what we have) into SVG distances (what we want) */
469 const client_to_svg = parseFloat(bg.getAttribute("width"))/r1.width;
471 /* Convert hdelta from client rect to SVG coordinates */
472 const svg_hdelta = hdelta * client_to_svg;
474 /* Since this
<text> element has an "x" attribute it's easier for
475 * us to shift that than it is to mess with the "left" style. */
476 ct.setAttribute("x", parseFloat(ct.getAttribute("x")) + svg_hdelta);
479 /*****************************************/
480 /* Next, set up the ticket date and time */
481 /*****************************************/
483 function set_ticket_expiry() {
484 /* There are two parameters, time and date, that we store in one
485 * underlying "date" variable. Default both to an hour and a
486 * half from now. This is what the CharmPass app does for
487 * one-way tickets. */
488 const date = new Date();
490 /* BaltimoreLink and MARC Train are valid for an hour and a half */
492 const params = new URLSearchParams(document.location.search);
493 if (params.get("servicename") == "Commuter Bus") {
494 /* But commuter bus tickets are only valid for ten minutes */
498 /* We use the low-level get/setTime to change the number of
499 * milliseconds since the epoch that this date represents
500 * Obviously correct, and avoids all suspicious corner cases
501 * for a few more decades. */
502 date.setTime(date.getTime() + (minutes*
60*
1000));
504 tt = document.getElementById("tickettime");
505 tt.textContent = date.toLocaleTimeString();
507 const td = document.getElementById("ticketdate");
513 td.textContent = date.toLocaleDateString("en-US", dateopts);
517 /*********************************************************/
518 /* Finally, the onclick handler for the night/day switch */
519 /*********************************************************/
521 /* We always start in "day" mode */
525 sky.style.fill = "#efb02f";
528 function set_night() {
529 sky.style.fill = "#
143b66";
532 function swap_colors() {
543 /******************************************/
544 /* Display the ticket (and hide the menu) */
545 /******************************************/
548 const svg = document.querySelector("svg");
549 const menu = document.getElementById("menu");
550 svg.style.display = "initial";
551 menu.style.display = "none";
554 /*****************************************************/
555 /* Add event handlers for all of the functions above */
556 /*****************************************************/
558 const params = new URLSearchParams(document.location.search);
559 if (params.get("go")) {
560 /* First unhide the SVG (swap it with the form) */
561 window.addEventListener("load", go);
563 /* Center the ticket once when the page has loaded */
564 window.addEventListener("load", center_ticket);
566 /* Re-center the ticket when the window is resized */
567 window.addEventListener("resize", center_ticket);
569 /* Set the service identifier when the page has loaded */
570 window.addEventListener("load", set_service_id);
572 /* Set the service name when the page has loaded */
573 window.addEventListener("load", set_service_name);
575 /* Set the security code text when the page has loaded */
576 window.addEventListener("load", set_code);
578 /* Center the security code text when the page has loaded; in
579 * particular, after we set the code. */
580 window.addEventListener("load", center_code);
582 /* Set the ticket expiration date/time upon page load */
583 window.addEventListener("load", set_ticket_expiry);
585 /* Swap colors when the screen is tapped */
586 document.body.addEventListener("click", swap_colors);