summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gaethvor.py (renamed from src/main.py)82
-rw-r--r--src/static/confetti.min.js1
-rw-r--r--src/static/main.css32
-rw-r--r--src/static/main.js18
-rw-r--r--src/templates/game.html12
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>