Skip to content

Instantly share code, notes, and snippets.

@HarryR
Created July 28, 2024 18:12
Show Gist options
  • Save HarryR/dfb555e2447a297c06a396333c424bbf to your computer and use it in GitHub Desktop.
Save HarryR/dfb555e2447a297c06a396333c424bbf to your computer and use it in GitHub Desktop.
Draws a cool morphing triangle thing
<!DOCTYPE html>
<html>
<head>
<title>Triangle</title>
</head>
<body>
<canvas id="canvas" width=500 height=500></canvas>
<script>
/**
* @param {[number,number]} a
* @param {[number,number]} b
*/
function euclidDistance(a, b) {
const c = (b[0] - a[0])**2;
const d = (b[1] - a[1])**2;
return Math.sqrt( c + d );
}
/**
* https://math.stackexchange.com/questions/175896/finding-a-point-along-a-line-a-certain-distance-away-from-another-point#answer-1630886
* @param {[number,number]} a Point
* @param {[number,number]} b Point
* @param {number} t Distance (0.0=a to 1.0=b) between point A and point B
*/
function pointBetween(a, b, t) {
return [
((1 - t) * a[0]) + (t * b[0]),
((1 - t) * a[1]) + (t * b[1])
];
}
const N = 3;
const R = 0.90;
const S = 1/3.4;
const FPS = 15
class Corner {
/** @type Corner */
next;
/** @type Corner */
prev;
/** @type [number,number] */
l;
/** @type [number,number] */
r;
/** @type [number,number] */
m;
/** @type [number,number] */
n;
/**
* @param {number} x
* @param {number} y
*/
constructor(x, y) {
this.x = x;
this.y = y;
}
}
/**
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} angle
* @param {number} distance
* @returns [[number,number],[number,number]]
*/
function findAngledPoints(x1, y1, x2, y2, angle, distance) {
// Calculate the vector of the line
const dx = x2 - x1;
const dy = y2 - y1;
// Calculate the length of the line
const length = Math.sqrt(dx * dx + dy * dy);
// Normalize the vector
const unitDx = dx / length;
const unitDy = dy / length;
// Convert angle to radians
const angleRad = angle * Math.PI / 180;
// Calculate the rotated vector
const rotatedDx = Math.cos(angleRad) * unitDx - Math.sin(angleRad) * unitDy;
const rotatedDy = Math.sin(angleRad) * unitDx + Math.cos(angleRad) * unitDy;
// Calculate the new points at the given distance
const newX1 = x1 + rotatedDx * distance;
const newY1 = y1 + rotatedDy * distance;
const newX2 = x2 + rotatedDx * distance;
const newY2 = y2 + rotatedDy * distance;
/*
return [
{ x: newX1, y: newY1 },
{ x: newX2, y: newY2 }
];
*/
return [
[newX1, newY1],
[newX2, newY2]
];
}
// line intercept math by Paul Bourke http://paulbourke.net/geometry/pointlineplane/
// Determine the intersection point of two line segments
// Return FALSE if the lines don't intersect
function intersect(x1, y1, x2, y2, x3, y3, x4, y4)
{
// Check if none of the lines are of length 0
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return false
}
denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1))
// Lines are parallel
if (denominator === 0) {
return false
}
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator
const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator
// is the intersection along the segments
if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {
return false
}
// Return a object with the x and y coordinates of the intersection
const x = x1 + ua * (x2 - x1)
const y = y1 + ua * (y2 - y1)
return [x, y]
}
/**
* @param {[number,number]} a
* @param {[number,number]} b
*/
function makeBonk(a, b) {
const d = euclidDistance(a, b) / 1.5;
const e = findAngledPoints(a[0], a[1], b[0], b[1], 90, d);
const f = new Path2D();
f.moveTo(a[0],a[1]);
f.bezierCurveTo(e[0][0], e[0][1], e[1][0], e[1][1], b[0], b[1]);
f.closePath();
return f;
}
function psychedelicPastelColor(time) {
// Adjust these values to change the speed and intensity of the color cycling
const frequency = S/2;
const amplitude = 55;
const center = 200;
// Calculate the RGB values using sine waves
const r = Math.sin(frequency * (time) + 0) * amplitude + center;
const g = Math.sin(frequency * (time/2) + 2) * amplitude + center;
const b = Math.sin(frequency * (time/3) + 4) * amplitude + center;
// Ensure the values are within the 0-255 range
const clamp = (value) => Math.max(0, Math.min(255, Math.round(value)));
// Return the color as an RGB string
return `rgb(${clamp(r)}, ${clamp(g)}, ${clamp(b)})`;
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {number} offset
*/
function draw(ctx, offset) {
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const middle_x = ctx.canvas.width / 2;
const middle_y = ctx.canvas.height / 2;
const k = (Math.PI * 2) / N;
/** @type Corner[] */
const p = [];
for( let i = 0; i < N; i++ )
{
const rx = middle_x + (Math.sin((offset/100) + (k * i)) * (middle_x*R));
const ry = middle_y + (Math.cos((offset/100) + (k * i)) * (middle_y*R));
p.push(new Corner(rx, ry));
}
// Link up next & previous
for( let i = 0; i < N; i++ )
{
const corner = p[i];
corner.next = p[(i+1)%p.length];
corner.prev = p[(i+N-1)%p.length];
}
const localSl = 1/(N + Math.sin(offset/11));
const localSr = 1/(N + Math.sin(offset/9));
// Fill in the left and right points
for( let i = 0; i < N; i++ )
{
const corner = p[i];
const {next,prev} = corner;
corner.r = pointBetween([corner.x,corner.y], [next.x,next.y], localSl);
corner.l = pointBetween([corner.x,corner.y], [prev.x,prev.y], localSr);
}
// Find the intersection between [L->R] & [Corner->Next.R]
// Then find the intersection between that line and the previous corner
for( let i = 0; i < N; i++ )
{
const {x,y,l,r,next,prev} = p[i];
p[i].m = intersect(
l[0], l[1], r[0], r[1],
x, y, next.r[0], next.r[1]
);
p[i].n = intersect(
r[0], r[1], prev.x, prev.y,
x, y, next.r[0], next.r[1]
);
}
ctx.beginPath();
const colorIncrement = 1.5;
let j = 0;
let i;
for( i = 0; i < N; i++ )
{
const {x,y,m,n,l,r,next} = p[i];
ctx.fillStyle = psychedelicPastelColor(offset + j);
j += colorIncrement;
const q = new Path2D();
q.moveTo(x,y);
q.lineTo(m[0],m[1]);
q.lineTo(l[0],l[1]);
q.closePath();
ctx.fill(q);
ctx.fillStyle = psychedelicPastelColor(offset + j);
j += colorIncrement;
const s = new Path2D();
s.moveTo(r[0],r[1]);
s.lineTo(n[0],n[1]);
s.lineTo(m[0],m[1]);
s.closePath();
ctx.fill(s);
ctx.fillStyle = psychedelicPastelColor(offset + j);
j += colorIncrement;
ctx.fill(makeBonk([x,y],r));
ctx.fillStyle = psychedelicPastelColor(offset + j);
j += colorIncrement;
ctx.fill(makeBonk(r,next.l));
ctx.fillStyle = psychedelicPastelColor(offset + j);
j += colorIncrement;
}
ctx.fillStyle = psychedelicPastelColor(offset + j);
j += colorIncrement;
// Central fill
const t = new Path2D();
t.moveTo(p[0].n[0], p[0].n[1]);
for( let i = 1; i < N; i++ ) {
t.lineTo(p[i].n[0], p[i].n[1]);
}
t.closePath();
ctx.fill(t);
}
const can1 = document.getElementById('canvas');
const ctx1 = can1.getContext('2d');
const bc = document.createElement('canvas');
bc.width = can1.width;
bc.height = can1.height;
const ctx2 = bc.getContext('2d');
let counter = 0;
setInterval(() => {
draw(ctx2, counter);
ctx1.fillStyle = 'yellow';
ctx1.fillRect(0, 0, can1.width, can1.height);
ctx1.drawImage(bc, 0, 0);
counter += 1;
}, 1000/FPS);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment