feat: updated styling and animations
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Solomon Laing 2026-02-13 09:07:38 +10:30
parent 7917801d45
commit 800b2864a6
11 changed files with 11065 additions and 83 deletions

10421
Solomons Website.pdf Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 221 KiB

72
demo-v1-chaotic.js Normal file
View File

@ -0,0 +1,72 @@
// Version 1: More random and chaotic
const staggerVisualizerEl = document.querySelector('.stagger-visualizer');
const fragment = document.createDocumentFragment();
const grid = [12, 6];
const col = grid[0];
const row = grid[1];
const numberOfElements = col * row;
for (let i = 0; i < numberOfElements; i++) {
let div = document.createElement('div');
div.className = 'part';
div.dataset.index = i;
fragment.appendChild(div);
}
staggerVisualizerEl.appendChild(fragment);
const staggersAnimation = anime.createTimeline({
easing: 'easeInOutSine',
loop: true,
autoplay: false
});
staggersAnimation
// Chaotic burst outward with per-element randomness
.add('.part', {
translateX: (el) => anime.utils.random(-15, 15),
translateY: (el) => anime.utils.random(-15, 15),
rotate: (el) => anime.utils.random(-180, 180),
scale: (el) => anime.utils.random(0.3, 0.8),
duration: 800,
easing: 'easeOutQuad',
delay: anime.stagger(30, { grid: grid, from: 'center' })
}, 0)
// Floating upward with wave effect
.add('.part', {
translateY: (el, i) => -anime.utils.random(2, 8),
rotate: (el) => anime.utils.random(-45, 45),
duration: 1200,
easing: 'easeInOutSine',
delay: (el, i) => i * 20 + anime.utils.random(-100, 100)
}, '-=400')
// Scattered tumble
.add('.part', {
translateX: (el) => anime.utils.random(-20, 20),
translateY: (el) => anime.utils.random(-20, 20),
rotate: (el) => anime.utils.random(-360, 360),
scale: (el) => anime.utils.random(0.5, 1.2),
duration: 1000,
easing: 'easeInOutQuad',
delay: anime.stagger(40, { grid: grid, from: 'random' })
})
// Converge back with randomized paths
.add('.part', {
translateX: 0,
translateY: 0,
rotate: (el) => anime.utils.random(0, 360),
scale: 1,
duration: 1200,
easing: 'easeOutElastic',
delay: anime.stagger(50, { grid: grid, from: 'center' })
})
// Gentle pulse
.add('.part', {
scale: (el) => anime.utils.random(0.9, 1.1),
opacity: (el) => anime.utils.random(0.5, 1),
duration: 600,
easing: 'easeInOutSine',
delay: anime.stagger(30)
}, '-=200');
staggersAnimation.play();

72
demo-v2-fluid.js Normal file
View File

@ -0,0 +1,72 @@
// Version 2: Fluid and organic with floating effect
const staggerVisualizerEl = document.querySelector('.stagger-visualizer');
const fragment = document.createDocumentFragment();
const grid = [12, 6];
const col = grid[0];
const row = grid[1];
const numberOfElements = col * row;
for (let i = 0; i < numberOfElements; i++) {
let div = document.createElement('div');
div.className = 'part';
div.dataset.index = i;
fragment.appendChild(div);
}
staggerVisualizerEl.appendChild(fragment);
const staggersAnimation = anime.createTimeline({
easing: 'easeInOutSine',
loop: true,
autoplay: false
});
staggersAnimation
// Initial expansion with gentle stagger
.add('.part', {
translateX: anime.stagger((el, i) => anime.utils.random(-6, 6), { grid: grid, from: 'center', axis: 'x' }),
translateY: anime.stagger((el, i) => anime.utils.random(-6, 6), { grid: grid, from: 'center', axis: 'y' }),
scale: anime.stagger([0.7, 1.1], { grid: grid, from: 'center' }),
duration: 1000,
easing: 'easeOutElastic',
delay: anime.stagger(50, { grid: grid, from: 'center' })
}, 0)
// Floating wave motion
.add('.part', {
translateY: (el, i) => anime.utils.random(-5, 5),
rotate: (el) => anime.utils.random(-30, 30),
opacity: [(el) => anime.utils.random(0.6, 0.8), 1],
duration: 2000,
easing: 'easeInOutSine',
delay: (el, i) => i * 30 + anime.utils.random(0, 300)
}, '-=500')
// Gentle compression and expansion
.add('.part', {
scaleX: (el) => anime.utils.random(0.9, 1.05),
scaleY: (el) => anime.utils.random(0.9, 1.05),
duration: 1200,
easing: 'easeInOutSine',
delay: anime.stagger(40, { grid: grid, from: 'center' })
}, '-=1000')
// Slow drift
.add('.part', {
translateX: (el) => anime.utils.random(-3, 3),
translateY: (el) => anime.utils.random(-3, 3),
rotate: (el, i) => i % 2 === 0 ? 8 : -8,
duration: 1500,
easing: 'easeInOutQuad',
delay: (el, i) => i * 25
})
// Return to rest gently
.add('.part', {
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
duration: 1500,
easing: 'easeInOutQuad',
delay: anime.stagger(35, { grid: grid, from: 'center' })
});
staggersAnimation.play();

72
demo-v3-balanced.js Normal file
View File

@ -0,0 +1,72 @@
// Version 3: Balanced - chaotic bursts with organic flow
const staggerVisualizerEl = document.querySelector('.stagger-visualizer');
const fragment = document.createDocumentFragment();
const grid = [12, 6];
const col = grid[0];
const row = grid[1];
const numberOfElements = col * row;
for (let i = 0; i < numberOfElements; i++) {
let div = document.createElement('div');
div.className = 'part';
div.dataset.index = i;
fragment.appendChild(div);
}
staggerVisualizerEl.appendChild(fragment);
const staggersAnimation = anime.createTimeline({
easing: 'easeInOutSine',
loop: true,
autoplay: false
});
staggersAnimation
// Burst outward
.add('.part', {
translateX: anime.stagger('-.15rem', { grid: grid, from: 'center', axis: 'x' }),
translateY: anime.stagger('-.15rem', { grid: grid, from: 'center', axis: 'y' }),
scale: 0.5,
duration: 300,
easing: 'easeOutQuad',
delay: anime.stagger(60, { grid: grid, from: 'center' })
}, 0)
// Spring outward further with randomness
.add('.part', {
translateX: (el, i) => anime.utils.random(-8, 8),
translateY: (el, i) => anime.utils.random(-8, 8),
scale: (el) => anime.utils.random(0.7, 1.2),
rotate: (el) => anime.utils.random(-90, 90),
duration: 600,
easing: 'easeOutQuad',
delay: anime.stagger(100, { grid: grid, from: 'center' })
})
// Floating phase - organic motion
.add('.part', {
translateY: (el, i) => anime.utils.random(-4, 4),
rotate: (el, i) => anime.utils.random(-45, 45),
duration: 1400,
easing: 'easeInOutSine',
delay: (el, i) => i * 25 + anime.utils.random(-150, 150)
})
// Swirl inward
.add('.part', {
translateX: (el, i) => anime.utils.random(-10, 10),
translateY: (el, i) => anime.utils.random(-10, 10),
rotate: (el) => anime.utils.random(-180, 180),
duration: 800,
easing: 'easeInOutQuad',
delay: anime.stagger(40, { grid: grid, from: 'center' })
})
// Converge smoothly
.add('.part', {
translateX: 0,
translateY: 0,
rotate: (el) => anime.utils.random(0, 360),
scale: 1,
duration: 1000,
easing: 'easeOutExpo',
delay: anime.stagger(50, { grid: grid, from: 'center' })
});
staggersAnimation.play();

125
demo-v4-extended-loop.js Normal file
View File

@ -0,0 +1,125 @@
// Version 4: Extended Loop with Smooth Transitions
// Creates a much longer animation cycle that loops smoothly without harsh resets
const staggerVisualizerEl = document.querySelector('.stagger-visualizer');
const fragment = document.createDocumentFragment();
const grid = [12, 6];
const col = grid[0];
const row = grid[1];
const numberOfElements = col * row;
for (let i = 0; i < numberOfElements; i++) {
let div = document.createElement('div');
div.className = 'part';
div.dataset.index = i;
fragment.appendChild(div);
}
staggerVisualizerEl.appendChild(fragment);
// Extended animation cycle - 20 seconds total, much longer so loop is less jarring
const staggersAnimation = anime.createTimeline({
easing: 'easeInOutSine',
loop: true,
autoplay: false
});
staggersAnimation
// Initial expansion phase
.add('.part', {
translateX: anime.stagger('-.15rem', { grid: grid, from: 'center', axis: 'x' }),
translateY: anime.stagger('-.15rem', { grid: grid, from: 'center', axis: 'y' }),
scale: 0.5,
duration: 300,
easing: 'easeOutQuad',
delay: anime.stagger(60, { grid: grid, from: 'center' })
}, 0)
// Chaotic burst phase
.add('.part', {
translateX: (el, i) => anime.utils.random(-10, 10),
translateY: (el, i) => anime.utils.random(-10, 10),
scale: (el) => anime.utils.random(0.7, 1.3),
rotate: (el) => anime.utils.random(-120, 120),
duration: 800,
easing: 'easeOutQuad',
delay: anime.stagger(80, { grid: grid, from: 'center' })
})
// Floating drift phase 1
.add('.part', {
translateY: (el, i) => anime.utils.random(-6, 6),
rotate: (el) => anime.utils.random(-60, 60),
duration: 2000,
easing: 'easeInOutSine',
delay: (el, i) => i * 20 + anime.utils.random(-200, 200)
})
// Gentle swirl inward
.add('.part', {
translateX: (el, i) => anime.utils.random(-5, 5),
translateY: (el, i) => anime.utils.random(-5, 5),
rotate: (el) => anime.utils.random(-45, 45),
duration: 1500,
easing: 'easeInOutQuad',
delay: anime.stagger(50, { grid: grid, from: 'center' })
})
// Floating drift phase 2 - different direction
.add('.part', {
translateY: (el, i) => anime.utils.random(-8, 8),
rotate: (el) => anime.utils.random(-30, 30),
duration: 2200,
easing: 'easeInOutSine',
delay: (el, i) => i * 22 + anime.utils.random(-150, 150)
})
// Slow convergence
.add('.part', {
translateX: (el) => anime.utils.random(-2, 2),
translateY: (el) => anime.utils.random(-2, 2),
rotate: (el) => anime.utils.random(-15, 15),
duration: 1800,
easing: 'easeInOutQuad',
delay: anime.stagger(60, { grid: grid, from: 'center' })
})
// Gentle return to center with subtle movement
.add('.part', {
translateX: (el) => {
const index = parseInt(el.dataset.index);
return anime.utils.random(-1, 1);
},
translateY: (el) => {
const index = parseInt(el.dataset.index);
return anime.utils.random(-1, 1);
},
rotate: 0,
scale: 1,
opacity: 1,
duration: 2500,
easing: 'easeOutQuad',
delay: anime.stagger(50, { grid: grid, from: 'center' })
})
// Long rest phase with breathing effect
.add('.part', {
scale: (el) => anime.utils.random(0.95, 1.05),
opacity: (el) => anime.utils.random(0.8, 1),
duration: 3000,
easing: 'easeInOutSine',
delay: (el, i) => i * 15
})
// Return to perfect rest
.add('.part', {
translateX: 0,
translateY: 0,
rotate: 0,
scale: 1,
opacity: 1,
duration: 1500,
easing: 'easeOutQuad',
delay: anime.stagger(40, { grid: grid, from: 'center' })
});
staggersAnimation.play();

136
demo-v4-gaussian-noise.js Normal file
View File

@ -0,0 +1,136 @@
// Version 4: Gaussian Noise Overlay - Perpetual organic motion
const staggerVisualizerEl = document.querySelector('.stagger-visualizer');
const fragment = document.createDocumentFragment();
const grid = [12, 6];
const col = grid[0];
const row = grid[1];
const numberOfElements = col * row;
// Store element data for continuous noise animation
const elementData = [];
for (let i = 0; i < numberOfElements; i++) {
let div = document.createElement('div');
div.className = 'part';
div.dataset.index = i;
fragment.appendChild(div);
// Store random animation parameters for each element
elementData.push({
element: div,
index: i,
// Random frequencies for sine wave oscillations
freqX: anime.utils.random(0.0008, 0.0012),
freqY: anime.utils.random(0.0006, 0.0010),
freqRot: anime.utils.random(0.0004, 0.0009),
freqOpacity: anime.utils.random(0.0005, 0.0008),
// Random amplitudes for movement
ampX: anime.utils.random(3, 8),
ampY: anime.utils.random(3, 8),
ampRot: anime.utils.random(10, 25),
// Phase offsets so they don't all move together
phaseX: anime.utils.random(0, Math.PI * 2),
phaseY: anime.utils.random(0, Math.PI * 2),
phaseRot: anime.utils.random(0, Math.PI * 2),
phaseOpacity: anime.utils.random(0, Math.PI * 2),
});
}
staggerVisualizerEl.appendChild(fragment);
// Initial burst animation
const staggersAnimation = anime.createTimeline({
easing: 'easeInOutSine',
loop: true,
autoplay: false
});
staggersAnimation
// Burst outward
.add('.part', {
translateX: anime.stagger('-.15rem', { grid: grid, from: 'center', axis: 'x' }),
translateY: anime.stagger('-.15rem', { grid: grid, from: 'center', axis: 'y' }),
scale: 0.5,
duration: 300,
easing: 'easeOutQuad',
delay: anime.stagger(60, { grid: grid, from: 'center' })
}, 0)
// Spring outward further with randomness
.add('.part', {
translateX: (el, i) => anime.utils.random(-8, 8),
translateY: (el, i) => anime.utils.random(-8, 8),
scale: (el) => anime.utils.random(0.7, 1.2),
rotate: (el) => anime.utils.random(-90, 90),
duration: 600,
easing: 'easeOutQuad',
delay: anime.stagger(100, { grid: grid, from: 'center' })
})
// Floating phase - long duration
.add('.part', {
translateY: (el, i) => anime.utils.random(-4, 4),
rotate: (el, i) => anime.utils.random(-45, 45),
duration: 4000,
easing: 'easeInOutSine',
delay: (el, i) => i * 25 + anime.utils.random(-150, 150)
})
// Gentle convergence
.add('.part', {
translateX: 0,
translateY: 0,
rotate: (el) => anime.utils.random(0, 360),
scale: 1,
duration: 2000,
easing: 'easeOutQuad',
delay: anime.stagger(40, { grid: grid, from: 'center' })
})
// Rest phase
.add('.part', {
opacity: 1,
duration: 6000,
delay: 0
}, '-=1000');
staggersAnimation.play();
// Continuous Gaussian noise animation - using requestAnimationFrame
let startTime = Date.now();
const applyNoise = () => {
const elapsed = Date.now() - startTime;
elementData.forEach((data) => {
// Multi-layered sine waves create Gaussian-like noise
const noiseX = Math.sin(elapsed * data.freqX + data.phaseX) * data.ampX +
Math.sin(elapsed * data.freqX * 0.3 + data.phaseX) * data.ampX * 0.3;
const noiseY = Math.sin(elapsed * data.freqY + data.phaseY) * data.ampY +
Math.sin(elapsed * data.freqY * 0.4 + data.phaseY) * data.ampY * 0.4;
const noiseRot = Math.sin(elapsed * data.freqRot + data.phaseRot) * data.ampRot +
Math.sin(elapsed * data.freqRot * 0.5 + data.phaseRot) * data.ampRot * 0.2;
// Smooth opacity breathing
const opacityVariation = Math.sin(elapsed * data.freqOpacity + data.phaseOpacity) * 0.12 + 0.88;
// Apply noise to element
data.element.style.filter = `opacity(${opacityVariation})`;
// Get current computed transform and add noise
// This is applied on top of anime's transforms
const currentTransform = window.getComputedStyle(data.element).transform;
const matrix = currentTransform.match(/matrix.*\((.+)\)/)[1].split(', ');
const currentX = parseFloat(matrix[4]) || 0;
const currentY = parseFloat(matrix[5]) || 0;
data.element.style.setProperty('--noise-x', `${noiseX}px`);
data.element.style.setProperty('--noise-y', `${noiseY}px`);
data.element.style.setProperty('--noise-rot', `${noiseRot}deg`);
});
requestAnimationFrame(applyNoise);
};
// Start noise animation after a brief delay to let anime init
setTimeout(() => {
applyNoise();
}, 100);

94
demo-v5-pure-gaussian.js Normal file
View File

@ -0,0 +1,94 @@
// Version 5: Pure Gaussian Noise - Infinite organic motion without looping
const staggerVisualizerEl = document.querySelector('.stagger-visualizer');
const fragment = document.createDocumentFragment();
const grid = [12, 6];
const col = grid[0];
const row = grid[1];
const numberOfElements = col * row;
// Store element data for continuous noise
const elementData = [];
for (let i = 0; i < numberOfElements; i++) {
let div = document.createElement('div');
div.className = 'part';
div.dataset.index = i;
fragment.appendChild(div);
// Each element gets unique noise parameters
elementData.push({
element: div,
index: i,
// Multiple frequency layers for natural-looking noise
freq1X: anime.utils.random(0.25, 0.75),
freq2X: anime.utils.random(0.1, 0.25),
freq1Y: anime.utils.random(0.2, 0.6),
freq2Y: anime.utils.random(0.05, 0.2),
freq1Rot: anime.utils.random(0.1, 0.4),
freq2Rot: anime.utils.random(0.025, 0.1),
freqScale: anime.utils.random(0.15, 0.4),
freqOpacity: anime.utils.random(0.05, 0.25),
// Amplitudes
ampX: anime.utils.random(4, 12),
ampY: anime.utils.random(4, 12),
ampRot: anime.utils.random(20, 45),
ampScale: anime.utils.random(0.05, 0.15),
// Phase offsets for variation
phase1X: Math.random() * Math.PI * 2,
phase2X: Math.random() * Math.PI * 2,
phase1Y: Math.random() * Math.PI * 2,
phase2Y: Math.random() * Math.PI * 2,
phase1Rot: Math.random() * Math.PI * 2,
phase2Rot: Math.random() * Math.PI * 2,
phaseScale: Math.random() * Math.PI * 2,
phaseOpacity: Math.random() * Math.PI * 2,
// Random drift direction preference
driftX: anime.utils.random(-0.5, 0.5),
driftY: anime.utils.random(-0.5, 0.5),
});
}
staggerVisualizerEl.appendChild(fragment);
// Continuous Gaussian noise animation using requestAnimationFrame
let animationStart = Date.now();
let lastFrameTime = animationStart;
const updateNoise = () => {
const now = Date.now();
const elapsed = (now - animationStart) / 1000; // Convert to seconds
lastFrameTime = now;
elementData.forEach((data) => {
// Multi-layered sine waves create smooth Gaussian-like noise
// Layer 1: Low frequency, high amplitude (main motion)
const noiseX1 = Math.sin(elapsed * data.freq1X + data.phase1X) * data.ampX;
const noiseY1 = Math.sin(elapsed * data.freq1Y + data.phase1Y) * data.ampY;
const noiseRot1 = Math.sin(elapsed * data.freq1Rot + data.phase1Rot) * data.ampRot;
// Layer 2: Higher frequency, lower amplitude (detail/jitter)
const noiseX2 = Math.sin(elapsed * data.freq2X + data.phase2X) * data.ampX * 0.4;
const noiseY2 = Math.sin(elapsed * data.freq2Y + data.phase2Y) * data.ampY * 0.3;
const noiseRot2 = Math.sin(elapsed * data.freq2Rot + data.phase2Rot) * data.ampRot * 0.3;
// Combine layers
const totalX = noiseX1 + noiseX2 + (data.driftX * elapsed * 0.01);
const totalY = noiseY1 + noiseY2 + (data.driftY * elapsed * 0.01);
const totalRot = noiseRot1 + noiseRot2;
// Scale breathing
const scale = 1 + (Math.sin(elapsed * data.freqScale + data.phaseScale) * data.ampScale);
// Opacity breathing - very subtle
const opacity = 0.7 + Math.sin(elapsed * data.freqOpacity + data.phaseOpacity) * 0.25;
// Apply transforms directly to style
data.element.style.transform = `translateX(${totalX}px) translateY(${totalY}px) rotate(${totalRot}deg) scale(${Math.max(0.3, scale)})`;
data.element.style.opacity = opacity;
});
requestAnimationFrame(updateNoise);
};
// Start the noise animation
updateNoise();

View File

@ -7,32 +7,34 @@
<title>SolomonLai.ng</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet" />
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="stagger-visualizer"></div>
<main>
<div class="vcard">
<div class="vcard-header">
<h1 class="vcard-name vcard-header-left"><span class="name-first">Solomon</span></h1>
<div class="vcard-header-right">
<div class="vcard-header-title">
<div class="vcard-header-top">
<h1 class="vcard-name"><span class="name-last">Laing</span></h1>
<p class="vcard-subtitle">Full Stack Software Developer</p>
</div>
<button class="theme-toggle" aria-label="Toggle theme">
<svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="20" height="20">
<svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="18" height="18">
<path fill="currentColor"
d="M223.5 32C100 32 0 132.3 0 256s100 224 223.5 224c60.6 0 115.5-24.2 155.8-63.4 5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6-96.9 0-175.5-78.8-175.5-176 0-65.8 36-123.1 89.3-153.3 6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z" />
d="M144.7 98.7c-21 34.1-33.1 74.3-33.1 117.3c0 98 62.8 181.4 150.4 211.7c-12.4 2.8-25.3 4.3-38.6 4.3C126 432 48 354 48 256.5C48 175.8 88.6 105.1 150.3 72c-2 8.6-3.6 17.5-5.6 26.7zm27.2-34.3C90 120.2 32 183 32 256.5C32 362.7 117.4 448 223.5 448c42.3 0 81.4-13.6 113.2-36.8c4.2-3 6.2-8.3 5.1-13.4s-5.6-8.8-10.7-9.8C258.4 373.7 200 311.7 200 236.5c0-53 27.8-99.5 69.6-125.8c4.5-2.8 6.9-8 6.1-13.2s-5-9.5-10.2-10.4c-12.1-2-24.5-3.1-37.1-3.1c-19.4 0-38.1 2.8-55.8 7.9l-.7 .2c-.6 .2-1.2 .3-1.7 .5c0 0 0 0 0 0l-.1 0c-.3 .1-.6 .2-1 .2c0 0 0 0 0 0zm0 0" />
</svg>
<svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="20" height="20">
<svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="18" height="18">
<path fill="currentColor"
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391l-19.9 107.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM256 160a96 96 0 1 0 0 192 96 96 0 1 0 0-192z" />
d="M375.7 19.7c-1.5-8-6.9-14.7-14.4-17.8s-16.1-2.2-22.8 2.4L256 61.1 173.5 4.2c-6.7-4.6-15.3-5.5-22.8-2.4s-12.9 9.8-14.4 17.8l-18.1 98.5L19.7 136.3c-8 1.5-14.7 6.9-17.8 14.4s-2.2 16.1 2.4 22.8L61.1 256 4.2 338.5c-4.6 6.7-5.5 15.3-2.4 22.8s9.8 12.9 17.8 14.4l98.5 18.1 18.1 98.5c1.5 8 6.9 14.7 14.4 17.8s16.1 2.2 22.8-2.4L256 450.9l82.5 56.9c6.7 4.6 15.3 5.5 22.8 2.4s12.9-9.8 14.4-17.8l18.1-98.5 98.5-18.1c8-1.5 14.7-6.9 17.8-14.4s2.2-16.1-2.4-22.8L450.9 256l56.9-82.5c4.6-6.7 5.5-15.3 2.4-22.8s-9.8-12.9-17.8-14.4l-98.5-18.1L375.7 19.7zM269.6 110l65.6-45.2 14.4 78.3c1.8 9.8 9.5 17.5 19.3 19.3l78.3 14.4L402 242.4c-5.7 8.2-5.7 19 0 27.2l45.2 65.6-78.3 14.4c-9.8 1.8-17.5 9.5-19.3 19.3l-14.4 78.3L269.6 402c-8.2-5.7-19-5.7-27.2 0l-65.6 45.2-14.4-78.3c-1.8-9.8-9.5-17.5-19.3-19.3L64.8 335.2 110 269.6c5.7-8.2 5.7-19 0-27.2L64.8 176.8l78.3-14.4c9.8-1.8 17.5-9.5 19.3-19.3l14.4-78.3L242.4 110c8.2 5.7 19 5.7 27.2 0zM256 368a112 112 0 1 0 0-224 112 112 0 1 0 0 224zM192 256a64 64 0 1 1 128 0 64 64 0 1 1 -128 0z" />
</svg>
</button>
</div>
<hr class="vcard-divider" />
<p class="vcard-subtitle">Full Stack Software Developer</p>
</div>
</div>
<div class="vcard-body">
@ -88,7 +90,7 @@
<path fill="currentColor"
d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z" />
</svg>
<a href="assets/resume.pdf" target="_blank">Download Resume <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="12" height="12" style="vertical-align: middle; margin-bottom: 2px;"><path fill="currentColor" d="M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l82.7 0L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3l0 82.7c0 17.7 14.3 32 32 32s32-14.3 32-32l0-160c0-17.7-14.3-32-32-32L320 0zM80 32C35.8 32 0 67.8 0 112L0 432c0 44.2 35.8 80 80 80l320 0c44.2 0 80-35.8 80-80l0-160c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 160c0 8.8-7.2 16-16 16L80 448c-8.8 0-16-7.2-16-16l0-320c0-8.8 7.2-16 16-16l160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L80 32z"/></svg></a>
<a href="assets/resume.pdf" target="_blank">Download Resume <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="12" height="12" style="vertical-align: middle; margin-bottom: 2px;"><path fill="currentColor" d="M384 32c17.7 0 32 14.3 32 32l0 160c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-82.7L169.4 323.3c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L306.7 96 224 96c-17.7 0-32-14.3-32-32s14.3-32 32-32l160 0z"/></svg></a>
</li>
</ul>
</div>
@ -113,9 +115,8 @@
<span>GitHub</span>
</a>
<a class="social-link card" href="https://git.inkletblot.com/inkletblot/" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5.67 143.05 628.65 387.55" width="20" height="20">
<path fill="currentColor" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/>
<path fill="currentColor" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z"/>
</svg>
<span>Gitea</span>
</a>
@ -135,13 +136,11 @@
</div>
</main>
<div class="stagger-visualizer"></div>
<script src="https://cdn.jsdelivr.net/npm/animejs@4.0.0/lib/anime.iife.min.js"></script>
<script src="theme.js"></script>
<script src="main.js"></script>
<script src="cards.js"></script>
<script src="demo.js"></script>
<script src="demo-v5-pure-gaussian.js"></script>
</body>
</html>

113
style.css
View File

@ -2,44 +2,47 @@
[data-theme="dark"] {
--bg: #17171A;
--card-bg: #17171A;
--card-border: #CFD1D4;
--card-bg-rgb: 23, 23, 26;
--card-border: #C9CBCE;
--text-primary: #F7F9FC;
--text-secondary: #999BA4;
--accent-blue: #0083E2;
--divider: #CFD1D4;
--divider: #C9CBCE;
--icon-color: #F7F9FC;
--part-color: #F7F9FC;
--part-bg-color: rgba(247, 249, 252, 0.06);
--card-shadow: none;
--stagger-bg: rgba(0, 131, 226, 0.03);
--stagger-border: rgba(0, 131, 226, 0.05);
}
[data-theme="light"] {
--bg: #F7F9FC;
--card-bg: #F7F9FC;
--card-bg-rgb: 247, 249, 252;
--card-border: #C9CBCE;
--text-primary: #393F53;
--text-secondary: #6F7588;
--accent-blue: #0075CB;
--divider: #C9CBCE;
--icon-color: #393F53;
--part-color: #393F53;
--part-bg-color: rgba(57, 63, 83, 0.06);
--card-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
--stagger-bg: rgba(0, 131, 226, 0.08);
--stagger-border: rgba(0, 131, 226, 0.12);
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg: #F7F9FC;
--card-bg: #F7F9FC;
--card-bg-rgb: 247, 249, 252;
--card-border: #C9CBCE;
--text-primary: #393F53;
--text-secondary: #6F7588;
--accent-blue: #0075CB;
--divider: #C9CBCE;
--icon-color: #393F53;
--part-color: #393F53;
--part-bg-color: rgba(57, 63, 83, 0.06);
--card-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
--stagger-bg: rgba(0, 131, 226, 0.08);
--stagger-border: rgba(0, 131, 226, 0.12);
}
}
@ -57,7 +60,7 @@ html {
}
body {
font-family: Roboto, sans-serif;
font-family: Montserrat, sans-serif;
color: var(--text-primary);
background: var(--bg);
display: flex;
@ -65,6 +68,8 @@ body {
align-items: center;
min-height: 100vh;
padding: 2rem 1rem;
position: relative;
overflow-x: hidden;
}
p,
@ -97,19 +102,37 @@ a:visited {
transition: color 500ms ease, background-color 500ms ease, border-color 500ms ease, box-shadow 500ms ease, opacity 500ms ease !important;
}
.stagger-visualizer {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
pointer-events: none;
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-auto-rows: 1fr;
gap: 0;
}
.stagger-visualizer .part {
background-color: var(--stagger-bg);
border: 1px solid var(--stagger-border);
border-radius: 1px;
}
.vcard {
position: relative;
z-index: 1;
max-width: 1000px;
width: 100%;
border: 1px solid var(--card-border);
border-radius: 12px;
background: var(--card-bg);
box-shadow: var(--card-shadow);
background: radial-gradient(circle at center, rgba(var(--card-bg-rgb), 0.95) 0%, rgba(var(--card-bg-rgb), 0.7) 100%);
padding: 24px;
display: flex;
flex-direction: column;
gap: 1.25rem;
border-radius: 16px;
}
.vcard-header {
@ -120,15 +143,14 @@ a:visited {
.vcard-header-right {
flex: 1;
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-direction: column;
gap: 0.25rem;
}
.vcard-header-title {
.vcard-header-top {
display: flex;
flex-flow: column;
gap: 0.25rem;
text-align: left;
justify-content: space-between;
align-items: flex-start;
}
.vcard-name {
@ -148,7 +170,7 @@ a:visited {
}
.vcard-subtitle {
font-size: 0.75rem;
font-size: 0.9rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
@ -157,7 +179,7 @@ a:visited {
.theme-toggle {
background: none;
border: none;
border: 1px solid var(--divider);
color: var(--text-primary);
cursor: pointer;
padding: 6px;
@ -191,8 +213,8 @@ a:visited {
img {
width: 100%;
aspect-ratio: 1;
max-width: 280px;
max-width: 100%;
aspect-ratio: 3 / 4;
border-radius: 10px;
object-fit: cover;
}
@ -200,7 +222,7 @@ a:visited {
.vcard-content {
display: flex;
flex-flow: column-reverse;
flex-flow: column;
gap: 1.25rem;
}
@ -300,7 +322,9 @@ a:visited {
svg {
flex-shrink: 0;
color: var(--icon-color);
width: 28px;
height: 28px;
color: var(--accent-blue);
}
span {
@ -315,24 +339,6 @@ a:visited {
margin-top: 0.5rem;
}
.stagger-visualizer {
position: fixed;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 60rem;
height: 30rem;
top: 0;
rotate: 90deg;
}
.part {
width: 5rem;
height: 5rem;
border: 1px solid var(--part-bg-color);
background-color: var(--part-bg-color);
}
@media (min-width: 768px) {
.vcard {
@ -344,22 +350,19 @@ a:visited {
gap: .5rem;
}
.vcard-header-text {
text-align: left;
}
.vcard-body {
flex-direction: row;
gap: 2rem;
}
.vcard-image {
flex: 0 0 auto;
min-width: 220px;
flex: 0 0 35%;
max-width: 35%;
align-self: stretch;
img {
height: 100%;
aspect-ratio: auto;
object-fit: cover;
border-radius: 10px;
}
@ -380,16 +383,4 @@ a:visited {
flex-wrap: wrap;
gap: 1rem;
}
.stagger-visualizer {
width: 120rem;
height: 60rem;
rotate: unset;
}
.part {
width: 10rem;
height: 10rem;
}
}

View File

@ -10,8 +10,8 @@ function getPreferredTheme() {
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
iconMoon.style.display = theme === 'light' ? 'block' : 'none';
iconSun.style.display = theme === 'dark' ? 'block' : 'none';
iconMoon.style.display = theme === 'dark' ? 'block' : 'none';
iconSun.style.display = theme === 'light' ? 'block' : 'none';
}
applyTheme(getPreferredTheme());