Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
484 changes: 284 additions & 200 deletions package-lock.json

Large diffs are not rendered by default.

33 changes: 0 additions & 33 deletions src/components/cd-cover/index.ts

This file was deleted.

11 changes: 0 additions & 11 deletions src/components/cd-cover/styles.css

This file was deleted.

Binary file added src/components/logo/assets/cd-labs-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions src/components/logo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import './styles.css';
import LOGO from './assets/cd-labs-logo.png';

// --------------------------------------------------------------------------------

export function buildMainLogo() {
const logoContainer = document.createElement('div');
const logoImg = document.createElement('img');
const logoText = document.createElement('p');

logoContainer.className = 'main-logo';

logoImg.src = LOGO;
logoImg.alt = "CD-Labs Logo";

logoText.textContent = "CD-Labs";

logoContainer.appendChild(logoImg);
logoContainer.appendChild(logoText);

return logoContainer;
}

// --------------------------------------------------------------------------------

export function changeLogoOnScroll() {
const logoContainer = document.querySelector('.main-logo') as HTMLElement;
if (!logoContainer) return;

if (window.scrollY > 50) {
logoContainer.classList.add('scrolled');
} else {
logoContainer.classList.remove('scrolled');
}
}
46 changes: 46 additions & 0 deletions src/components/logo/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.main-logo {
position: fixed;
top: 40px;
left: 55px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;

img {
width: 95px;
height: 95px;
image-rendering: pixelated;
transition: width 0.3s, height 0.3s;
}

img:hover {
animation: rotate 0.5s steps(4) infinite;
}

p {
margin-top: 9px;
text-align: center;
text-transform: uppercase;
font-family: "Handjet", sans-serif;
font-size: 2em;
font-weight: 900;
transition: opacity 0.3s;
}
}

.main-logo.scrolled img {
width: 60px;
height: 60px;
}

.main-logo.scrolled p {
opacity: 0;
pointer-events: none;
}


@keyframes rotate {
100% {
transform: rotate(360deg);
}
}

96 changes: 96 additions & 0 deletions src/components/pixel-grid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import './styles.css';
import { createPixelPattern } from './pixel.ts';

export type GRID_CONFIG = {
rows: number;
colors: string[];
};

const EMPTY_RATIO = 0.5; // Fixed constant for empty bias

// --------------------------------------------------------------------------------

export function createPixelGrid(config: GRID_CONFIG, alignment: 'left' | 'right'): HTMLCanvasElement {
const canvas = createCanvasElement();
const ctx = canvas.getContext('2d');

if (!ctx) return canvas;

// Use ResizeObserver for responsive updates
const resizeObserver = new ResizeObserver(() => {
const parent = canvas.parentElement;
if (parent) {
renderPixelGrid(canvas, ctx, config, alignment, parent.clientWidth, parent.clientHeight);
}
});

// Initial render
setTimeout(() => {
const parent = canvas.parentElement;
if (parent) {
resizeObserver.observe(parent);
renderPixelGrid(canvas, ctx, config, alignment, parent.clientWidth, parent.clientHeight);
}
}, 0);

return canvas;
}

// --------------------------------------------------------------------------------

function createCanvasElement(): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.className = 'pixel-grid';
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
return canvas;
}

function renderPixelGrid(
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
config: GRID_CONFIG,
alignment: 'left' | 'right',
width: number,
height: number
) {
// Setup canvas
canvas.width = width;
canvas.height = height;
ctx.clearRect(0, 0, width, height);

// Calculate pixel size based on rows (perfect squares)
const pixelSize = height / config.rows;

// Calculate number of columns dynamically based on width and pixel size
const cols = Math.ceil(width / pixelSize);

const canvasRect = canvas.getBoundingClientRect();

// Render each pixel
for (let row = 0; row < config.rows; row++) {
for (let col = 0; col < cols; col++) {
const screenX = canvasRect.left + (col * pixelSize);
const color = createPixelPattern({
row,
col,
totalRows: config.rows,
totalCols: cols,
screenX,
screenWidth: window.innerWidth,
alignment,
emptyRatio: EMPTY_RATIO,
colors: config.colors
});

if (color) {
ctx.fillStyle = color;
const x = Math.floor(col * pixelSize);
const y = Math.floor(row * pixelSize);
const size = Math.ceil(pixelSize) + 1;
ctx.fillRect(x, y, size, size);
}
}
}
}
70 changes: 70 additions & 0 deletions src/components/pixel-grid/pixel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

type PixelInput = {
row: number;
col: number;
totalRows: number;
totalCols: number;
screenX: number;
screenWidth: number;
alignment: 'left' | 'right';
emptyRatio: number;
colors: readonly string[];
};

// --------------------------------------------------------------------------------

export function createPixelPattern(input: PixelInput): string | null {
const normalizedX = input.col / (input.totalCols - 1);
const normalizedY = input.row / (input.totalRows - 1);
const normalizedScreenX = input.screenX / input.screenWidth;

const isEmpty = shouldPixelBeEmpty(
normalizedX,
normalizedScreenX,
normalizedY,
input.alignment,
input.emptyRatio
);

return isEmpty ? null : getRandomColor(input.colors);
}

// --------------------------------------------------------------------------------

function shouldPixelBeEmpty(
_normalizedX: number, // Container position (unused, kept for future use)
normalizedScreenX: number,
normalizedY: number,
alignment: 'left' | 'right',
emptyRatio: number
): boolean {
const boundary = calculateBoundary(normalizedY, emptyRatio);

if (alignment === 'right') {
return normalizedScreenX > boundary ? hasGhostPixel() : hasScatterEffect(normalizedScreenX, boundary, normalizedY);
} else {
return normalizedScreenX < boundary ? hasGhostPixel() : hasScatterEffect(normalizedScreenX, boundary, normalizedY);
}
}

function calculateBoundary(normalizedY: number, emptyRatio: number): number {
const waveOffset = Math.sin(normalizedY * Math.PI * 3) * 0.1;
const randomOffset = (Math.random() - 0.5) * 0.15;
return emptyRatio + waveOffset + randomOffset;
}

function hasScatterEffect(screenX: number, boundary: number, normalizedY: number): boolean {
const distance = Math.abs(screenX - boundary);
const scatterChance = Math.pow(1 - distance / boundary, 2) * 0.25;
const rowVariation = Math.sin(normalizedY * Math.PI * 2) * 0.1;

return Math.random() < (scatterChance + rowVariation);
}

function hasGhostPixel(): boolean {
return Math.random() >= 0.3;
}

function getRandomColor(colors: readonly string[]): string {
return colors[Math.floor(Math.random() * colors.length)];
}
20 changes: 20 additions & 0 deletions src/components/pixel-grid/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
canvas.pixel-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
overflow: hidden;
background: transparent;
}

.pixel {
transition: background 0.2s;
}

.pixel-empty {
background: transparent !important;
opacity: 0;
}
37 changes: 37 additions & 0 deletions src/components/top-nav/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import './styles.css';

type LinkItem = [
name: string,
path: string,
]

const navLinks: LinkItem[] = [
['Side Quests', '/projects'],
['About', '/about'],
['Logs', '/logs'],
['Contact', '/contact'],
]

//-----------------------------------------------------------------------

export function buildTopNav() {
const navBox = document.createElement("div");
const ul = document.createElement("ul");

navBox.id = "top-nav";

navLinks.forEach(link => {
const [name, path] = link;
const li = document.createElement("li");
const a = document.createElement("a");

a.href = path;
a.textContent = name;

li.appendChild(a);
ul.appendChild(li);
})

navBox.appendChild(ul);
return navBox
}
Loading