Created
July 28, 2024 18:12
-
-
Save HarryR/dfb555e2447a297c06a396333c424bbf to your computer and use it in GitHub Desktop.
Draws a cool morphing triangle thing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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