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
41 changes: 25 additions & 16 deletions src/components/pixel-grid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,27 @@ export type GRID_CONFIG = {
colors: string[];
};

const EMPTY_RATIO = 0.5; // Fixed constant for empty bias
const EMPTY_RATIO = 0.3; // Controls the "empty zone" bias (0.0 = all empty, 1.0 = no empty zone)

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

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

if (!ctx) return canvas;

// Use ResizeObserver for responsive updates
let resizeTimeout: number;

// Use ResizeObserver with debouncing for responsive updates
const resizeObserver = new ResizeObserver(() => {
const parent = canvas.parentElement;
if (parent) {
renderPixelGrid(canvas, ctx, config, alignment, parent.clientWidth, parent.clientHeight);
}
clearTimeout(resizeTimeout);
resizeTimeout = window.setTimeout(() => {
const parent = canvas.parentElement;
if (parent) {
renderPixelGrid(canvas, ctx, config, alignment, parent.clientWidth, parent.clientHeight);
}
}, 100); // 100ms debounce
});

// Initial render
Expand Down Expand Up @@ -49,7 +54,7 @@ function renderPixelGrid(
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
config: GRID_CONFIG,
alignment: 'left' | 'right',
alignment: 'left' | 'right' | 'full',
width: number,
height: number
) {
Expand All @@ -63,23 +68,27 @@ function renderPixelGrid(

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

const canvasRect = canvas.getBoundingClientRect();

// Pre-calculate common values
const totalRows = config.rows;
const totalCols = cols;
const colors = config.colors;

// Render each pixel
for (let row = 0; row < config.rows; row++) {
for (let col = 0; col < cols; col++) {
const screenX = canvasRect.left + (col * pixelSize);
// Use canvas-relative position for consistent 50/50 split
const canvasX = col * pixelSize;
const color = createPixelPattern({
row,
col,
totalRows: config.rows,
totalCols: cols,
screenX,
screenWidth: window.innerWidth,
totalRows,
totalCols,
screenX: canvasX,
screenWidth: width, // Use canvas width instead of viewport width
alignment,
emptyRatio: EMPTY_RATIO,
colors: config.colors
colors
});

if (color) {
Expand Down
75 changes: 49 additions & 26 deletions src/components/pixel-grid/pixel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type PixelInput = {
totalCols: number;
screenX: number;
screenWidth: number;
alignment: 'left' | 'right';
alignment: 'left' | 'right' | 'full';
emptyRatio: number;
colors: readonly string[];
};
Expand All @@ -23,7 +23,9 @@ export function createPixelPattern(input: PixelInput): string | null {
normalizedScreenX,
normalizedY,
input.alignment,
input.emptyRatio
input.emptyRatio,
input.col,
input.totalCols
);

return isEmpty ? null : getRandomColor(input.colors);
Expand All @@ -34,35 +36,56 @@ export function createPixelPattern(input: PixelInput): string | null {
function shouldPixelBeEmpty(
_normalizedX: number, // Container position (unused, kept for future use)
normalizedScreenX: number,
normalizedY: number,
alignment: 'left' | 'right',
emptyRatio: number
_normalizedY: number, // Not used in this implementation
alignment: 'left' | 'right' | 'full',
_emptyRatio: number, // Not used in simple split
col: number,
totalCols: number
): boolean {
const boundary = calculateBoundary(normalizedY, emptyRatio);

// For 'full' alignment, stretch the grid to 100% with no empty zones
if (alignment === 'full') {
return false; // Show all pixels across the entire grid
}

// 60/40 split - pixels appear on opposite side of alignment with more coverage
const splitPoint = 0.45; // 40% threshold means 60% coverage on the opposite side

let shouldHideBasedOnAlignment = false;

if (alignment === 'right') {
return normalizedScreenX > boundary ? hasGhostPixel() : hasScatterEffect(normalizedScreenX, boundary, normalizedY);
// For right alignment: show pixels on the LEFT 60% (inverted)
shouldHideBasedOnAlignment = normalizedScreenX > splitPoint;
} else {
return normalizedScreenX < boundary ? hasGhostPixel() : hasScatterEffect(normalizedScreenX, boundary, normalizedY);
// For left alignment: show pixels on the RIGHT 60% (inverted)
shouldHideBasedOnAlignment = normalizedScreenX < splitPoint;
}
}

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;
// If already hidden by alignment, return true
if (shouldHideBasedOnAlignment) {
return true;
}

// Apply random hiding to only the last column of the visible area
const splitCol = Math.floor(totalCols * splitPoint); // Column where the split happens
let shouldApplyRandomHiding = false;

if (alignment === 'right') {
// For right alignment: pixels show on LEFT side (cols 0 to splitCol)
// Target only the last visible column (rightmost edge of visible area)
const isLastVisibleColumn = col === splitCol;
shouldApplyRandomHiding = isLastVisibleColumn;
} else {
// For left alignment: pixels show on RIGHT side (cols splitCol to totalCols)
// Target only the first visible column (leftmost edge of visible area)
const isFirstVisibleColumn = col === splitCol + 1;
shouldApplyRandomHiding = isFirstVisibleColumn;
}

if (shouldApplyRandomHiding) {
return Math.random() < 0.50; // 40% chance for random gaps in the target column
}

return false; // All other visible pixels are shown
}

function getRandomColor(colors: readonly string[]): string {
Expand Down
3 changes: 2 additions & 1 deletion src/views/about/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export function aboutView() {

const firstSection = createAboutSection(aboutContent.manifesto);
firstSection.classList.add('first-section');
firstSection.appendChild(createPixelGridBackground('left', pixelGridConfigs));
firstSection.appendChild(createPixelGridBackground('full', pixelGridConfigs));


page.appendChild(firstSection);
page.appendChild(createPixelBannerCTA(aboutContent.ctaBannerA));
Expand Down
7 changes: 5 additions & 2 deletions src/views/utils/backgrounds-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export function createVideoBackground(videoWebm: string, videoMp4: string, oneSh
return background;
}

export function createPixelGridBackground(contentAlignment: 'left' | 'right', configs: GRID_CONFIG) {
const alignment = contentAlignment === 'right' ? 'left' : 'right';
export function createPixelGridBackground(contentAlignment: 'left' | 'right' | 'full', configs: GRID_CONFIG) {
// For 'full' alignment, pass it directly. For 'left'/'right', invert the alignment
const alignment = contentAlignment === 'full'
? 'full'
: (contentAlignment === 'right' ? 'left' : 'right');
return createPixelGrid(configs, alignment);
}