feat: use webgl shaders instead of wasm
This commit is contained in:
parent
38efffa9b9
commit
51dc6c9ee6
24 changed files with 1712 additions and 1607 deletions
|
|
@ -1,13 +1,9 @@
|
|||
export function frostedBox(content: string, extraClasses?: string): string {
|
||||
return `
|
||||
<div class="site-shell relative my-[2ch] overflow-hidden px-[2ch] py-[2ch] ${extraClasses ?? ""}">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 h-[200%]"
|
||||
style="background-color: rgba(0, 0, 0, 0.75); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); mask-image: linear-gradient(to bottom, black 0% 50%, transparent 50% 100%); -webkit-mask-image: linear-gradient(to bottom, black 0% 50%, transparent 50% 100%);"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<div class="absolute inset-0 border-2 border-white" aria-hidden="true"></div>
|
||||
<div class="relative z-10 h-full min-h-0">
|
||||
<div class="site-shell site-panel-frame relative my-[2ch] overflow-hidden px-[2ch] py-[2ch] ${extraClasses ?? ""}">
|
||||
<div class="site-panel-frost pointer-events-none" aria-hidden="true"></div>
|
||||
<div class="site-panel-border" aria-hidden="true"></div>
|
||||
<div class="site-panel-content h-full min-h-0">
|
||||
${content}
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
const MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
||||
const STILL_STEPS = 5;
|
||||
|
||||
type BackgroundMode = "animated" | "still" | "failed";
|
||||
|
||||
interface BackgroundActions {
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
renderStill: (steps: number) => void;
|
||||
}
|
||||
|
||||
function getMode(reducedMotion: boolean): BackgroundMode {
|
||||
return reducedMotion ? "still" : "animated";
|
||||
}
|
||||
|
||||
function applyCanvasState(mode: BackgroundMode) {
|
||||
const canvas = document.getElementById("canvas");
|
||||
document.body.dataset.backgroundMode = mode;
|
||||
if (canvas) {
|
||||
canvas.toggleAttribute("hidden", mode === "failed");
|
||||
}
|
||||
}
|
||||
|
||||
export function initBackgroundControls(actions: BackgroundActions) {
|
||||
const media = window.matchMedia(MOTION_QUERY);
|
||||
let mode: BackgroundMode = getMode(media.matches);
|
||||
let failed = false;
|
||||
|
||||
const applyMode = (restartAnimation = false) => {
|
||||
if (failed) return;
|
||||
|
||||
mode = getMode(media.matches);
|
||||
applyCanvasState(mode);
|
||||
|
||||
if (mode === "animated") {
|
||||
if (restartAnimation) {
|
||||
actions.stop();
|
||||
}
|
||||
|
||||
actions.start();
|
||||
return;
|
||||
}
|
||||
|
||||
actions.stop();
|
||||
actions.renderStill(STILL_STEPS);
|
||||
};
|
||||
|
||||
const restartAnimation = () => {
|
||||
if (document.visibilityState === "hidden" || media.matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyMode(true);
|
||||
};
|
||||
|
||||
media.addEventListener("change", () => {
|
||||
applyMode();
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
restartAnimation();
|
||||
});
|
||||
|
||||
window.addEventListener("pageshow", () => {
|
||||
restartAnimation();
|
||||
});
|
||||
|
||||
return {
|
||||
applyInitialMode() {
|
||||
applyMode();
|
||||
},
|
||||
setFailed() {
|
||||
failed = true;
|
||||
mode = "failed";
|
||||
applyCanvasState(mode);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -28,8 +28,10 @@ export function renderFooter() {
|
|||
const mirror = getMirrorLink();
|
||||
|
||||
footer.innerHTML = `
|
||||
<div class="site-panel px-[2ch] py-[1ch]">
|
||||
<div class="site-footer-inner">
|
||||
<div class="site-panel-frame px-[2ch] py-[1ch]">
|
||||
<div class="site-panel-frost" aria-hidden="true"></div>
|
||||
<div class="site-panel-border" aria-hidden="true"></div>
|
||||
<div class="site-panel-content site-footer-inner">
|
||||
<a href="${REPO_URL}">src</a>
|
||||
<span aria-hidden="true">|</span>
|
||||
<a href="/qa/rss.xml" data-native-link>rss</a>
|
||||
|
|
|
|||
1515
src/lib/webgl-background.ts
Normal file
1515
src/lib/webgl-background.ts
Normal file
File diff suppressed because it is too large
Load diff
24
src/main.ts
24
src/main.ts
|
|
@ -1,7 +1,6 @@
|
|||
import "~/styles/globals.css";
|
||||
import init, { render_still, start, stop } from "cgol";
|
||||
import { route, initRouter } from "~/router";
|
||||
import { initBackgroundControls } from "~/lib/background";
|
||||
import { initWebGLBackground } from "~/lib/webgl-background";
|
||||
import { renderFooter } from "~/lib/site";
|
||||
import { homePage } from "~/pages/home";
|
||||
import { qaPage } from "~/pages/qa";
|
||||
|
|
@ -12,24 +11,5 @@ route("/qa", "Jet Pham - Q+A", qaPage);
|
|||
route("*", "404 - Jet Pham", notFoundPage);
|
||||
|
||||
renderFooter();
|
||||
const background = initBackgroundControls({
|
||||
start() {
|
||||
start();
|
||||
},
|
||||
stop() {
|
||||
stop();
|
||||
},
|
||||
renderStill(steps) {
|
||||
render_still(steps);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await init();
|
||||
background.applyInitialMode();
|
||||
} catch (e) {
|
||||
background.setFailed();
|
||||
console.error("WASM init failed:", e);
|
||||
}
|
||||
|
||||
initWebGLBackground();
|
||||
initRouter();
|
||||
|
|
|
|||
|
|
@ -6,30 +6,31 @@ export function homePage(outlet: HTMLElement) {
|
|||
outlet.innerHTML = `
|
||||
<div class="flex flex-col items-center justify-start">
|
||||
${frostedBox(`
|
||||
<div class="flex flex-col items-center justify-center gap-[2ch] md:flex-row">
|
||||
<div class="flex flex-col items-center justify-center gap-[1.25ch] md:gap-[2ch] md:flex-row">
|
||||
<div class="order-1 flex flex-col items-center md:order-2">
|
||||
<h1 class="sr-only">Jet Pham</h1>
|
||||
<div aria-hidden="true">${Jet}</div>
|
||||
<div aria-hidden="true" data-emitter-ansi>${Jet}</div>
|
||||
<p class="mt-[2ch]">Software Extremist</p>
|
||||
</div>
|
||||
<div class="order-2 shrink-0 md:order-1">
|
||||
<img
|
||||
data-emitter-image
|
||||
src="/jet.svg"
|
||||
alt="A picture of Jet wearing a beanie in purple and blue lighting"
|
||||
width="250"
|
||||
height="250"
|
||||
class="aspect-square w-full max-w-[250px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
|
||||
class="aspect-square w-full max-w-[220px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
|
||||
style="background-color: #a80055; color: transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="section-block mt-[2ch] border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0">
|
||||
<legend class="-mx-[0.5ch] px-[0.5ch] text-white">Contact</legend>
|
||||
<fieldset class="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0" style="border-color: var(--white)">
|
||||
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--white)">Contact</legend>
|
||||
<button type="button" id="copy-email" class="qa-inline-action">jet@extremist.software</button>
|
||||
<span id="copy-email-status" class="qa-meta ml-[1ch]" aria-live="polite"></span>
|
||||
</fieldset>
|
||||
<fieldset class="section-block mt-[2ch] border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0">
|
||||
<legend class="-mx-[0.5ch] px-[0.5ch] text-white">Links</legend>
|
||||
<fieldset class="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0" style="border-color: var(--white)">
|
||||
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--white)">Links</legend>
|
||||
<ol>
|
||||
<li><a href="https://git.extremist.software" class="inline-flex items-center">Forgejo</a></li>
|
||||
<li><a href="https://github.com/jetpham" class="inline-flex items-center">GitHub</a></li>
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ async function render() {
|
|||
outlet.innerHTML = "";
|
||||
await r.handler(outlet, params);
|
||||
document.title = r.title;
|
||||
document.dispatchEvent(new Event("site:render"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +71,7 @@ async function render() {
|
|||
await notFoundHandler(outlet, {});
|
||||
}
|
||||
document.title = notFoundTitle;
|
||||
document.dispatchEvent(new Event("site:render"));
|
||||
}
|
||||
|
||||
export function initRouter() {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@
|
|||
--light-magenta: #ff55ff;
|
||||
--yellow: #ffff55;
|
||||
--white: #ffffff;
|
||||
--panel-bg-alpha: 0.2;
|
||||
--panel-gloss-alpha: 0.05;
|
||||
--panel-border-inset: 0.6rem;
|
||||
--panel-bg: rgba(0, 0, 0, var(--panel-bg-alpha));
|
||||
--panel-blur: 10px;
|
||||
--fallback-glow-a: rgba(85, 255, 255, 0.18);
|
||||
--fallback-glow-b: rgba(85, 85, 255, 0.16);
|
||||
--fallback-glow-c: rgba(255, 85, 85, 0.14);
|
||||
}
|
||||
|
||||
html {
|
||||
|
|
@ -43,6 +51,8 @@ html {
|
|||
body {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--black);
|
||||
|
|
@ -53,6 +63,38 @@ body {
|
|||
line-height: 1;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: -20vmax;
|
||||
z-index: -30;
|
||||
background: var(--black);
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body[data-background-mode="failed"]::before {
|
||||
opacity: 1;
|
||||
animation: fallback-drift 18s linear infinite alternate;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body[data-background-mode="failed"]::before {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fallback-drift {
|
||||
from {
|
||||
transform: translate3d(-3%, -2%, 0) scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(3%, 2%, 0) scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--light-blue);
|
||||
color: var(--black);
|
||||
|
|
@ -98,20 +140,49 @@ body {
|
|||
}
|
||||
|
||||
.site-shell {
|
||||
width: min(100%, 60%);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.site-panel {
|
||||
border: 2px solid var(--white);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
@media (min-width: 768px) {
|
||||
.site-shell {
|
||||
width: min(100%, 60%);
|
||||
}
|
||||
}
|
||||
|
||||
.site-panel-frame {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.site-panel-frost {
|
||||
position: absolute;
|
||||
inset: var(--panel-border-inset);
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, var(--panel-gloss-alpha)),
|
||||
transparent 24%
|
||||
),
|
||||
var(--panel-bg);
|
||||
}
|
||||
|
||||
.site-panel-border {
|
||||
position: absolute;
|
||||
inset: var(--panel-border-inset);
|
||||
border: 2px solid var(--white);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.site-panel-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.site-region {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue