First commit of ayerble.com
Create vite configuration Create Three.js animation background Add docker support
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.vite
|
||||||
|
*.md
|
||||||
|
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vite/
|
||||||
|
|
||||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:22-alpine
|
||||||
|
RUN npm install -g serve
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
EXPOSE 3627
|
||||||
|
CMD ["serve", "dist", "-l", "3627"]
|
||||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
ayerble:
|
||||||
|
build: .
|
||||||
|
container_name: ayerble
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3627:3627"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
49
index.html
Normal file
49
index.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>ayerble</title>
|
||||||
|
<meta name="description" content="ayerble — personal pages and projects." />
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<canvas id="bg-canvas" aria-hidden="true"></canvas>
|
||||||
|
<a class="skip-link" href="#main">Skip to main content</a>
|
||||||
|
|
||||||
|
<main id="main">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">ayerble</h1>
|
||||||
|
<p class="subtitle">// personal pages & projects</p>
|
||||||
|
|
||||||
|
<nav aria-label="Site links">
|
||||||
|
<ul class="site-list" id="site-list">
|
||||||
|
<li>
|
||||||
|
<a href="https://aaronwilliampo.com" target="_blank" rel="noopener noreferrer"
|
||||||
|
aria-label="aaronwilliampo.com (opens in new tab)">
|
||||||
|
aaronwilliampo.com
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://git.aaronwilliampo.com" target="_blank" rel="noopener noreferrer"
|
||||||
|
aria-label="git.aaronwilliampo.com (opens in new tab)">
|
||||||
|
git.aaronwilliampo.com
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://yerb-engine.aaronwilliampo.com" target="_blank" rel="noopener noreferrer"
|
||||||
|
aria-label="yerb-engine.aaronwilliampo.com (opens in new tab)">
|
||||||
|
yerb-engine.aaronwilliampo.com
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer><small>© 2026 ayerble</small></footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1186
package-lock.json
generated
Normal file
1186
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "ayerble.com",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"three": "^0.172.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/three": "^0.172.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
58
src/background.ts
Normal file
58
src/background.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the Three.js wireframe background scene.
|
||||||
|
* @param canvas – the `<canvas>` element to render into.
|
||||||
|
*/
|
||||||
|
export function initBackground(canvas: HTMLCanvasElement): void {
|
||||||
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const aspect = window.innerWidth / window.innerHeight;
|
||||||
|
const camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 200);
|
||||||
|
camera.position.set(0, 0, 10);
|
||||||
|
camera.lookAt(new THREE.Vector3(0, 0, 0));
|
||||||
|
|
||||||
|
// Wireframe sphere at exact origin
|
||||||
|
const geo = new THREE.SphereGeometry(3.5, 32, 32);
|
||||||
|
const mat = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0x00ffe7,
|
||||||
|
wireframe: true,
|
||||||
|
opacity: 0.28,
|
||||||
|
transparent: true,
|
||||||
|
});
|
||||||
|
const mesh = new THREE.Mesh(geo, mat);
|
||||||
|
mesh.position.set(0, 0, 0);
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
|
// Grid floor
|
||||||
|
const grid = new THREE.GridHelper(30, 30, 0xff2d78, 0xff2d78);
|
||||||
|
grid.material.opacity = 0.1;
|
||||||
|
grid.material.transparent = true;
|
||||||
|
grid.position.y = -3.2;
|
||||||
|
scene.add(grid);
|
||||||
|
|
||||||
|
// Handle resize
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
const w = window.innerWidth;
|
||||||
|
const h = window.innerHeight;
|
||||||
|
renderer.setSize(w, h);
|
||||||
|
camera.aspect = w / h;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
});
|
||||||
|
|
||||||
|
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
(function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
if (!reduced) {
|
||||||
|
mesh.rotation.y += 0.003;
|
||||||
|
mesh.rotation.z += 0.004;
|
||||||
|
grid.rotation.y += 0.0008;
|
||||||
|
}
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
11
src/main.ts
Normal file
11
src/main.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import './style.css';
|
||||||
|
import { initBackground } from './background';
|
||||||
|
import { initNavigation } from './navigation';
|
||||||
|
|
||||||
|
const canvas = document.getElementById('bg-canvas') as HTMLCanvasElement | null;
|
||||||
|
if (canvas) {
|
||||||
|
initBackground(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
initNavigation();
|
||||||
|
|
||||||
40
src/navigation.ts
Normal file
40
src/navigation.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Initialise arrow-key (↑ / ↓ / Enter) navigation for the site link list.
|
||||||
|
*/
|
||||||
|
export function initNavigation(): void {
|
||||||
|
const links = Array.from(
|
||||||
|
document.querySelectorAll<HTMLAnchorElement>('#site-list a'),
|
||||||
|
);
|
||||||
|
let idx = -1;
|
||||||
|
|
||||||
|
function setActive(i: number): void {
|
||||||
|
links.forEach((l) => l.classList.remove('active'));
|
||||||
|
if (i >= 0 && i < links.length) {
|
||||||
|
links[i].classList.add('active');
|
||||||
|
links[i].focus();
|
||||||
|
idx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
setActive(idx < links.length - 1 ? idx + 1 : 0);
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
setActive(idx > 0 ? idx - 1 : links.length - 1);
|
||||||
|
} else if (e.key === 'Enter' && idx >= 0) {
|
||||||
|
links[idx].click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync idx when focus moves via mouse / tab
|
||||||
|
links.forEach((l, i) => {
|
||||||
|
l.addEventListener('focus', () => {
|
||||||
|
idx = i;
|
||||||
|
l.classList.add('active');
|
||||||
|
});
|
||||||
|
l.addEventListener('blur', () => l.classList.remove('active'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
139
src/style.css
Normal file
139
src/style.css
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after { animation: none !important; transition: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--cyan: #00ffe7;
|
||||||
|
--magenta: #ff2d78;
|
||||||
|
--yellow: #f5e642;
|
||||||
|
--text: #c8d0e0;
|
||||||
|
--text-dim: #6b7280;
|
||||||
|
--font: "Courier New", Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
html { background: #0d0d0f; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanlines */
|
||||||
|
body::after {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0px, transparent 3px,
|
||||||
|
rgba(0,0,0,.15) 3px, rgba(0,0,0,.15) 4px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background canvas */
|
||||||
|
#bg-canvas {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skip link */
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: 1rem;
|
||||||
|
background: var(--cyan);
|
||||||
|
color: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: .4rem .8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 10000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.skip-link:focus { top: 1rem; }
|
||||||
|
|
||||||
|
/* Main content */
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2.5rem 3rem;
|
||||||
|
background: rgba(13, 13, 15, 0.65);
|
||||||
|
border: 1px solid rgba(0, 255, 231, 0.08);
|
||||||
|
border-radius: 6px;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2rem, 8vw, 3.5rem);
|
||||||
|
letter-spacing: .12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--cyan);
|
||||||
|
margin-bottom: .4rem;
|
||||||
|
}
|
||||||
|
h1 span { color: var(--magenta); }
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: .9rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link list */
|
||||||
|
.site-list {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-list li a {
|
||||||
|
display: block;
|
||||||
|
font-size: clamp(.95rem, 2.5vw, 1.15rem);
|
||||||
|
color: #8a95a8;
|
||||||
|
text-decoration: none;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
padding: .55rem 1.5rem;
|
||||||
|
border: 1px solid rgba(138, 149, 168, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color .12s, border-color .12s, background .12s;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Arrow-key highlight + hover + focus all share the same style */
|
||||||
|
.site-list li a.active,
|
||||||
|
.site-list li a:hover,
|
||||||
|
.site-list li a:focus-visible {
|
||||||
|
color: var(--cyan);
|
||||||
|
border-color: var(--cyan);
|
||||||
|
background: rgba(0,255,231,.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Visible keyboard focus ring for AODA */
|
||||||
|
.site-list li a:focus-visible {
|
||||||
|
outline: 2px solid var(--yellow);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
font-size: .75rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
2
src/vite-env.d.ts
vendored
Normal file
2
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user