<div id="rain-glass-bg" aria-hidden="true">
<canvas id="rain-glass-canvas"></canvas>
</div>
<style>
#rain-glass-bg{
position:fixed; inset:0; width:100vw; height:100vh;
z-index:0; pointer-events:none; overflow:hidden;
background-color: var(--rain-bg-color, transparent);
background-image:
radial-gradient(ellipse at 20% 15%,rgba(255,255,255,.16),transparent 42%),
radial-gradient(ellipse at 82% 70%,rgba(220,230,240,.10),transparent 48%),
linear-gradient(135deg,rgba(245,247,248,.18),rgba(210,218,224,.10));
}
#rain-glass-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
@media(prefers-reduced-motion:reduce){#rain-glass-canvas{opacity:.92}}
</style>
<script>
(function(){
'use strict';
const canvas=document.getElementById('rain-glass-canvas');
if(!canvas)return;
const ctx=canvas.getContext('2d',{alpha:true});
const CONFIG={
backgroundColor:'#f7f7f7', // любой цвет фона
fogLayers:7,
fogSpeed:.000035,
dropletCount:105,
minSize:1.2,maxSize:5, // насколько капли вытягиваются
minSpeed:.18,maxSpeed:.72, // скорость стекания
trailChance:.22,
trailLength:[18,25],
condensationCount:130,
dropOpacity:.70, // прозрачность капель
highlightOpacity:.34,
shadowOpacity:.10,
ringOnlyRadius:2,
jitterMin:.84,jitterMax:1.16,
streakChance:.85,
shadowBlurPx:3.2,
fogUpdateMs:110,
largeSpriteVariants:10, // сколько вариантов капель есть
ringSpriteVariants:4, // сколько вариантов капель есть
spriteBakeRadius:50,
spritePad:24,
};
let W=0,H=0,dpr=1,droplets=[],condensation=[];
let seed=Math.random()*10000,last=0;
const reduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const rand=(a,b)=>a+Math.random()*(b-a);
function resize(){
dpr=Math.min(devicePixelRatio||1,2);
W=innerWidth;H=innerHeight;
canvas.width=Math.round(W*dpr);canvas.height=Math.round(H*dpr);
canvas.style.width=W+'px';canvas.style.height=H+'px';
ctx.setTransform(dpr,0,0,dpr,0,0);
buildCondensation();buildDroplets();
}
function buildCondensation(){
condensation=[];
for(let i=0;i<CONFIG.condensationCount;i++)
condensation.push({x:rand(0,W),y:rand(0,H),r:rand(.5,2.2),a:rand(.018,.065),p:rand(0,Math.PI*2)});
}
function makeJitter(){
const j=[];
for(let i=0;i<8;i++)j.push(rand(CONFIG.jitterMin,CONFIG.jitterMax));
return j;
}
function makeStreaks(){
const streaks=[];
const count=Math.round(rand(2,4));
for(let i=0;i<count;i++)streaks.push({a:rand(-2.3,-0.9),from:rand(.05,.2),len:rand(.35,.75)});
return streaks;
}
function blobPath(jitter,r){
const pts=jitter.length;
const path=new Path2D();
const coords=[];
for(let i=0;i<pts;i++){
const angle=(i/pts)*Math.PI*2;
coords.push([Math.cos(angle)*r*jitter[i], Math.sin(angle)*r*jitter[i]]);
}
path.moveTo((coords[0][0]+coords[pts-1][0])/2,(coords[0][1]+coords[pts-1][1])/2);
for(let i=0;i<pts;i++){
const next=coords[(i+1)%pts];
const mid=[(coords[i][0]+next[0])/2,(coords[i][1]+next[1])/2];
path.quadraticCurveTo(coords[i][0],coords[i][1],mid[0],mid[1]);
}
path.closePath();
return path;
}
let largeSprites=[],ringSprites=[],shadowSprite=null;
const SB=CONFIG.spriteBakeRadius, SP=CONFIG.spritePad, SS=(SB+SP)*2, SC=SB+SP;
function bakeLargeSprite(){
const c=document.createElement('canvas');c.width=SS;c.height=SS;
const sctx=c.getContext('2d');
sctx.translate(SC,SC);
const r=SB;
const jitter=makeJitter();
const streaks=Math.random()<CONFIG.streakChance?makeStreaks():[];
const path=blobPath(jitter,r);
const body=sctx.createRadialGradient(-r*.3,-r*.34,r*.05,0,0,r*1.05);
body.addColorStop(0,'rgba(255,255,255,1)');
body.addColorStop(.32,'rgba(250,253,255,.48)');
body.addColorStop(.7,'rgba(220,230,236,.16)');
body.addColorStop(1,'rgba(205,215,222,0)');
sctx.fillStyle=body;sctx.fill(path);
sctx.beginPath();
sctx.ellipse(0,0,r*.94,r*.94,0,Math.PI*0.15,Math.PI*0.95);
sctx.lineWidth=Math.max(.6,r*.11);
sctx.strokeStyle='rgba(40,50,60,.55)';
sctx.stroke();
sctx.lineWidth=Math.max(.4,r*.04);
sctx.strokeStyle='rgba(255,255,255,.5)';
sctx.stroke(path);
sctx.beginPath();
sctx.ellipse(-r*.32,-r*.36,Math.max(.6,r*.24),Math.max(.4,r*.13),-0.5,0,Math.PI*2);
sctx.fillStyle=`rgba(255,255,255,${CONFIG.highlightOpacity})`;sctx.fill();
sctx.beginPath();
sctx.ellipse(r*.26,r*.14,Math.max(.4,r*.09),Math.max(.3,r*.06),0.4,0,Math.PI*2);
sctx.fillStyle=`rgba(255,255,255,${CONFIG.highlightOpacity*.5})`;sctx.fill();
if(streaks.length){
sctx.strokeStyle='rgba(255,255,255,.9)';
sctx.lineCap='round';
for(const s of streaks){
sctx.lineWidth=Math.max(.4,r*.045);
sctx.beginPath();
sctx.moveTo(Math.cos(s.a)*r*s.from,Math.sin(s.a)*r*s.from);
sctx.lineTo(Math.cos(s.a)*r*s.len,Math.sin(s.a)*r*s.len);
sctx.stroke();
}
}
return c;
}
function bakeRingSprite(){
const c=document.createElement('canvas');c.width=SS;c.height=SS;
const sctx=c.getContext('2d');
sctx.translate(SC,SC);
const r=SB;
const path=blobPath(makeJitter(),r);
sctx.lineWidth=Math.max(.5,r*.28);
sctx.strokeStyle='rgba(255,255,255,1)';
sctx.stroke(path);
sctx.beginPath();sctx.arc(-r*.28,-r*.3,Math.max(.4,r*.32),0,Math.PI*2);
sctx.fillStyle='rgba(255,255,255,1)';sctx.fill();
return c;
}
function bakeShadowSprite(){
const pad=CONFIG.shadowBlurPx*4+10;
const c=document.createElement('canvas');c.width=SS+pad*2;c.height=SS+pad*2;
const sctx=c.getContext('2d');
sctx.translate(SS/2+pad,SS/2+pad);
sctx.filter=`blur(${CONFIG.shadowBlurPx}px)`;
sctx.beginPath();
sctx.ellipse(SB*.12,SB*.22,SB*.94,SB*.98,0,0,Math.PI*2);
sctx.fillStyle='rgba(55,65,75,1)';
sctx.fill();
return {canvas:c,offset:SS/2+pad};
}
function bakeSprites(){
largeSprites=[];for(let i=0;i<CONFIG.largeSpriteVariants;i++)largeSprites.push(bakeLargeSprite());
ringSprites=[];for(let i=0;i<CONFIG.ringSpriteVariants;i++)ringSprites.push(bakeRingSprite());
shadowSprite=bakeShadowSprite();
}
function buildDroplets(){
droplets=[];
for(let i=0;i<CONFIG.dropletCount;i++){
const s=rand(CONFIG.minSize,CONFIG.maxSize),large=s>4.8;
const isRing=s<CONFIG.ringOnlyRadius;
droplets.push({
x:rand(0,W),y:rand(-H,H),size:s,
speed:rand(CONFIG.minSpeed,CONFIG.maxSpeed)*(large?.82:1),
wobble:rand(.04,.22),phase:rand(0,Math.PI*2),
elongation:large?rand(1.7,3.8):rand(1.05,1.8),
trail:large&&Math.random()<CONFIG.trailChance?rand(...CONFIG.trailLength):0,
drift:rand(.05,.20),opacity:rand(CONFIG.dropOpacity*.55,CONFIG.dropOpacity*1.15),
isRing,
spriteIdx:Math.floor(Math.random()*(isRing?CONFIG.ringSpriteVariants:CONFIG.largeSpriteVariants)),
});
}
}
// ---- туман: считается не каждый кадр, а в отдельный буфер по таймеру ----
let fogBuf=null,fogBufCtx=null,lastFogUpdate=0;
function renderFogToBuffer(t){
if(!fogBuf){fogBuf=document.createElement('canvas');fogBufCtx=fogBuf.getContext('2d');}
if(fogBuf.width!==canvas.width||fogBuf.height!==canvas.height){
fogBuf.width=canvas.width;fogBuf.height=canvas.height;
fogBufCtx.setTransform(dpr,0,0,dpr,0,0);
}
const fctx=fogBufCtx;
fctx.clearRect(0,0,W,H);
const base=fctx.createLinearGradient(0,0,W,H);
base.addColorStop(0,'rgba(245,248,250,.12)');
base.addColorStop(.45,'rgba(225,232,237,.055)');
base.addColorStop(1,'rgba(245,248,250,.10)');
fctx.fillStyle=base;fctx.fillRect(0,0,W,H);
for(let i=0;i<CONFIG.fogLayers;i++){
const p=seed+i*1.71;
const x=W*(.12+i*.145)+Math.sin(t*CONFIG.fogSpeed*1000+p)*W*.12;
const y=H*(.18+(i%3)*.31)+Math.cos(t*CONFIG.fogSpeed*800+p*.7)*H*.09;
const r=Math.max(W,H)*(.24+Math.random()*.18);
const g=fctx.createRadialGradient(x,y,0,x,y,r);
const a=.018+i*.002;
g.addColorStop(0,`rgba(255,255,255,${a+.018})`);
g.addColorStop(.42,`rgba(235,241,245,${a})`);
g.addColorStop(1,'rgba(235,241,245,0)');
fctx.fillStyle=g;fctx.fillRect(0,0,W,H);
}
}
function drawCondensation(t){
for(const p of condensation){
const r=p.r*(1+Math.sin(t*.00025+p.p)*.1);
ctx.beginPath();ctx.arc(p.x,p.y,r,0,Math.PI*2);
ctx.fillStyle=`rgba(255,255,255,${p.a})`;ctx.fill();
}
}
// ============================================================
// РЕНДЕР КАПЕЛЬ — теперь это просто drawImage готового спрайта
// ============================================================
function dropXY(d,t){
return [d.x+Math.sin(t*.00055+d.phase)*d.drift, d.y, Math.sin(t*.0012*d.wobble+d.phase)*d.wobble];
}
function drawDropShadow(d,t){
const [x,y,wob]=dropXY(d,t);
const rx=d.size,ry=d.size*d.elongation;
ctx.save();
ctx.translate(x,y);ctx.rotate(wob);
ctx.scale(rx/SB,ry/SB);
ctx.globalAlpha=CONFIG.shadowOpacity*(d.size/7);
ctx.drawImage(shadowSprite.canvas,-shadowSprite.offset,-shadowSprite.offset);
ctx.restore();
}
function drawDropBody(d,t){
const [x,y,wob]=dropXY(d,t);
const rx=d.size,ry=d.size*d.elongation;
const sprite=(d.isRing?ringSprites:largeSprites)[d.spriteIdx];
ctx.save();
ctx.translate(x,y);ctx.rotate(wob);
ctx.scale(rx/SB,ry/SB);
ctx.globalAlpha=d.isRing?Math.min(1,d.opacity*1.2):Math.min(1,d.opacity*1.05);
ctx.drawImage(sprite,-SC,-SC);
ctx.restore();
if(d.trail>0){
ctx.save();
ctx.globalAlpha=1;
ctx.translate(x,y);
const trail=ctx.createLinearGradient(0,ry*.45,0,ry*.45+d.trail);
trail.addColorStop(0,`rgba(230,238,243,${d.opacity*.34})`);
trail.addColorStop(.35,`rgba(225,234,240,${d.opacity*.12})`);
trail.addColorStop(1,'rgba(225,234,240,0)');
ctx.beginPath();
ctx.moveTo(-rx*.28,ry*.48);
ctx.quadraticCurveTo(rx*.05,ry*.72+d.trail*.45,rx*.02,ry*.62+d.trail);
ctx.lineWidth=Math.max(.7,rx*.24);ctx.strokeStyle=trail;ctx.stroke();
ctx.restore();
}
}
function update(dt){
if(reduced)return;
for(const d of droplets){
d.y+=d.speed*dt*.06;
if(d.y-d.size*d.elongation>H+20){
d.y=-rand(20,H*.35);d.x=rand(0,W);
d.spriteIdx=Math.floor(Math.random()*(d.isRing?CONFIG.ringSpriteVariants:CONFIG.largeSpriteVariants));
}
}
}
function render(now){
const dt=Math.min(40,now-(last||now));last=now;
ctx.clearRect(0,0,W,H);
if(now-lastFogUpdate>=CONFIG.fogUpdateMs){
renderFogToBuffer(now);
lastFogUpdate=now;
}
if(fogBuf)ctx.drawImage(fogBuf,0,0,fogBuf.width,fogBuf.height,0,0,W,H);
drawCondensation(now);
for(const d of droplets)drawDropShadow(d,now);
for(const d of droplets)drawDropBody(d,now);
update(dt);
requestAnimationFrame(render);
}
bakeSprites();
document.getElementById('rain-glass-bg').style.setProperty('--rain-bg-color', CONFIG.backgroundColor);
resize();
addEventListener('resize',resize,{passive:true});
requestAnimationFrame(render);
})();
</script>