diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/gaethvor.py (renamed from src/main.py) | 82 | ||||
| -rw-r--r-- | src/static/confetti.min.js | 1 | ||||
| -rw-r--r-- | src/static/main.css | 32 | ||||
| -rw-r--r-- | src/static/main.js | 18 | ||||
| -rw-r--r-- | src/templates/game.html | 12 |
5 files changed, 114 insertions, 31 deletions
diff --git a/src/main.py b/src/gaethvor.py index a3e6b11..dce226a 100644 --- a/src/main.py +++ b/src/gaethvor.py @@ -1,19 +1,53 @@ import os +import sys import random import json import sqlite3 from contextlib import closing # with close sqlite +import pathlib +import logging import flask from PIL import Image as PIL_Image # Image file exif data for GPS coordinate extraction import PIL.ExifTags as PIL_ExifTags import geopy.distance -app = flask.Flask(__name__) -pic_root_path = 'src/static/pics/mine' +class CustomFormatter(logging.Formatter): + grey = "\x1b[90;20m" + blue = "\x1b[34;20m" + yellow = "\x1b[33;20m" + red = "\x1b[31;20m" + bold_red = "\x1b[31;1m" + reset = "\x1b[0m" + format = "%(asctime)s,%(msecs)03d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s" -pics = os.listdir(pic_root_path) + FORMATS = { + logging.DEBUG: grey + format + reset, + logging.INFO: blue + format + reset, + logging.WARNING: yellow + format + reset, + logging.ERROR: red + format + reset, + logging.CRITICAL: bold_red + format + reset + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +logger = logging.getLogger(__name__) +debug = 'PROD' not in os.environ +logger.setLevel(logging.DEBUG if debug else logging.WARNING) +logging.getLogger('werkzeug').setLevel(logging.DEBUG if debug else logging.WARNING) + +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setFormatter(CustomFormatter()) +logger.root.addHandler(stream_handler) +logger.info(f"Debug: {'en' if debug else 'dis'}abled") + + +app = flask.Flask(__name__, template_folder=os.path.abspath(os.path.join(pathlib.Path(__file__).parent, 'templates/'))) def decimal_coords(coords, ref): decimal_degrees = coords[0] + coords[1] / 60 + coords[2] / 3600 @@ -54,7 +88,7 @@ def get_random_image(): @app.route("/guess.json", methods=['POST']) def guess(): data = json.loads(flask.request.data) - exif = get_exif(os.path.join(pic_root_path, data['image_name'])) + exif = get_exif(os.path.join(pics_root_path, data['image_name'])) correct_coordinates = image_coordinates(exif) guess_coordinates = (data['coordinates']['lat'], data['coordinates']['lng']) @@ -88,6 +122,32 @@ def guess(): def game(): return flask.render_template('game.html') +if "PICS" not in os.environ: + logger.critical( + "PICS environment variable not set\n" + "You must specify a path to a folder with pictures that contain coordinate metadata for the game to use\n" + f"Usage: PICS=/path/to/your/pictures {sys.argv[0]}") +pics_root_path = os.path.abspath(os.environ["PICS"]) +logger.info(f'Serving images from path: {pics_root_path}') +pics_original = os.listdir(pics_root_path) +pics = [] + +@app.route("/pic/<path:name>") +def pic(name): + global pics_root_path + return flask.send_from_directory(pics_root_path, name, as_attachment=True) + +# Check if all images have GPS coordinates on application start +for pic in pics_original: + img_path = os.path.join(pics_root_path, pic) + try: + exif = get_exif(img_path) + correct_coordinates = image_coordinates(exif) + pics.append(pic) + except Exception as ex: + logger.error(f'Error loading GPS coordinates for: {img_path}') + raise ex + with closing(sqlite3.connect("guess_where.db")) as connection: with closing(connection.cursor()) as cursor: @@ -102,13 +162,9 @@ with closing(sqlite3.connect("guess_where.db")) as connection: ) """).fetchall() +def main(): + global app + app.run(debug=debug) - -# Check if all images have GPS coordinates on application start -for pic in pics: - img_path = os.path.join(pic_root_path, pic) - try: - correct_coordinates = image_coordinates(img_path) - except Exception as ex: - print(f'Error loading GPS coordinates for: {img_path}') - +if __name__ == '__main__': + main() diff --git a/src/static/confetti.min.js b/src/static/confetti.min.js new file mode 100644 index 0000000..17d0899 --- /dev/null +++ b/src/static/confetti.min.js @@ -0,0 +1 @@ +"use strict";(()=>{var b={position:{x:window.innerWidth/2,y:window.innerHeight/3},count:75,size:1,velocity:200,fade:!1},a=class{static init(t){return{...b,...t}}};var n=class{static range(t,i){return Math.random()*(i-t)+t}};var o=class e{x;y;constructor(t=0,i=0){this.x=t,this.y=i}copy(){return new e(this.x,this.y)}magnitude(){return Math.hypot(this.x,this.y)}normalize(){let t=this.magnitude();if(t===0)return this.x=0,this.y=0,this;let i=1/t;return this.x*=i,this.y*=i,this}scale(t){return this.x*=t,this.y*=t,this}};var h=class{config;position;size;velocity;rotation;rotation_speed;hue;opacity;fadeRate;constructor(t,i){this.config=i,this.position=t.copy(),this.size=this.initSize(),this.velocity=this.initVelocity(),this.rotation=n.range(0,360),this.rotation_speed=n.range(-250,250),this.hue=n.range(0,360),this.opacity=100,this.fadeRate=100/n.range(.5,2.5)}initSize(){let t=n.range(2,10)*this.config.size,i=n.range(2,4)*this.config.size;return new o(t,i)}initVelocity(){let t=n.range(-.5,.5),i=n.range(-.75,.25),r=new o(t,i).normalize();return r.x*=Math.random()*this.config.velocity*4,r.y*=Math.random()*this.config.velocity*4,r}update(t){this.velocity.x+=n.range(-350,350)*t,this.velocity.y+=750*(this.size.y/(10*this.config.size))*t;let i=Math.pow(.98,t*60);this.velocity.scale(i),this.position.x+=this.velocity.x*t,this.position.y+=this.velocity.y*t,this.rotation+=this.rotation_speed*t,this.config.fade&&(this.opacity-=this.fadeRate*t,this.opacity<0&&(this.opacity=0))}draw(t){t.drawRect(this.position,this.size,this.rotation,this.hue,this.opacity)}cull(t){let i=Math.max(this.size.x,this.size.y)*2;return this.position.y>t.height+i||this.opacity<=0}};var c=class{canvas;context;dpr;rafId;boundResize;constructor(){let t=document.createElement("canvas"),i=t.getContext("2d");if(!i)throw new Error("Unable to create 2d context");this.canvas=t,this.context=i,this.dpr=Math.max(1,window.devicePixelRatio||1),this.rafId=0,this.boundResize=this.resize.bind(this),this.resize(),window.addEventListener("resize",this.boundResize),t.style.position="fixed",t.style.top="0",t.style.left="0",t.style.width="100%",t.style.height="100%",t.style.margin="0",t.style.padding="0",t.style.zIndex="999999999",t.style.pointerEvents="none",document.body.appendChild(t),this.context.imageSmoothingEnabled=!0}resize(){this.rafId&&cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(()=>{let{innerWidth:t,innerHeight:i}=window;this.canvas.width=Math.round(t*this.dpr),this.canvas.height=Math.round(i*this.dpr),this.canvas.style.width=`${t}px`,this.canvas.style.height=`${i}px`,this.context.setTransform(this.dpr,0,0,this.dpr,0,0),this.rafId=0})}get width(){return this.canvas.width/this.dpr}get height(){return this.canvas.height/this.dpr}clear(){this.context.clearRect(0,0,this.canvas.width,this.canvas.height)}drawRect(t,i,r,s,g){let u=r*Math.PI/180,f=Math.cos(u),l=Math.sin(u),m=i.x*this.dpr,v=i.y*this.dpr,y=t.x*this.dpr,x=t.y*this.dpr;this.context.setTransform(f,l,-l,f,y,x),this.context.beginPath(),this.context.rect(-m/2,-v/2,m,v),this.context.fillStyle=`oklch(0.85 0.25 ${s}deg / ${g}%)`,this.context.fill(),this.context.setTransform(this.dpr,0,0,this.dpr,0,0)}cleanup(){window.removeEventListener("resize",this.boundResize),this.rafId&&cancelAnimationFrame(this.rafId),this.canvas.remove()}};var d=class e{particles=[];renderer;lastTime=0;rafId=0;boundUpdate;static sharedEngine=null;constructor(){this.renderer=new c,this.boundUpdate=this.update.bind(this),this.rafId=window.requestAnimationFrame(this.boundUpdate)}update(t){let i=Math.min((t-this.lastTime)/1e3,.064);if(this.lastTime=t,this.particles.length===0){this.cleanup();return}this.renderer.clear();let r=0;for(let s=0;s<this.particles.length;s++)this.particles[s].update(i),this.particles[s].cull(this.renderer)||(this.particles[s].draw(this.renderer),this.particles[r++]=this.particles[s]);this.particles.length=r,this.rafId=window.requestAnimationFrame(this.boundUpdate)}cleanup(){this.rafId&&window.cancelAnimationFrame(this.rafId),this.renderer.cleanup(),e.sharedEngine=null}trigger(t){let i=new o(t.position.x,t.position.y);for(let r=0;r<t.count;r++)this.particles.push(new h(i,t))}static acquire(){return e.sharedEngine||(e.sharedEngine=new e),e.sharedEngine}};function p(e){let t=a.init(e);d.acquire().trigger(t)}window.confetti=p;})(); diff --git a/src/static/main.css b/src/static/main.css index 2c9da29..96d571d 100644 --- a/src/static/main.css +++ b/src/static/main.css @@ -102,7 +102,7 @@ html, body { /* Background image */ #background-image { - background-image: url("/static/pics/mine/Lithaunsplads - Saxogade.webp"); + background-image: url("/pic/Lithaunsplads - Saxogade.webp"); position: absolute; background-position: center; filter: blur(2vh); /* vh because it's zoom agnostic */ @@ -123,14 +123,12 @@ html, body { color: white; z-index: 500; pointer-events: none; - /* center */ - left: 0; - right: 0; - margin-inline: auto; - width: fit-content; - /* Display next to each other */ - display: flex; - gap: 1em; + font-size: 1rem; + width: 100%; + /* Center children */ + display: flex; + align-items: center; + height: 100vh; } #overlay-distance > * { @@ -138,12 +136,24 @@ html, body { } #overlay-distance { - background: #0008; + background: #0007; + backdrop-filter: blur(8px); pointer-events: none; user-select: none; border-radius: 0.5em; padding: 0.5em; - height: 100%; + height: 4em; + margin: auto; /* Center horizontally */ + font-size: 1.75rem; +} + +#overlay-bottom { + position: absolute; + bottom: 1em; + font-size: 1.25rem; + /* Center horizontally */ + left: 0; + width: 100%; } .bottom-button { diff --git a/src/static/main.js b/src/static/main.js index ac442fa..9523a0d 100644 --- a/src/static/main.js +++ b/src/static/main.js @@ -24,7 +24,7 @@ main_image.onload = function() { function load_image (image_name) { current_image_name = image_name; - url = `/static/pics/mine/${image_name}`; + url = `/pic/${image_name}`; /* JS foo to change image https://stackoverflow.com/questions/19396390/load-image-from-javascript */ var newImg = new Image; newImg.onload = function() { @@ -111,12 +111,25 @@ async function guess(image_name, coordinates) { // Fit answer & guess in view var group = new L.featureGroup([correct_marker, guess_marker]); - map.fitBounds(group.getBounds(), {padding: [50, 50]}); + map.fitBounds(group.getBounds(), {padding: [20, 200]}); overlay_distance.hidden = false; overlay_distance.children[1].innerHTML = `≈ ${json.distance_meters} meters` ; guess_button.hidden = true; next_button.hidden = false; + + if (json.distance_meters < 50) { + const rect = overlay_distance.getBoundingClientRect(); + const x = rect.x + overlay_distance.clientWidth / 2; + const y = rect.y + overlay_distance.clientHeight / 2; + confetti({ + position: { x: x, y: y }, // Origin position + count: 100, // Number of particles + size: 3, // Size of the particles + velocity: 200, // Initial particle velocity + fade: false // Particles fall off the screen, or fade out + }); + } } /* Correct location marker */ @@ -187,4 +200,3 @@ document.addEventListener('keydown', function(e) { } } }); - diff --git a/src/templates/game.html b/src/templates/game.html index df9b731..a08fd80 100644 --- a/src/templates/game.html +++ b/src/templates/game.html @@ -10,6 +10,7 @@ <link rel="stylesheet" href="/static/main.css" /> <script src="/static/wheel-zoom.min.js"></script> <script src="/static/main.js" defer></script> + <script src="/static/confetti.min.js" defer></script> </head> <body> <div id="background-image"></div> @@ -31,12 +32,15 @@ <h1></h1> </div> - <button class="bottom-button" id='guess-button' hidden>Guess<br> - <span class="subtle-light">(Space button)<span></button> + <div id="overlay-bottom"> + <button class="bottom-button" id='guess-button' hidden>Guess<br> + <span class="subtle-light">(Space button)</span></button> - <button class="bottom-button" id='next-button' hidden onClick="next()">Next<br> - <span class="subtle-light">(Space button)<span></button> + <button class="bottom-button" id='next-button' hidden onClick="next()">Next<br> + <span class="subtle-light">(Space button)</span></button> + </div> </div> </body> + </html> |
