Skip to content

Instantly share code, notes, and snippets.

@mrjacobbloom
Created August 15, 2020 23:30
Show Gist options
  • Save mrjacobbloom/c2900094203cd1a118ca7fa35ce09b71 to your computer and use it in GitHub Desktop.
Save mrjacobbloom/c2900094203cd1a118ca7fa35ce09b71 to your computer and use it in GitHub Desktop.
/**
* 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