grid · canvas · interactive background
// grid studio
Fondos interactivos con export listo para pegar
Ahora está optimizado para trabajar rápido: preview y código en tabs, opciones dentro de modal y varios tipos de malla.
square · 0x0 · 0 fps
Código exportado (html + css + js)
<div class="grid-studio-wrapper">
<canvas id="grid-studio-canvas"></canvas>
</div>
<style>
.grid-studio-wrapper {
width: 100%;
height: 420px;
border-radius: 16px;
overflow: hidden;
}
#grid-studio-canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
<script>
const options = {
"gridType": "square",
"seed": 3942,
"cellSize": 30,
"amplitude": 16,
"noiseScale": 0.018,
"speed": 0.45,
"octaves": 3,
"persistence": 0.52,
"lacunarity": 2.05,
"flowX": 0,
"flowY": 0,
"lineWidth": 1.1,
"lineAlpha": 0.56,
"dotSize": 1.25,
"dotAlpha": 0.58,
"pointerEnabled": true,
"pointerRadius": 165,
"pointerStrength": 32,
"showDots": true,
"showHorizontal": true,
"showVertical": true,
"animate": true,
"blendMode": "multiply",
"backgroundColor": "#f8fafc",
"lineColor": "#0f766e",
"dotColor": "#ea580c"
};
const canvas = document.getElementById('grid-studio-canvas');
const ctx = canvas.getContext('2d');
let width = 0;
let height = 0;
let dpr = 1;
let start = performance.now();
const pointer = { x: 0, y: 0, active: false };
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function fract(value) {
return value - Math.floor(value);
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function smoothstep(t) {
return t * t * (3 - 2 * t);
}
function hash2d(x, y, seed) {
return fract(Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453123);
}
function valueNoise2d(x, y, seed) {
const x0 = Math.floor(x);
const y0 = Math.floor(y);
const tx = x - x0;
const ty = y - y0;
const n00 = hash2d(x0, y0, seed);
const n10 = hash2d(x0 + 1, y0, seed);
const n01 = hash2d(x0, y0 + 1, seed);
const n11 = hash2d(x0 + 1, y0 + 1, seed);
const ux = smoothstep(tx);
const uy = smoothstep(ty);
return lerp(lerp(n00, n10, ux), lerp(n01, n11, ux), uy);
}
function fractalNoise2d(x, y, seed, octaves, persistence, lacunarity) {
let amplitude = 1;
let frequency = 1;
let value = 0;
let norm = 0;
for (let octave = 0; octave < octaves; octave += 1) {
value += valueNoise2d(x * frequency, y * frequency, seed + octave * 131) * amplitude;
norm += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return norm === 0 ? 0 : value / norm;
}
function toRgba(hex, alpha) {
const clean = hex.replace('#', '').trim();
if (!/^[0-9a-fA-F]{6}$/.test(clean)) {
return 'rgba(0,0,0,' + clamp(alpha, 0, 1).toFixed(3) + ')';
}
const r = Number.parseInt(clean.slice(0, 2), 16);
const g = Number.parseInt(clean.slice(2, 4), 16);
const b = Number.parseInt(clean.slice(4, 6), 16);
return 'rgba(' + r + ',' + g + ',' + b + ',' + clamp(alpha, 0, 1).toFixed(3) + ')';
}
function pointerEffect(baseX, baseY) {
if (!options.pointerEnabled || !pointer.active) {
return { x: 0, y: 0 };
}
const dx = baseX - pointer.x;
const dy = baseY - pointer.y;
const distance = Math.hypot(dx, dy);
if (distance <= 0 || distance >= options.pointerRadius) {
return { x: 0, y: 0 };
}
const influence = Math.pow(1 - distance / options.pointerRadius, 2) * options.pointerStrength;
return {
x: (dx / distance) * influence,
y: (dy / distance) * influence,
};
}
function getGridDimensions(safeCell, sampleStep) {
const spacing = safeCell * sampleStep;
if (options.gridType === 'radial') {
const radius = Math.max(120, Math.min(width, height) * 0.52);
const rows = Math.max(8, Math.ceil(radius / spacing));
const cols = Math.max(20, Math.ceil((Math.PI * 2 * radius) / spacing));
return { cols, rows, spacing };
}
if (options.gridType === 'isometric') {
const diagonals = Math.ceil((width + height) / spacing) + 6;
return { cols: diagonals, rows: diagonals, spacing };
}
const cols = Math.ceil(width / spacing) + 2;
const rows = Math.ceil(height / spacing) + 2;
return { cols, rows, spacing };
}
function basePointFor(row, col, cols, rows, spacing, elapsed) {
const centerX = width / 2;
const centerY = height / 2;
if (options.gridType === 'staggered') {
return {
x: col * spacing + (row % 2 === 0 ? 0 : spacing * 0.5) - spacing,
y: row * spacing - spacing,
};
}
if (options.gridType === 'hex') {
return {
x: col * spacing * 0.88 + (row % 2 === 0 ? 0 : spacing * 0.44) - spacing,
y: row * spacing * 0.76 - spacing,
};
}
if (options.gridType === 'isometric') {
return {
x: (col - row) * spacing * 0.72 + centerX,
y: (col + row) * spacing * 0.36 - spacing * 2,
};
}
if (options.gridType === 'radial') {
const radius = row * spacing * 0.72;
const angle = (col / Math.max(cols, 1)) * Math.PI * 2;
return {
x: centerX + Math.cos(angle) * radius,
y: centerY + Math.sin(angle) * radius,
};
}
if (options.gridType === 'waves') {
return {
x: col * spacing - spacing,
y: row * spacing - spacing +
Math.sin(col * 0.6 + elapsed * options.speed * 2) * spacing * 0.35 +
Math.cos(row * 0.4 + elapsed * options.speed * 1.2) * spacing * 0.2,
};
}
if (options.gridType === 'neural') {
return {
x: col * spacing - spacing + (hash2d(col * 1.7, row * 2.3, options.seed + 41) - 0.5) * spacing * 0.55,
y: row * spacing - spacing + (hash2d(col * 2.1, row * 1.9, options.seed + 83) - 0.5) * spacing * 0.55,
};
}
return {
x: col * spacing - spacing,
y: row * spacing - spacing,
};
}
function resize() {
const rect = canvas.getBoundingClientRect();
width = Math.max(1, Math.floor(rect.width));
height = Math.max(1, Math.floor(rect.height));
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function draw(now) {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = options.backgroundColor;
ctx.fillRect(0, 0, width, height);
const safeCell = Math.max(8, options.cellSize);
const initialDims = getGridDimensions(safeCell, 1);
const maxPoints = options.gridType === 'dots' ? 12000 : options.gridType === 'neural' ? 3200 : 8200;
const totalPoints = initialDims.cols * initialDims.rows;
const sampleStep = totalPoints > maxPoints ? Math.ceil(Math.sqrt(totalPoints / maxPoints)) : 1;
const dims = getGridDimensions(safeCell, sampleStep);
const cols = dims.cols;
const rows = dims.rows;
const spacing = dims.spacing;
const elapsed = options.animate ? (now - start) / 1000 : 0;
const isDotsGrid = options.gridType === 'dots';
const isNeuralGrid = options.gridType === 'neural';
const neuralLinksEnabled = isNeuralGrid && (options.showHorizontal || options.showVertical);
ctx.strokeStyle = toRgba(options.lineColor, options.lineAlpha);
ctx.lineWidth = options.lineWidth;
ctx.fillStyle = toRgba(options.dotColor, options.dotAlpha);
ctx.globalCompositeOperation = options.blendMode;
function drawLine(from, to, alphaScale) {
const alpha = clamp(options.lineAlpha * (alphaScale === undefined ? 1 : alphaScale), 0, 1);
ctx.strokeStyle = toRgba(options.lineColor, alpha);
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
}
let previousRow = null;
for (let row = 0; row < rows; row += 1) {
const currentRow = [];
for (let col = 0; col < cols; col += 1) {
const basePoint = basePointFor(row, col, cols, rows, spacing, elapsed);
const nx = basePoint.x * options.noiseScale + elapsed * options.speed + options.flowX * 0.02;
const ny = basePoint.y * options.noiseScale + elapsed * options.speed * 0.72 + options.flowY * 0.02;
const noise = fractalNoise2d(nx, ny, options.seed, options.octaves, options.persistence, options.lacunarity);
const angle = noise * Math.PI * 2;
const magnitude = (noise - 0.5) * 2 * options.amplitude;
let offsetX = Math.cos(angle) * magnitude;
let offsetY = Math.sin(angle) * magnitude;
const pointerDelta = pointerEffect(basePoint.x, basePoint.y);
offsetX += pointerDelta.x;
offsetY += pointerDelta.y;
const point = {
x: basePoint.x + offsetX,
y: basePoint.y + offsetY,
};
currentRow.push(point);
if (!isDotsGrid && !isNeuralGrid && options.showHorizontal && col > 0) {
const left = currentRow[col - 1];
if (left) {
drawLine(left, point, 1);
}
}
if (!isDotsGrid && !isNeuralGrid && options.showVertical && previousRow) {
const top = previousRow[col];
if (top) {
drawLine(top, point, 1);
}
}
if (neuralLinksEnabled) {
const left = currentRow[col - 1];
const farLeft = currentRow[col - 2];
const top = previousRow ? previousRow[col] : null;
const topLeft = previousRow ? previousRow[col - 1] : null;
const topRight = previousRow ? previousRow[col + 1] : null;
if (options.showHorizontal) {
if (left && hash2d(col + 11, row + 17, options.seed) > 0.18) {
drawLine(left, point, 0.95);
}
if (farLeft && hash2d(col + 19, row + 31, options.seed) > 0.72) {
drawLine(farLeft, point, 0.45);
}
}
if (options.showVertical) {
if (top && hash2d(col + 23, row + 13, options.seed) > 0.12) {
drawLine(top, point, 0.85);
}
if (topLeft && hash2d(col + 29, row + 7, options.seed) > 0.62) {
drawLine(topLeft, point, 0.52);
}
if (topRight && hash2d(col + 37, row + 5, options.seed) > 0.62) {
drawLine(topRight, point, 0.52);
}
}
}
if (options.showDots && options.dotSize > 0) {
ctx.beginPath();
ctx.arc(point.x, point.y, options.dotSize, 0, Math.PI * 2);
ctx.fill();
}
}
if (
options.gridType === 'radial' &&
!isNeuralGrid &&
!isDotsGrid &&
options.showHorizontal &&
currentRow.length > 2
) {
const first = currentRow[0];
const last = currentRow[currentRow.length - 1];
if (first && last) {
drawLine(last, first, 1);
}
}
previousRow = currentRow;
}
ctx.globalCompositeOperation = 'source-over';
}
function frame(now) {
draw(now);
requestAnimationFrame(frame);
}
function updatePointer(event) {
if (!options.pointerEnabled) {
return;
}
const rect = canvas.getBoundingClientRect();
pointer.x = event.clientX - rect.left;
pointer.y = event.clientY - rect.top;
pointer.active = true;
}
resize();
window.addEventListener('resize', resize);
canvas.addEventListener('pointermove', updatePointer);
canvas.addEventListener('pointerleave', () => {
pointer.active = false;
});
requestAnimationFrame(frame);
</script>