Skip to content

Instantly share code, notes, and snippets.

@s4y
Last active August 10, 2020 13:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save s4y/bb4540fcd12a2bfc52a79912ab2388e4 to your computer and use it in GitHub Desktop.
Save s4y/bb4540fcd12a2bfc52a79912ab2388e4 to your computer and use it in GitHub Desktop.
A fun little button. Uses WebGL. License: CC BY 4.0
<!DOCTYPE html>
<meta name=viewport content="width=device-width">
<style>
html, body, #app {
height: 100%;
}
html {
background: rgb(220, 0, 220, 1);
}
body {
margin: 0;
}
.bgCanvas {
display: block;
width: 100%;
height: 100%;
}
#roundrect {
width: 40vw;
height: 20vw;
background: white;
border-radius: 5vw;
transition: transform 0.4s, box-shadow 0.4s;
cursor: pointer;
}
#roundrect:not(.touch):hover {
transform: scale(1.1);
box-shadow: 0 0 2vw rgba(0.5, 0.5, 0.5, 0.5);
}
/* CSS precedence is sometimes silly */
#roundrect:not(.touch):active, #roundrect:active {
transform: scale(0.9);
transition-duration: 0.18s, 0.4s;
box-shadow: 0 0 0.4vw 0.3vw rgba(0.5, 0.5, 0.5, 0.4);
}
#roundrect.touch:active {
transition-duration: 0.08s;
transition-timing-function: ease-out;
}
</style>
<div style="
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
"><div id=roundrect></div></div>
<div id=app></div>
<script>
class View {
constructor() {
this.subviews = [];
}
addSubview(subview) {
this.subviews.push(subview)
this.el.appendChild(subview.el);
}
}
class HTMLElementView extends View {
constructor(el) {
super();
this.el = el;
}
}
const planeMesh = new Float32Array([
-1, 1, 0, -1, -1, 0,
1, 1, 0, 1, -1, 0,
]);
class GLCanvasView extends HTMLElementView {
constructor() {
const el = document.createElement('canvas');
super(el);
this.initGl();
this.resizeObserver = new ResizeObserver(() => {
this.resize();
});
this.resizeObserver.observe(this.el);
this.animationFrame = null;
this.clicks = [];
roundrect.addEventListener('pointerdown', () => {
this.clicks.push(performance.now());
this.ensureDrawing();
});
roundrect.addEventListener('touchstart', e => {
roundrect.classList.add('touch');
e.preventDefault();
});
}
get contextOptions() {
return {
alpha: true,
antialias: false,
powerPreference: 'low-power',
};
}
get vs() {
return `
attribute vec3 p_in;
varying vec3 p;
void main() {
p = p_in;
gl_Position = vec4(p_in, 1.);
}
`;
}
get fs() {
return `
precision mediump float;
varying vec3 p;
uniform float aspect;
uniform float t;
const float PI = asin(1.0) * 2.;
// From https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
r.xy = (p.x>0.0)?r.xy : r.zw;
r.x = (p.y>0.0)?r.x : r.y;
vec2 q = abs(p)-b+r.x;
return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
void main() {
vec3 fitp = p * vec3(1, 1./aspect, 1.);
float edgeDist = sdRoundedBox(fitp.xy, vec2(0.4, 0.2) * 0.9, vec4(0.1));
if (edgeDist < 0.) {
discard;
}
float edgeBri = 1.-sin(clamp(abs(1.-pow(edgeDist * 3., 1.8) - 1.2 + t * 3.), 0., 0.5) * PI);
gl_FragColor = vec4(0,0,0,1) * edgeBri * clamp(pow(1.0-edgeDist, 5.), 0., 1.) * 0.7;
}
`;
}
initGl() {
const gl = this.el.getContext('webgl', this.contextOptions);
this.gl = gl;
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, this.vs);
gl.compileShader(vs);
// console.log(gl.getShaderInfoLog(vs));
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, this.fs);
gl.compileShader(fs);
// console.log(gl.getShaderInfoLog(fs));
const prog = this.prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
// if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
// console.log(gl.getProgramInfoLog(prog));
// return;
// }
gl.useProgram(prog);
this.aspectLoc = gl.getUniformLocation(prog, "aspect");
this.tLoc = gl.getUniformLocation(prog, "t");
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, planeMesh, gl.STATIC_DRAW);
const pLoc = gl.getUniformLocation(prog, "p_in");
gl.enableVertexAttribArray(pLoc);
gl.vertexAttribPointer(pLoc, 3, gl.FLOAT, false, 0, 0);
}
resize() {
const {el, gl} = this;
el.width = el.clientWidth * devicePixelRatio;
el.height = el.clientHeight * devicePixelRatio;
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.uniform1f(this.aspectLoc, el.width / el.height);
this.draw();
}
ensureDrawing() {
if (this.animationFrame !== null)
return;
const handleAnimationFrame = now => {
this.drawAt(now);
if (this.clicks.length) {
this.animationFrame = requestAnimationFrame(handleAnimationFrame);
} else {
this.animationFrame = null;
}
};
handleAnimationFrame(performance.now());
}
draw() {
this.drawAt(performance.now());
}
drawAt(now) {
const {gl} = this;
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
for (let i = 0; i < this.clicks.length; i++) {
const t = (now-this.clicks[i])/1000;
if (t > 3) {
this.clicks.splice(i--, 1);
continue;
}
gl.uniform1f(this.tLoc, t);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, planeMesh.length / 3);
}
}
}
class AppView extends HTMLElementView {
constructor(el) {
super(el);
this.bgCanvas = new GLCanvasView();
this.bgCanvas.el.classList.add('bgCanvas');
this.addSubview(this.bgCanvas);
}
}
const appView = new AppView(document.getElementById('app'));
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment