Skip to content

Commit 762f5aa

Browse files
authored
feat: [#85] First home page integration
Merge big PR
2 parents 93f70e9 + fc45ffc commit 762f5aa

40 files changed

+1558
-979
lines changed

package-lock.json

Lines changed: 284 additions & 200 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/cd-cover/index.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/components/cd-cover/styles.css

Lines changed: 0 additions & 11 deletions
This file was deleted.
768 Bytes
Loading

src/components/logo/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import './styles.css';
2+
import LOGO from './assets/cd-labs-logo.png';
3+
4+
// --------------------------------------------------------------------------------
5+
6+
export function buildMainLogo() {
7+
const logoContainer = document.createElement('div');
8+
const logoImg = document.createElement('img');
9+
const logoText = document.createElement('p');
10+
11+
logoContainer.className = 'main-logo';
12+
13+
logoImg.src = LOGO;
14+
logoImg.alt = "CD-Labs Logo";
15+
16+
logoText.textContent = "CD-Labs";
17+
18+
logoContainer.appendChild(logoImg);
19+
logoContainer.appendChild(logoText);
20+
21+
return logoContainer;
22+
}
23+
24+
// --------------------------------------------------------------------------------
25+
26+
export function changeLogoOnScroll() {
27+
const logoContainer = document.querySelector('.main-logo') as HTMLElement;
28+
if (!logoContainer) return;
29+
30+
if (window.scrollY > 50) {
31+
logoContainer.classList.add('scrolled');
32+
} else {
33+
logoContainer.classList.remove('scrolled');
34+
}
35+
}

src/components/logo/styles.css

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
.main-logo {
2+
position: fixed;
3+
top: 40px;
4+
left: 55px;
5+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
6+
z-index: 10;
7+
8+
img {
9+
width: 95px;
10+
height: 95px;
11+
image-rendering: pixelated;
12+
transition: width 0.3s, height 0.3s;
13+
}
14+
15+
img:hover {
16+
animation: rotate 0.5s steps(4) infinite;
17+
}
18+
19+
p {
20+
margin-top: 9px;
21+
text-align: center;
22+
text-transform: uppercase;
23+
font-family: "Handjet", sans-serif;
24+
font-size: 2em;
25+
font-weight: 900;
26+
transition: opacity 0.3s;
27+
}
28+
}
29+
30+
.main-logo.scrolled img {
31+
width: 60px;
32+
height: 60px;
33+
}
34+
35+
.main-logo.scrolled p {
36+
opacity: 0;
37+
pointer-events: none;
38+
}
39+
40+
41+
@keyframes rotate {
42+
100% {
43+
transform: rotate(360deg);
44+
}
45+
}
46+

src/components/pixel-grid/index.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import './styles.css';
2+
import { createPixelPattern } from './pixel.ts';
3+
4+
export type GRID_CONFIG = {
5+
rows: number;
6+
colors: string[];
7+
};
8+
9+
const EMPTY_RATIO = 0.5; // Fixed constant for empty bias
10+
11+
// --------------------------------------------------------------------------------
12+
13+
export function createPixelGrid(config: GRID_CONFIG, alignment: 'left' | 'right'): HTMLCanvasElement {
14+
const canvas = createCanvasElement();
15+
const ctx = canvas.getContext('2d');
16+
17+
if (!ctx) return canvas;
18+
19+
// Use ResizeObserver for responsive updates
20+
const resizeObserver = new ResizeObserver(() => {
21+
const parent = canvas.parentElement;
22+
if (parent) {
23+
renderPixelGrid(canvas, ctx, config, alignment, parent.clientWidth, parent.clientHeight);
24+
}
25+
});
26+
27+
// Initial render
28+
setTimeout(() => {
29+
const parent = canvas.parentElement;
30+
if (parent) {
31+
resizeObserver.observe(parent);
32+
renderPixelGrid(canvas, ctx, config, alignment, parent.clientWidth, parent.clientHeight);
33+
}
34+
}, 0);
35+
36+
return canvas;
37+
}
38+
39+
// --------------------------------------------------------------------------------
40+
41+
function createCanvasElement(): HTMLCanvasElement {
42+
const canvas = document.createElement('canvas');
43+
canvas.className = 'pixel-grid';
44+
canvas.style.width = '100%';
45+
canvas.style.height = '100%';
46+
canvas.style.display = 'block';
47+
return canvas;
48+
}
49+
50+
function renderPixelGrid(
51+
canvas: HTMLCanvasElement,
52+
ctx: CanvasRenderingContext2D,
53+
config: GRID_CONFIG,
54+
alignment: 'left' | 'right',
55+
width: number,
56+
height: number
57+
) {
58+
// Setup canvas
59+
canvas.width = width;
60+
canvas.height = height;
61+
ctx.clearRect(0, 0, width, height);
62+
63+
// Calculate pixel size based on rows (perfect squares)
64+
const pixelSize = height / config.rows;
65+
66+
// Calculate number of columns dynamically based on width and pixel size
67+
const cols = Math.ceil(width / pixelSize);
68+
69+
const canvasRect = canvas.getBoundingClientRect();
70+
71+
// Render each pixel
72+
for (let row = 0; row < config.rows; row++) {
73+
for (let col = 0; col < cols; col++) {
74+
const screenX = canvasRect.left + (col * pixelSize);
75+
const color = createPixelPattern({
76+
row,
77+
col,
78+
totalRows: config.rows,
79+
totalCols: cols,
80+
screenX,
81+
screenWidth: window.innerWidth,
82+
alignment,
83+
emptyRatio: EMPTY_RATIO,
84+
colors: config.colors
85+
});
86+
87+
if (color) {
88+
ctx.fillStyle = color;
89+
const x = Math.floor(col * pixelSize);
90+
const y = Math.floor(row * pixelSize);
91+
const size = Math.ceil(pixelSize) + 1;
92+
ctx.fillRect(x, y, size, size);
93+
}
94+
}
95+
}
96+
}

src/components/pixel-grid/pixel.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
2+
type PixelInput = {
3+
row: number;
4+
col: number;
5+
totalRows: number;
6+
totalCols: number;
7+
screenX: number;
8+
screenWidth: number;
9+
alignment: 'left' | 'right';
10+
emptyRatio: number;
11+
colors: readonly string[];
12+
};
13+
14+
// --------------------------------------------------------------------------------
15+
16+
export function createPixelPattern(input: PixelInput): string | null {
17+
const normalizedX = input.col / (input.totalCols - 1);
18+
const normalizedY = input.row / (input.totalRows - 1);
19+
const normalizedScreenX = input.screenX / input.screenWidth;
20+
21+
const isEmpty = shouldPixelBeEmpty(
22+
normalizedX,
23+
normalizedScreenX,
24+
normalizedY,
25+
input.alignment,
26+
input.emptyRatio
27+
);
28+
29+
return isEmpty ? null : getRandomColor(input.colors);
30+
}
31+
32+
// --------------------------------------------------------------------------------
33+
34+
function shouldPixelBeEmpty(
35+
_normalizedX: number, // Container position (unused, kept for future use)
36+
normalizedScreenX: number,
37+
normalizedY: number,
38+
alignment: 'left' | 'right',
39+
emptyRatio: number
40+
): boolean {
41+
const boundary = calculateBoundary(normalizedY, emptyRatio);
42+
43+
if (alignment === 'right') {
44+
return normalizedScreenX > boundary ? hasGhostPixel() : hasScatterEffect(normalizedScreenX, boundary, normalizedY);
45+
} else {
46+
return normalizedScreenX < boundary ? hasGhostPixel() : hasScatterEffect(normalizedScreenX, boundary, normalizedY);
47+
}
48+
}
49+
50+
function calculateBoundary(normalizedY: number, emptyRatio: number): number {
51+
const waveOffset = Math.sin(normalizedY * Math.PI * 3) * 0.1;
52+
const randomOffset = (Math.random() - 0.5) * 0.15;
53+
return emptyRatio + waveOffset + randomOffset;
54+
}
55+
56+
function hasScatterEffect(screenX: number, boundary: number, normalizedY: number): boolean {
57+
const distance = Math.abs(screenX - boundary);
58+
const scatterChance = Math.pow(1 - distance / boundary, 2) * 0.25;
59+
const rowVariation = Math.sin(normalizedY * Math.PI * 2) * 0.1;
60+
61+
return Math.random() < (scatterChance + rowVariation);
62+
}
63+
64+
function hasGhostPixel(): boolean {
65+
return Math.random() >= 0.3;
66+
}
67+
68+
function getRandomColor(colors: readonly string[]): string {
69+
return colors[Math.floor(Math.random() * colors.length)];
70+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
canvas.pixel-grid {
2+
position: absolute;
3+
top: 0;
4+
left: 0;
5+
width: 100%;
6+
height: 100%;
7+
pointer-events: none;
8+
z-index: -1;
9+
overflow: hidden;
10+
background: transparent;
11+
}
12+
13+
.pixel {
14+
transition: background 0.2s;
15+
}
16+
17+
.pixel-empty {
18+
background: transparent !important;
19+
opacity: 0;
20+
}

src/components/top-nav/index.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import './styles.css';
2+
3+
type LinkItem = [
4+
name: string,
5+
path: string,
6+
]
7+
8+
const navLinks: LinkItem[] = [
9+
['Side Quests', '/projects'],
10+
['About', '/about'],
11+
['Logs', '/logs'],
12+
['Contact', '/contact'],
13+
]
14+
15+
//-----------------------------------------------------------------------
16+
17+
export function buildTopNav() {
18+
const navBox = document.createElement("div");
19+
const ul = document.createElement("ul");
20+
21+
navBox.id = "top-nav";
22+
23+
navLinks.forEach(link => {
24+
const [name, path] = link;
25+
const li = document.createElement("li");
26+
const a = document.createElement("a");
27+
28+
a.href = path;
29+
a.textContent = name;
30+
31+
li.appendChild(a);
32+
ul.appendChild(li);
33+
})
34+
35+
navBox.appendChild(ul);
36+
return navBox
37+
}

0 commit comments

Comments
 (0)