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