Puzzle start using new lib +bg animation

This commit is contained in:
Neale Pickett 2023-09-08 18:05:51 -06:00
parent a896788cc5
commit 551afe04a5
5 changed files with 275 additions and 76 deletions

126
theme/background.mjs Normal file
View File

@ -0,0 +1,126 @@
function randint(max) {
return Math.floor(Math.random() * max)
}
const MILLISECOND = 1
const SECOND = MILLISECOND * 1000
class Line {
/**
* @param {CanvasRenderingContext2D} ctx canvas context
* @param {Number} hue Hue, in % of one circle [0,tau)
* @param {Number} a First point of line
* @param {Number} b Second point of line
*/
constructor(ctx, hue, a, b) {
this.ctx = ctx
this.hue = hue
this.a = a
this.b = b
}
bounce(point, v) {
let ret = [
point[0] + v[0],
point[1] + v[1],
]
if ((ret[0] > this.ctx.canvas.width) || (ret[0] < 0)) {
v[0] *= -1
ret[0] += v[0] * 2
}
if ((ret[1] > this.ctx.canvas.height) || (ret[1] < 0)) {
v[1] *= -1
ret[1] += v[1] * 2
}
return ret
}
Add(hue, a, b) {
return new Line(
this.ctx,
(this.hue + hue) % 1.0,
this.bounce(this.a, a),
this.bounce(this.b, b),
)
}
Draw() {
this.ctx.save()
this.ctx.strokeStyle = `hwb(${this.hue}turn 0% 50%)`
this.ctx.beginPath()
this.ctx.moveTo(this.a[0], this.a[1])
this.ctx.lineTo(this.b[0], this.b[1])
this.ctx.stroke()
this.ctx.restore()
}
}
class LengoBackground {
constructor() {
this.canvas = document.createElement("canvas")
document.body.insertBefore(this.canvas, document.body.firstChild)
this.canvas.style.position = "fixed"
this.canvas.style.zIndex = -1000
this.canvas.style.opacity = 0.3
this.canvas.style.top = 0
this.canvas.style.left = 0
this.canvas.style.width = "99vw"
this.canvas.style.height = "99vh"
this.canvas.width = 2000
this.canvas.height = 2000
this.ctx = this.canvas.getContext("2d")
this.ctx.lineWidth = 1
this.lines = []
for (let i = 0; i < 18; i++) {
this.lines.push(
new Line(this.ctx, 0, [0, 0], [0, 0])
)
}
this.velocities = {
hue: 0.001,
a: [20 + randint(10), 20 + randint(10)],
b: [5 + randint(10), 5 + randint(10)],
}
this.nextFrame = performance.now()-1
this.frameInterval = 100 * MILLISECOND
//addEventListener("resize", e => this.resizeEvent())
//this.resizeEvent()
//this.animate(this.nextFrame)
setInterval(() => this.animate(this.nextFrame+1), SECOND/6)
}
/**
* Animate one frame
*
* @param {DOMHighResTimeStamp} timestamp
*/
animate(timestamp) {
if (timestamp >= this.nextFrame) {
this.lines.shift()
let lastLine = this.lines.pop()
let nextLine = lastLine.Add(this.velocities.hue, this.velocities.a, this.velocities.b)
this.lines.push(lastLine)
this.lines.push(nextLine)
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
for (let line of this.lines) {
line.Draw()
}
this.nextFrame += this.frameInterval
}
//requestAnimationFrame((ts) => this.animate(ts))
}
}
function init() {
new LengoBackground()
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}

View File

@ -1,29 +1,58 @@
/* http://paletton.com/#uid=63T0u0k7O9o3ouT6LjHih7ltq4c */ /*
* Colors
*
* This uses the alpha channel to apply hue tinting to elements, to get a
* similar effect in light or dark mode.
*
* http://paletton.com/#uid=33x0u0klrl-4ON9dhtKtAdqMQ4T
*/
body { body {
font-family: sans-serif; background: #010e19;
max-width: 40em; color: #edd488;
background: #282a33;
color: #f6efdc;
} }
body.wide { main {
max-width: 100%; background: #000d;
} }
a:any-link { h1, h2, h3, h4, h5, h6 {
color: #8b969a; color: #cb2408cc;
} }
h1 { h1 {
background: #5e576b; background: #cb240844;
color: #9e98a8;
} }
.Fail, .Error, #messages { a:any-link {
background: #3a3119; color: #b9cbd8;
color: #ffcc98;
} }
.Fail:before { .notification {
content: "Fail: "; background: #ac8f3944;
} }
.Error:before { .error {
content: "Error: "; background: red;
color: white;
}
@media (prefers-color-scheme: light) {
body {
background: #b9cbd8;
color: black;
}
main {
background: #fffd;
}
a:any-link {
color: #092b45;
}
}
body {
font-family: sans-serif;
}
main {
max-width: 40em;
margin: auto;
padding: 1px 3px;
border-radius: 5px;
}
h1 {
padding: 3px;
} }
p { p {
margin: 1em 0em; margin: 1em 0em;
@ -36,9 +65,11 @@ input, select {
margin: 0.2em; margin: 0.2em;
max-width: 30em; max-width: 30em;
} }
nav { .notification, .error {
border: solid black 2px; padding: 0 1em;
border-radius: 8px;
} }
nav ul, .category ul { nav ul, .category ul {
padding: 1em; padding: 1em;
} }
@ -58,7 +89,6 @@ input:invalid {
} }
#messages { #messages {
min-height: 3em; min-height: 3em;
border: solid black 2px;
} }
#rankings { #rankings {
width: 100%; width: 100%;
@ -91,40 +121,7 @@ input:invalid {
#devel { #devel {
background-color: #eee; overflow: auto;
color: black;
overflow: scroll;
}
#devel .string {
color: #9c27b0;
}
#devel .body {
background-color: #ffc107;
}
.kvpair {
border: solid black 2px;
}
.spinner {
display: inline-block;
width: 64px;
height: 64px;
display: block;
width: 46px;
height: 46px;
margin: 1px;
border-radius: 50%;
border: 5px solid #fff;
border-color: #fff transparent #fff transparent;
animation: rotate 1.2s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
li[draggable]::before { li[draggable]::before {

View File

@ -326,6 +326,20 @@ class Server {
GetContent(category, points, filename) { GetContent(category, points, filename) {
return this.fetch(`/content/${category}/${points}/${filename}`) return this.fetch(`/content/${category}/${points}/${filename}`)
} }
/**
* Return a Puzzle object.
*
* New Puzzle objects only know their category and point value.
* See docstrings on the Puzzle object for more information.
*
* @param {String} category
* @param {Number} points
* @returns {Puzzle}
*/
GetPuzzle(category, points) {
return new Puzzle(this, category, points)
}
} }
export { export {

View File

@ -1,32 +1,34 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<title>Puzzle</title> <title>Puzzle</title>
<link rel="stylesheet" href="basic.css"> <link rel="stylesheet" href="basic.css">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<meta charset="utf-8"> <meta charset="utf-8">
<script src="puzzle.js"></script> <script src="puzzle.mjs" type="module"></script>
<script> <script src="background.mjs" type="module"></script>
</script>
</head> </head>
<body> <body>
<h1>Puzzle</h1> <main>
<h1 id="title">[loading]</h1>
<section> <section>
<div id="puzzle"><span class="spinner"></span></div> <div id="puzzle">
<p class="notification">
Starting script...
</p>
</div>
<ul id="files"></ul> <ul id="files"></ul>
<p>Puzzle by <span id="authors"></span></p> <p>Puzzle by <span id="authors">[loading]</span></p>
</section> </section>
<div id="messages"></div>
<form> <form>
<input type="hidden" name="cat"> <input type="hidden" name="cat">
<input type="hidden" name="points"> <input type="hidden" name="points">
<input type="hidden" name="xAnswer">
Team ID: <input type="text" name="id"> <br> Team ID: <input type="text" name="id"> <br>
Answer: <input type="text" name="answer" id="answer"> <span id="answer_ok"></span><br> Answer: <input type="text" name="answer" id="answer"> <span id="answer_ok"></span><br>
<input type="submit" value="Submit"> <input type="submit" value="Submit">
</form> </form>
<div id="devel"></div> </main>
<div id="devel" class="notification"></div>
<nav> <nav>
<ul> <ul>
<li><a href="index.html">Puzzles</a></li> <li><a href="index.html">Puzzles</a></li>

View File

@ -0,0 +1,60 @@
import * as moth from "./moth.mjs"
function puzzleElement(clear=true) {
let e = document.querySelector("#puzzle")
if (clear) {
while (e.firstChild) e.firstChild.remove()
}
return e
}
function error(message) {
let e = puzzleElement().appendChild(document.createElement("p"))
e.classList.add("error")
e.textContent = message
}
async function loadPuzzle(category, points) {
let server = new moth.Server()
let puzzle = server.GetPuzzle(category, points)
await puzzle.Populate()
let title = `${category} ${points}`
document.querySelector("title").textContent = title
document.querySelector("#title").textContent = title
document.querySelector("#authors").textContent = puzzle.Authors.join(", ")
puzzleElement().innerHTML = puzzle.Body
}
function hashchange() {
// Tell user we're loading
puzzleElement().appendChild(document.createElement("progress"))
for (let qs of ["#authors", "#title", "title"]) {
for (let e of document.querySelectorAll(qs)) {
e.textContent = "[loading]"
}
}
let hashpart = location.hash.split("#")[1] || ""
let catpoints = hashpart.split(":")
let category = catpoints[0]
let points = Number(catpoints[1])
if (!category && !points) {
error(`Doesn't look like a puzzle reference: ${hashpart}`)
return
}
loadPuzzle(category, points)
.catch(err => error(err))
}
function init() {
window.addEventListener("hashchange", hashchange)
hashchange()
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}