/
Morgala
/
fluid
Обзор
Документация
Войти
/
Morgala
/
fluid
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
index.html
254 строки
32 KB
Morgala
update: index.html
01 июн 2026, 05:40
Верифицирован
01 июн 2026, 05:40
af7ba79
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> <meta name="theme-color" content="#000000"> <title>Жидкость</title> <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;600;700&display=swap" rel="stylesheet"> <style> :root{ --ink:#f1eeff; --mut:rgba(241,238,255,.55); --edge:rgba(255,255,255,.14); --glass:rgba(18,15,30,.62); --accent:#b49bff; } *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;} html,body{margin:0;height:100%;overflow:hidden;background:#000;font-family:'Quicksand',system-ui,sans-serif;color:var(--ink);} #fluid{position:fixed;inset:0;width:100%;height:100%;display:block;touch-action:none;z-index:1;} .veil{position:fixed;inset:0;z-index:2;pointer-events:none;background:radial-gradient(125% 95% at 50% 42%,transparent 60%,rgba(0,0,0,.45) 100%);} .title{position:fixed;top:max(14px,env(safe-area-inset-top));left:0;right:0;z-index:4;text-align:center;pointer-events:none;} .title b{font-weight:700;font-size:15px;letter-spacing:3px;text-transform:uppercase;text-shadow:0 2px 20px rgba(150,120,255,.5);opacity:.85;} .hint{position:fixed;top:46%;left:0;right:0;z-index:4;text-align:center;pointer-events:none;color:rgba(241,238,255,.8);font-size:14px;letter-spacing:.5px;transition:opacity .8s;text-shadow:0 2px 12px #000;padding:0 30px;} .dock{position:fixed;left:50%;transform:translateX(-50%);bottom:0;z-index:6;width:min(620px,100%); background:var(--glass);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-top:1px solid var(--edge); border-radius:22px 22px 0 0;padding:6px 14px max(16px,env(safe-area-inset-bottom));box-shadow:0 -12px 40px rgba(0,0,0,.5); transition:transform .4s cubic-bezier(.2,.8,.2,1);} .dock.min{transform:translateX(-50%) translateY(calc(100% - 34px));} .grab{height:22px;display:flex;align-items:center;justify-content:center;cursor:pointer;} .grab::before{content:"";width:42px;height:4px;border-radius:4px;background:rgba(255,255,255,.3);} .lab{font-size:10px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--mut);margin:8px 2px 7px;} .scroll{display:flex;gap:8px;overflow-x:auto;padding-bottom:3px;scrollbar-width:none;} .scroll::-webkit-scrollbar{display:none;} .pill{display:flex;align-items:center;gap:8px;padding:10px 15px;border-radius:999px;border:1px solid var(--edge); background:rgba(255,255,255,.05);color:var(--ink);font:inherit;font-weight:600;font-size:13px;white-space:nowrap;cursor:pointer;transition:.15s;} .pill:active{transform:scale(.94);} .pill .dot{width:15px;height:15px;border-radius:50%;flex:0 0 auto;box-shadow:0 0 8px rgba(0,0,0,.4) inset;} .pill.on{border-color:var(--accent);background:rgba(180,155,255,.22);box-shadow:0 0 0 1px rgba(180,155,255,.4);} .fx{display:flex;gap:8px;flex-wrap:wrap;} .fx .pill{font-size:12px;padding:9px 14px;} .foot{display:flex;align-items:center;gap:12px;margin-top:12px;} .brush{display:flex;align-items:center;gap:9px;flex:1;min-width:120px;} .brush span{font-size:12px;font-weight:600;color:var(--mut);} input[type=range]{flex:1;-webkit-appearance:none;height:4px;border-radius:4px;background:linear-gradient(90deg,#b49bff,#67e8ff);outline:none;} input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;border-radius:50%;cursor:pointer; background:radial-gradient(circle at 34% 30%,#fff,#b49bff);border:2px solid #0a0716;box-shadow:0 0 12px rgba(180,155,255,.7);} input[type=range]::-moz-range-thumb{width:18px;height:18px;border-radius:50%;border:2px solid #0a0716;background:#b49bff;} .ico{flex:0 0 auto;width:42px;height:38px;border-radius:12px;border:1px solid var(--edge);background:rgba(255,255,255,.05); color:var(--ink);font:inherit;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.15s;} .ico:active{transform:scale(.92);} .ico.on{border-color:var(--accent);background:rgba(180,155,255,.25);} .ico.small{font-size:12px;font-weight:700;width:auto;padding:0 13px;} .timerow{display:flex;align-items:center;gap:10px;margin-top:12px;} .timerow .tlab{font-size:12px;font-weight:600;color:var(--mut);flex:0 0 auto;} .seg{display:flex;gap:6px;flex:1;} .seg button{flex:1;padding:9px 4px;border-radius:10px;border:1px solid var(--edge);background:rgba(255,255,255,.05); color:var(--mut);font:inherit;font-weight:600;font-size:13px;cursor:pointer;transition:.15s;} .seg button:active{transform:scale(.92);} .seg button.on{border-color:var(--accent);background:rgba(180,155,255,.25);color:var(--ink);} .coach{position:absolute;top:-50px;left:50%;transform:translateX(-50%);background:linear-gradient(135deg,#b49bff,#7c5cff); color:#0a0716;font-weight:700;font-size:12.5px;padding:9px 15px;border-radius:999px;white-space:nowrap; box-shadow:0 8px 26px rgba(124,92,255,.55);pointer-events:none;animation:bob 1.4s ease-in-out infinite;transition:opacity .4s;z-index:8;} .coach::after{content:"";position:absolute;bottom:-7px;left:50%;transform:translateX(-50%); border-left:7px solid transparent;border-right:7px solid transparent;border-top:8px solid #7c5cff;} .coach.gone{opacity:0;} @keyframes bob{0%,100%{margin-top:0;}50%{margin-top:-6px;}} .nogl{position:fixed;inset:0;z-index:9;display:none;align-items:center;justify-content:center;padding:30px;text-align:center;color:#ffb4b4;} </style> </head> <body> <canvas id="fluid"></canvas> <div class="veil"></div> <div class="title"><b>жидкость</b></div> <div class="hint" id="hint">Веди пальцем по экрану</div> <div class="dock min" id="dock"> <div class="coach" id="coach"><span id="coachText">Нажми полоску — настройки</span></div> <div class="grab" id="grab"></div> <div class="lab">Настроение</div> <div class="scroll" id="presets"></div> <div class="lab">Плюшки</div> <div class="fx" id="fx"></div> <div class="timerow"><span class="tlab">Время</span><div class="seg" id="speed"><button data-s="0">⏸</button><button data-s="0.5">½×</button><button data-s="1">1×</button><button data-s="2">2×</button><button data-s="3">3×</button></div></div> <div class="foot"> <div class="brush"><span>Кисть</span><input type="range" id="brush" min="0.1" max="0.6" step="0.01"></div> <button class="ico" id="auto" title="Сам рисует">✨</button> <button class="ico small" id="clear">Очистить</button> </div> </div> <div class="nogl" id="nogl">Браузер не поддерживает WebGL — открой в современном браузере.</div> <script> (function(){ "use strict"; const canvas=document.getElementById('fluid'); const coarse=matchMedia('(pointer:coarse)').matches; const DEFAULTS={ SIM_RESOLUTION:128, DYE_RESOLUTION:coarse?768:1024, PRESSURE_ITERATIONS:25, PRESSURE:0.8, VELOCITY_DISSIPATION:0.2, DENSITY_DISSIPATION:1.0, VORTICITY_ON:true, CURL:20, VISCOSITY_ON:false, VISCOSITY:0.5, VISC_ITER:14, GRAVITY_ON:false, GRAVITY:0.4, ANGLE:180, SPLAT_RADIUS:0.28, SPLAT_FORCE:6000, COLOR_MODE:0, PALETTE:'ocean', SHADING:true, BLOOM:true, NEON_ON:false, SUN_ON:false, PIXEL_ON:false, PIXEL:0.45, GLITCH_ON:false, GLITCH:0, SHATTER_ON:false, SHATTER:0.4, STEAM_ON:false, STEAM:0.5, AUTO:false, AUTO_INTERVAL:1.4, TIME_SCALE:1 }; let config=Object.assign({},DEFAULTS); const PALETTES={ rainbow:null, neon:[0.83,0.50,0.33,0.95,0.16], sunset:[0.02,0.05,0.08,0.95,0.92,0.00], ocean:[0.50,0.55,0.60,0.47,0.62], fire:[0.00,0.02,0.05,0.08,0.11], space:[0.62,0.68,0.74,0.80,0.55], aurora:[0.33,0.42,0.48,0.52,0.58], candy:[0.90,0.95,0.83,0.55,0.50] }; const PRESETS=[ {id:'water', name:'Вода', grad:'linear-gradient(135deg,#0a4d8c,#7df0ff)', cfg:{PALETTE:'ocean',VORTICITY_ON:true,CURL:18,VELOCITY_DISSIPATION:0.2,DENSITY_DISSIPATION:1.0,PRESSURE_ITERATIONS:28,SHADING:true,BLOOM:true,COLOR_MODE:0}}, {id:'honey', name:'Мёд', grad:'linear-gradient(135deg,#ff9d2b,#ffd23b)', cfg:{PALETTE:'sunset',VISCOSITY_ON:true,VISCOSITY:0.85,GRAVITY_ON:true,GRAVITY:0.3,ANGLE:180,VELOCITY_DISSIPATION:1.2,DENSITY_DISSIPATION:0.4,VORTICITY_ON:false,CURL:0,PRESSURE_ITERATIONS:30,SHADING:true,BLOOM:true,COLOR_MODE:0}}, {id:'smoke', name:'Дым', grad:'linear-gradient(135deg,#2bf0ff,#9d3bff)', cfg:{PALETTE:'neon',VORTICITY_ON:true,CURL:28,GRAVITY_ON:true,GRAVITY:0.45,ANGLE:0,VELOCITY_DISSIPATION:0.15,DENSITY_DISSIPATION:0.55,SHADING:false,BLOOM:true,COLOR_MODE:0}}, {id:'ink', name:'Чернила', grad:'conic-gradient(#ff3b3b,#ffd23b,#3bff7a,#3bd0ff,#9d3bff,#ff3b3b)', cfg:{PALETTE:'rainbow',VORTICITY_ON:true,CURL:30,GRAVITY_ON:true,GRAVITY:0.18,ANGLE:180,VELOCITY_DISSIPATION:0.1,DENSITY_DISSIPATION:0.18,SHADING:true,BLOOM:true,COLOR_MODE:0}}, {id:'fire', name:'Огонь', grad:'linear-gradient(135deg,#ff2b00,#ffd23b)', cfg:{PALETTE:'fire',VORTICITY_ON:true,CURL:32,GRAVITY_ON:true,GRAVITY:0.4,ANGLE:0,VELOCITY_DISSIPATION:0.12,DENSITY_DISSIPATION:0.6,SHADING:false,BLOOM:true,COLOR_MODE:0}}, {id:'neon', name:'Кислота', grad:'linear-gradient(135deg,#ff2bd6,#2bf0ff)', cfg:{PALETTE:'neon',VORTICITY_ON:true,CURL:34,VELOCITY_DISSIPATION:0.1,DENSITY_DISSIPATION:0.7,SHADING:false,BLOOM:true,COLOR_MODE:0}}, {id:'space', name:'Космос', grad:'linear-gradient(135deg,#1b1f6b,#9d2bff)', cfg:{PALETTE:'space',VORTICITY_ON:true,CURL:30,VELOCITY_DISSIPATION:0.1,DENSITY_DISSIPATION:0.55,SHADING:true,BLOOM:true,COLOR_MODE:0}}, {id:'candy', name:'Карамель',grad:'linear-gradient(135deg,#ff8ad6,#67e8ff)', cfg:{PALETTE:'candy',VORTICITY_ON:true,CURL:20,VELOCITY_DISSIPATION:0.18,DENSITY_DISSIPATION:0.8,SHADING:true,BLOOM:true,COLOR_MODE:0}}, {id:'chaos', name:'Хаос', grad:'linear-gradient(135deg,#39ff14,#ff2bd6)', cfg:{PALETTE:'neon',VORTICITY_ON:true,CURL:48,VELOCITY_DISSIPATION:0.07,DENSITY_DISSIPATION:0.75,SHADING:false,BLOOM:true,COLOR_MODE:0}} ]; const FX=[ {key:'BLOOM',name:'Свечение'}, {key:'SUN_ON',name:'Лучи'}, {key:'SHADING',name:'Объём'}, {key:'PIXEL_ON',name:'Пиксели'}, {key:'NEON_ON',name:'Неон'}, {key:'SHATTER_ON',name:'Осколки'} ]; // ---------- WebGL context ---------- function getContext(c){const p={alpha:false,depth:false,stencil:false,antialias:false,preserveDrawingBuffer:false};let gl=c.getContext('webgl2',p);const w2=!!gl;if(!gl)gl=c.getContext('webgl',p)||c.getContext('experimental-webgl',p);if(!gl)return null; let hf,lin;if(w2){gl.getExtension('EXT_color_buffer_float');lin=gl.getExtension('OES_texture_float_linear');}else{hf=gl.getExtension('OES_texture_half_float');lin=gl.getExtension('OES_texture_half_float_linear');} const ht=w2?gl.HALF_FLOAT:(hf&&hf.HALF_FLOAT_OES);let rgba,rg,r;if(w2){rgba=fmt(gl.RGBA16F,gl.RGBA,ht);rg=fmt(gl.RG16F,gl.RG,ht);r=fmt(gl.R16F,gl.RED,ht);}else{rgba=fmt(gl.RGBA,gl.RGBA,ht);rg=rgba;r=rgba;} return {gl,ext:{formatRGBA:rgba,formatRG:rg,formatR:r,halfFloatTexType:ht,supportLinearFiltering:!!lin}}; function fmt(internal,format,type){if(!renderable(internal,format,type)){if(internal===gl.R16F)return fmt(gl.RG16F,gl.RG,type);if(internal===gl.RG16F)return fmt(gl.RGBA16F,gl.RGBA,type);return {internalFormat:gl.RGBA,format:gl.RGBA};}return {internalFormat:internal,format:format};} function renderable(internal,format,type){const t=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,t);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);gl.texImage2D(gl.TEXTURE_2D,0,internal,4,4,0,format,type,null);const fb=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,fb);gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,t,0);return gl.checkFramebufferStatus(gl.FRAMEBUFFER)===gl.FRAMEBUFFER_COMPLETE;}} const CTX=getContext(canvas); if(!CTX){document.getElementById('nogl').style.display='flex';return;} const gl=CTX.gl, ext=CTX.ext; let fragPrec='highp';const hp=gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER,gl.HIGH_FLOAT);if(!hp||hp.precision===0)fragPrec='mediump'; const PREC='precision '+fragPrec+' float;\nprecision '+fragPrec+' sampler2D;\n'; function compile(type,src,kw){let pre=type===gl.FRAGMENT_SHADER?PREC:'';if(kw)kw.forEach(k=>pre+='#define '+k+'\n');const s=gl.createShader(type);gl.shaderSource(s,pre+src);gl.compileShader(s);if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))console.error(gl.getShaderInfoLog(s));return s;} function program(vs,fs){const pr=gl.createProgram();gl.attachShader(pr,vs);gl.attachShader(pr,fs);gl.bindAttribLocation(pr,0,'aPosition');gl.linkProgram(pr);if(!gl.getProgramParameter(pr,gl.LINK_STATUS))console.error(gl.getProgramInfoLog(pr));const u={},n=gl.getProgramParameter(pr,gl.ACTIVE_UNIFORMS);for(let i=0;i<n;i++){const nm=gl.getActiveUniform(pr,i).name;u[nm]=gl.getUniformLocation(pr,nm);}return {program:pr,uniforms:u,bind(){gl.useProgram(pr);}};} const baseVert=compile(gl.VERTEX_SHADER,`attribute vec2 aPosition;varying vec2 vUv,vL,vR,vT,vB;uniform vec2 texelSize;void main(){vUv=aPosition*0.5+0.5;vL=vUv-vec2(texelSize.x,0.0);vR=vUv+vec2(texelSize.x,0.0);vT=vUv+vec2(0.0,texelSize.y);vB=vUv-vec2(0.0,texelSize.y);gl_Position=vec4(aPosition,0.0,1.0);}`); const splatFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv;uniform sampler2D uTarget;uniform float aspectRatio;uniform vec3 color;uniform vec2 point;uniform float radius;void main(){vec2 p=vUv-point.xy;p.x*=aspectRatio;vec3 s=exp(-dot(p,p)/radius)*color;gl_FragColor=vec4(texture2D(uTarget,vUv).xyz+s,1.0);}`); const advFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv;uniform sampler2D uVelocity,uSource;uniform vec2 texelSize,dyeTexelSize;uniform float dt,dissipation; vec4 bilerp(sampler2D s,vec2 uv,vec2 ts){vec2 st=uv/ts-0.5;vec2 i=floor(st),f=fract(st);vec4 a=texture2D(s,(i+vec2(0.5,0.5))*ts),b=texture2D(s,(i+vec2(1.5,0.5))*ts),c=texture2D(s,(i+vec2(0.5,1.5))*ts),d=texture2D(s,(i+vec2(1.5,1.5))*ts);return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);} void main(){ #ifdef MANUAL_FILTERING vec2 coord=vUv-dt*bilerp(uVelocity,vUv,texelSize).xy*texelSize;vec4 res=bilerp(uSource,coord,dyeTexelSize); #else vec2 coord=vUv-dt*texture2D(uVelocity,vUv).xy*texelSize;vec4 res=texture2D(uSource,coord); #endif gl_FragColor=res/(1.0+dissipation*dt);}`,ext.supportLinearFiltering?null:['MANUAL_FILTERING']); const divFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv,vL,vR,vT,vB;uniform sampler2D uVelocity;void main(){float L=texture2D(uVelocity,vL).x,R=texture2D(uVelocity,vR).x,T=texture2D(uVelocity,vT).y,B=texture2D(uVelocity,vB).y;vec2 C=texture2D(uVelocity,vUv).xy;if(vL.x<0.0)L=-C.x;if(vR.x>1.0)R=-C.x;if(vT.y>1.0)T=-C.y;if(vB.y<0.0)B=-C.y;gl_FragColor=vec4(0.5*(R-L+T-B),0.0,0.0,1.0);}`); const curlFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vL,vR,vT,vB;uniform sampler2D uVelocity;void main(){float L=texture2D(uVelocity,vL).y,R=texture2D(uVelocity,vR).y,T=texture2D(uVelocity,vT).x,B=texture2D(uVelocity,vB).x;gl_FragColor=vec4(0.5*(R-L-T+B),0.0,0.0,1.0);}`); const vortFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv,vL,vR,vT,vB;uniform sampler2D uVelocity,uCurl;uniform float curl,dt;void main(){float L=texture2D(uCurl,vL).x,R=texture2D(uCurl,vR).x,T=texture2D(uCurl,vT).x,B=texture2D(uCurl,vB).x,C=texture2D(uCurl,vUv).x;vec2 f=0.5*vec2(abs(T)-abs(B),abs(R)-abs(L));f/=length(f)+0.0001;f*=curl*C;f.y*=-1.0;vec2 v=texture2D(uVelocity,vUv).xy+f*dt;gl_FragColor=vec4(clamp(v,-1000.0,1000.0),0.0,1.0);}`); const viscFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv,vL,vR,vT,vB;uniform sampler2D uVelocity;uniform float alpha;void main(){vec2 L=texture2D(uVelocity,vL).xy,R=texture2D(uVelocity,vR).xy,T=texture2D(uVelocity,vT).xy,B=texture2D(uVelocity,vB).xy,C=texture2D(uVelocity,vUv).xy;gl_FragColor=vec4((C+alpha*(L+R+T+B))/(1.0+4.0*alpha),0.0,1.0);}`); const forceFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv;uniform sampler2D uVelocity,uDye;uniform vec2 force;uniform float dt;void main(){float d=length(texture2D(uDye,vUv).rgb);vec2 v=texture2D(uVelocity,vUv).xy+force*d*dt;gl_FragColor=vec4(v,0.0,1.0);}`); const presFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv,vL,vR,vT,vB;uniform sampler2D uPressure,uDivergence;void main(){float L=texture2D(uPressure,vL).x,R=texture2D(uPressure,vR).x,T=texture2D(uPressure,vT).x,B=texture2D(uPressure,vB).x,dv=texture2D(uDivergence,vUv).x;gl_FragColor=vec4((L+R+B+T-dv)*0.25,0.0,0.0,1.0);}`); const gradFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv,vL,vR,vT,vB;uniform sampler2D uPressure,uVelocity;void main(){float L=texture2D(uPressure,vL).x,R=texture2D(uPressure,vR).x,T=texture2D(uPressure,vT).x,B=texture2D(uPressure,vB).x;vec2 v=texture2D(uVelocity,vUv).xy-vec2(R-L,T-B);gl_FragColor=vec4(v,0.0,1.0);}`); const clearFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv;uniform sampler2D uTexture;uniform float value;void main(){gl_FragColor=value*texture2D(uTexture,vUv);}`); const dispFS=compile(gl.FRAGMENT_SHADER,`varying vec2 vUv;uniform sampler2D uTexture,uVelocity,uCurl;uniform vec2 uTexel;uniform float uTime,uMode,uShading,uBloom,uPixel,uGlitch,uShatter,uSteam,uNeon,uSun; float h21(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);} vec2 h22(vec2 p){return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);} float vnoise(vec2 p){vec2 i=floor(p),f=fract(p);f=f*f*(3.0-2.0*f);float a=h21(i),b=h21(i+vec2(1,0)),c=h21(i+vec2(0,1)),d=h21(i+vec2(1,1));return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);} float fbm(vec2 p){float s=0.0,a=0.5;for(int i=0;i<4;i++){s+=a*vnoise(p);p*=2.0;a*=0.5;}return s;} vec3 speedColor(float sp){float t=clamp(sp*3.2,0.0,1.0);vec3 a=vec3(0.0,0.04,0.18),b=vec3(0.0,0.75,1.0),c=vec3(1.0,0.92,0.35);return t<0.5?mix(a,b,t*2.0):mix(b,c,(t-0.5)*2.0);} vec3 vortColor(float c){float v=clamp(c*4.0,-1.0,1.0);vec3 neg=vec3(0.15,0.45,1.0),pos=vec3(1.0,0.3,0.2);return mix(vec3(0.02,0.02,0.05),v<0.0?neg:pos,abs(v));} vec3 field(vec2 uv){if(uMode<0.5)return texture2D(uTexture,uv).rgb;else if(uMode<1.5)return speedColor(length(texture2D(uVelocity,uv).xy));else return vortColor(texture2D(uCurl,uv).x);} void main(){vec2 uv=vUv; if(uPixel>0.001){float bl=mix(480.0,22.0,uPixel);uv=(floor(uv*bl)+0.5)/bl;} float crack=0.0,shardB=1.0; if(uShatter>0.001){float sc=mix(4.0,13.0,uShatter);vec2 g=uv*sc,cell=floor(g),f=fract(g);uv+=(h22(cell)-0.5)*0.14*uShatter;shardB=0.78+0.44*h21(cell+7.0);float e=min(min(f.x,1.0-f.x),min(f.y,1.0-f.y));crack=1.0-smoothstep(0.0,0.06,e);} vec3 col=field(uv); if(uNeon>0.5){vec3 lc=field(uv-vec2(uTexel.x,0.0)),rc=field(uv+vec2(uTexel.x,0.0)),tc=field(uv+vec2(0.0,uTexel.y)),bc=field(uv-vec2(0.0,uTexel.y));float gx=length(rc)-length(lc),gy=length(tc)-length(bc);float ed=clamp(length(vec2(gx,gy))*9.0,0.0,3.0);col=col*ed;} else if(uMode<0.5&&uShading>0.5){vec3 lc=field(uv-vec2(uTexel.x,0.0)),rc=field(uv+vec2(uTexel.x,0.0)),tc=field(uv+vec2(0.0,uTexel.y)),bc=field(uv-vec2(0.0,uTexel.y));vec3 nrm=normalize(vec3(length(rc)-length(lc),length(tc)-length(bc),length(uTexel)));col*=clamp(dot(nrm,vec3(0.0,0.0,1.0))+0.7,0.7,1.0);} float lum=max(col.r,max(col.g,col.b)); if(uBloom>0.5)col+=col*lum*0.5; if(uSun>0.5){vec2 dir=(vUv-0.5)*(0.5/28.0);vec2 c=vUv-dir*h21(vUv*vec2(91.0,131.0));float illum=1.0,acc=0.0;for(int i=0;i<28;i++){c-=dir;vec3 s=field(c);float b=max(s.r,max(s.g,s.b));acc+=smoothstep(0.12,0.55,b)*illum;illum*=0.95;}col+=vec3(acc*1.5/28.0);} if(uShatter>0.001){col*=shardB;col+=vec3(0.7,0.85,1.0)*crack*0.95*uShatter;col*=(1.0-crack*0.2*uShatter);} if(uSteam>0.001){vec2 sp=vUv*vec2(3.0,2.2);sp.y+=uTime*0.12;sp.x+=uTime*0.02;float s=smoothstep(0.5,0.95,fbm(sp));float amt=uSteam*s*(0.25+0.75*lum);col=mix(col,vec3(0.92,0.96,1.0),amt*0.6);col+=vec3(0.9,0.95,1.0)*amt*0.15;} gl_FragColor=vec4(col,1.0);}`); const P={splat:program(baseVert,splatFS),adv:program(baseVert,advFS),div:program(baseVert,divFS),curl:program(baseVert,curlFS),vort:program(baseVert,vortFS),visc:program(baseVert,viscFS),force:program(baseVert,forceFS),pres:program(baseVert,presFS),grad:program(baseVert,gradFS),clear:program(baseVert,clearFS),disp:program(baseVert,dispFS)}; const blit=(function(){gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,-1,1,1,1,1,-1]),gl.STATIC_DRAW);gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,gl.createBuffer());gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array([0,1,2,0,2,3]),gl.STATIC_DRAW);gl.vertexAttribPointer(0,2,gl.FLOAT,false,0,0);gl.enableVertexAttribArray(0);return t=>{if(t==null){gl.viewport(0,0,gl.drawingBufferWidth,gl.drawingBufferHeight);gl.bindFramebuffer(gl.FRAMEBUFFER,null);}else{gl.viewport(0,0,t.width,t.height);gl.bindFramebuffer(gl.FRAMEBUFFER,t.fbo);}gl.drawElements(gl.TRIANGLES,6,gl.UNSIGNED_SHORT,0);};})(); let dye,velocity,divergence,curl,pressure; function createFBO(w,h,internal,format,type,filter){gl.activeTexture(gl.TEXTURE0);const tex=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,tex);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,filter);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,filter);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);gl.texImage2D(gl.TEXTURE_2D,0,internal,w,h,0,format,type,null);const fbo=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,fbo);gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,tex,0);gl.viewport(0,0,w,h);gl.clearColor(0,0,0,1);gl.clear(gl.COLOR_BUFFER_BIT);return {texture:tex,fbo,width:w,height:h,texelSizeX:1/w,texelSizeY:1/h,attach(id){gl.activeTexture(gl.TEXTURE0+id);gl.bindTexture(gl.TEXTURE_2D,tex);return id;}};} function createDouble(w,h,i,f,t,fl){let a=createFBO(w,h,i,f,t,fl),b=createFBO(w,h,i,f,t,fl);return {width:w,height:h,texelSizeX:1/w,texelSizeY:1/h,get read(){return a;},set read(v){a=v;},get write(){return b;},set write(v){b=v;},swap(){const t=a;a=b;b=t;}};} function getRes(res){let ar=gl.drawingBufferWidth/gl.drawingBufferHeight;if(ar<1)ar=1/ar;const mn=Math.round(res),mx=Math.round(res*ar);return gl.drawingBufferWidth>gl.drawingBufferHeight?{width:mx,height:mn}:{width:mn,height:mx};} function initFBOs(){const sim=getRes(config.SIM_RESOLUTION),dy=getRes(config.DYE_RESOLUTION),type=ext.halfFloatTexType;const rgba=ext.formatRGBA,rg=ext.formatRG,r=ext.formatR,fl=ext.supportLinearFiltering?gl.LINEAR:gl.NEAREST;gl.disable(gl.BLEND); dye=createDouble(dy.width,dy.height,rgba.internalFormat,rgba.format,type,fl);velocity=createDouble(sim.width,sim.height,rg.internalFormat,rg.format,type,fl);divergence=createFBO(sim.width,sim.height,r.internalFormat,r.format,type,gl.NEAREST);curl=createFBO(sim.width,sim.height,r.internalFormat,r.format,type,gl.NEAREST);pressure=createDouble(sim.width,sim.height,r.internalFormat,r.format,type,gl.NEAREST);} function scalePR(x){const pr=Math.min(window.devicePixelRatio||1,2);return Math.floor(x*pr);} function resizeCanvas(){const w=scalePR(canvas.clientWidth),h=scalePR(canvas.clientHeight);if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;return true;}return false;} function setTexel(u){gl.uniform2f(u,velocity.texelSizeX,velocity.texelSizeY);} function step(dt){gl.disable(gl.BLEND);const doVort=config.VORTICITY_ON&&config.CURL>0;const needCurl=doVort||config.COLOR_MODE===2; if(needCurl){P.curl.bind();setTexel(P.curl.uniforms.texelSize);gl.uniform1i(P.curl.uniforms.uVelocity,velocity.read.attach(0));blit(curl);} if(doVort){P.vort.bind();setTexel(P.vort.uniforms.texelSize);gl.uniform1i(P.vort.uniforms.uVelocity,velocity.read.attach(0));gl.uniform1i(P.vort.uniforms.uCurl,curl.attach(1));gl.uniform1f(P.vort.uniforms.curl,config.CURL);gl.uniform1f(P.vort.uniforms.dt,dt);blit(velocity.write);velocity.swap();} if(config.GRAVITY_ON&&config.GRAVITY>0){const rad=config.ANGLE*Math.PI/180,mag=config.GRAVITY*40;P.force.bind();gl.uniform1i(P.force.uniforms.uVelocity,velocity.read.attach(0));gl.uniform1i(P.force.uniforms.uDye,dye.read.attach(1));gl.uniform2f(P.force.uniforms.force,Math.sin(rad)*mag,Math.cos(rad)*mag);gl.uniform1f(P.force.uniforms.dt,dt);blit(velocity.write);velocity.swap();} if(config.VISCOSITY_ON&&config.VISCOSITY>0){P.visc.bind();setTexel(P.visc.uniforms.texelSize);gl.uniform1f(P.visc.uniforms.alpha,config.VISCOSITY*0.9);for(let i=0;i<config.VISC_ITER;i++){gl.uniform1i(P.visc.uniforms.uVelocity,velocity.read.attach(0));blit(velocity.write);velocity.swap();}} P.div.bind();setTexel(P.div.uniforms.texelSize);gl.uniform1i(P.div.uniforms.uVelocity,velocity.read.attach(0));blit(divergence); P.clear.bind();gl.uniform1i(P.clear.uniforms.uTexture,pressure.read.attach(0));gl.uniform1f(P.clear.uniforms.value,config.PRESSURE);blit(pressure.write);pressure.swap(); P.pres.bind();setTexel(P.pres.uniforms.texelSize);gl.uniform1i(P.pres.uniforms.uDivergence,divergence.attach(0));for(let i=0;i<config.PRESSURE_ITERATIONS;i++){gl.uniform1i(P.pres.uniforms.uPressure,pressure.read.attach(1));blit(pressure.write);pressure.swap();} P.grad.bind();setTexel(P.grad.uniforms.texelSize);gl.uniform1i(P.grad.uniforms.uPressure,pressure.read.attach(0));gl.uniform1i(P.grad.uniforms.uVelocity,velocity.read.attach(1));blit(velocity.write);velocity.swap(); P.adv.bind();setTexel(P.adv.uniforms.texelSize);if(!ext.supportLinearFiltering)gl.uniform2f(P.adv.uniforms.dyeTexelSize,velocity.texelSizeX,velocity.texelSizeY);let vId=velocity.read.attach(0);gl.uniform1i(P.adv.uniforms.uVelocity,vId);gl.uniform1i(P.adv.uniforms.uSource,vId);gl.uniform1f(P.adv.uniforms.dt,dt);gl.uniform1f(P.adv.uniforms.dissipation,config.VELOCITY_DISSIPATION);blit(velocity.write);velocity.swap(); if(!ext.supportLinearFiltering)gl.uniform2f(P.adv.uniforms.dyeTexelSize,dye.texelSizeX,dye.texelSizeY);gl.uniform1i(P.adv.uniforms.uVelocity,velocity.read.attach(0));gl.uniform1i(P.adv.uniforms.uSource,dye.read.attach(1));gl.uniform1f(P.adv.uniforms.dissipation,config.DENSITY_DISSIPATION);blit(dye.write);dye.swap();} function render(t){gl.disable(gl.BLEND);P.disp.bind();const u=P.disp.uniforms;gl.uniform2f(u.uTexel,1/canvas.width,1/canvas.height);gl.uniform1f(u.uTime,t);gl.uniform1f(u.uMode,config.COLOR_MODE);gl.uniform1f(u.uShading,config.SHADING?1:0);gl.uniform1f(u.uBloom,config.BLOOM?1:0);gl.uniform1f(u.uPixel,config.PIXEL_ON?config.PIXEL:0);gl.uniform1f(u.uGlitch,0);gl.uniform1f(u.uShatter,config.SHATTER_ON?config.SHATTER:0);gl.uniform1f(u.uSteam,config.STEAM_ON?config.STEAM:0);gl.uniform1f(u.uNeon,config.NEON_ON?1:0);gl.uniform1f(u.uSun,config.SUN_ON?1:0);gl.uniform1i(u.uTexture,dye.read.attach(0));gl.uniform1i(u.uVelocity,velocity.read.attach(1));gl.uniform1i(u.uCurl,curl.attach(2));blit(null);} function correctRadius(r){const ar=canvas.width/canvas.height;return ar>1?r*ar:r;} function splat(x,y,dx,dy,color){P.splat.bind();gl.uniform1i(P.splat.uniforms.uTarget,velocity.read.attach(0));gl.uniform1f(P.splat.uniforms.aspectRatio,canvas.width/canvas.height);gl.uniform2f(P.splat.uniforms.point,x,y);gl.uniform3f(P.splat.uniforms.color,dx,dy,0);gl.uniform1f(P.splat.uniforms.radius,correctRadius(config.SPLAT_RADIUS/100));blit(velocity.write);velocity.swap();gl.uniform1i(P.splat.uniforms.uTarget,dye.read.attach(0));gl.uniform3f(P.splat.uniforms.color,color.r,color.g,color.b);blit(dye.write);dye.swap();} function multipleSplats(n){for(let i=0;i<n;i++){const c=genColor();c.r*=10;c.g*=10;c.b*=10;splat(Math.random(),Math.random(),1000*(Math.random()-0.5),1000*(Math.random()-0.5),c);}} function HSVtoRGB(h,s,v){const i=Math.floor(h*6),f=h*6-i,p=v*(1-s),q=v*(1-f*s),t=v*(1-(1-f)*s);let r,g,b;switch(i%6){case 0:r=v;g=t;b=p;break;case 1:r=q;g=v;b=p;break;case 2:r=p;g=v;b=t;break;case 3:r=p;g=q;b=v;break;case 4:r=t;g=p;b=v;break;default:r=v;g=p;b=q;}return {r,g,b};} function genColor(){const arr=PALETTES[config.PALETTE];const h=arr?arr[Math.floor(Math.random()*arr.length)]:Math.random();const c=HSVtoRGB(h,1,1);c.r*=0.15;c.g*=0.15;c.b*=0.15;return c;} const pointers=new Map(); function mk(){return {texX:0,texY:0,prevX:0,prevY:0,dX:0,dY:0,down:false,moved:false,color:genColor()};} function cDX(d){const ar=canvas.width/canvas.height;return ar<1?d*ar:d;} function cDY(d){const ar=canvas.width/canvas.height;return ar>1?d/ar:d;} let hintShown=true;function hideHint(){if(hintShown){hintShown=false;document.getElementById('hint').style.opacity='0';}} canvas.addEventListener('pointerdown',e=>{hideHint();let p=pointers.get(e.pointerId);if(!p){p=mk();pointers.set(e.pointerId,p);}p.down=true;p.color=genColor();p.texX=e.clientX/canvas.clientWidth;p.texY=1-e.clientY/canvas.clientHeight;p.prevX=p.texX;p.prevY=p.texY;const c=genColor();c.r*=8;c.g*=8;c.b*=8;splat(p.texX,p.texY,(Math.random()-0.5)*10,(Math.random()-0.5)*30,c);}); canvas.addEventListener('pointermove',e=>{const p=pointers.get(e.pointerId);if(!p||!p.down)return;p.prevX=p.texX;p.prevY=p.texY;p.texX=e.clientX/canvas.clientWidth;p.texY=1-e.clientY/canvas.clientHeight;p.dX=cDX(p.texX-p.prevX);p.dY=cDY(p.texY-p.prevY);p.moved=Math.abs(p.dX)>0||Math.abs(p.dY)>0;},{passive:true}); function endP(e){const p=pointers.get(e.pointerId);if(p)p.down=false;pointers.delete(e.pointerId);} window.addEventListener('pointerup',endP);window.addEventListener('pointercancel',endP); function applyInputs(){pointers.forEach(p=>{if(p.moved){p.moved=false;splat(p.texX,p.texY,p.dX*config.SPLAT_FORCE,p.dY*config.SPLAT_FORCE,p.color);}});} let autoT=0;function ambient(dt){if(!config.AUTO)return;autoT+=dt;if(autoT>=config.AUTO_INTERVAL){autoT=0;const c=genColor();c.r*=8;c.g*=8;c.b*=8;const a=Math.random()*6.283,s=600+Math.random()*900;splat(Math.random(),Math.random(),Math.cos(a)*s,Math.sin(a)*s,c);}} let last=Date.now(),tElapsed=0; function loop(){const now=Date.now();let dt=(now-last)/1000;if(dt>0.016666)dt=0.016666;last=now;tElapsed+=dt;if(resizeCanvas())initFBOs();applyInputs();ambient(dt);const ts=config.TIME_SCALE;if(ts>0)step(dt*ts);render(tElapsed);requestAnimationFrame(loop);} // ---------- UI ---------- const $=id=>document.getElementById(id); let activePreset='water'; function buildPresets(){const box=$('presets');PRESETS.forEach(pr=>{const el=document.createElement('button');el.className='pill';el.dataset.id=pr.id;el.innerHTML='<span class="dot" style="background:'+pr.grad+'"></span>'+pr.name;el.onclick=()=>applyPreset(pr.id,true);box.appendChild(el);});} function buildFX(){const box=$('fx');FX.forEach(fx=>{const el=document.createElement('button');el.className='pill';el.dataset.key=fx.key;el.textContent=fx.name;el.onclick=()=>{config[fx.key]=!config[fx.key];el.classList.toggle('on',config[fx.key]);};box.appendChild(el);});} function syncFX(){$('fx').querySelectorAll('.pill').forEach(el=>el.classList.toggle('on',!!config[el.dataset.key]));} function syncPresets(){$('presets').querySelectorAll('.pill').forEach(el=>el.classList.toggle('on',el.dataset.id===activePreset));} function applyPreset(id,burst){const pr=PRESETS.find(p=>p.id===id);if(!pr)return;const keepAuto=config.AUTO,keepBrush=config.SPLAT_RADIUS,keepTime=config.TIME_SCALE;config=Object.assign({},DEFAULTS,pr.cfg);config.AUTO=keepAuto;config.SPLAT_RADIUS=keepBrush;config.TIME_SCALE=keepTime;activePreset=id;syncPresets();syncFX();if(typeof syncSpeed==='function')syncSpeed();if(burst)hideHint();} $('brush').addEventListener('input',()=>{config.SPLAT_RADIUS=+$('brush').value;}); $('auto').addEventListener('click',()=>{config.AUTO=!config.AUTO;$('auto').classList.toggle('on',config.AUTO);}); $('clear').addEventListener('click',()=>{initFBOs();}); let docMin=true,coachState=0; $('grab').addEventListener('click',()=>{docMin=!docMin;$('dock').classList.toggle('min',docMin); if(coachState===0&&!docMin){coachState=1;$('coachText').textContent='Нажми ещё раз — закрыть';} else if(coachState===1&&docMin){coachState=2;$('coach').classList.add('gone');}}); function buildSpeed(){$('speed').querySelectorAll('button').forEach(b=>{b.onclick=()=>{config.TIME_SCALE=+b.dataset.s;syncSpeed();};});} function syncSpeed(){$('speed').querySelectorAll('button').forEach(b=>b.classList.toggle('on',+b.dataset.s===config.TIME_SCALE));} buildPresets();buildFX();buildSpeed(); resizeCanvas();initFBOs(); applyPreset('water',false); $('brush').value=config.SPLAT_RADIUS; syncFX();syncSpeed(); multipleSplats(8+(Math.random()*6|0)); window.addEventListener('resize',()=>{if(resizeCanvas())initFBOs();}); requestAnimationFrame(loop); })(); </script> </body> </html>