]> gitweb.michael.orlitzky.com - charm-bypass.git/blob - index.html.in
index.html.in: begin the menu process
[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="Generate 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="Generate Ticket" />
269 </fieldset>
270 </form>
271 <form>
272 <fieldset>
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" />
277 </fieldset>
278 </form>
279 </div>
280
281 @SVGDATA@
282
283 <script>
284
285 /***********************************************/
286 /* First, center the ticket within the browser */
287 /***********************************************/
288
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);
295
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;
299
300 /* This is how much we need to translate the SVG */
301 const hdelta = vc - c;
302
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";
308 }
309
310
311 /******************************/
312 /* Set the service identifier */
313 /******************************/
314 function set_service_id() {
315 const sid = document.getElementById("serviceid");
316
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");
321 }
322
323 /* Otherwise, leave it at "F" */
324 }
325
326 /******************************/
327 /* Set the service name */
328 /******************************/
329 function set_service_name() {
330 const sid = document.getElementById("servicename");
331
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");
336 }
337
338 /* Otherwise, leave it at "BaltimoreLink" */
339 }
340
341 /****************************************/
342 /* Set and reposition the security code */
343 /****************************************/
344
345 function set_code() {
346 const ct = document.getElementById("codetext");
347
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");
352 }
353 else {
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"];
359
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;
366 }
367 }
368
369
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");
374
375 /* First, find the center of the red box */
376 const r1 = bg.getBoundingClientRect();
377 const c1 = r1.left + (r1.width / 2);
378
379 /* Now the center of the code text */
380 const r2 = ct.getBoundingClientRect();
381 const c2 = r2.left + (r2.width / 2);
382
383 /* What do we add to c2 to make it equal to c1? */
384 const hdelta = c1 - c2;
385
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;
401
402 /* Convert hdelta from client rect to SVG coordinates */
403 const svg_hdelta = hdelta * client_to_svg;
404
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);
408 }
409
410 /*****************************************/
411 /* Next, set up the ticket date and time */
412 /*****************************************/
413
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();
420
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));
426
427 tt = document.getElementById("tickettime");
428 tt.textContent = date.toLocaleTimeString();
429
430 const td = document.getElementById("ticketdate");
431 const dateopts = {
432 day: "2-digit",
433 month: "2-digit",
434 year: "2-digit"
435 };
436 td.textContent = date.toLocaleDateString("en-US", dateopts);
437 }
438
439
440 /*********************************************************/
441 /* Finally, the onclick handler for the night/day switch */
442 /*********************************************************/
443
444 /* We always start in "day" mode */
445 is_day = true;
446
447 function set_day() {
448 sky.style.fill = "#efb02f";
449 }
450
451 function set_night() {
452 sky.style.fill = "#143b66";
453 }
454
455 function swap_colors() {
456 if (is_day) {
457 set_night();
458 is_day = false;
459 }
460 else {
461 set_day();
462 is_day = true;
463 }
464 }
465
466 /******************************************/
467 /* Display the ticket (and hide the menu) */
468 /******************************************/
469
470 function go() {
471 const svg = document.querySelector("svg");
472 const menu = document.getElementById("menu");
473 svg.style.display = "initial";
474 menu.style.display = "none";
475 }
476
477 /*****************************************************/
478 /* Add event handlers for all of the functions above */
479 /*****************************************************/
480
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);
485
486 /* Center the ticket once when the page has loaded */
487 window.addEventListener("load", center_ticket);
488
489 /* Re-center the ticket when the window is resized */
490 window.addEventListener("resize", center_ticket);
491
492 /* Set the service identifier when the page has loaded */
493 window.addEventListener("load", set_service_id);
494
495 /* Set the service name when the page has loaded */
496 window.addEventListener("load", set_service_name);
497
498 /* Set the security code text when the page has loaded */
499 window.addEventListener("load", set_code);
500
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);
504
505 /* Set the ticket expiration date/time upon page load */
506 window.addEventListener("load", set_ticket_expiry);
507
508 /* Swap colors when the screen is tapped */
509 document.body.addEventListener("click", swap_colors);
510 }
511
512 </script>
513 </body>
514 </html>