massi-world

this website of mine
Log | Files | Refs

commit 4c12411cee2fc462d19478e5f76285d0b0f015c1
parent 9eb9cfb5f45998b3e5b2e7e938fa818c13ba2660
Author: massi <mdsiboldi@gmail.com>
Date:   Wed,  7 Aug 2024 19:15:47 -0700

easter egg number two

Diffstat:
MMakefile | 2+-
Ashared/static/shared/assets/mw-spritesheet.png | 0
Msite/md/fretfret.templ.md | 2+-
Msite/style.templ.css | 130++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/site.clj | 28++++++++++++++++++++--------
Ats/the-world.ts | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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(); + } + }) +});