Skip to content

Instantly share code, notes, and snippets.

@pfirpfel
Last active April 20, 2021 12:42
Show Gist options
  • Save pfirpfel/56b4ef27922d02d89273c8740fb88bea to your computer and use it in GitHub Desktop.
Save pfirpfel/56b4ef27922d02d89273c8740fb88bea to your computer and use it in GitHub Desktop.
Wavy knot
<html>
<head>
<title>Wavy Clip Mask</title>
</head>
<body>
<input id="karma" type="range" min="0" max="100" value="50" style="width: 500px;"/>
<br>
<canvas id="canvas"></canvas>
<script src="knot.js" type="text/javascript"></script>
<script src="wave.js" type="text/javascript"></script>
<script src="main.js" type="text/javascript"></script>
<script>
init();
</script>
</body>
</html>
var generateKnot = function(thickness, padding){
var a = thickness; // thickness of knot strings
var b = padding; // padding between crossings
var transformPath = function(path, transX, transY){
return path.map(function(coord){
return [coord[0] + transX, coord[1] + transY];
});
};
var rotatePath = function(path, centerX, centerY, angle){
return path.map(function(coord){
var s = Math.sin(angle);
var c = Math.cos(angle);
var x = coord[0] - centerX;
var y = coord[1] - centerY;
var newX = x * c - y * s;
var newY = x * s + y * c;
return [newX + centerX, newY + centerY];
});
};
var c = 3*a + 2*b;
var d = 4*a + 3*b;
var e = a + b;
var baseP = [[0,0],[c,0],[c,c],[a+b,c],[a+b,c-a],[c-a,c-a],[c-a,a],[a,a],[a,d],[0,d]];
var baseI = [[0,0],[a,0],[a,c],[0,c]];
var baseJ = [[0,0],[c,0],[c,e+a],[c-a,e+a],[c-a,a],[a,a],[a,d],[0,d]];
var knot = [];
// Ps
knot.push(transformPath(rotatePath(baseP, 0, 0, Math.PI*1.5), 0, c));
knot.push(transformPath(rotatePath(baseP, 0, 0, Math.PI*0.5), d+d+2*b+a, d+2*b+a+e));
// Js
knot.push(transformPath(baseJ, d+b, 0));
knot.push(transformPath(rotatePath(baseJ, 0, 0, Math.PI*0.5), d+d+2*b+a, c-a));
knot.push(transformPath(rotatePath(baseJ, 0, 0, Math.PI), c+e+a+b, d+d+2*b+a));
knot.push(transformPath(rotatePath(baseJ, 0, 0, Math.PI*1.5), 0, c+d+b));
//Is
knot.push(transformPath(rotatePath(baseI, 0, 0, Math.PI*0.5), c+c+b, d+b));
knot.push(transformPath(baseI, c-a, c+b));
knot.push(transformPath(baseI, 6*a+6*b, c+b));
var height = 2*d+2*b+a;
var hypo = height*Math.pow(2,1/2);
// rotate knot around center
knot = knot.map(function(path){
return rotatePath(path, height/2, height/2, Math.PI*0.25);
});
// move knot to visible canvas
knot = knot.map(function(path){
return transformPath(path, (hypo-height)/2-((2*a+2*b)*Math.pow(2,1/2)/2), (hypo-height)/2);
});
knot = knot.map(function(path){
return path.map(function(coord){
return coord.map(Math.round);
});
});
return knot;
}
if(typeof exports != 'undefined') exports.generateKnot = generateKnot;
var init = function(){
// knot properties
var thinkness = 20;
var padding = 7;
// generate knot
var shape = generateKnot(thinkness, padding);
// calculate knot dimensions
var height = (9 * thinkness + 8 * padding) * Math.pow(2, 1/2);
var cutoffEdges = (2 * thinkness + 2 * padding) * Math.pow(2, 1/2);
// generate wave level
var wave = new waveBox({
x: 0,
y: 0,
width: height - cutoffEdges,
height: height,
initialLevel: 0.5,
springs: 25
});
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
canvas.width = Math.ceil(height - cutoffEdges);
canvas.height = Math.ceil(height);
var drawPath = function(path){
var i;
ctx.moveTo(path[0][0], path[0][1]);
for(i = 0; i < path.length; i++){
ctx.lineTo(path[i][0], path[i][1]);
}
ctx.closePath();
};
ctx.strokeStyle = "#303030";
ctx.lineWidth = 2;
var boundaries = wave.getBoundaries();
var render = function(timestamp){
var karma = document.getElementById('karma').value;
var karmaPercentage = karma / 100;
wave.setLevel(karmaPercentage);
ctx.clearRect(boundaries.x1, boundaries.y1, boundaries.x2, boundaries.y2);
ctx.save();
ctx.beginPath();
shape.forEach(drawPath);
ctx.clip();
ctx.fillStyle = "#e0e0e0";
ctx.fill();
ctx.fillStyle = "#000000";
wave.draw(ctx, timestamp, false);
ctx.stroke();
ctx.restore();
window.requestAnimationFrame(render);
}
}
window.requestAnimationFrame(render);
}
var waveBox = function(config){
config = config || {};
/**----------------------
Configurable values
--------------------------**/
// top left origin of avg 100% level
// (animation has overflow above this point)
var originX = (config.x !== undefined) ? config.x : 50;
var originY = (config.y !== undefined) ? config.y : 50;
// width of the whole box
var width = (config.width !== undefined) ? config.width : 250;
// height of maxium level
// (distance between origin and "bottom")
// note: the water has an overflow
var height = (config.height !== undefined) ? config.height : 200;
// number of springs
// the higher the prettier (and slower)
var numOfsprings = (config.springs !== undefined) ? config.springs : 25;
// current wate level, change via setLevel(level)
// value between 0.0 and 1.0
var relativeLevel = (config.initialLevel !== undefined) ? config.initialLevel : 0.5;
// characteristics of the waves
var tension = 0.025;
var dampening = 0.08;
var spread = 0.1;
// characteristics of the splash
var splashInterval = 1600; // in ms
var splashVelocityBase = 0.1;
var splashVelocityDynamic = 0.05;
//---------------------------------------------
// area which gets affected by the animation
var boundaries = {
x1: Math.max(0, originX - 1),
y1: Math.max(0, originY - width * 0.3), // includes top overflow
x2: originX + width + 1,
y2: originY + height
};
// expose boundaries
this.getBoundaries = function(){
return {
x1: boundaries.x1,
y1: boundaries.y1,
x2: boundaries.x2,
y2: boundaries.y2
};
};
// init springs
var springs = Array.apply(null, Array(numOfsprings)).map(function () {
return {
level: relativeLevel,
velocity: 0
};
});
// update level and velocity of spring
var updateSpring = function(spring){
var levelDelta = relativeLevel - spring.level;
spring.velocity += (tension * levelDelta - spring.velocity * dampening);
spring.level += spring.velocity;
return spring;
}
// calculate values of springs in new frame
var update = function(){
var currentPass, passes = 4, curr = 0;
springs = springs.map(updateSpring);
var leftDeltas = [], rightDeltas = [];
for(currentPass = 0; currentPass < passes; currentPass++){
for(curr = 0; curr < springs.length; curr++){
if(curr > 0){
leftDeltas[curr] = spread * (springs[curr].level - springs[curr - 1].level);
springs[curr - 1].velocity += leftDeltas[curr];
}
if(curr < springs.length - 1){
rightDeltas[curr] = spread * (springs[curr].level - springs[curr + 1].level);
springs[curr + 1].velocity += rightDeltas[curr];
}
}
for(curr = 0; curr < springs.length; curr++){
if(curr > 0){
springs[curr - 1].level += leftDeltas[curr];
}
if(curr < springs.length - 1){
springs[curr + 1].level += rightDeltas[curr];
}
}
}
};
// drop something into the water at a specific position
var splash = function(index, velocity){
if(index > 0 && index < springs.length){
springs[index].velocity = velocity;
}
};
var springWidth = width / numOfsprings;
var drawWaveBox = function(ctx, clear){
clear = (typeof clear !== 'undefined') ? clear : true;
ctx.save();
if(clear) ctx.clearRect(boundaries.x1, boundaries.y1, boundaries.x2, boundaries.y2);
ctx.beginPath();
var lastY = originY + height * (1 - springs[0].level);
ctx.moveTo(originX, lastY);
for(var i = 1; i < springs.length; i++){
var y = originY + height * (1 - springs[i].level);
var x = originX + i * springWidth;
ctx.quadraticCurveTo(x - springWidth * 0.5, lastY, x, y);
lastY = y;
}
x = originX + springs.length * springWidth;
ctx.quadraticCurveTo(x - springWidth * 0.5, lastY, x ,lastY);
ctx.lineTo(x, originY + height);
ctx.lineTo(originX, originY + height);
ctx.closePath();
ctx.fill();
ctx.restore();
}
var lastSplash = 0;
var drawWaveFrame = function(ctx, timestamp, clear){
// determine whether we have to "splash"
if(lastSplash == 0 || (timestamp - lastSplash) > splashInterval){
var splashIndex = Math.floor(Math.random() * springs.length);
var splashVelocity = splashVelocityBase + Math.random() * splashVelocityDynamic;
splash(splashIndex, splashVelocity);
lastSplash = timestamp || 0;
}
// calculate new waves
update();
// draw them
drawWaveBox(ctx, clear);
};
this.setLevel = function(level){
if(level >= 0 && level <= 1){
relativeLevel = level;
}
};
this.draw = drawWaveFrame;
};
@pfirpfel
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment