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=
"Generate 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=
"Generate Ticket" />
273 <legend>MARC Train
</legend>
274 <input type=
"hidden" name=
"serviceid" value=
"R" />
275 <input type=
"hidden" name=
"servicename" value=
"MARC Train" />
276 <input type=
"submit" name=
"go" value=
"Generate Ticket" />
285 /***********************************************/
286 /* First, center the ticket within the browser */
287 /***********************************************/
289 function center_ticket() {
290 /* We're relying on the SVG being the full height of the
291 * viewport already, and on the aspect ratio being
292 * preserved. First, find the center of the ticket. */
293 const r = document.getElementById("ticket").getBoundingClientRect();
294 const c = r.left + (r.width /
2);
296 /* That's the center-line of the ticket. We want to move it to
297 * the center-line of the viewport. */
298 const vc = document.documentElement.clientWidth /
2;
300 /* This is how much we need to translate the SVG */
301 const hdelta = vc - c;
303 /* But before we can set the absolute left-coordinate of the
304 * SVG, we need to know where it is now. Note: without the
305 * "px" this doesn't default to pixels like CSS does. */
306 const svg = document.querySelector("svg");
307 svg.style.left = (svg.getBoundingClientRect().left + hdelta) + "px";
311 /******************************/
312 /* Set the service identifier */
313 /******************************/
314 function set_service_id() {
315 const sid = document.getElementById("serviceid");
317 /* Get the "serviceid" from the querystring if it's there */
318 let params = new URLSearchParams(document.location.search);
319 if (params.get("serviceid")) {
320 sid.textContent = params.get("serviceid");
323 /* Otherwise, leave it at "F" */
326 /******************************/
327 /* Set the service name */
328 /******************************/
329 function set_service_name() {
330 const sid = document.getElementById("servicename");
332 /* Get the "servicename" from the querystring if it's there */
333 let params = new URLSearchParams(document.location.search);
334 if (params.get("servicename")) {
335 sid.textContent = params.get("servicename");
338 /* Otherwise, leave it at "BaltimoreLink" */
341 /****************************************/
342 /* Set and reposition the security code */
343 /****************************************/
345 function set_code() {
346 const ct = document.getElementById("codetext");
348 /* Get the "code" from the querystring if it's there */
349 let params = new URLSearchParams(document.location.search);
350 if (params.get("code")) {
351 ct.textContent = params.get("code");
354 /* Otherwise, use a random code */
355 const bucket = ["
0","
1","
2","
3","
4","
5","
6","
7","
8","
9",
356 "A","B","C","D","E","F","G","H","I","J",
357 "K","L","M","N","O","P","Q","R","S","T",
358 "U","V","W","X","Y","Z"];
360 /* Two random ints between
0 and
35 */
361 const i1 = Math.floor(Math.random() *
36);
362 const i2 = Math.floor(Math.random() *
36);
363 const d1 = bucket[i1];
364 const d2 = bucket[i2];
365 ct.textContent = d1 + d2;
370 function center_code() {
371 /* Center the security code inside its red box */
372 const ct = document.getElementById("codetext");
373 const bg = document.getElementById("codebg");
375 /* First, find the center of the red box */
376 const r1 = bg.getBoundingClientRect();
377 const c1 = r1.left + (r1.width /
2);
379 /* Now the center of the code text */
380 const r2 = ct.getBoundingClientRect();
381 const c2 = r2.left + (r2.width /
2);
383 /* What do we add to c2 to make it equal to c1? */
384 const hdelta = c1 - c2;
386 /* We've measured everything so far in "client rect"
387 * coordinates, because that's the only available measurement
388 * we have for the width of the
<text> element after futzing
389 * with its contents. But when we reposition that
<text>
390 * element, it will be by adjusting its "x" attribute, and
391 * that attribute uses a different coordinate system than the
392 * client rect does. Specifically, "x" refers to an offset
393 * within the SVG's coordinate system, and the client rect
394 * coordinates are pixels on-screen. To convert between the
395 * two, we can take the "width" attribute of the background
396 * element and compare it to the width of the background
397 * element's client rect. Since the size of the background is
398 * fixed, this should give us a multiplier that turns client recr
399 * distances (what we have) into SVG distances (what we want) */
400 const client_to_svg = parseFloat(bg.getAttribute("width"))/r1.width;
402 /* Convert hdelta from client rect to SVG coordinates */
403 const svg_hdelta = hdelta * client_to_svg;
405 /* Since this
<text> element has an "x" attribute it's easier for
406 * us to shift that than it is to mess with the "left" style. */
407 ct.setAttribute("x", parseFloat(ct.getAttribute("x")) + svg_hdelta);
410 /*****************************************/
411 /* Next, set up the ticket date and time */
412 /*****************************************/
414 function set_ticket_expiry() {
415 /* There are two parameters, time and date, that we store in one
416 * underlying "date" variable. Default both to an hour and a
417 * half from now. This is what the CharmPass app does for
418 * one-way tickets. */
419 const date = new Date();
421 /* Add an hour and a half. We use the low-level get/setTime to
422 * change the number of milliseconds since the epoch that this
423 * date represents. Obviously correct, and avoids all suspicious
424 * corner cases (well, for a few more decades). */
425 date.setTime(date.getTime() + (
90*
60*
1000));
427 tt = document.getElementById("tickettime");
428 tt.textContent = date.toLocaleTimeString();
430 const td = document.getElementById("ticketdate");
436 td.textContent = date.toLocaleDateString("en-US", dateopts);
440 /*********************************************************/
441 /* Finally, the onclick handler for the night/day switch */
442 /*********************************************************/
444 /* We always start in "day" mode */
448 sky.style.fill = "#efb02f";
451 function set_night() {
452 sky.style.fill = "#
143b66";
455 function swap_colors() {
466 /******************************************/
467 /* Display the ticket (and hide the menu) */
468 /******************************************/
471 const svg = document.querySelector("svg");
472 const menu = document.getElementById("menu");
473 svg.style.display = "initial";
474 menu.style.display = "none";
477 /*****************************************************/
478 /* Add event handlers for all of the functions above */
479 /*****************************************************/
481 const params = new URLSearchParams(document.location.search);
482 if (params.get("go")) {
483 /* First unhide the SVG (swap it with the form) */
484 window.addEventListener("load", go);
486 /* Center the ticket once when the page has loaded */
487 window.addEventListener("load", center_ticket);
489 /* Re-center the ticket when the window is resized */
490 window.addEventListener("resize", center_ticket);
492 /* Set the service identifier when the page has loaded */
493 window.addEventListener("load", set_service_id);
495 /* Set the service name when the page has loaded */
496 window.addEventListener("load", set_service_name);
498 /* Set the security code text when the page has loaded */
499 window.addEventListener("load", set_code);
501 /* Center the security code text when the page has loaded; in
502 * particular, after we set the code. */
503 window.addEventListener("load", center_code);
505 /* Set the ticket expiration date/time upon page load */
506 window.addEventListener("load", set_ticket_expiry);
508 /* Swap colors when the screen is tapped */
509 document.body.addEventListener("click", swap_colors);