initial personal site

This commit is contained in:
2025-01-30 12:09:47 -05:00
parent 53d229a2c2
commit b5a4ccd539
3 changed files with 131 additions and 868 deletions

View File

@ -1,361 +0,0 @@
const container = document.getElementById('patch-container');
const svgOverlay = document.getElementById('cable-overlay');
const portsState = {};
const LED_COLORS = [
// Gruvbox dark bright colors
'#fb4934', // red
'#b8bb26', // green
'#fabd2f', // yellow
'#83a598', // blue
'#d3869b', // purple
'#8ec07c', // aqua
'#fe8019', // orange
];
const CABLE_COLORS = [
// Gruvbox light dark colors
'#9d0006', // red
'#79740e', // green
'#b57614', // yellow
'#076678', // blue
'#8f3f71', // purple
'#427b58', // aqua
'#af3a03', // orange
];
let cables = [];
let dragSource = null;
let tempCablePath = null;
const NUM_SEGMENTS = 64;
const GRAVITY = 0.6;
const DAMPING = 0.4;
const CONSTRAINT_ITERATIONS = 15;
const SWAY_AMPLITUDE = 0.05;
const SWAY_FREQUENCY = 0.002;
function getPortCenter(portEl) {
const rect = portEl.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
/**********************************************
* LED logic
**********************************************/
function initPortState(portId) {
if (!portsState[portId]) {
portsState[portId] = {
connectionCount: 0,
ledColor: null,
ledIsLit: false,
nextBlinkTime: 0
};
}
}
function addConnection(portId) {
initPortState(portId);
const st = portsState[portId];
st.connectionCount++;
if (st.connectionCount === 1) {
// choose a color if not set
if (!st.ledColor) {
st.ledColor = LED_COLORS[Math.floor(Math.random() * LED_COLORS.length)];
}
// schedule next blink
st.nextBlinkTime = performance.now();
}
}
function removeConnection(portId) {
initPortState(portId);
const st = portsState[portId];
if (st.connectionCount > 0) {
st.connectionCount--;
}
if (st.connectionCount <= 0) {
st.connectionCount = 0;
st.ledIsLit = false;
updatePortLED(portId);
}
}
/** Check if we should toggle LED, then update DOM. */
function updateLEDs(now) {
for (const portId in portsState) {
const st = portsState[portId];
if (st.connectionCount > 0) {
if (now >= st.nextBlinkTime) {
st.ledIsLit = !st.ledIsLit;
const interval = 50 + Math.random() * 150; // 200..800 ms
st.nextBlinkTime = now + interval;
}
}
updatePortLED(portId);
}
}
function updatePortLED(portId) {
const st = portsState[portId];
const portEl = document.getElementById(portId);
if (!portEl) return;
const ledEl = portEl.querySelector('.led');
if (!ledEl) return;
if (st.connectionCount > 0 && st.ledColor) {
ledEl.style.background = st.ledIsLit ? st.ledColor : '#222';
} else {
ledEl.style.background = '#222';
}
}
/**********************************************
* Cable creation & removal
**********************************************/
function randomColor() {
return CABLE_COLORS[Math.floor(Math.random() * CABLE_COLORS.length)];
}
function createCablePath(color) {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute('stroke', color);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-width', '3');
path.classList.add('cable-path');
// We want to click cables to remove them
path.addEventListener('click', onCableClick);
svgOverlay.appendChild(path);
return path;
}
function createRope(sourceEl, targetEl, numSegments) {
const s = getPortCenter(sourceEl);
const t = getPortCenter(targetEl);
const rope = [];
for (let i = 0; i <= numSegments; i++) {
const alpha = i / numSegments;
rope.push({
x: s.x + (t.x - s.x) * alpha,
y: s.y + (t.y - s.y) * alpha,
vx: 0, vy: 0
});
}
return rope;
}
function connect(sourceId, targetId, color) {
if (!sourceId || !targetId || sourceId === targetId) return;
const sourceEl = document.getElementById(sourceId);
const targetEl = document.getElementById(targetId);
if (!sourceEl || !targetEl) return;
color = color || randomColor();
const rope = createRope(sourceEl, targetEl, NUM_SEGMENTS);
const dx = rope[rope.length - 1].x - rope[0].x;
const dy = rope[rope.length - 1].y - rope[0].y;
const totalDist = Math.sqrt(dx * dx + dy * dy);
const restLength = totalDist / NUM_SEGMENTS;
const pathEl = createCablePath(color);
const swayOffset = Math.random() * 1000;
cables.push({
sourceId, targetId,
pathEl, ropeSegments: rope,
restLength, color, swayOffset
});
addConnection(sourceId);
addConnection(targetId);
}
function removeCable(cable) {
svgOverlay.removeChild(cable.pathEl);
cables = cables.filter(c => c !== cable);
removeConnection(cable.sourceId);
removeConnection(cable.targetId);
}
function onCableClick(e) {
const pathEl = e.currentTarget;
const cable = cables.find(c => c.pathEl === pathEl);
if (cable) removeCable(cable);
}
/**********************************************
* Dragging logic (user can draw cables)
**********************************************/
function handlePortMouseDown(e) {
e.preventDefault();
dragSource = e.currentTarget;
const color = randomColor();
tempCablePath = createCablePath(color);
tempCablePath.style.pointerEvents = 'none';
}
function handleMouseMove(e) {
if (!dragSource || !tempCablePath) return;
const s = getPortCenter(dragSource);
const mx = e.clientX, my = e.clientY;
const midX = (s.x + mx) / 2, midY = (s.y + my) / 2;
const d = `
M ${s.x},${s.y}
C ${midX},${s.y}
${midX},${my}
${mx},${my}
`;
tempCablePath.setAttribute('d', d);
}
function handleMouseUp(e) {
const dropTargetEl = document.elementFromPoint(e.clientX, e.clientY);
const portEl = dropTargetEl && dropTargetEl.closest('.port');
if (portEl) {
const sourceId = dragSource.id;
const targetId = portEl.id;
if (sourceId !== targetId) {
const color = tempCablePath.getAttribute('stroke');
svgOverlay.removeChild(tempCablePath);
tempCablePath = null;
connect(sourceId, targetId, color);
} else {
svgOverlay.removeChild(tempCablePath);
tempCablePath = null;
}
} else {
svgOverlay.removeChild(tempCablePath);
tempCablePath = null;
}
dragSource = null;
}
/***********************************************
* NAS drive-lights random blinking
***********************************************/
let driveLights = [];
function initDriveLights() {
driveLights = document.querySelectorAll('.drive-light');
driveLights.forEach(light => {
light.dataset.nextBlinkTime = '0';
});
}
function updateDriveLights(timestamp) {
driveLights.forEach(light => {
let nextBlink = parseFloat(light.dataset.nextBlinkTime) || 0;
if (timestamp >= nextBlink) {
light.classList.toggle('lit');
const interval = 300 + Math.random() * 500; // 300..800 ms
light.dataset.nextBlinkTime = (timestamp + interval).toString();
}
});
}
/**********************************************
* Main animation loop
**********************************************/
function animate(timestamp) {
updateLEDs(timestamp);
updateDriveLights(timestamp);
const dt = 1.0;
cables.forEach(cable => {
const rope = cable.ropeSegments;
const n = rope.length;
if (n < 2) return;
const sourceEl = document.getElementById(cable.sourceId);
const targetEl = document.getElementById(cable.targetId);
if (!sourceEl || !targetEl) return;
const s = getPortCenter(sourceEl);
const t = getPortCenter(targetEl);
// anchors
rope[0].x = s.x; rope[0].y = s.y;
rope[0].vx = 0; rope[0].vy = 0;
rope[n - 1].x = t.x; rope[n - 1].y = t.y;
rope[n - 1].vx = 0; rope[n - 1].vy = 0;
// 1) gravity + wind for interior segments
for (let i = 1; i < n - 1; i++) {
rope[i].vy += GRAVITY;
const wave = Math.sin((timestamp + cable.swayOffset + i * 50) * SWAY_FREQUENCY);
rope[i].vx += wave * SWAY_AMPLITUDE;
rope[i].vx *= DAMPING;
rope[i].vy *= DAMPING;
rope[i].x += rope[i].vx * dt;
rope[i].y += rope[i].vy * dt;
}
// 2) constraints
for (let iter = 0; iter < CONSTRAINT_ITERATIONS; iter++) {
for (let i = 0; i < n - 1; i++) {
let p1 = rope[i], p2 = rope[i + 1];
let dx = p2.x - p1.x, dy = p2.y - p1.y;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.001) dist = 0.001;
let diff = (dist - cable.restLength) / dist;
if (i !== 0) {
p1.x += 0.5 * dx * diff;
p1.y += 0.5 * dy * diff;
}
if (i + 1 !== n - 1) {
p2.x -= 0.5 * dx * diff;
p2.y -= 0.5 * dy * diff;
}
}
}
// 3) recalc velocities
for (let i = 1; i < n - 1; i++) {
rope[i].vx = rope[i].x - (rope[i].x - rope[i].vx * dt);
rope[i].vy = rope[i].y - (rope[i].y - rope[i].vy * dt);
}
// 4) update path
let pathD = `M ${rope[0].x},${rope[0].y}`;
for (let i = 1; i < n; i++) {
pathD += ` L ${rope[i].x},${rope[i].y}`;
}
cable.pathEl.setAttribute('d', pathD);
});
requestAnimationFrame(animate);
}
/**********************************************
* Startup
**********************************************/
initDriveLights();
document.querySelectorAll('.port').forEach(portEl => {
portEl.addEventListener('mousedown', handlePortMouseDown);
});
container.addEventListener('mousemove', handleMouseMove);
container.addEventListener('mouseup', handleMouseUp);
requestAnimationFrame(animate);
connect('r1s0_portA', 'r1s2_portA',randomColor());
connect('r1s2_portB', 'r2s1_portA',randomColor());
connect('r1s2_portC', 'r1s3_portA',randomColor());
connect('r2s1_portB', 'r2s2_portS',randomColor());
connect('r2s2_portA', 'r2s3_portA',randomColor());
connect('r2s2_portB', 'r3s1_portA',randomColor());
connect('r2s2_portC', 'r3s2_portA',randomColor());
connect('r2s2_portD', 'r3s3_portA',randomColor());
connect('r2s1_portD', 'r2s1_portG',randomColor());
connect('r2s1_portK', 'r3s2_portB',randomColor());

View File

@ -3,321 +3,72 @@
padding: 0
}
/* Basic reset */
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #1d2021;
overflow: hidden;
/* No default scrollbars */
font-family: "JetBrains Mono", serif;
background: #e8e8ea;
font-family: "Newsreader", serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
font-size: 20px;
color: #1A1C1A;
}
.header {
column-span:all;
}
.holder {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
}
h1 {
font-size: 48px;
color: #ebdbb2;
font-size: 5rem;
font-weight: 700;
font-style: normal;
}
h2 {
font-size: 36px;
color: #ebdbb2;
.regimg {
max-width: 14em;
max-height: 20em;
height: auto;
align-self: center;
}
h3 {
font-size: 28px;
color: #ebdbb2;
.largeimage {
width: auto;
object-fit: cover;
object-position: center;
column-span: none;
display: block;
float: left;
}
/*******************************************
* Main Container (holds racks + cables)
*******************************************/
#patch-container {
position: relative;
.content {
/* height: 100%;
width: 100%;
height: 100%;
overflow: hidden;
}
/* SVG overlay for cables, on top of racks */
#cable-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
/* put cables on top */
pointer-events: none;
/* let clicks pass through by default */
}
/* Each cable path can be clicked on => remove */
.cable-path {
pointer-events: auto;
/* re-enable clicks for cable paths specifically */
cursor: pointer;
transition: stroke 0.2s;
}
.cable-path:hover {
stroke: #cc241d !important;
}
/*******************************************
* Data center (the racks)
*******************************************/
#datacenter {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
/* If you want horizontal scrolling if there are many racks, use:
overflow-x: auto;
*/
overflow-x: auto;
overflow-y: hidden;
z-index: 1;
/* behind cable overlay */
}
.rack {
width: 16em;
/* each racks width */
/* fill all available vertical space */
background: #282828;
border: 2px solid #3c3836;
margin: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
/* stack servers top to bottom */
}
.rack-title {
text-align: center;
text-transform: uppercase;
font-size: 14px;
color: #ebdbb2;
margin: 8px 0;
letter-spacing: 1px;
}
.server {
background: #32302f;
border: 1px solid #3c3836;
margin: 6px;
/* spacing within the rack */
}
.server-under-construction {
/* Diagonal stripes:
repeating-linear-gradient(angle, color1 start, color1 end, color2 start, color2 end) */
background: repeating-linear-gradient(
45deg,
#282828 0,
#282828 12px,
#32302f 12px,
#32302f 24px
);
color:#000;
border: 2px dashed #282828; /* Give it a dashed border for effect */
}
.server-row {
padding: 5px 8px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
}
.text-row {
font-size: 14px;
color: #ebdbb2;
}
.port-row {
gap: 8px;
display: flex;
justify-content: right;
}
/* If you want a spacer between servers, you can use a simple div with .spacer. */
.spacer {
height: 20px;
}
/*******************************************
* Port + LED styling
*******************************************/
.port {
position: relative;
width: 1rem;
height: 1rem;
background: #32302f;
border: 2px solid #504945;
border-radius: 50%;
cursor: pointer;
}
.port:hover {
background: #7c6f64;
border-color: #ebdbb2;
}
a:link {
color: #b16286;
}
/* visited link */
a:visited {
color: #8f3f71;
}
/* mouse over link */
a:hover {
color: #d3869b;
}
/* selected link */
a:active {
color: #b16286;
}
.led {
position: absolute;
top: -5px;
right: -5px;
width: .4rem;
height: .4rem;
background: #1d2021;
/* off state */
border: 2px solid #504945;
border-radius: 50%;
box-shadow: 0 0 2px #000;
}
.temp-row {
position: relative;
flex: 1;
}
.temp-label {
color: #eee;
font-size: 11px;
margin-right: 8px;
}
.temp-bar {
flex: 1;
height: 10px;
background: linear-gradient(to right, #f80, #f00);
box-shadow: 0 0 6px rgba(255,80,0,0.5);
animation: flickerHeat 1.5s infinite ease-in-out alternate;
}
@keyframes flickerHeat {
0% { opacity: 1; transform: scaleX(1); }
100% { opacity: 0.7; transform: scaleX(1.05); }
}
/***************************************************
* POWER USAGE LIGHTS (Random Blink)
***************************************************/
.power-lights-row {
gap: 4px;
}
.power-light {
width: 10px; height: 10px;
background: #222;
border: 2px solid #555;
border-radius: 2px;
box-shadow: 0 0 2px #000;
}
.power-light.lit {
background: #0f0; /* or something bright to indicate usage */
}
/************************************************
* FAN SPACER: Animated fans
************************************************/
.fans {
background: #7c6f64;
margin: 6px;
/* spacing within the rack */
}
.fan-row {
display: flex;
display: grid;
gap: 10px;
justify-content: center;
padding: 5px 0;
grid-template: repeat(4, 1fr) / repeat(2, 1fr);
grid-auto-flow: column; */
height: 100%;
width: 40em;
min-width: 35em;
column-count: 2;
column-gap: 1em;
column-rule: thin solid black;
column-fill: auto;
font-size: 1.5rem;
}
.fan {
width: 5rem;
height: 5rem;
border-radius: 50%;
background-image: url('fan.svg');
filter: invert(29%) sepia(3%) saturate(1342%) hue-rotate(338deg) brightness(93%) contrast(90%);
animation: spin 1s linear infinite;
transform-origin: center;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
transition: all 1s linear;
}
.fan:hover {
animation: 5s;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/************************************************
* NAS SPACER: Random blinking drive lights
************************************************/
.nas {
background: #504945;
border: 1px solid #7c6f64;
margin: 6px;
/* spacing within the rack */
}
.drive-lights-row {
display: flex;
gap: 4px;
justify-content: center;
padding: 5px 0;
flex-wrap: wrap;
/* if many lights, wrap to next line */
}
.drive-light {
width: 12px;
height: 12px;
background: #1d2021;
/* off by default */
border: 2px solid #504945;
border-radius: 2px;
box-shadow: 0 0 2px #000;
}
.drive-light.lit {
background: #b8bb26;
/* or any color you like */
.item {
padding: .2em;
margin: .2em;
}