]> gitweb.michael.orlitzky.com - charm-bypass.git/blob - index.html.in
454cca8758c764c53b367f0c108bc7a07c491c63
[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
6 <title>
7 CharmBypass.
8 </title>
9
10 <style>
11 /*
12 * Reset styles for the html and body elements, the only two HTML
13 * elements we use. */
14 html, body {
15 margin: 0;
16 padding: 0;
17 border: 0;
18 font-weight: normal;
19 font-style: inherit;
20 font-size: 100%;
21 line-height: 1.5;
22 font-family: inherit;
23 text-align: inherit;
24 text-decoration: none;
25 vertical-align: baseline;
26 background: transparent;
27 }
28
29 html, body {
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. */
33 overflow: hidden;
34 }
35
36 svg {
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. */
42 position: fixed;
43 top: 0;
44 left: 0;
45 height: 100%;
46
47 /* Hide everything by default. We only show it once the user has
48 * submitted the menu form */
49 display: none;
50 }
51
52 /* The blinking fade in/out animation for the ticket date and time */
53 @keyframes blink {
54 25% {
55 opacity: 0.5;
56 }
57 50% {
58 opacity: 0;
59 }
60 75% {
61 opacity: 0.5;
62 }
63 }
64
65 #tickettime, #ticketdate {
66 /* 300 two-second blinks is ten minutes */
67 animation: blink 2s linear 300;
68 }
69
70 /* Define, load, and specify the custom font we use for the ticket
71 * date, time, and service name. */
72 @font-face {
73 font-family: "CharmBypass Regular";
74 src:
75 url("data:font/woff2;base64,@CBPREGULAR@") format("woff2")
76 }
77
78 #servicename, #tickettime, #ticketdate, #codetext {
79 font-family: "CharmBypass Regular", sans-serif;
80 }
81
82 @font-face {
83 font-family: "CharmBypass Bold";
84 src:
85 url("data:font/woff2;base64,@CBPBOLD@") format("woff2")
86 }
87
88 #serviceid {
89 font-family: "CharmBypass Bold", sans-serif;
90 font-weight: bold;
91 }
92
93 /************************/
94 /* Scrolling animations */
95 /************************/
96
97 /* Bus */
98 @keyframes busroll {
99 from {
100 transform: translateX(0%);
101 }
102 to {
103 transform: translateX(-100%);
104 }
105 }
106
107 #bus {
108 animation: busroll 23s linear infinite;
109 }
110
111
112 /* Tram */
113 @keyframes tramroll {
114 from {
115 transform: translateX(0%);
116 }
117 to {
118 transform: translateX(100%);
119 }
120 }
121
122 #tram {
123 animation: tramroll 17s linear infinite;
124 }
125
126
127 /* Tram */
128 @keyframes trainroll {
129 from {
130 transform: translateX(0%);
131 }
132 to {
133 transform: translateX(100%);
134 }
135 }
136
137 #train {
138 animation: trainroll 11s linear infinite;
139 }
140
141
142 /* Clouds */
143 @keyframes cloudsfloat {
144 from {
145 transform: translateX(0%);
146 }
147 to {
148 transform: translateX(-50%);
149 }
150 }
151
152 #clouds {
153 animation: cloudsfloat 40s linear infinite;
154 }
155
156 @keyframes cloudscopyfloat {
157 from {
158 transform: translateX(0%);
159 }
160 to {
161 transform: translateX(-50%);
162 }
163 }
164
165 #cloudscopy {
166 animation: cloudscopyfloat 40s linear infinite;
167 }
168
169
170 /* Trees */
171 @keyframes treespass {
172 from {
173 transform: translateX(0%);
174 }
175 to {
176 transform: translateX(-50%);
177 }
178 }
179
180 #trees {
181 /* The trees move a little faster than the clouds */
182 animation: treespass 30s linear infinite;
183 }
184
185 @keyframes treescopypass {
186 from {
187 transform: translateX(0%);
188 }
189 to {
190 transform: translateX(-50%);
191 }
192 }
193
194 #treescopy {
195 /* The trees move a little faster than the clouds */
196 animation: treescopypass 30s linear infinite;
197 }
198
199
200 /* City skyline */
201 @keyframes cityscroll {
202 from {
203 transform: translateX(0%);
204 }
205 to {
206 transform: translateX(-50%);
207 }
208 }
209
210 #city {
211 /* The city moves faster than the clouds but slower
212 * than the trees */
213 animation: cityscroll 35s linear infinite;
214 }
215
216 @keyframes citycopyscroll {
217 from {
218 transform: translateX(0%);
219 }
220 to {
221 transform: translateX(-50%);
222 }
223 }
224
225 #citycopy {
226 /* The city moves faster than the clouds but slower
227 * than the trees */
228 animation: citycopyscroll 35s linear infinite;
229 }
230 </style>
231 </head>
232
233 <body>
234 <div id="menu">
235 <form>
236 <fieldset>
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" />
240 </fieldset>
241 </form>
242 <form>
243 <fieldset>
244 <legend>Commuter Bus</legend>
245 <input type="hidden" name="serviceid" value="R" />
246 <input type="hidden" name="servicename" value="Commuter Bus" />
247 <div>
248 <input type="radio" id="zone1" name="zone" value="Zone 1" />
249 <label for="zone1">Zone 1</label>
250 </div>
251 <div>
252 <input type="radio" id="zone2" name="zone" value="Zone 2" />
253 <label for="zone2">Zone 2</label>
254 </div>
255 <div>
256 <input type="radio" id="zone3" name="zone" value="Zone 3" />
257 <label for="zone3">Zone 3</label>
258 </div>
259 <div>
260 <input type="radio" id="zone4" name="zone" value="Zone 4" />
261 <label for="zone4">Zone 4</label>
262 </div>
263 <div>
264 <input type="radio" id="zone5" name="zone" value="Zone 5" />
265 <label for="zone5">Zone 5</label>
266 </div>
267
268 <input type="submit" name="go" value="Get Ticket" />
269 </fieldset>
270 </form>
271 <form>
272 <fieldset>
273 <legend>MARC Train</legend>
274
275 <h3>Origin</h3>
276 <div>
277 <input type="radio" id="origin1" name="origin" value="BAL" />
278 <label for="origin1">Baltimore/Penn (Penn Line)</label>
279 </div>
280 <div>
281 <input type="radio" id="origin2" name="origin" value="BCA" />
282 <label for="origin2">Baltimore/Camden (Camden Line)</label>
283 </div>
284 <div>
285 <input type="radio" id="origin3" name="origin" value="BWE" />
286 <label for="origin3">Bowie State (Penn Line)</label>
287 </div>
288 <div>
289 <input type="radio" id="origin4" name="origin" value="BWI" />
290 <label for="origin4">BWI Airport (Penn Line)</label>
291 </div>
292 <div>
293 <input type="radio" id="origin5" name="origin" value="CPK" />
294 <label for="origin5">College Park (Camden Line)</label>
295 </div>
296 <div>
297 <input type="radio" id="origin6" name="origin" value="SEB" />
298 <label for="origin6">Seabrook (Penn Line)</label>
299 </div>
300 <div>
301 <input type="radio" id="origin7" name="origin" value="WAS" />
302 <label for="origin7">Washington D.C.</label>
303 </div>
304 <div>
305 <input type="radio" id="origin8" name="origin" value="WBL" />
306 <label for="origin8">West Baltimore (Penn Line)</label>
307 </div>
308
309 <h3>Destination</h3>
310 <div>
311 <input type="radio" id="destination1" name="destination" value="BAL" />
312 <label for="destination1">Baltimore/Penn (Penn Line)</label>
313 </div>
314 <div>
315 <input type="radio" id="destination2" name="destination" value="BCA" />
316 <label for="destination2">Baltimore/Camden (Camden Line)</label>
317 </div>
318 <div>
319 <input type="radio" id="destination3" name="destination" value="BWE" />
320 <label for="destination3">Bowie State (Penn Line)</label>
321 </div>
322 <div>
323 <input type="radio" id="destination4" name="destination" value="BWI" />
324 <label for="destination4">BWI Airport (Penn Line)</label>
325 </div>
326 <div>
327 <input type="radio" id="destination5" name="destination" value="CPK" />
328 <label for="destination5">College Park (Camden Line)</label>
329 </div>
330 <div>
331 <input type="radio" id="destination6" name="destination" value="SEB" />
332 <label for="destination6">Seabrook (Penn Line)</label>
333 </div>
334 <div>
335 <input type="radio" id="destination7" name="destination" value="WAS" />
336 <label for="destination7">Washington D.C.</label>
337 </div>
338 <div>
339 <input type="radio" id="destination8" name="destination" value="WBL" />
340 <label for="destination8">West Baltimore (Penn Line)</label>
341 </div>
342
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" />
346 </fieldset>
347 </form>
348 </div>
349
350 @SVGDATA@
351
352 <script>
353
354 /***********************************************/
355 /* First, center the ticket within the browser */
356 /***********************************************/
357
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);
364
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;
368
369 /* This is how much we need to translate the SVG */
370 const hdelta = vc - c;
371
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";
377 }
378
379
380 /******************************/
381 /* Set the service identifier */
382 /******************************/
383 function set_service_id() {
384 const sid = document.getElementById("serviceid");
385
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");
390 }
391
392 /* Otherwise, leave it at "F" */
393 }
394
395 /******************************/
396 /* Set the service name */
397 /******************************/
398 function set_service_name() {
399 const sid = document.getElementById("servicename");
400
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");
405 }
406
407 /* Otherwise, leave it at "BaltimoreLink" */
408 }
409
410 /****************************************/
411 /* Set and reposition the security code */
412 /****************************************/
413
414 function set_code() {
415 const ct = document.getElementById("codetext");
416
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");
421 }
422 else {
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"];
428
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;
435 }
436 }
437
438
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");
443
444 /* First, find the center of the red box */
445 const r1 = bg.getBoundingClientRect();
446 const c1 = r1.left + (r1.width / 2);
447
448 /* Now the center of the code text */
449 const r2 = ct.getBoundingClientRect();
450 const c2 = r2.left + (r2.width / 2);
451
452 /* What do we add to c2 to make it equal to c1? */
453 const hdelta = c1 - c2;
454
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;
470
471 /* Convert hdelta from client rect to SVG coordinates */
472 const svg_hdelta = hdelta * client_to_svg;
473
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);
477 }
478
479 /*****************************************/
480 /* Next, set up the ticket date and time */
481 /*****************************************/
482
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();
489
490 /* BaltimoreLink and MARC Train are valid for an hour and a half */
491 let minutes = 90;
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 */
495 minutes = 10;
496 }
497
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));
503
504 tt = document.getElementById("tickettime");
505 tt.textContent = date.toLocaleTimeString();
506
507 const td = document.getElementById("ticketdate");
508 const dateopts = {
509 day: "2-digit",
510 month: "2-digit",
511 year: "2-digit"
512 };
513 td.textContent = date.toLocaleDateString("en-US", dateopts);
514 }
515
516
517 /*********************************************************/
518 /* Finally, the onclick handler for the night/day switch */
519 /*********************************************************/
520
521 /* We always start in "day" mode */
522 is_day = true;
523
524 function set_day() {
525 sky.style.fill = "#efb02f";
526 }
527
528 function set_night() {
529 sky.style.fill = "#143b66";
530 }
531
532 function swap_colors() {
533 if (is_day) {
534 set_night();
535 is_day = false;
536 }
537 else {
538 set_day();
539 is_day = true;
540 }
541 }
542
543 /******************************************/
544 /* Display the ticket (and hide the menu) */
545 /******************************************/
546
547 function go() {
548 const svg = document.querySelector("svg");
549 const menu = document.getElementById("menu");
550 svg.style.display = "initial";
551 menu.style.display = "none";
552 }
553
554 /*****************************************************/
555 /* Add event handlers for all of the functions above */
556 /*****************************************************/
557
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);
562
563 /* Center the ticket once when the page has loaded */
564 window.addEventListener("load", center_ticket);
565
566 /* Re-center the ticket when the window is resized */
567 window.addEventListener("resize", center_ticket);
568
569 /* Set the service identifier when the page has loaded */
570 window.addEventListener("load", set_service_id);
571
572 /* Set the service name when the page has loaded */
573 window.addEventListener("load", set_service_name);
574
575 /* Set the security code text when the page has loaded */
576 window.addEventListener("load", set_code);
577
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);
581
582 /* Set the ticket expiration date/time upon page load */
583 window.addEventListener("load", set_ticket_expiry);
584
585 /* Swap colors when the screen is tapped */
586 document.body.addEventListener("click", swap_colors);
587 }
588
589 </script>
590 </body>
591 </html>