, nebo jej dej do externího souboru
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('interactive-grid');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const TARGET_GRID_SPACING = 20; // Cílová vzdálenost mezi středy bodů (skutečná se dopočítá)
const BASE_DOT_SIZE = 4; // Základní velikost tečky
const INFLUENCE_RADIUS = 120; // Poloměr, ve kterém kurzor ovlivňuje tečky
const EXTRA_HEIGHT = 100; // o kolik px má grid přesáhnout pod obrázek
// Tyto proměnné budou přepsány v resize()
let gridSpacingX = TARGET_GRID_SPACING;
let gridSpacingY = TARGET_GRID_SPACING;
let offsetX = 0;
let offsetY = 0;
class Dot {
constructor(x, y) {
this.x = x;
this.y = y;
this.updatePosition();
this.size = BASE_DOT_SIZE;
// Mírně prolneme starý noise (pro zachování nějakých shluků) s čistou náhodou
let n = Math.sin(x * 0.8) + Math.cos(y * 0.8);
n += Math.sin(x * 0.3 - y * 0.4);
this.noiseValue = (n + 2) / 4;
// Hlavní hybatel nahodilosti: jak moc je tento konkrétní bod ochotný růst
this.randomWeight = Math.random();
}
updatePosition() {
this.cx = offsetX + this.x * gridSpacingX;
this.cy = offsetY + this.y * gridSpacingY;
}
update(mouseX, mouseY) {
const dx = mouseX - this.cx;
const dy = mouseY - this.cy;
const dist = Math.sqrt(dx * dx + dy * dy);
let targetSize = BASE_DOT_SIZE;
if (dist < INFLUENCE_RADIUS) {
// Blízkost od 0 (na okraji) do 1 (přímo na kurzoru)
const proximity = Math.max(0, 1 - (dist / INFLUENCE_RADIUS));
// Zkombinujeme blízkost s náhodnou váhou bodu (částečně ovlivněno noise).
// Tím docílíme toho, že i dost blízko myši zůstanou některé body malé.
const combinedWeight = (this.randomWeight * 0.7) + (this.noiseValue * 0.3);
const influence = proximity * combinedWeight;
// Definice skokového zvětšení (schody) na základě vlivu (influence) a fixní blízkosti
if (dist < 15) {
targetSize = 30; // Úplný střed spojí vždy kousek k sobě
} else if (influence > 0.45) {
targetSize = 30; // Hodně ochotné body zformují velký blok
} else if (influence > 0.3) {
targetSize = 14; // Velký čtverec, ale neslitý
} else if (influence > 0.15) {
targetSize = 8;
} else if (influence > 0.05) {
targetSize = BASE_DOT_SIZE;
}
}
this.size += (targetSize - this.size) * 0.15;
}
draw(ctx) {
const halfSize = this.size / 2;
// Zaokrouhlení na celé pixely pro maximální ostrost
const drawX = Math.floor(this.cx - halfSize);
const drawY = Math.floor(this.cy - halfSize);
const drawSize = Math.max(1, Math.round(this.size));
ctx.fillRect(drawX, drawY, drawSize, drawSize);
}
}
let dots = [];
let mousePos = { x: -1000, y: -1000 };
let targetMousePos = { x: -1000, y: -1000 };
const resize = () => {
// Vezmeme rozměry z nejbližší nad canvasem (ta má definovanou výšku),
// ne z přímého rodičovského
, který výšku nemá.
const parent = canvas.closest('section');
const logicalWidth = parent ? parent.clientWidth : window.innerWidth;
const baseHeight = parent ? parent.clientHeight : window.innerHeight;
const logicalHeight = baseHeight + EXTRA_HEIGHT;
// Podpora pro Retina / High DPI displeje pro ostré hrany
const dpr = window.devicePixelRatio || 1;
canvas.width = logicalWidth * dpr;
canvas.height = logicalHeight * dpr;
canvas.style.width = logicalWidth + 'px';
canvas.style.height = logicalHeight + 'px';
// Reset transformace před scale (aby se DPR nenásobilo při opakovaném resize)
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
// Rezerva, aby se první/poslední tečka vešla celá (s ohledem na základní velikost)
const padding = BASE_DOT_SIZE / 2;
const usableWidth = logicalWidth - BASE_DOT_SIZE;
const usableHeight = logicalHeight - BASE_DOT_SIZE;
// Kolik mezer (ne bodů) se vejde při cílovém rozestupu
const colsCount = Math.max(1, Math.round(usableWidth / TARGET_GRID_SPACING));
const rowsCount = Math.max(1, Math.round(usableHeight / TARGET_GRID_SPACING));
// Skutečný rozestup, aby se body přesně vešly mezi okraje
gridSpacingX = usableWidth / colsCount;
gridSpacingY = usableHeight / rowsCount;
offsetX = padding;
offsetY = padding;
dots = [];
for (let x = 0; x <= colsCount; x++) {
for (let y = 0; y <= rowsCount; y++) {
dots.push(new Dot(x, y));
}
}
};
// Měníme velikost gridu při změně okna
window.addEventListener('resize', resize);
// Přepočítáme i po úplném načtení (fonty, obrázky na pozadí), kdy už sekce zná svou výšku
window.addEventListener('load', resize);
resize();
const render = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
mousePos.x += (targetMousePos.x - mousePos.x) * 0.2;
mousePos.y += (targetMousePos.y - mousePos.y) * 0.2;
// Nastavená barva Gridu dle tvoje zadání z minula
ctx.fillStyle = '#587AE0';
for (const dot of dots) {
dot.update(mousePos.x, mousePos.y);
dot.draw(ctx);
}
requestAnimationFrame(render);
};
render();
// Událost odchytáváme na okně, aby trackovala kurzor bez ztráty fokusu
window.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
targetMousePos = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
});
document.addEventListener('mouseleave', () => {
targetMousePos = { x: -1000, y: -1000 };
});
});
} catch (e) {
console.error('[Custom Script "grid"]', e);
}
})();
Statement
We stand at the intersection of material science and industrial mastery, where laboratory innovation is transformed into certified construction reality.
Selective Paste Intrusion (SPI) is an advanced 3D concrete printing technology based on printing directly into a sand bed. This approach makes it possible to manufacture large-scale, highly complex geometries without the need for temporary support structures. SPI significantly reduces the cost of producing prototypes, mock-ups, or one-off components, making it an efficient solution for both experimental and customized applications with minimized material consumption and CO2 production.