Compare commits

...

2 commits

Author SHA1 Message Date
Leon Mika 3107bd8a2f Finished the results page 2025-06-22 02:21:04 +02:00
Leon Mika d6b09e2305 Started working on the score spinner 2025-06-21 14:35:50 +02:00
6 changed files with 188 additions and 7 deletions

View file

@ -52,8 +52,8 @@ func main() {
app.Get("/", func(c *fiber.Ctx) error {
return c.Render("index", fiber.Map{})
})
app.Get("/end", func(c *fiber.Ctx) error {
return c.Render("end", fiber.Map{})
app.Get("/results", func(c *fiber.Ctx) error {
return c.Render("results", fiber.Map{})
})
app.Get("/:qid", func(c *fiber.Ctx) error {
qID, err := c.ParamsInt("qid")
@ -71,7 +71,7 @@ func main() {
return err
}
nextURL := prefix + "/end"
nextURL := prefix + "/results"
reachedEnd := true
if idx+1 < len(questions.Questions) {
nextURL = fmt.Sprintf("%v/%d", prefix, idx+2)

View file

@ -0,0 +1,97 @@
import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
import { gameState } from "./gamestate.js";
const ANIMATION_DURATION = 1500.0;
const MAX_QUESTIONS = 10;
const CIRCLE_RADIUS = 50;
export default class extends Controller {
static targets = ['path', 'pathBack', 'count', 'postScore', 'rank'];
connect() {
this._startT = 0;
requestAnimationFrame(this._startAnimating.bind(this));
}
_startAnimating(t) {
this._finalScore = 10;
this._startT = t;
this._finalArcPostition = 360 * this._finalScore / MAX_QUESTIONS;
this._currentlyDisplayedScore = 0;
this.countTarget.innerText = "0%";
requestAnimationFrame(this._animateFrame.bind(this));
}
_animateFrame(t) {
let tt = t - this._startT;
let tScaled = tt / ANIMATION_DURATION;
if (tt >= ANIMATION_DURATION) {
if (this._finalArcPostition == 360) {
this.pathTarget.setAttribute("d", this._describeCircle(CIRCLE_RADIUS, CIRCLE_RADIUS, CIRCLE_RADIUS - 5));
} else {
this.pathTarget.setAttribute("d", this._describeArc(CIRCLE_RADIUS, CIRCLE_RADIUS, CIRCLE_RADIUS - 5, 0, this._finalArcPostition));
}
let scoreToDisplay = (this._finalScore * 100 / MAX_QUESTIONS) | 0;
this.countTarget.innerText = `${scoreToDisplay}%`;
window.setTimeout(() => this._showPostStore(), 1);
return;
}
let arcT = Math.sin(tt / ANIMATION_DURATION * Math.PI / 2.0);
this.pathTarget.setAttribute("d", this._describeArc(CIRCLE_RADIUS, CIRCLE_RADIUS, CIRCLE_RADIUS - 5, 0, this._finalArcPostition * arcT));
let scoreToDisplay = (arcT * this._finalScore * 100 / MAX_QUESTIONS) | 0;
if (scoreToDisplay != this._currentlyDisplayedScore) {
this._currentlyDisplayedScore = scoreToDisplay;
this.countTarget.innerText = `${scoreToDisplay}%`;
}
requestAnimationFrame(this._animateFrame.bind(this));
}
_polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees - 180) * Math.PI / 180;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
_describeCircle(x, y, radius) {
var start = this._polarToCartesian(x, y, radius, 0);
var mid = this._polarToCartesian(x, y, radius, 180);
var end = this._polarToCartesian(x, y, radius, 360);
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, 0, 0, mid.x, mid.y,
"A", radius, radius, 0, 1, 0, end.x, end.y,
].join(" ");
return d;
}
_describeArc(x, y, radius, startAngle, endAngle){
var start = this._polarToCartesian(x, y, radius, endAngle);
var end = this._polarToCartesian(x, y, radius, startAngle);
var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
].join(" ");
return d;
}
_showPostStore() {
this.rankTarget.innerText = "Developer";
this.postScoreTarget.classList.remove("hidden");
}
}

View file

@ -2,8 +2,10 @@ import { Application } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.
import ClearStateController from "./controllers/clearstate.js";
import PickerController from "./controllers/picker.js";
import YourScoreController from "./controllers/yourscore.js";
window.Stimulus = Application.start();
Stimulus.register("picker", PickerController);
Stimulus.register("clearstate", ClearStateController);
Stimulus.register("clearstate", ClearStateController);
Stimulus.register("yourscore", YourScoreController);

View file

@ -1,3 +1,7 @@
:root {
--score-color: green;
}
div.offscreen {
position: fixed;
left: -100px;
@ -85,6 +89,65 @@ input[type=radio] {
display: none !important;
}
.vspacer {
height: 2em;
}
/**
* Your score
*/
div.score-wrapper {
text-align: center;
}
div.yourscore {
margin-inline: auto;
position: relative;
text-align: left;
width: 250px;
height: 250px;
}
div.yourscore svg {
position: absolute;
width: 100%;
height: 100%;
z-index: -10;
}
div.yourscore svg path {
stroke: #ccc;
}
div.yourscore svg path.score {
stroke: var(--score-color);
}
div.yourscore .counter {
position: absolute;
font-size: 4em;
top: calc(50% - 0.75em);
left: 0;
width: 100%;
text-align: center;
color: var(--score-color);
z-index: 10;
}
div.post-score {
margin-block-start: 2em;
}
div.rank {
font-size: 1.5em;
text-align: center;
}
/**
* Page transition
*/

View file

@ -1,3 +0,0 @@
<h1>Well Done</h1>
<p>You reached the end</p>

22
views/results.html Normal file
View file

@ -0,0 +1,22 @@
<h1>Results</h1>
<div data-controller="yourscore" class="score-wrapper">
<p>Your final score is</p>
<div>
<div class="yourscore">
<svg xml:space="preserve" id="svg2" x="0" y="0" version="1.1" viewBox="0 0 100 100">
<g transform="rotate(90,50,50)">
<path data-yourscore-target="pathBack" fill="none" stroke-width="10" d="M 5 50 A 45 45 0 0 0 95 50 A 45 45 0 1 0 5 50"/>
<path data-yourscore-target="path" fill="none" stroke-width="10" class="score"/>
</g>
</svg>
<div data-yourscore-target="count" class="counter">0%</div>
</div>
</div>
<div data-yourscore-target="postScore" class="post-score hidden">
<div>You achieved a rank of</div>
<div data-yourscore-target="rank" class="rank"></div>
<div class="vspacer"></div>
<a href="{{prefix}}/end" class="button">Continue</a>
</div>
</div>