···
24
+
--ripple-color: rgba(77, 250, 123, 0.04);
25
+
--ripple-color-strong: rgba(77, 250, 123, 0.06);
26
+
--matrix-color: rgba(77, 250, 123, 0.2);
27
+
--matrix-glow: rgba(77, 250, 123, 0.1);
28
+
--hover-glow: rgba(77, 250, 123, 0.15);
···
color: var(--text-color);
53
+
background: linear-gradient(rgba(10, 23, 15, 0.82), rgba(10, 23, 15, 0.92));
55
+
pointer-events: none;
58
+
#matrix-background {
66
+
pointer-events: none;
···
border-left: 3px solid transparent;
371
-
transition: all 0.3s ease;
400
+
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
401
+
position: relative;
border-left-color: var(--accent-color);
376
-
background-color: rgba(77, 250, 123, 0.03);
408
+
background-color: rgba(21, 39, 32, 0.95);
411
+
.feed-item::before {
413
+
position: absolute;
418
+
background: radial-gradient(circle at var(--mouse-x, 0%) var(--mouse-y, 0%),
419
+
rgba(77, 250, 123, 0.06) 0%,
420
+
rgba(77, 250, 123, 0.04) 30%,
421
+
rgba(77, 250, 123, 0) 70%);
424
+
transform: scale(0);
425
+
transition: opacity 0.5s ease, transform 0.7s cubic-bezier(0.19, 1, 0.22, 1);
426
+
pointer-events: none;
···
462
+
.feed-item:hover::before {
464
+
transform: scale(1.5);
···
621
-
transition: background-color 0.2s ease;
676
+
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
680
+
position: relative;
628
-
background-color: #1a3028;
684
+
background-color: rgba(21, 39, 32, 0.95);
border-left-color: var(--accent-color);
688
+
.link-item::before {
690
+
position: absolute;
695
+
background: radial-gradient(circle at var(--mouse-x, 0%) var(--mouse-y, 0%),
696
+
rgba(77, 250, 123, 0.06) 0%,
697
+
rgba(77, 250, 123, 0.04) 30%,
698
+
rgba(77, 250, 123, 0) 70%);
701
+
transform: scale(0);
702
+
transition: opacity 0.5s ease, transform 0.7s cubic-bezier(0.19, 1, 0.22, 1);
703
+
pointer-events: none;
706
+
.link-item:hover::before {
708
+
transform: scale(1.5);
font-family: 'JetBrains Mono', monospace;
···
1035
+
<canvas id="matrix-background"></canvas>
<div class="header-container">
<div class="header-left">
···
document.addEventListener('DOMContentLoaded', async () => {
1072
+
// Matrix background effect
1073
+
const canvas = document.getElementById('matrix-background');
1074
+
const ctx = canvas.getContext('2d');
1076
+
// Set canvas size to match window
1077
+
function resizeCanvas() {
1078
+
canvas.width = window.innerWidth;
1079
+
canvas.height = window.innerHeight;
1082
+
window.addEventListener('resize', resizeCanvas);
1084
+
// Vine/plant-related characters and elements
1085
+
const vineChars = '┃┃│┋┇┊┆╽╿┴┬╵╷└┕┖┗┘┙┚┛╘╙╚╛╯╰╱╲⌠⌡╎▏▕⏐▌▐░▒▓◥◤◢◣⎸⎹│';
1086
+
const leafChars = '☘❀✿❁❃❇❈❉❊❋✣✤✥✦✧✩✪✫✬✭✮✾✿❀❁❂❃❄⚘♠♣⚜⚘☘';
1087
+
const branchChars = '┌┐┘└├┬┴┤┼─┄┈┉┊┋╱╲╳☂⚢⌒~∞≈≋⋆✧✦✫';
1088
+
const fontSize = 14;
1089
+
const columns = Math.floor(canvas.width / fontSize * 0.7); // Fewer columns for sparser vines
1091
+
// Drop positions for each column
1094
+
// Initialize drops at random positions
1095
+
for (let i = 0; i < columns; i++) {
1096
+
// Random starting position
1097
+
drops[i] = Math.random() * -canvas.height;
1100
+
// Set up column types - some will be vines, some will have leaves
1101
+
const columnTypes = [];
1102
+
for (let i = 0; i < columns; i++) {
1103
+
// 70% of columns are vines, 25% are leaves, 5% are cross-connections
1104
+
const rand = Math.random();
1106
+
columnTypes[i] = 'vine';
1107
+
} else if (rand < 0.95) {
1108
+
columnTypes[i] = 'leaf';
1110
+
columnTypes[i] = 'branch';
1114
+
// Store connections between vines
1115
+
const connections = [];
1117
+
// Helper function to find nearby columns
1118
+
function findNearbyColumns(columnIndex, maxDistance = 3) {
1119
+
const nearby = [];
1120
+
for (let i = 0; i < columns; i++) {
1121
+
if (i !== columnIndex && Math.abs(i - columnIndex) <= maxDistance) {
1128
+
// Last time random chars were changed
1129
+
const lastCharChangeTime = [];
1130
+
// The current characters displayed
1131
+
const currentChars = [];
1132
+
// Width/thickness of vines
1133
+
const vineThickness = [];
1135
+
for (let i = 0; i < columns; i++) {
1136
+
lastCharChangeTime[i] = [];
1137
+
currentChars[i] = [];
1139
+
// Random vine thickness between 1-3
1140
+
vineThickness[i] = Math.floor(Math.random() * 3) + 1;
1142
+
for (let j = 0; j < canvas.height / fontSize; j++) {
1143
+
lastCharChangeTime[i][j] = 0;
1145
+
if (columnTypes[i] === 'vine') {
1146
+
// Choose vine characters based on position and thickness
1148
+
// Top of vine - might be a leaf or flower
1149
+
currentChars[i][j] = Math.random() < 0.6 ?
1150
+
leafChars.charAt(Math.floor(Math.random() * leafChars.length)) :
1151
+
vineChars.charAt(Math.floor(Math.random() * vineChars.length));
1153
+
// Main vine character
1154
+
const vineIndex = Math.min(vineThickness[i] * 3, vineChars.length - 1);
1155
+
currentChars[i][j] = vineChars.charAt(Math.floor(Math.random() * vineIndex));
1157
+
} else if (columnTypes[i] === 'leaf') {
1158
+
// Leaf character - only at top or occasional spots along the vine
1159
+
if (j === 0 || Math.random() < 0.2) {
1160
+
currentChars[i][j] = leafChars.charAt(Math.floor(Math.random() * leafChars.length));
1162
+
// Connecting vine
1163
+
currentChars[i][j] = vineChars.charAt(Math.floor(Math.random() * 5)); // Thin vine characters
1165
+
} else if (columnTypes[i] === 'branch') {
1166
+
// This is a branching column - will form connections between vines
1168
+
// Top of branch might be a leaf or flower
1169
+
currentChars[i][j] = leafChars.charAt(Math.floor(Math.random() * leafChars.length));
1171
+
// Branch characters - horizontal or diagonal connectors
1172
+
currentChars[i][j] = branchChars.charAt(Math.floor(Math.random() * branchChars.length));
1178
+
// Time when animation started
1179
+
const startTime = Date.now();
1181
+
// Track connections between vines
1182
+
const crossConnections = [];
1184
+
// Draw the rainforest vine effect
1185
+
function drawVineEffect() {
1186
+
// Semi-transparent background to create fade effect
1187
+
ctx.fillStyle = 'rgba(10, 23, 15, 0.05)';
1188
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
1190
+
const now = Date.now();
1193
+
ctx.font = `${fontSize}px 'JetBrains Mono', monospace`;
1194
+
ctx.textAlign = 'center';
1196
+
// First, create cross-connections
1197
+
// Create new cross-connections occasionally
1198
+
if (Math.random() < 0.01) {
1199
+
// Find a source vine that's grown enough
1200
+
const sourceIndex = Math.floor(Math.random() * columns);
1201
+
if (drops[sourceIndex] > 100 && columnTypes[sourceIndex] === 'vine') {
1202
+
// Find a nearby column to connect to
1203
+
const nearby = findNearbyColumns(sourceIndex, 3);
1204
+
if (nearby.length > 0) {
1205
+
const targetIndex = nearby[Math.floor(Math.random() * nearby.length)];
1206
+
if (drops[targetIndex] > 80) {
1207
+
// The height should be somewhere between the two vines
1208
+
const sourceHeight = drops[sourceIndex];
1209
+
const targetHeight = drops[targetIndex];
1210
+
const connectionHeight = Math.min(sourceHeight, targetHeight) * 0.8;
1212
+
// Create the connection
1213
+
crossConnections.push({
1214
+
source: sourceIndex,
1215
+
target: targetIndex,
1216
+
height: connectionHeight,
1217
+
character: branchChars.charAt(Math.floor(Math.random() * branchChars.length)),
1225
+
// For each column
1226
+
for (let i = 0; i < columns; i++) {
1227
+
// Calculate current position of this vine
1228
+
const x = i * fontSize * 1.5; // Space vines further apart
1230
+
// For each character in this column
1231
+
for (let j = 0; j < Math.ceil(drops[i] / fontSize); j++) {
1232
+
const y = j * fontSize;
1234
+
// Skip rendering some characters to create gaps in vines
1235
+
if (Math.random() < 0.05 && j > 3) continue;
1237
+
// Calculate age of this character
1238
+
const charAge = now - lastCharChangeTime[i][j];
1240
+
// Randomly change some characters over time - slower rate for natural movement
1241
+
if (j === 0 && (Math.random() < 0.005 || charAge > 8000)) {
1242
+
// Top character might change between leaves/flowers
1243
+
if (columnTypes[i] === 'leaf' || Math.random() < 0.6) {
1244
+
currentChars[i][j] = leafChars.charAt(Math.floor(Math.random() * leafChars.length));
1246
+
currentChars[i][j] = vineChars.charAt(Math.floor(Math.random() * vineChars.length));
1248
+
lastCharChangeTime[i][j] = now;
1249
+
} else if (j > 0 && Math.random() < 0.001) {
1250
+
// Occasionally grow new leaves along the vine
1251
+
if (Math.random() < 0.2) {
1252
+
currentChars[i][j] = leafChars.charAt(Math.floor(Math.random() * leafChars.length));
1254
+
const vineIndex = Math.min(vineThickness[i] * 3, vineChars.length - 1);
1255
+
currentChars[i][j] = vineChars.charAt(Math.floor(Math.random() * vineIndex));
1257
+
lastCharChangeTime[i][j] = now;
1260
+
// Calculate distance from head of the vine
1261
+
const distanceFromHead = (drops[i] - y);
1263
+
// Determine color based on position and type
1264
+
if (j === 0 && (currentChars[i][j] === '❀' || currentChars[i][j] === '✿' ||
1265
+
currentChars[i][j] === '❁' || currentChars[i][j] === '✾')) {
1266
+
// Flowers are more colorful - pinkish
1267
+
ctx.fillStyle = 'rgba(255, 180, 220, 0.9)';
1268
+
ctx.shadowColor = 'rgba(255, 150, 200, 0.6)';
1269
+
ctx.shadowBlur = 5;
1270
+
} else if (currentChars[i][j] === '☘' || leafChars.includes(currentChars[i][j])) {
1271
+
// Leaf characters are brightest with different green
1272
+
ctx.fillStyle = 'rgba(120, 255, 150, 0.9)';
1273
+
ctx.shadowColor = 'rgba(77, 250, 123, 0.5)';
1274
+
ctx.shadowBlur = 3;
1275
+
} else if (distanceFromHead < fontSize) {
1276
+
// Growing tip of vine is brightest
1277
+
ctx.fillStyle = 'rgba(120, 255, 150, 0.9)';
1278
+
ctx.shadowColor = 'rgba(77, 250, 123, 0.5)';
1279
+
ctx.shadowBlur = 5;
1280
+
} else if (distanceFromHead < fontSize * 8) {
1281
+
// Newer part of vine is brighter
1282
+
const opacity = 0.8 - (distanceFromHead / (fontSize * 10));
1283
+
ctx.fillStyle = `rgba(77, 180, 100, ${opacity.toFixed(2)})`;
1284
+
ctx.shadowColor = 'transparent';
1285
+
ctx.shadowBlur = 0;
1287
+
// Older parts of vine are darker
1288
+
const opacity = Math.max(0, 0.4 - (distanceFromHead / (canvas.height * 2)));
1289
+
// Darker green for older vines
1290
+
ctx.fillStyle = `rgba(40, 120, 60, ${opacity.toFixed(2)})`;
1291
+
ctx.shadowColor = 'transparent';
1292
+
ctx.shadowBlur = 0;
1295
+
// Add slight random swaying to vines
1296
+
const swayAmount = Math.sin((now / 2000) + i) * 2; // Gentle swaying effect
1297
+
const adjustedX = x + swayAmount;
1299
+
// Draw the character
1300
+
if (y < canvas.height) {
1301
+
// Adjust size for special characters
1302
+
if (leafChars.includes(currentChars[i][j])) {
1303
+
ctx.font = `${fontSize * 1.2}px 'JetBrains Mono', monospace`;
1304
+
ctx.fillText(currentChars[i][j], adjustedX, y);
1305
+
ctx.font = `${fontSize}px 'JetBrains Mono', monospace`; // Reset font
1307
+
ctx.fillText(currentChars[i][j], adjustedX, y);
1312
+
// Move the vine down - slower for natural growth
1313
+
drops[i] += fontSize * (0.02 + Math.random() * 0.03);
1315
+
// Reset vine when it reaches bottom or randomly (much less frequent)
1316
+
if (drops[i] > canvas.height * 2 || (Math.random() < 0.0005 && drops[i] > canvas.height * 0.6)) {
1317
+
drops[i] = Math.random() * -30;
1318
+
// Maybe change vine type
1319
+
if (Math.random() < 0.3) {
1320
+
columnTypes[i] = Math.random() < 0.7 ? 'vine' : 'leaf';
1321
+
vineThickness[i] = Math.floor(Math.random() * 3) + 1;
1326
+
// Draw cross connections between vines
1327
+
crossConnections.forEach((connection, index) => {
1328
+
const sourceX = connection.source * fontSize * 1.5;
1329
+
const targetX = connection.target * fontSize * 1.5;
1330
+
const y = connection.height;
1331
+
const heightIndex = Math.floor(y / fontSize);
1333
+
// Calculate a safe display Y - make sure it's within the grown vines
1334
+
const safeY = Math.min(
1335
+
Math.min(drops[connection.source], drops[connection.target]),
1339
+
// Convert to display coords
1340
+
const displayY = Math.floor(safeY / fontSize) * fontSize;
1342
+
// Only draw if connection is within visible area
1343
+
if (displayY < 0 || displayY > canvas.height) return;
1345
+
// Connection age effect
1346
+
const age = now - connection.created;
1347
+
const maxAge = 20000; // 20 seconds lifetime for connections
1349
+
// Remove old connections
1350
+
if (age > maxAge) {
1351
+
crossConnections.splice(index, 1);
1355
+
// Fade in/out effect
1356
+
let opacity = 1.0;
1359
+
opacity = age / 1000;
1360
+
} else if (age > maxAge - 2000) {
1362
+
opacity = (maxAge - age) / 2000;
1365
+
// Draw connection
1366
+
const connectionWidth = Math.abs(targetX - sourceX);
1367
+
const steps = Math.ceil(connectionWidth / (fontSize * 0.8));
1369
+
// Lighter green for branches
1370
+
ctx.fillStyle = `rgba(120, 255, 150, ${opacity.toFixed(2)})`;
1371
+
ctx.shadowColor = 'rgba(77, 250, 123, 0.4)';
1372
+
ctx.shadowBlur = 2;
1374
+
// Draw branch character at each step
1377
+
if (sourceX < targetX) {
1385
+
for (let s = 0; s <= steps; s++) {
1386
+
// Calculate position
1387
+
const progress = s / steps;
1388
+
const stepX = sourceX + (targetX - sourceX) * progress;
1389
+
const wiggle = Math.sin(progress * Math.PI) * 5;
1391
+
// Choose appropriate connection character
1392
+
let connChar = branchChar;
1394
+
// Special characters for start, middle and end
1397
+
} else if (s === steps) {
1399
+
} else if (s === Math.floor(steps/2)) {
1400
+
// Add a leaf or flower in the middle sometimes
1401
+
if (Math.random() < 0.3) {
1402
+
connChar = leafChars.charAt(Math.floor(Math.random() * leafChars.length));
1404
+
connChar = s % 2 === 0 ? '┼' : '┴';
1407
+
// Occasional decorative elements
1408
+
if (Math.random() < 0.1) {
1413
+
ctx.fillText(connChar, stepX, displayY + wiggle);
1417
+
// Schedule next frame
1418
+
requestAnimationFrame(drawVineEffect);
1421
+
// Start the animation
// Add hover event listeners after DOM content is loaded
function setupHoverEffects() {
// Keep track of the currently active item
···
document.querySelectorAll('.feed-item').forEach(item => {
item.addEventListener('mouseenter', () => {
999
-
// Close all sections in previously hovered item
1000
-
if (currentHoveredItem && currentHoveredItem !== item) {
1001
-
// Remove this section - we no longer show the full content
1003
-
// No need to close preview content now since it's controlled by CSS hover
1005
-
// References are now controlled by CSS hover
// Set this as current hovered item
currentHoveredItem = item;
1011
-
// Remove this section - we no longer show the full content
1434
+
// Track mouse position for the ripple effect
1435
+
item.addEventListener('mousemove', (e) => {
1436
+
// Get position relative to the element
1437
+
const rect = item.getBoundingClientRect();
1438
+
const x = ((e.clientX - rect.left) / rect.width) * 100;
1439
+
const y = ((e.clientY - rect.top) / rect.height) * 100;
1013
-
// Preview content is shown automatically by CSS on hover
1441
+
// Set custom properties for the radial gradient
1442
+
item.style.setProperty('--mouse-x', `${x}%`);
1443
+
item.style.setProperty('--mouse-y', `${y}%`);
···
<article id="${entry.articleId}" class="feed-item" ${dateAttr}>
<div class="feed-item-row">
1359
-
<div class="feed-item-date">${monthNames[month]} ${getDayWithOrdinal(date)}, ${year}</div>
1789
+
<div class="feed-item-date">${getDayWithOrdinal(date)} ${shortMonthNames[month]} ${year}</div>
<div class="feed-item-author">${entry.author}</div>
<div class="feed-item-content-wrapper">
<div class="feed-item-title"><a href="${entry.link}" target="_blank">${entry.title}</a></div><div class="feed-item-preview">${entry.contentHtml}</div>
···
1652
-
// Set up hover effects
2082
+
// Set up hover effects and ripple animations
2085
+
// Create a ripple effect that travels across the content area
2086
+
const feedContainer = document.querySelector('.feed-container');
2087
+
feedContainer.addEventListener('mousemove', (e) => {
2088
+
// Ripple between items as mouse moves
2089
+
const items = document.querySelectorAll('.feed-item, .link-item');
2090
+
items.forEach(item => {
2091
+
const rect = item.getBoundingClientRect();
2092
+
const centerX = rect.left + rect.width / 2;
2093
+
const centerY = rect.top + rect.height / 2;
2095
+
// Calculate distance from mouse to center of item
2096
+
const dx = e.clientX - centerX;
2097
+
const dy = e.clientY - centerY;
2098
+
const distance = Math.sqrt(dx * dx + dy * dy);
2100
+
// Calculate fade based on distance
2101
+
const maxDistance = 400; // max distance for effect
2102
+
const intensity = Math.max(0, 1 - (distance / maxDistance));
2104
+
if (intensity > 0.05) {
2105
+
// Extremely subtle glow - minimized for optimal text readability
2106
+
item.style.boxShadow = `0 0 ${intensity * 8}px var(--hover-glow)`;
2107
+
item.style.transform = `scale(${1 + intensity * 0.005})`;
2108
+
item.style.transition = 'box-shadow 0.4s ease-out, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)';
2110
+
item.style.boxShadow = 'none';
2111
+
item.style.transform = 'scale(1)';
2116
+
// Add hover tracking for link items too
2117
+
document.querySelectorAll('.link-item').forEach(item => {
2118
+
item.addEventListener('mousemove', (e) => {
2119
+
// Get position relative to the element
2120
+
const rect = item.getBoundingClientRect();
2121
+
const x = ((e.clientX - rect.left) / rect.width) * 100;
2122
+
const y = ((e.clientY - rect.top) / rect.height) * 100;
2124
+
// Set custom properties for the radial gradient
2125
+
item.style.setProperty('--mouse-x', `${x}%`);
2126
+
item.style.setProperty('--mouse-y', `${y}%`);
// Process all external links from entries
const linksContainer = document.getElementById('link-items');
const allExternalLinks = [];
···
<div class="link-item" data-year="${date.getFullYear()}" data-month="${date.getMonth()}">
1839
-
<div class="link-item-date">${monthNames[date.getMonth()]} ${getLinkDayWithOrdinal(date)}, ${date.getFullYear()}</div>
2314
+
<div class="link-item-date">${getLinkDayWithOrdinal(date)} ${shortMonthNames[date.getMonth()]} ${date.getFullYear()}</div>
<div class="link-item-source" title="From: ${link.sourceTitle}">
<a href="${link.sourceLink}" target="_blank" style="color: inherit; text-decoration: none;">