<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Smear Art</title>
<style>*{margin:0;padding:0;box-sizing:border-box}body,html{width:100%;height:100%;overflow:hidden;background:#000}#container{position:relative;width:600px;height:600px;margin:0 auto;background:#000}.partition{position:absolute;overflow:hidden}.partition canvas{width:100%;height:100%;display:block;image-rendering:pixelated;image-rendering:crisp-edges;position:relative;z-index:10}</style>
</head>
<body>
<div id="container"></div>
<script>
const container = document.getElementById('container');
const partitions = [{"x":-2,"y":0,"w":308,"h":600,"pattern":"random-blocks","color1":"#ceff00","color2":"#000000","cellSize":10,"speed":5,"smearAmount":2,"direction":{"dx":-1,"dy":0,"name":"left"},"enableDirectionChange":true,"directionChangeInterval":3861.3076612313293,"sourceInterval":1000,"smearMode":"replace","trailIntensity":0.85,"opacity":1},{"x":303,"y":3,"w":311,"h":605,"pattern":"cellular-automata","color1":"#f865b0","color2":"#000000","cellSize":10,"speed":5,"smearAmount":2,"direction":{"dx":1,"dy":0,"name":"right"},"enableDirectionChange":false,"directionChangeInterval":3000,"sourceInterval":1000,"smearMode":"trail","trailIntensity":0.98,"opacity":1},{"x":163,"y":-2,"w":222,"h":611,"pattern":"perlin","color1":"#f4faff","color2":"#000000","cellSize":10,"speed":5,"smearAmount":2,"direction":{"dx":-1,"dy":1,"name":"down-left"},"enableDirectionChange":true,"directionChangeInterval":2805.7492425761707,"sourceInterval":1000,"smearMode":"replace","trailIntensity":0.85,"opacity":0.81}];
class SmearPartition {
constructor(config) {
this.config = config;
this.div = document.createElement('div');
this.div.className = 'partition';
this.div.style.left = config.x + 'px';
this.div.style.top = config.y + 'px';
this.div.style.width = config.w + 'px';
this.div.style.height = config.h + 'px';
this.canvas = document.createElement('canvas');
this.canvas.width = Math.floor(config.w);
this.canvas.height = Math.floor(config.h);
this.ctx = this.canvas.getContext('2d');
this.ctx.imageSmoothingEnabled = false;
this.div.appendChild(this.canvas);
container.appendChild(this.div);
this.direction = config.direction;
this.enableDirectionChange = config.enableDirectionChange || false;
this.directionChangeInterval = config.directionChangeInterval || 3000;
this.lastDirectionChange = 0;
this.lastSourceTime = 0;
this.imageData = null;
this.opacity = config.opacity !== undefined ? config.opacity : 1.0;
this.fgRGB = this.hexToRGB(config.color1);
this.bgRGB = this.hexToRGB(config.color2 || '#000000');
this.sourceParams = this.generateSourceParams();
this.gradientCache = null;
this.caState = null;
this.initPattern();
this.start();
}
generateSourceParams() {
return {
steps: 3 + Math.floor(Math.random() * 6),
stripeWidth: 2 + Math.floor(Math.random() * 8),
checkSize: 1 + Math.floor(Math.random() * 4),
phase: 0,
dashLength: 3 + Math.floor(Math.random() * 10),
gapLength: 2 + Math.floor(Math.random() * 6),
density: 0.05 + Math.random() * 0.25,
frequency: 1 + Math.random() * 4,
sinePhase: Math.random() * Math.PI * 2,
seed1: Math.random() * 100,
seed2: Math.random() * 100,
seed3: Math.random() * 100,
counter: Math.floor(Math.random() * 65536),
caRule: Math.random() > 0.5 ? 30 : 110
};
}
hexToRGB(hex) {
const r = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return r ? {
r: parseInt(r[1], 16),
g: parseInt(r[2], 16),
b: parseInt(r[3], 16)
} : { r: 255, g: 255, b: 255 };
}
lerpColor(t) {
return {
r: Math.round(this.bgRGB.r + (this.fgRGB.r - this.bgRGB.r) * t),
g: Math.round(this.bgRGB.g + (this.fgRGB.g - this.bgRGB.g) * t),
b: Math.round(this.bgRGB.b + (this.fgRGB.b - this.bgRGB.b) * t)
};
}
getSourceValue(pos, length) {
const t = pos / length;
const cfg = this.config;
switch(cfg.pattern) {
case 'noise':
return Math.random() > 0.5 ? this.fgRGB : this.bgRGB;
case 'linear-gradient':
return this.lerpColor(t);
case 'stepped-gradient':
const steps = this.sourceParams.steps || 5;
return this.lerpColor(Math.floor(t * steps) / (steps - 1));
case 'random-gradient':
if (!this.gradientCache || this.gradientCache.length !== length) {
this.generateRandomGradient(length);
}
return this.lerpColor(this.gradientCache[pos]);
case 'stripes':
return (Math.floor(pos / (cfg.cellSize || 10)) % 2 === 0) ? this.fgRGB : this.bgRGB;
case 'checkerboard':
return ((Math.floor(pos / (cfg.cellSize || 10)) + (this.sourceParams.phase || 0)) % 2 === 0) ? this.fgRGB : this.bgRGB;
case 'random-blocks':
const blockIndex = Math.floor(pos / (cfg.cellSize || 10));
const blockRandom = Math.sin(blockIndex * 12.9898 + this.sourceParams.seed1) * 43758.5453;
return (blockRandom - Math.floor(blockRandom)) > 0.5 ? this.fgRGB : this.bgRGB;
case 'dashes':
const cycle = (this.sourceParams.dashLength || 6) + (this.sourceParams.gapLength || 3);
return (pos % cycle < (this.sourceParams.dashLength || 6)) ? this.fgRGB : this.bgRGB;
case 'dots':
return Math.random() < (this.sourceParams.density || 0.15) ? this.fgRGB : this.bgRGB;
case 'sine':
const sv = (Math.sin((t * (this.sourceParams.frequency || 2) * Math.PI * 2) + (this.sourceParams.sinePhase || 0)) + 1) / 2;
return this.lerpColor(sv);
case 'perlin':
const p1 = Math.sin(t * 3.7 + this.sourceParams.seed1) * 0.5;
const p2 = Math.sin(t * 7.3 + this.sourceParams.seed2) * 0.25;
const p3 = Math.sin(t * 13.1 + this.sourceParams.seed3) * 0.125;
return this.lerpColor(Math.max(0, Math.min(1, (p1 + p2 + p3 + 0.875) / 1.75)));
case 'binary-counter':
return ((this.sourceParams.counter >> (pos % 16)) & 1) ? this.fgRGB : this.bgRGB;
case 'cellular-automata':
if (!this.caState || this.caState.length !== length) {
this.initCellularAutomata(length);
}
return this.caState[pos] ? this.fgRGB : this.bgRGB;
default:
return Math.random() > 0.5 ? this.fgRGB : this.bgRGB;
}
}
generateRandomGradient(length) {
const np = 3 + Math.floor(Math.random() * 4);
const cp = [0];
for (let i = 1; i < np - 1; i++) cp.push(Math.random());
cp.push(1);
cp.sort((a, b) => a - b);
const cv = cp.map(() => Math.random());
this.gradientCache = [];
for (let i = 0; i < length; i++) {
const t = i / length;
let si = 0;
for (let j = 0; j < cp.length - 1; j++) {
if (t >= cp[j] && t <= cp[j + 1]) {
si = j;
break;
}
}
const st = (t - cp[si]) / (cp[si + 1] - cp[si]);
this.gradientCache.push(cv[si] + (cv[si + 1] - cv[si]) * st);
}
}
initCellularAutomata(length) {
this.caState = [];
for (let i = 0; i < length; i++) {
this.caState.push(Math.random() > 0.5);
}
}
evolveCellularAutomata() {
if (!this.caState) return;
const rule = this.sourceParams.caRule || 30;
const os = [...this.caState];
const len = this.caState.length;
for (let i = 0; i < len; i++) {
const p = (os[(i - 1 + len) % len] ? 4 : 0) +
(os[i] ? 2 : 0) +
(os[(i + 1) % len] ? 1 : 0);
this.caState[i] = ((rule >> p) & 1) === 1;
}
}
updateSourceParams() {
const cfg = this.config;
switch(cfg.pattern) {
case 'checkerboard':
this.sourceParams.phase = (this.sourceParams.phase + 1) % 2;
break;
case 'sine':
this.sourceParams.sinePhase += 0.5;
break;
case 'binary-counter':
this.sourceParams.counter++;
break;
case 'cellular-automata':
this.evolveCellularAutomata();
break;
case 'random-gradient':
this.gradientCache = null;
break;
case 'perlin':
this.sourceParams.seed1 += 0.3;
this.sourceParams.seed2 += 0.2;
this.sourceParams.seed3 += 0.1;
break;
}
}
generateSourceEdge() {
this.updateSourceParams();
const data = this.imageData.data;
const w = this.canvas.width;
const h = this.canvas.height;
const length = w;
const cfg = this.config;
for (let i = 0; i < length; i++) {
const x = i;
const y = 0; // top edge
let color;
// 2D patterns need both x and y coordinates
if (cfg.pattern === 'checkerboard') {
const cellX = Math.floor(x / (cfg.cellSize || 10));
const cellY = Math.floor(y / (cfg.cellSize || 10));
color = ((cellX + cellY) % 2 === 0) ? this.fgRGB : this.bgRGB;
} else if (cfg.pattern === 'random-blocks') {
const cellX = Math.floor(x / (cfg.cellSize || 10));
const cellY = Math.floor(y / (cfg.cellSize || 10));
const seed = cellX * 73856093 ^ cellY * 19349663;
const blockRandom = Math.sin(seed + this.sourceParams.seed1) * 43758.5453;
color = (blockRandom - Math.floor(blockRandom)) > 0.5 ? this.fgRGB : this.bgRGB;
} else {
// 1D patterns
color = this.getSourceValue(i, length);
}
const idx = i * 4;
data[idx] = color.r;
data[idx + 1] = color.g;
data[idx + 2] = color.b;
data[idx + 3] = Math.round(this.opacity * 255);
}
}
changeDirection() {
const dirs = [
{ dx: 0, dy: 1, name: 'down' },
{ dx: 1, dy: 1, name: 'down-right' },
{ dx: 1, dy: 0, name: 'right' },
{ dx: 1, dy: -1, name: 'up-right' },
{ dx: 0, dy: -1, name: 'up' },
{ dx: -1, dy: -1, name: 'up-left' },
{ dx: -1, dy: 0, name: 'left' },
{ dx: -1, dy: 1, name: 'down-left' }
];
const currentIndex = dirs.findIndex(d => d.dx === this.direction.dx && d.dy === this.direction.dy);
if (currentIndex === -1) return;
const newIndex = Math.random() > 0.5 ? (currentIndex + 1) % 8 : (currentIndex + 7) % 8;
this.direction = dirs[newIndex];
}
initPattern() {
const w = this.canvas.width;
const h = this.canvas.height;
const length = w;
const cfg = this.config;
this.imageData = this.ctx.createImageData(w, h);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let color;
// 2D patterns need both x and y
if (cfg.pattern === 'checkerboard') {
const cellX = Math.floor(x / (cfg.cellSize || 10));
const cellY = Math.floor(y / (cfg.cellSize || 10));
color = ((cellX + cellY) % 2 === 0) ? this.fgRGB : this.bgRGB;
} else if (cfg.pattern === 'random-blocks') {
const cellX = Math.floor(x / (cfg.cellSize || 10));
const cellY = Math.floor(y / (cfg.cellSize || 10));
const seed = cellX * 73856093 ^ cellY * 19349663;
const blockRandom = Math.sin(seed + this.sourceParams.seed1) * 43758.5453;
color = (blockRandom - Math.floor(blockRandom)) > 0.5 ? this.fgRGB : this.bgRGB;
} else {
// 1D patterns
color = this.getSourceValue(x, length);
}
const idx = (y * w + x) * 4;
this.imageData.data[idx] = color.r;
this.imageData.data[idx + 1] = color.g;
this.imageData.data[idx + 2] = color.b;
this.imageData.data[idx + 3] = Math.round(this.opacity * 255);
}
}
this.ctx.putImageData(this.imageData, 0, 0);
}
smear() {
if (!this.imageData) return;
const cfg = this.config;
const data = this.imageData.data;
const w = this.canvas.width;
const h = this.canvas.height;
const newData = this.ctx.createImageData(w, h);
const newPixels = newData.data;
const mode = cfg.smearMode || 'replace';
const intensity = cfg.trailIntensity || 0.85;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let srcX = x - this.direction.dx * cfg.smearAmount;
let srcY = y - this.direction.dy * cfg.smearAmount;
srcX = (srcX + w) % w;
srcY = (srcY + h) % h;
const srcIdx = (srcY * w + srcX) * 4;
const dstIdx = (y * w + x) * 4;
if (mode === 'trail') {
newPixels[dstIdx] = data[srcIdx] * intensity + data[dstIdx] * (1 - intensity);
newPixels[dstIdx + 1] = data[srcIdx + 1] * intensity + data[dstIdx + 1] * (1 - intensity);
newPixels[dstIdx + 2] = data[srcIdx + 2] * intensity + data[dstIdx + 2] * (1 - intensity);
newPixels[dstIdx + 3] = Math.round(this.opacity * 255);
} else {
newPixels[dstIdx] = data[srcIdx];
newPixels[dstIdx + 1] = data[srcIdx + 1];
newPixels[dstIdx + 2] = data[srcIdx + 2];
newPixels[dstIdx + 3] = Math.round(this.opacity * 255);
}
}
}
this.imageData = newData;
this.ctx.putImageData(this.imageData, 0, 0);
}
start() {
const animate = (timestamp) => {
if (!this.lastDirectionChange) this.lastDirectionChange = timestamp;
if (!this.lastSourceTime) this.lastSourceTime = timestamp;
if (this.enableDirectionChange && timestamp - this.lastDirectionChange >= this.directionChangeInterval) {
this.changeDirection();
this.lastDirectionChange = timestamp;
this.directionChangeInterval = 1000 + Math.random() * 4000;
}
if (timestamp - this.lastSourceTime >= (this.config.sourceInterval || 1000)) {
this.generateSourceEdge();
this.lastSourceTime = timestamp;
}
this.smear();
setTimeout(() => requestAnimationFrame(animate), 1000 / this.config.speed);
};
requestAnimationFrame(animate);
}
}
partitions.forEach(config => new SmearPartition(config));
</script>
</body>
</html>