Skip to content

Instantly share code, notes, and snippets.

@lostintangent
Created November 28, 2020 04:42
Show Gist options
  • Save lostintangent/ad09fe8fe20be528cf0b324dc1098390 to your computer and use it in GitHub Desktop.
Save lostintangent/ad09fe8fe20be528cf0b324dc1098390 to your computer and use it in GitHub Desktop.
ScrollTrigger Underwater world #anydayshaders 15
<digital-art dpr="auto" aria-hidden="true">
<script type="buffer" name="position" data-size="2">
[-1, -1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1]
</script>
<script type="vert">
precision highp float;
uniform float time;
uniform vec2 resolution;
varying vec4 vPos;
attribute vec4 position;
void main() {
vPos = position;
gl_Position = position;
}
</script>
<script type="frag">
precision highp float;
uniform vec2 resolution;
uniform float time;
uniform float scrollPos;
const float PI = 3.141592654;
const float gridSize = 10.;
const float fishGrid = 12.;
vec2 coords() {
vec2 p = gl_FragCoord.xy / resolution - .5;
float aspect = resolution.x / resolution.y;
p.x *= aspect;
return p;
}
vec2 rotate(vec2 p, float a) {
return vec2(p.x * cos(a) - p.y * sin(a),
p.x * sin(a) + p.y * cos(a));
}
// function from https://www.shadertoy.com/view/3ll3zr
float sdHeart(in vec2 p, float s) {
p /= s;
vec2 q = p;
q.x *= 0.5 + .5 * q.y;
q.y -= abs(p.x) * .63;
return (length(q) - .7) * s;
}
float sdCircle(in vec2 p, float r) {
return length(p) - r;
}
float sdStar(in vec2 p, in float r, in int n, in float m)
{
// next 4 lines can be precomputed for a given shape
float an = 3.141593/float(n);
float en = 3.141593/m; // m is between 2 and n
vec2 acs = vec2(cos(an),sin(an));
vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon,
float bn = mod(atan(p.x,p.y),2.0*an) - an;
p = length(p)*vec2(cos(bn),abs(sin(bn)));
p -= r*acs;
p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
return length(p)*sign(p.x);
}
float distanceField(vec2 p) {
// return sdHeart(p, 9.0 + 2.5 * sin(time * 1e-3));
float t = time * 1e-3 * 20.;
return sdStar(rotate(mod(p, gridSize) - vec2(gridSize*.5) - vec2(sin(floor(p.y / gridSize) * gridSize + t * .03) * .7, sin(floor(p.x / gridSize) * gridSize + t * .05) * .3), t * .002 + floor(p.y / gridSize) * .2 + floor(p.x / gridSize) * .3), 4., 5, 3.5 + .1
* floor(p.x / gridSize) + sin(t * .1) * .2) + sin(4. * p.x * .5 + time * 2e-3) * cos(p.y * .5 + time * 2e-3) * .05;
}
float bubblesDF(vec2 p) {
float grid = 3.;
float t = time * 1e-4;
float xi = floor(p.x / grid) * grid;
float yi = floor(p.y / grid) * grid;
vec2 p1 = vec2(sin(yi * .4 + t * 2.), cos(xi + t * 3.)) * grid * .25;
return sdCircle(mod(p, grid) - grid * .5 - p1, .5 + .2 * sin(t + xi)) + sin(p.x * 2.) * cos(p.y * 3.) * .1;
}
float subtract(float a, float b) {
return max(-a, b);
}
vec2 fishCoords(vec2 p) {
float grid = fishGrid;
float rot = 45. * PI / 180.;
vec2 motion = vec2(time * 1e-3, 0.);
float xi = floor(p.x / grid) * grid;
float yi = floor(p.y / grid) * grid;
return rotate(mod(rotate(p - motion, rot), grid) - (grid) * .5, -rot);
}
float fishDF(vec2 p) {
vec2 p0 = fishCoords(p);
float df = 9999.;
df = min(df, sdCircle(p0 * vec2(1.5, 1.) - vec2(.3, 0), 1.));
df = subtract(sdCircle(p0 * vec2(1.5, 1.) - vec2(-.8, 0), 1.), df);
df = min(df, sdCircle(p0 - vec2(2., 0), 1.4));
return df + sin(p.x) * cos(p.y * 2.) * .1;
}
vec3 clownFishTexture(vec2 p) {
vec2 p0 = fishCoords(p);
float y = sdCircle(p0 - vec2(2.7, .2), .2);
float x = -.3 + sin(p.x * 2. - time * 2e-3 + cos(p.y * 1.2));
vec3 stripedOrange = vec3(.8, .3, .0) + smoothstep(0., .3, x);
vec3 black = vec3(0.);
return mix(black, stripedOrange, smoothstep(0., .1, y));
}
vec3 shade(in vec2 p) {
vec2 p00 = coords() - vec2(0, scrollPos * .33);
vec2 p0 = p00 + vec2(sin(1. + time * 2e-4) * sin(p00.x * 10. + time * 1e-4) * .2, 0.);
float sdf = distanceField(p);
float sdfBubbles = bubblesDF(p);
vec3 fg = vec3(.2 + sin(floor(p.x/gridSize) * gridSize), .3+ .2 * sin(floor(p.y/gridSize) * gridSize), .5) * .7
+ .5;
vec3 bg = vec3(0., .1, .2) + vec3(0, .1, .1) * smoothstep(0., .05, clamp(sin(p0.x * 10. + p0.y * 20. + time * 3e-4), 0., 1.));
float star = smoothstep(0., .1, sdf);
float fish = smoothstep(0., .1, fishDF(p00 * 27.));
vec3 fishFg = clownFishTexture(p00 * 27.);
vec3 bg2 = vec3(0, .1, .1) * smoothstep(0., .05, clamp(sin(p0.x * 5. + p0.y * 8.) * sin(2. + p0.x * 8. + p0.y * 16.), 0., 1.));
vec3 bg3 = smoothstep(0., .1, -sdfBubbles) * vec3(.1);
return mix(fg, mix(fishFg, bg, fish), star) + bg2 + bg3;
}
void main () {
vec2 p0 = coords() - vec2(0., sin(time * 2e-5)) - vec2(0, scrollPos);
vec2 p = rotate(p0, 45. * PI / 180.);
vec3 col = shade(p * 27.);
gl_FragColor = vec4(col, 1.0);
}
</script>
</digital-art>
console.clear();
class DigitalArt extends HTMLElement {
constructor() {
super();
this.canvas = null;
this.gl = null;
this.onResize = this.onResize.bind(this);
this.onScroll = this.onScroll.bind(this);
this.loop = this.loop.bind(this);
}
static register() {
customElements.define("digital-art", DigitalArt);
}
connectedCallback() {
if (! this.gl) {
this.setup();
}
}
disconnectedCallback() {
this.dispose();
}
get devicePixelRatio() {
return parseFloat(this.getAttribute('dpr')) || window.devicePixelRatio;
}
onScroll(e) {
const { gl, program } = this;
const uScrollPos = gl.getUniformLocation(program, "scrollPos");
gl.uniform1f(uScrollPos, window.scrollY / document.body.clientHeight);
}
onResize() {
const { canvas, gl, program } = this;
const width = this.clientWidth;
const height = this.clientHeight;
const dpr = this.devicePixelRatio;
canvas.width = width * dpr;
canvas.height = height * dpr;
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
const uResolution = gl.getUniformLocation(program, "resolution");
gl.uniform2fv(uResolution, [gl.drawingBufferWidth, gl.drawingBufferHeight]);
}
createShader(type, code) {
const { gl } = this;
const sh = gl.createShader(type, code);
gl.shaderSource(sh, code);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
throw gl.getShaderInfoLog(sh);
}
return sh;
}
createBuffers() {
const { gl, program } = this;
const bufferScripts = [...this.querySelectorAll('[type=buffer]')];
this.buffers = {};
let count = -1;
bufferScripts.forEach(container => {
const name = container.getAttribute('name') || 'position';
const recordSize = parseInt(container.getAttribute('data-size'), 10) || 1;
const data = new Float32Array(JSON.parse(container.textContent.trim()));
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
);
const attribLoc = gl.getAttribLocation(program, name);
this.buffers[name] = { buffer, data, attribLoc, recordSize };
count = Math.max(count, (data.length / recordSize)|0);
gl.enableVertexAttribArray(attribLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(attribLoc, recordSize, gl.FLOAT, false, 0, 0);
});
this.count = count;
}
loop(time = 0) {
const { gl, program } = this;
const uTime = gl.getUniformLocation(program, "time");
gl.uniform1f(uTime, time);
gl.drawArrays(gl.TRIANGLES, 0, this.count);
this.frame = requestAnimationFrame(this.loop);
}
createPrograms() {
const { gl } = this;
const fragScript = this.querySelector('[type=frag]');
const vertScript = this.querySelector('[type=vert]');
const HEADER = 'precision highp float;';
const DEFAULT_VERT = HEADER + 'attribute vec4 position;void main(){gl_Position=position;}';
const DEFAULT_FRAG = HEADER + 'void main(){gl_FragColor=vec4(1.,0,0,1.);}';
this.fragCode = fragScript?.textContent || DEFAULT_FRAG;
this.vertCode = vertScript?.textContent || DEFAULT_VERT;
const program = gl.createProgram();
this.program = program;
this.gl = gl;
this.fragShader = this.createShader(gl.FRAGMENT_SHADER, this.fragCode);
this.vertShader = this.createShader(gl.VERTEX_SHADER, this.vertCode);
gl.attachShader(program, this.fragShader);
gl.attachShader(program, this.vertShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw gl.getProgramInfoLog(program);
}
}
setup() {
this.canvas = document.createElement('canvas');
this.dpr = window.devicePixelRatio;
this.appendChild(this.canvas);
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl');
this.createPrograms();
const { program, gl } = this;
gl.useProgram(program);
this.createBuffers();
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
this.onResize();
window.addEventListener('resize', this.onResize, false);
window.addEventListener('scroll', this.onScroll, false);
this.frame = requestAnimationFrame(this.loop);
}
dispose() {
cancelAnimationFrame(this.loop);
this.frame = -1;
window.removeEventListener('scroll', this.onScroll, false);
window.removeEventListener('resize', this.onResize, false);
Object.entries(this.buffers).forEach(([name, buf]) => {
this.gl.deleteBuffer(buf.buffer);
});
this.gl.deleteProgram(this.program);
const loseCtx = this.gl.getExtension('WEBGL_lose_context');
if (loseCtx && typeof loseCtx.loseContext === 'function') {
loseCtx.loseContext();
}
this.removeChild(this.canvas);
this.gl = null;
this.canvas = null;
this.buffers = {}
}
}
DigitalArt.register();
body {
margin: 0;
height: 500vh;
overflow-x: hidden;
}
digital-art {
position: fixed;
top: 0; left: 0;
display: block;
width: 100vw;
height: 100vh;
pointer-events: none;
}
digital-art canvas {
display: block;
width: 100%;
height: 100%;
}
/* Hide scrollbars in windows */
::-webkit-scrollbar {
display: none;
}
body {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox (not sure if that works) */
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment