commit 4c12411cee2fc462d19478e5f76285d0b0f015c1
parent 9eb9cfb5f45998b3e5b2e7e938fa818c13ba2660
Author: massi <mdsiboldi@gmail.com>
Date: Wed, 7 Aug 2024 19:15:47 -0700
easter egg number two
Diffstat:
6 files changed, 299 insertions(+), 25 deletions(-)
diff --git a/Makefile b/Makefile
@@ -22,7 +22,7 @@ build:
--sourcemap \
--outdir=build/shared/shared/js \
--format=esm \
- ts/* ;
+ ts/*.ts ;
# link shared files to each subdomain
cd build/massi.world && ln -s ../shared/* .
diff --git a/shared/static/shared/assets/mw-spritesheet.png b/shared/static/shared/assets/mw-spritesheet.png
Binary files differ.
diff --git a/site/md/fretfret.templ.md b/site/md/fretfret.templ.md
@@ -49,4 +49,4 @@ guitar wizard.
# Contributing
Email hello at this domain, with fretfret in the subject, with
- patches or bug reports.
+ patches, bug reports, or feature requests.
diff --git a/site/style.templ.css b/site/style.templ.css
@@ -31,16 +31,16 @@ html {
font-size: 20px;
color: var(--fg);
background: var(--bg);
-
+ font-family: Verdana, Geneva, sans-serif;
}
body {
box-sizing: border-box;
- line-height: 1.5;
+ line-height: 1.7;
margin: 0px auto;
max-width: 35rem;
min-height: 100vh;
- padding: 0.75rem 0.75rem 2.5rem 0.75rem;
+ padding: 0.75rem 0.75rem 10rem 0.75rem;
position: relative;
}
@@ -53,13 +53,13 @@ footer {
a {
color: var(--link);
text-decoration: 1px underline;
- text-underline-offset: 0.1em;
+ text-underline-offset: 0.2em;
}
.md h1 {
font-size: 1.4rem;
- font-weight: normal;
- margin: 0.5rem 0;
+ font-weight: bold;
+ margin: 2rem 0 1rem;
}
.md p, .md ul, .md ol {
@@ -82,27 +82,39 @@ h1 > a, h2 > a, h3 > a, h4 > a {
}
header {
+ display: flex;
+ flex-wrap: wrap;
+ justify-items: flex-start;
font-size: 1.2rem;
- padding-bottom: 1.8rem;
+ padding-bottom: 3rem;
+}
+
+header > span {
+ display: flex;
+ align-items: center;
}
header > span:not(:last-child) {
- display: inline-block;
}
header a {
color: inherit;
+ text-decoration: none;
+}
+
+header a:hover {
+ text-decoration: 1px underline;
}
header span:last-child a {
- font-size: 1.8rem;
+ font-size: 2rem;
text-decoration: none;
}
-/* line break for last header */
-header > span:not(:first-child):last-child::before {
- content: '\A';
- white-space: pre;
+.flex-break {
+ flex-basis: 100%;
+ width: 0;
+ height: 0;
}
ul {
@@ -186,7 +198,95 @@ picture, picture img {
header span:last-child img {
height: 2rem;
- position: relative;
- top: 0.3rem;
padding: 0 0.3rem;
}
+
+:root {
+ --world-size: 30px;
+}
+
+#the-world {
+ position: relative;
+ width: var(--world-size);
+ height: var(--world-size);
+ background-image: url("@(web-root)/shared/assets/mw-spritesheet.png");
+ background-size: auto 100%;
+ image-rendering: pixelated;
+ transition: transform 5s ease-out;
+}
+
+.header-home:not(:last-child) {
+ --world-size: 20px;
+}
+
+header a {
+ display: inline-flex;
+ align-items: center;
+}
+
+.header-home {
+ margin-left: calc(-0.5rem - 20px);
+}
+
+#contains-the-world {
+ margin-top: 0.3rem;
+ margin-right: 0.5rem;
+ display: inline-block;
+ width: var(--world-size);
+ height: var(--world-size);
+ position: relative;
+}
+
+@media (max-width: 600px) {
+ .header-home {
+ margin-left: calc(-0.5rem - 40px);
+ }
+ :root {
+ --world-size: 60px;
+ }
+ .header-home:not(:last-child) {
+ margin-left: calc(-0.5rem - 30px);
+ }
+ .header-home:not(:last-child) {
+ --world-size: 40px;
+ }
+}
+
+@font-face {
+ font-family: "Playfair Display";
+ src:
+ local("Playfair Display"),
+ url("@(web-root)/shared/fonts/PlayfairDisplay-VariableFont_wght.ttf");
+}
+
+@font-face {
+ font-family: "Atkinson Hyperlegible";
+ src:
+ local("Atkinson Hyperlegible"),
+ url("@(web-root)/shared/fonts/Atkinson-Hyperlegible-Regular-102.ttf");
+}
+
+@font-face {
+ font-family: "Atkinson Hyperlegible";
+ font-weight: 500 900;
+ src:
+ local("Atkinson Hyperlegible Bold"),
+ url("@(web-root)/shared/fonts/Atkinson-Hyperlegible-Bold-102.ttf");
+}
+
+@font-face {
+ font-family: "Atkinson Hyperlegible";
+ font-style: oblique;
+ src:
+ local("Atkinson Hyperlegible Italic"),
+ url("@(web-root)/shared/fonts/Atkinson-Hyperlegible-Italic-102.ttf");
+}
+
+@font-face {
+ font-family: "Atkinson Hyperlegible";
+ font-weight: 500 900;
+ font-style: oblique;
+ src:
+ local("Atkinson Hyperlegible Bold Italic"),
+ url("@(web-root)/shared/fonts/Atkinson-Hyperlegible-BoldItalic-102.ttf");
+}
diff --git a/src/site.clj b/src/site.clj
@@ -41,9 +41,12 @@
{:outfile (p :out pgkey)})))
(defn site-header [spec]
- (concat (core/massi-world-domain-stuff (p :url spec) p spec)
- (list [:link {:rel "stylesheet" :href (p "/style.css")}]
- [:link {:rel "manifest" :href (p "/manifest.json")}])))
+ (concat
+ (list [:link {:rel "preload" :as "style" :href (p "/style.css")}]
+ [:link {:rel "stylesheet" :href (p "/style.css")}]
+ [:script {:src (p "/shared/js/the-world.js")}])
+ (core/massi-world-domain-stuff (p :url spec) p spec)
+ (list [:link {:rel "manifest" :href (p "/manifest.json")}])))
(defn with-head [pg-key-or-spec & body]
(h/html (h/raw "<!DOCTYPE html>")
@@ -73,12 +76,21 @@
([pgkey-or-spec transform-title]
[:a {:href (p :path pgkey-or-spec)} (-> pgkey-or-spec getspec :title transform-title)]))
+(def the-world
+ [:span {:id "contains-the-world"} [:div {:id "the-world"}]])
+
(defn header [& crumbs]
[:header
- (let [lst (conj crumbs (mw-anchor :home))]
- (concat (map (fn [x] [:span x [:span {:class "crumb-interposer"} svg-arrow]])
- (butlast lst))
- (list [:span (last lst)])))])
+ (concat
+ (list [:span {:class "header-home"}
+ (mw-anchor
+ :home
+ (fn [txt] (list the-world [:span {:class "home-anchor-text"} txt])))
+ (when crumbs [:span {:class "crumb-interposer"} svg-arrow])])
+ (map (fn [x] [:span x [:span {:class "crumb-interposer"} svg-arrow]])
+ (butlast crumbs))
+ (when (last crumbs)
+ (list [:span {:class "flex-break"}] [:span (last crumbs)])))])
(defn footer []
[:footer [:small
@@ -104,7 +116,7 @@
(let [out-file (p :out pgkey)
out-str (do (core/prnt 2 "🌀" "rendering " pgkey)
(str ((-> pgkey getspec :render))))
- out-str (if (cstr/ends-with? out-file ".html")
+ out-str (if (and (core/dev?) (cstr/ends-with? out-file ".html"))
(do
(core/prnt 2 "✨" (str "tidying html..."))
(core/prettify-html out-str))
diff --git a/ts/the-world.ts b/ts/the-world.ts
@@ -0,0 +1,162 @@
+document.addEventListener("DOMContentLoaded", () => {
+ const siblingEl = document.querySelector(".home-anchor-text") as HTMLSpanElement;
+ const worldEl = document.querySelector("#the-world") as HTMLSpanElement;
+ const spriteWidth = worldEl.getBoundingClientRect().width;
+ const initForce = 1;
+ const maxVelocity = 100;
+ let force = initForce;
+ const accel = 0.1;
+ const degreesInRotation = 360;
+ const frames = 12;
+ const degreesInFrame = degreesInRotation / frames;
+ const fps = 24;
+ const tickTime = 1000 / fps;
+
+ // targets 3 sprite frames per sec
+ const steadyIncrease = (degreesInFrame * 3) / fps;
+
+ const xOffset = (frame: number) => {
+ return frame * spriteWidth;
+ }
+
+ let rotation = 0; // in degrees
+ let frame = 0;
+ let velocity = 0;
+
+ const physicsTime = 10_000;
+ const esplodeTime = 3_000;
+
+ const isTimeFor = (ts, tTimer, tgtTime) => {
+ return ts - tTimer > tgtTime;
+ }
+
+ const drag = (v: number) => Math.max(v / 10, 0.005 * v * v);
+
+ const updatePosition = (v = velocity) => {
+ rotation = (rotation + v) % degreesInRotation;
+ frame = Math.floor(rotation / degreesInFrame);
+ }
+
+ const updateVelocity = (deltaV: number) => {
+ velocity = Math.min(maxVelocity, velocity + deltaV - drag(Math.round(velocity)));
+ }
+
+ const physicsTick = (isHovering: boolean) => {
+ updateVelocity(isHovering ? force : 0);
+ updatePosition();
+
+ if (isHovering) {
+ force += accel;
+ }
+ else {
+ force = initForce;
+ }
+
+ if (velocity > 45) {
+ shakeTick();
+ }
+ else {
+ resetShake();
+ }
+
+ updateFrame();
+ }
+
+ const steadyTick = (isHovering: boolean) => {
+ if (isHovering) {
+ updatePosition(steadyIncrease);
+ updateFrame();
+ }
+ }
+
+ const updateFrame = () => {
+ worldEl.style.backgroundPositionX = `${xOffset(frame)}px`;
+ }
+
+ const rBigPos = () => (Math.random() > 0.5 ? 1 : -1) * (2000 + Math.random() * 2000);
+
+ const esplode = () => {
+ console.log('boom!');
+ const pos = worldEl.getBoundingClientRect();
+ worldEl.style.position = "fixed";
+ worldEl.style.top = pos.y + "px";
+ worldEl.style.left = pos.x + "px";
+ const dest = `translate(${rBigPos()}px, ${rBigPos()}px) scale(${Math.random() > 0.5 ? 20 : 0.001 })`
+ worldEl.style.transform = dest;
+ }
+
+ let shakeDir = 1;
+ let shakeTimer: number | null = null;
+ const shakeTick = () => {
+ if (!shakeTimer) {
+ shakeTimer = setTimeout(() => {
+ esplode();
+ }, esplodeTime);
+ }
+ shakeDir *= -1;
+ worldEl.style.translate = `${shakeDir}px`;
+ }
+
+ const resetShake = () => {
+ if (shakeTimer !== null) {
+ clearTimeout(shakeTimer);
+ }
+ worldEl.style.translate = "";
+ }
+
+ let t = -Infinity;
+ let tPhysics: number | null = null;
+ let inPhysics = false;
+ let doAnimation = false;
+ const animate = (timestamp: DOMHighResTimeStamp = 0) => {
+ if (!doAnimation) {
+ return;
+ }
+
+ if (!tPhysics) {
+ tPhysics = timestamp;
+ }
+ if (isTimeFor(timestamp, t, tickTime)) {
+ t = timestamp;
+
+
+ const worldHovering = worldEl.matches(":hover");
+ const textHovering = siblingEl.matches(":hover");
+ const isHovering = worldHovering || textHovering;
+
+ // physics timer only run while hovering on world.
+ if (!worldHovering) {
+ tPhysics = timestamp;
+ }
+
+ inPhysics = (worldHovering && isTimeFor(timestamp, tPhysics, physicsTime)) || velocity > 0.5;
+
+ if (!inPhysics && !isHovering) {
+ doAnimation = false;
+ return;
+ }
+
+ if (!inPhysics) {
+ steadyTick(isHovering);
+ }
+ else {
+ physicsTick(worldHovering);
+ }
+ }
+ requestAnimationFrame(animate);
+ }
+
+ worldEl.addEventListener("mouseenter", () => {
+ if (!doAnimation) {
+ doAnimation = true;
+ animate();
+ }
+ })
+
+ siblingEl.addEventListener("mouseenter", () => {
+ if (!doAnimation) {
+ doAnimation = true;
+ animate();
+ }
+ })
+});