]> gitweb.michael.orlitzky.com - charm-bypass.git/blob - index.html.in
index.html.in: improved menu styling
[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: transit equity whether you like it or not
8 </title>
9
10 <style>
11 input[type=submit] {
12 margin-top: 1em;
13 margin-bottom: 1em;
14 }
15
16 svg {
17 /* Set the height to 100% of the screen, which we'll keep; and the
18 * initial position to (0,0), which we're going to change
19 * every time the window is resized, because the exact amount
20 * that we have to slide the SVG to the left to get the ticket
21 * into the center will change. */
22 position: fixed;
23 top: 0;
24 left: 0;
25 height: 100%;
26
27 /* Hide everything by default. We only show it once the user has
28 * submitted the menu form */
29 display: none;
30 }
31
32 /* The blinking fade in/out animation for the ticket date and time */
33 @keyframes blink {
34 25% {
35 opacity: 0.5;
36 }
37 50% {
38 opacity: 0;
39 }
40 75% {
41 opacity: 0.5;
42 }
43 }
44
45 #tickettime, #ticketdate {
46 /* 300 two-second blinks is ten minutes */
47 animation: blink 2s linear 300;
48 }
49
50 /* Define, load, and specify the custom font we use for the ticket
51 * date, time, and service name. */
52 @font-face {
53 font-family: "CharmBypass Regular";
54 src:
55 url("data:font/woff2;base64,@CBPREGULAR@") format("woff2")
56 }
57
58 #servicename, #tickettime, #ticketdate, #codetext {
59 font-family: "CharmBypass Regular", sans-serif;
60 }
61
62 @font-face {
63 font-family: "CharmBypass Bold";
64 src:
65 url("data:font/woff2;base64,@CBPBOLD@") format("woff2")
66 }
67
68 #serviceid {
69 font-family: "CharmBypass Bold", sans-serif;
70 font-weight: bold;
71 }
72
73 /************************/
74 /* Scrolling animations */
75 /************************/
76
77 /* Bus */
78 @keyframes busroll {
79 from {
80 transform: translateX(0%);
81 }
82 to {
83 transform: translateX(-100%);
84 }
85 }
86
87 #bus {
88 animation: busroll 23s linear infinite;
89 }
90
91
92 /* Tram */
93 @keyframes tramroll {
94 from {
95 transform: translateX(0%);
96 }
97 to {
98 transform: translateX(100%);
99 }
100 }
101
102 #tram {
103 animation: tramroll 17s linear infinite;
104 }
105
106
107 /* Tram */
108 @keyframes trainroll {
109 from {
110 transform: translateX(0%);
111 }
112 to {
113 transform: translateX(100%);
114 }
115 }
116
117 #train {
118 animation: trainroll 11s linear infinite;
119 }
120
121
122 /* Clouds */
123 @keyframes cloudsfloat {
124 from {
125 transform: translateX(0%);
126 }
127 to {
128 transform: translateX(-50%);
129 }
130 }
131
132 #clouds {
133 animation: cloudsfloat 40s linear infinite;
134 }
135
136 @keyframes cloudscopyfloat {
137 from {
138 transform: translateX(0%);
139 }
140 to {
141 transform: translateX(-50%);
142 }
143 }
144
145 #cloudscopy {
146 animation: cloudscopyfloat 40s linear infinite;
147 }
148
149
150 /* Trees */
151 @keyframes treespass {
152 from {
153 transform: translateX(0%);
154 }
155 to {
156 transform: translateX(-50%);
157 }
158 }
159
160 #trees {
161 /* The trees move a little faster than the clouds */
162 animation: treespass 30s linear infinite;
163 }
164
165 @keyframes treescopypass {
166 from {
167 transform: translateX(0%);
168 }
169 to {
170 transform: translateX(-50%);
171 }
172 }
173
174 #treescopy {
175 /* The trees move a little faster than the clouds */
176 animation: treescopypass 30s linear infinite;
177 }
178
179
180 /* City skyline */
181 @keyframes cityscroll {
182 from {
183 transform: translateX(0%);
184 }
185 to {
186 transform: translateX(-50%);
187 }
188 }
189
190 #city {
191 /* The city moves faster than the clouds but slower
192 * than the trees */
193 animation: cityscroll 35s linear infinite;
194 }
195
196 @keyframes citycopyscroll {
197 from {
198 transform: translateX(0%);
199 }
200 to {
201 transform: translateX(-50%);
202 }
203 }
204
205 #citycopy {
206 /* The city moves faster than the clouds but slower
207 * than the trees */
208 animation: citycopyscroll 35s linear infinite;
209 }
210 </style>
211 </head>
212
213 <body>
214 <div id="menu">
215 <h1>CharmBypass</h1>
216 <h2>transit equity whether you like it or not</h2>
217 <form>
218 <fieldset>
219 <legend>BaltimoreLink (Bus, Light Rail, Metro)</legend>
220 <input type="hidden" name="servicename" value="BaltimoreLink" />
221 <input type="submit" name="go" value="Get Ticket" />
222 </fieldset>
223 </form>
224 <form>
225 <fieldset>
226 <legend>Commuter Bus</legend>
227 <input type="hidden" name="serviceid" value="R" />
228 <input type="hidden" name="servicename" value="Commuter Bus" />
229
230 <h3>Zone</h3>
231 <div>
232 <input type="radio" id="zone1" name="zone" value="Zone 1" />
233 <label for="zone1">Zone 1</label>
234 </div>
235 <div>
236 <input type="radio" id="zone2" name="zone" value="Zone 2" />
237 <label for="zone2">Zone 2</label>
238 </div>
239 <div>
240 <input type="radio" id="zone3" name="zone" value="Zone 3" />
241 <label for="zone3">Zone 3</label>
242 </div>
243 <div>
244 <input type="radio" id="zone4" name="zone" value="Zone 4" />
245 <label for="zone4">Zone 4</label>
246 </div>
247 <div>
248 <input type="radio" id="zone5" name="zone" value="Zone 5" />
249 <label for="zone5">Zone 5</label>
250 </div>
251
252 <input type="submit" name="go" value="Get Ticket" />
253 </fieldset>
254 </form>
255 <form>
256 <fieldset>
257 <legend>MARC Train</legend>
258
259 <h3>Origin</h3>
260
261 <div>
262 <input type="radio" id="origin1" name="origin" value="BAL" />
263 <label for="origin1">Baltimore/Penn (Penn Line)</label>
264 </div>
265 <div>
266 <input type="radio" id="origin2" name="origin" value="BCA" />
267 <label for="origin2">Baltimore/Camden (Camden Line)</label>
268 </div>
269 <div>
270 <input type="radio" id="origin3" name="origin" value="BWE" />
271 <label for="origin3">Bowie State (Penn Line)</label>
272 </div>
273 <div>
274 <input type="radio" id="origin4" name="origin" value="BWI" />
275 <label for="origin4">BWI Airport (Penn Line)</label>
276 </div>
277 <div>
278 <input type="radio" id="origin5" name="origin" value="CPK" />
279 <label for="origin5">College Park (Camden Line)</label>
280 </div>
281 <div>
282 <input type="radio" id="origin6" name="origin" value="SEB" />
283 <label for="origin6">Seabrook (Penn Line)</label>
284 </div>
285 <div>
286 <input type="radio" id="origin7" name="origin" value="WAS" />
287 <label for="origin7">Washington D.C.</label>
288 </div>
289 <div>
290 <input type="radio" id="origin8" name="origin" value="WBL" />
291 <label for="origin8">West Baltimore (Penn Line)</label>
292 </div>
293
294 <h3>Destination</h3>
295 <div>
296 <input type="radio" id="destination1" name="destination" value="BAL" />
297 <label for="destination1">Baltimore/Penn (Penn Line)</label>
298 </div>
299 <div>
300 <input type="radio" id="destination2" name="destination" value="BCA" />
301 <label for="destination2">Baltimore/Camden (Camden Line)</label>
302 </div>
303 <div>
304 <input type="radio" id="destination3" name="destination" value="BWE" />
305 <label for="destination3">Bowie State (Penn Line)</label>
306 </div>
307 <div>
308 <input type="radio" id="destination4" name="destination" value="BWI" />
309 <label for="destination4">BWI Airport (Penn Line)</label>
310 </div>
311 <div>
312 <input type="radio" id="destination5" name="destination" value="CPK" />
313 <label for="destination5">College Park (Camden Line)</label>
314 </div>
315 <div>
316 <input type="radio" id="destination6" name="destination" value="SEB" />
317 <label for="destination6">Seabrook (Penn Line)</label>
318 </div>
319 <div>
320 <input type="radio" id="destination7" name="destination" value="WAS" />
321 <label for="destination7">Washington D.C.</label>
322 </div>
323 <div>
324 <input type="radio" id="destination8" name="destination" value="WBL" />
325 <label for="destination8">West Baltimore (Penn Line)</label>
326 </div>
327
328 <input type="hidden" name="serviceid" value="R" />
329 <input type="hidden" name="servicename" value="MARC Train" />
330 <input type="submit" name="go" value="Get Ticket" />
331 </fieldset>
332 </form>
333 </div>
334
335 @SVGDATA@
336
337 <script>
338
339 /***********************************************/
340 /* First, center the ticket within the browser */
341 /***********************************************/
342
343 function center_ticket() {
344 /* We're relying on the SVG being the full height of the
345 * viewport already, and on the aspect ratio being
346 * preserved. First, find the center of the ticket. */
347 const r = document.getElementById("ticket").getBoundingClientRect();
348 const c = r.left + (r.width / 2);
349
350 /* That's the center-line of the ticket. We want to move it to
351 * the center-line of the viewport. */
352 const vc = document.documentElement.clientWidth / 2;
353
354 /* This is how much we need to translate the SVG */
355 const hdelta = vc - c;
356
357 /* But before we can set the absolute left-coordinate of the
358 * SVG, we need to know where it is now. Note: without the
359 * "px" this doesn't default to pixels like CSS does. */
360 const svg = document.querySelector("svg");
361 svg.style.left = (svg.getBoundingClientRect().left + hdelta) + "px";
362 }
363
364
365 /******************************/
366 /* Set the service identifier */
367 /******************************/
368 function set_service_id() {
369 const sid = document.getElementById("serviceid");
370
371 /* Get the "serviceid" from the querystring if it's there */
372 const params = new URLSearchParams(document.location.search);
373 if (params.get("serviceid")) {
374 sid.textContent = params.get("serviceid");
375 }
376
377 /* Otherwise, leave it at "F" */
378 }
379
380 /******************************/
381 /* Set the service name */
382 /******************************/
383 function set_service_name() {
384 const sid = document.getElementById("servicename");
385
386 /* Get the "servicename" from the querystring if it's there */
387 const params = new URLSearchParams(document.location.search);
388 if (params.get("servicename")) {
389 sid.textContent = params.get("servicename");
390 }
391
392 /* Otherwise, leave it at "BaltimoreLink" */
393 }
394
395 /****************************************/
396 /* Set and reposition the security code */
397 /****************************************/
398
399 function set_code() {
400 const ct = document.getElementById("codetext");
401
402 /* Get the "code" from the querystring if it's there */
403 const params = new URLSearchParams(document.location.search);
404 if (params.get("code")) {
405 ct.textContent = params.get("code");
406 }
407 else {
408 /* Otherwise, use a random code */
409 const bucket = ["0","1","2","3","4","5","6","7","8","9",
410 "A","B","C","D","E","F","G","H","I","J",
411 "K","L","M","N","O","P","Q","R","S","T",
412 "U","V","W","X","Y","Z"];
413
414 /* Two random ints between 0 and 35 */
415 const i1 = Math.floor(Math.random() * 36);
416 const i2 = Math.floor(Math.random() * 36);
417 const d1 = bucket[i1];
418 const d2 = bucket[i2];
419 ct.textContent = d1 + d2;
420 }
421 }
422
423
424 function center_code() {
425 /* Center the security code inside its red box */
426 const ct = document.getElementById("codetext");
427 const bg = document.getElementById("codebg");
428
429 /* First, find the center of the red box */
430 const r1 = bg.getBoundingClientRect();
431 const c1 = r1.left + (r1.width / 2);
432
433 /* Now the center of the code text */
434 const r2 = ct.getBoundingClientRect();
435 const c2 = r2.left + (r2.width / 2);
436
437 /* What do we add to c2 to make it equal to c1? */
438 const hdelta = c1 - c2;
439
440 /* We've measured everything so far in "client rect"
441 * coordinates, because that's the only available measurement
442 * we have for the width of the <text> element after futzing
443 * with its contents. But when we reposition that <text>
444 * element, it will be by adjusting its "x" attribute, and
445 * that attribute uses a different coordinate system than the
446 * client rect does. Specifically, "x" refers to an offset
447 * within the SVG's coordinate system, and the client rect
448 * coordinates are pixels on-screen. To convert between the
449 * two, we can take the "width" attribute of the background
450 * element and compare it to the width of the background
451 * element's client rect. Since the size of the background is
452 * fixed, this should give us a multiplier that turns client recr
453 * distances (what we have) into SVG distances (what we want) */
454 const client_to_svg = parseFloat(bg.getAttribute("width"))/r1.width;
455
456 /* Convert hdelta from client rect to SVG coordinates */
457 const svg_hdelta = hdelta * client_to_svg;
458
459 /* Since this <text> element has an "x" attribute it's easier for
460 * us to shift that than it is to mess with the "left" style. */
461 ct.setAttribute("x", parseFloat(ct.getAttribute("x")) + svg_hdelta);
462 }
463
464 /*****************************************/
465 /* Next, set up the ticket date and time */
466 /*****************************************/
467
468 function set_ticket_expiry() {
469 /* There are two parameters, time and date, that we store in one
470 * underlying "date" variable. Default both to an hour and a
471 * half from now. This is what the CharmPass app does for
472 * one-way tickets. */
473 const date = new Date();
474
475 /* BaltimoreLink and MARC Train are valid for an hour and a half */
476 let minutes = 90;
477 const params = new URLSearchParams(document.location.search);
478 if (params.get("servicename") == "Commuter Bus") {
479 /* But commuter bus tickets are only valid for ten minutes */
480 minutes = 10;
481 }
482
483 /* We use the low-level get/setTime to change the number of
484 * milliseconds since the epoch that this date represents
485 * Obviously correct, and avoids all suspicious corner cases
486 * for a few more decades. */
487 date.setTime(date.getTime() + (minutes*60*1000));
488
489 tt = document.getElementById("tickettime");
490 tt.textContent = date.toLocaleTimeString();
491
492 const td = document.getElementById("ticketdate");
493 const dateopts = {
494 day: "2-digit",
495 month: "2-digit",
496 year: "2-digit"
497 };
498 td.textContent = date.toLocaleDateString("en-US", dateopts);
499 }
500
501
502 /*********************************************************/
503 /* Finally, the onclick handler for the night/day switch */
504 /*********************************************************/
505
506 /* We always start in "day" mode */
507 is_day = true;
508
509 function set_day() {
510 sky.style.fill = "#efb02f";
511 }
512
513 function set_night() {
514 sky.style.fill = "#143b66";
515 }
516
517 function swap_colors() {
518 if (is_day) {
519 set_night();
520 is_day = false;
521 }
522 else {
523 set_day();
524 is_day = true;
525 }
526 }
527
528 /******************************************/
529 /* Display the ticket (and hide the menu) */
530 /******************************************/
531
532 function go() {
533 /* To create our "window" onto the scene, we're going to slide the
534 * SVG off the left-hand side of the screen, and we don't want
535 * scroll bars to appear. */
536 document.body.style.overflow = "hidden";
537 document.body.style.margin = "0";
538 document.body.style.padding = "0";
539
540 const svg = document.querySelector("svg");
541 const menu = document.getElementById("menu");
542 svg.style.display = "initial";
543 menu.style.display = "none";
544 }
545
546 /*****************************************************/
547 /* Add event handlers for all of the functions above */
548 /*****************************************************/
549
550 const params = new URLSearchParams(document.location.search);
551 if (params.get("go")) {
552 /* First unhide the SVG (swap it with the form) */
553 window.addEventListener("load", go);
554
555 /* Center the ticket once when the page has loaded */
556 window.addEventListener("load", center_ticket);
557
558 /* Re-center the ticket when the window is resized */
559 window.addEventListener("resize", center_ticket);
560
561 /* Set the service identifier when the page has loaded */
562 window.addEventListener("load", set_service_id);
563
564 /* Set the service name when the page has loaded */
565 window.addEventListener("load", set_service_name);
566
567 /* Set the security code text when the page has loaded */
568 window.addEventListener("load", set_code);
569
570 /* Center the security code text when the page has loaded; in
571 * particular, after we set the code. */
572 window.addEventListener("load", center_code);
573
574 /* Set the ticket expiration date/time upon page load */
575 window.addEventListener("load", set_ticket_expiry);
576
577 /* Swap colors when the screen is tapped */
578 document.body.addEventListener("click", swap_colors);
579 }
580
581 </script>
582 </body>
583 </html>