Created
August 15, 2020 23:30
-
-
Save mrjacobbloom/c2900094203cd1a118ca7fa35ce09b71 to your computer and use it in GitHub Desktop.
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
/** | |
* Represents a value 0-1 inclusive. Just here to help me keep my math straight | |
*/ | |
type Normalized = number & { __normalized__: undefined }; | |
interface RGB { r: number; g: number; b: number; } | |
// CONTROLS | |
const TEXT = 'John Doe'; | |
const FG_COLOR: RGB = { r: 72, g: 125, b: 175 }; // note: bg color should be #FFEADA | |
const Y_DENSITY = 3; | |
const X_DENSITY = 2; | |
const MAX_ROT_SPEED = 0.008; | |
const GRADIENT_INNER_RADIUS = 0.5; | |
const GRADIENT_MAX_OPACITY = 0.3; | |
const REPULSION_DISTANCE = 20; | |
const GET_RADIUS = (size: Normalized) => 2 + size * 3; | |
const ANIM_LENGTH = 2000; // milliseconds | |
const ANIM_RADIUS = 200; | |
const ANIM_EASING = .05; | |
const TWO_PI = 2 * Math.PI; | |
const canvas = document.querySelector('canvas') as HTMLCanvasElement; | |
const ctx = canvas.getContext('2d')!; | |
const resizeCanvas = () => { | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
}; | |
window.addEventListener('resize', resizeCanvas); | |
resizeCanvas(); | |
const startTime = Date.now(); | |
class Vector { | |
// coords are relative | |
constructor(public cx: number, public cy: number) {} | |
public getAbsolute(canvas: HTMLCanvasElement): [number, number] { | |
return [(canvas.width / 2) + this.cx, (canvas.height / 2) + this.cy]; | |
} | |
public static add(...vectors: Vector[]): Vector { | |
let x = 0, y = 0; | |
for (const vector of vectors) { | |
x += vector.cx; | |
y += vector.cy; | |
} | |
return new Vector(x, y); | |
} | |
public static direction(a: Vector, b: Vector): number { | |
const dx = a.cx - b.cx; | |
const dy = a.cy - b.cy; | |
return Math.atan2(dy,dx); | |
} | |
public static distance(a: Vector, b: Vector): number { | |
const dx = a.cx - b.cx; | |
const dy = a.cy - b.cy; | |
return Math.sqrt((dx ** 2) + (dy ** 2)); | |
} | |
public static fromAbsolute(ax: number, ay: number, canvas: HTMLCanvasElement): Vector { | |
return new Vector(ax - (canvas.width / 2), ay - (canvas.height / 2)) | |
} | |
public static fromPolar(radius: number, theta: number): Vector { | |
return new Vector(radius * Math.cos(theta), radius * Math.sin(theta)); | |
} | |
} | |
let mousePos: Vector = new Vector(Infinity, Infinity); | |
canvas.addEventListener('mousemove', (event) => { | |
mousePos = Vector.fromAbsolute(event.offsetX, event.offsetY, canvas); | |
}); | |
class Blobule { | |
size: Normalized; | |
rotCenter: Vector; | |
rotSpeed: number; | |
rotRadius: number; | |
rotThetaOffset: number; | |
constructor(cx: number, cy: number) { | |
this.size = (1 - Math.sqrt(Math.random())) as Normalized; | |
this.rotThetaOffset = Math.random() * 360; | |
this.rotSpeed = Math.random() * MAX_ROT_SPEED * (1 - this.size); | |
const centerRadiusNorm = Math.random() as Normalized; | |
const centerTheta = Math.random() * TWO_PI; | |
this.rotCenter = new Vector(cx, cy) // todo: noise? Smaller blobules noisier? | |
this.rotRadius = centerRadiusNorm * 4; | |
} | |
public render(ctx: CanvasRenderingContext2D, animProgress: Normalized): void { | |
const rotRadius = this.rotRadius + (this.rotRadius * ANIM_RADIUS * (1 - animProgress)); | |
const opacity = GRADIENT_MAX_OPACITY * animProgress; | |
const rotVector = Vector.fromPolar(rotRadius, this.rotThetaOffset + (this.rotSpeed * Date.now())); | |
const mouseTheta = Vector.direction(this.rotCenter, mousePos); | |
const repulsionVector = Vector.fromPolar(REPULSION_DISTANCE, mouseTheta); | |
const [ax, ay] = Vector.add(this.rotCenter, rotVector, repulsionVector).getAbsolute(ctx.canvas); | |
ctx.beginPath() | |
const radius = GET_RADIUS(this.size); | |
const gradient = ctx.createRadialGradient(ax,ay,radius * GRADIENT_INNER_RADIUS, ax,ay,radius); | |
gradient.addColorStop(0, `rgba(${FG_COLOR.r}, ${FG_COLOR.g}, ${FG_COLOR.b}, ${opacity})`); | |
gradient.addColorStop(1, `rgba(${FG_COLOR.r}, ${FG_COLOR.g}, ${FG_COLOR.b}, 0)`); | |
ctx.fillStyle = gradient; | |
ctx.arc(ax, ay, radius, 0, 360); | |
ctx.fill(); | |
} | |
} | |
const blobules: Blobule[] = []; | |
const drawFrame = () => { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
const animProgress = Math.min(1, (Date.now() - startTime) / ANIM_LENGTH) ** ANIM_EASING as Normalized; | |
for (const blobule of blobules) { | |
blobule.render(ctx, animProgress); | |
} | |
requestAnimationFrame(drawFrame); | |
} | |
const initialize = () => { | |
const txtcanvas = new OffscreenCanvas(window.innerWidth, window.innerHeight); | |
const txtctx = txtcanvas.getContext('2d')!; | |
txtctx.font = 'bold 100px "Helvetica"'; | |
txtctx.fillStyle = 'red'; | |
txtctx.textAlign = 'left'; | |
txtctx.textBaseline = 'hanging'; | |
txtctx.fillText(TEXT, 0, 0); | |
const { width } = txtctx.measureText(TEXT); | |
const imageData = txtctx.getImageData(0, 0, width, 100); | |
for(let y = 0; y < imageData.height; y += Y_DENSITY) { | |
for(let x = 0; x < imageData.width; x += X_DENSITY) { | |
const red = imageData.data[(y * (imageData.width * 4)) + (x * 4)]; | |
if (red === 255) { | |
blobules.push(new Blobule(x - imageData.width / 2, y)); | |
} | |
} | |
} | |
requestAnimationFrame(drawFrame); | |
} | |
initialize(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment