Copy URL or Code
Paste to your AI coding assistant and say:
“Add cloud texture to my logo shape and make it float on the background”
Done. Your AI handles the rest.
Fully customizable. Use any PNG with transparency - the alpha channel defines the cloud shape.
"use client";
import React, { useEffect, useRef, useState, useCallback } from "react";
interface WeatherCloudProps {
imageSrc: string;
width?: number;
height?: number;
className?: string;
}
function colorGradient(fade: number) {
fade = (fade - 0.5) * 1.5 + 0.5;
const colors = [
{ red: 255, green: 255, blue: 255 },
{ red: 240, green: 245, blue: 250 },
{ red: 200, green: 220, blue: 240 },
{ red: 120, green: 180, blue: 230 },
{ red: 50, green: 140, blue: 220 },
{ red: 33, green: 120, blue: 209 },
];
const idx = Math.min(4, Math.max(0, Math.floor(fade * 5)));
const t = fade * 5 - idx;
const c1 = colors[idx];
const c2 = colors[idx + 1];
return {
red: Math.floor(c1.red + (c2.red - c1.red) * t),
green: Math.floor(c1.green + (c2.green - c1.green) * t),
blue: Math.floor(c1.blue + (c2.blue - c1.blue) * t),
};
}
function smoothNoise(x: number, y: number, scale: number): number {
const sx = x / scale, sy = y / scale;
const x0 = Math.floor(sx), y0 = Math.floor(sy);
const fx = sx - x0, fy = sy - y0;
const ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
const hash = (px: number, py: number) => {
const n = Math.sin(px * 127.1 + py * 311.7) * 43758.5453;
return n - Math.floor(n);
};
const v00 = hash(x0, y0), v10 = hash(x0 + 1, y0);
const v01 = hash(x0, y0 + 1), v11 = hash(x0 + 1, y0 + 1);
const v0 = v00 + ux * (v10 - v00), v1 = v01 + ux * (v11 - v01);
return v0 + uy * (v1 - v0);
}
function cloudNoise(x: number, y: number): number {
return smoothNoise(x, y, 25) * 0.6 + smoothNoise(x + 100, y + 100, 15) * 0.4 - 0.5;
}
export function WeatherCloud({ imageSrc, width = 200, height = 200, className = "" }: WeatherCloudProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const [startMouse, setStartMouse] = useState({ x: 0, y: 0 });
const [isReady, setIsReady] = useState(false);
const animationRef = useRef<number>(0);
const shapeMaskRef = useRef<Float32Array | null>(null);
const edgeDistRef = useRef<Float32Array | null>(null);
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
setStartMouse({ x: e.clientX, y: e.clientY });
};
const clampOffset = useCallback((newX: number, newY: number) => {
if (!containerRef.current) return { x: newX, y: newY };
const container = containerRef.current.getBoundingClientRect();
const maxX = container.width / 2 - 50, maxY = container.height / 2 - 50;
return { x: Math.max(-maxX, Math.min(maxX, newX)), y: Math.max(-maxY, Math.min(maxY, newY)) };
}, []);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
setOffset((prev) => clampOffset(prev.x + e.clientX - startMouse.x, prev.y + e.clientY - startMouse.y));
setStartMouse({ x: e.clientX, y: e.clientY });
};
const handleMouseUp = () => setIsDragging(false);
if (isDragging) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, startMouse, clampOffset]);
// Load image and extract shape mask
useEffect(() => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext("2d");
if (!tempCtx) return;
tempCtx.drawImage(img, 0, 0, width, height);
const imgData = tempCtx.getImageData(0, 0, width, height);
const pixels = imgData.data;
const mask = new Float32Array(width * height);
for (let i = 0; i < width * height; i++) mask[i] = pixels[i * 4 + 3] / 255;
shapeMaskRef.current = mask;
const edgeDist = new Float32Array(width * height);
const maxDist = 20;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = y * width + x;
if (mask[idx] < 0.5) { edgeDist[idx] = 0; continue; }
let minDist = maxDist;
for (let dy = -maxDist; dy <= maxDist && minDist > 0; dy++) {
for (let dx = -maxDist; dx <= maxDist; dx++) {
const nx = x + dx, ny = y + dy;
if (nx < 0 || nx >= width || ny < 0 || ny >= height || mask[ny * width + nx] < 0.5) {
const d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) minDist = d;
}
}
}
edgeDist[idx] = Math.min(minDist / maxDist, 1);
}
}
edgeDistRef.current = edgeDist;
setIsReady(true);
};
img.src = imageSrc;
}, [imageSrc, width, height]);
// Cloud simulation
useEffect(() => {
if (!isReady) return;
const canvas = canvasRef.current;
const mask = shapeMaskRef.current;
const edgeDist = edgeDistRef.current;
if (!canvas || !mask || !edgeDist) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const data = new Float32Array(width * height * 2);
const getEdgeFactor = (x: number, y: number): number => {
const idx = y * width + x;
if (mask[idx] < 0.5) return 0;
const baseDist = edgeDist[idx];
const noise = cloudNoise(x, y) * 0.3;
const noisedDist = Math.max(0, Math.min(1, baseDist + noise));
if (noisedDist >= 0.8) return 1;
if (noisedDist <= 0.1) return 0;
const t = (noisedDist - 0.1) / 0.7;
return t * t * (3 - 2 * t);
};
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 2;
const ef = getEdgeFactor(x, y);
if (ef > 0) {
const n1 = smoothNoise(x, y, 20), n2 = smoothNoise(x + 50, y + 50, 12), n3 = smoothNoise(x * 2, y * 2, 8);
data[i] = (n1 * 0.5 + n2 * 0.3 + n3 * 0.2) * 0.4 + 0.1 * ef * ef;
}
data[i + 1] = 0;
}
}
const imageData = ctx.createImageData(width, height);
const isInside = (x: number, y: number) => x >= 0 && x < width && y >= 0 && y < height && getEdgeFactor(x, y) > 0.1;
const setMaxVal = (i: number, val: number, x: number, y: number) => {
if (i >= 0 && i < data.length && isInside(x, y) && val > data[i + 1]) data[i + 1] = Math.min(val, 1.0);
};
let lastTime = 0;
const FPS_INTERVAL = 1000 / 3;
function animate(time: number) {
animationRef.current = requestAnimationFrame(animate);
if (time - lastTime < FPS_INTERVAL) return;
lastTime = time;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 2;
const density = data[i];
if (density < 0.01) continue;
const spread1 = density * 0.88, spread2 = density * 0.85;
if (y + 1 < height) setMaxVal((y + 1) * width * 2 + x * 2, spread1, x, y + 1);
if (y - 1 >= 0) setMaxVal((y - 1) * width * 2 + x * 2, spread1, x, y - 1);
if (x + 1 < width) setMaxVal(y * width * 2 + (x + 1) * 2, spread1, x + 1, y);
if (x - 1 >= 0) setMaxVal(y * width * 2 + (x - 1) * 2, spread1, x - 1, y);
if (y + 1 < height && x + 1 < width) setMaxVal((y + 1) * width * 2 + (x + 1) * 2, spread2, x + 1, y + 1);
if (y + 1 < height && x - 1 >= 0) setMaxVal((y + 1) * width * 2 + (x - 1) * 2, spread2, x - 1, y + 1);
if (y - 1 >= 0 && x + 1 < width) setMaxVal((y - 1) * width * 2 + (x + 1) * 2, spread2, x + 1, y - 1);
if (y - 1 >= 0 && x - 1 >= 0) setMaxVal((y - 1) * width * 2 + (x - 1) * 2, spread2, x - 1, y - 1);
}
}
const pixels = imageData.data;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 2, pi = (y * width + x) * 4;
let density = Math.min(1, data[i] + data[i + 1]);
const displayDensity = Math.max(0, Math.min(1, density + (Math.random() - 0.5) * 0.05));
const ef = getEdgeFactor(x, y);
if (ef > 0 && density > 0.05) {
const color = colorGradient(1 - displayDensity);
pixels[pi] = color.red; pixels[pi + 1] = color.green; pixels[pi + 2] = color.blue;
pixels[pi + 3] = Math.floor(255 * Math.min(1, density * 1.5) * ef);
} else {
pixels[pi] = pixels[pi + 1] = pixels[pi + 2] = pixels[pi + 3] = 0;
}
data[i] = density * 0.52; data[i + 1] = 0;
if (ef > 0.3 && Math.random() < 0.001) data[i] += Math.random() * 0.3 * ef;
}
}
ctx.putImageData(imageData, 0, 0);
}
animationRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationRef.current);
}, [isReady, width, height]);
return (
<div ref={containerRef} className={`relative w-full h-full overflow-hidden ${className}`}>
<div className="absolute inset-0" style={{ background: "linear-gradient(0deg, #62a0d8 0%, #2178d1 50%, #085cb3 100%)" }} />
<div className="absolute inset-0">
<div className="absolute top-1/2 left-1/2" style={{ transform: `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))` }}>
<div onMouseDown={handleMouseDown} className="cursor-grab active:cursor-grabbing select-none"
style={{ width: `${width}px`, height: `${height}px`, touchAction: "none", animation: isDragging ? "none" : "cloud-float 6s ease-in-out infinite", position: "relative" }}>
<div style={{ position: "absolute", width: "100%", height: "100%", top: "8px", left: "6px", filter: "blur(10px)", opacity: 0.3 }}>
<canvas width={width} height={height} style={{ width: "100%", height: "100%", filter: "brightness(0.3)" }}
ref={(el) => { if (el && canvasRef.current) { const ctx = el.getContext("2d"); if (ctx) { const copy = () => { if (canvasRef.current) ctx.drawImage(canvasRef.current, 0, 0); requestAnimationFrame(copy); }; copy(); } } }} />
</div>
<canvas ref={canvasRef} width={width} height={height} style={{ position: "absolute", width: "100%", height: "100%" }} />
</div>
</div>
</div>
<style jsx global>{`@keyframes cloud-float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-12px); } }`}</style>
</div>
);
}
export default WeatherCloud;import WeatherCloud from "@/components/WeatherCloud";
// Basic usage - provide a PNG with transparency
export default function HeroSection() {
return (
<div className="h-screen">
<WeatherCloud imageSrc="/your-shape.png" />
</div>
);
}
// Custom size
export function CustomSize() {
return (
<div className="h-[400px]">
<WeatherCloud imageSrc="/logo.png" width={300} height={200} />
</div>
);
}