More particles in canvas
Author: Mathias Bøe
For a long time I have liked the Hyperpower extension for the Hyper terminal. Although I am not using the terminal myself, I look at it as a "fun" terminal.
In this blog post, Ill try to explain my implementation of it. The code is written in TypeScript and uses React hooks. And to top it all of, it is built into a small project, a Haiku verification prototype.
Introduction
The application consists of two main components:
AnimatedTextAreaWrapper
: Manages the animated text area.Home
: Validates the Haiku and renders theAnimatedTextAreaWrapper
.
You can play around with it here
app/components/AnimatedTextAreaWrapper
haikuValidator.ts
_171"use client"_171import React, {useEffect, useRef, useState} from "react";_171_171interface Particle {_171 x: number;_171 y: number;_171 alpha: number;_171 color: number[];_171 velocity: {_171 x: number;_171 y: number;_171 };_171}_171_171const MAX_PARTICLES = 50;_171const PARTICLE_ALPHA_FADEOUT = 0.99;_171const PARTICLE_VELOCITY_RANGE = {_171 x: [0, 3], // controls the x direction particle velocity range_171 y: [-3.5, -1.5], // controls the y direction particle velocity range_171};_171_171interface AnimatedTextAreaProps {_171 haiku: string;_171 onChange: (event: any) => void;_171}_171_171export const AnimatedTextAreaWrapper = ({_171 haiku,_171 onChange,_171}: AnimatedTextAreaProps) => {_171 const canvasRef = useRef<HTMLCanvasElement>(null);_171 const particles = useRef<Particle[]>([]);_171 const [needsRedraw, setNeedsRedraw] = useState(false);_171 const [safeToRender, setSafeToRender] = useState(false);_171_171 useEffect(() => {_171 setSafeToRender(true);_171 }, []);_171_171 useEffect(() => {_171 if (typeof window === "undefined") return;_171_171 const canvas = canvasRef.current;_171 if (!canvas) return;_171_171 const ctx = canvas.getContext("2d");_171 if (!ctx) return;_171_171 const drawFrame = () => {_171 if (particles.current.length) {_171 ctx.clearRect(0, 0, canvas.width, canvas.height);_171 particles.current.forEach((particle) => {_171 particle.velocity.y += 0.075; // Gravity_171 particle.x += particle.velocity.x;_171 particle.y += particle.velocity.y;_171 particle.alpha *= PARTICLE_ALPHA_FADEOUT;_171_171 if (particle.alpha > 0.1) {_171 ctx.fillStyle = `rgba(${particle.color.join(",")}, ${_171 particle.alpha_171 })`;_171 ctx.fillRect(_171 Math.round(particle.x - 1),_171 Math.round(particle.y - 1),_171 3,_171 3,_171 );_171 }_171 });_171_171 particles.current = particles.current.filter(_171 (particle) => particle.alpha > 0.1,_171 );_171 }_171_171 if (particles.current.length > 0 || needsRedraw) {_171 requestAnimationFrame(drawFrame);_171 }_171 };_171_171 requestAnimationFrame(drawFrame);_171 }, [needsRedraw]);_171_171 const spawnParticles = (x: number, y: number) => {_171 const numParticles = 5 + Math.round(Math.random() * 5);_171 for (let i = 0; i < numParticles; i++) {_171 const color = [Math.random() * 155, Math.random() * 155 + 100, 255];_171 particles.current.push(createParticle(x, y, color));_171 }_171_171 if (!needsRedraw) {_171 setNeedsRedraw(true);_171 }_171 };_171_171 const createParticle = (x: number, y: number, color: number[]): Particle => {_171 return {_171 x,_171 y,_171 alpha: 1,_171 color,_171 velocity: {_171 x:_171 PARTICLE_VELOCITY_RANGE.x[0] +_171 Math.random() *_171 (PARTICLE_VELOCITY_RANGE.x[1] - PARTICLE_VELOCITY_RANGE.x[0]),_171 y:_171 PARTICLE_VELOCITY_RANGE.y[0] +_171 Math.random() *_171 (PARTICLE_VELOCITY_RANGE.y[1] - PARTICLE_VELOCITY_RANGE.y[0]),_171 },_171 };_171 };_171_171 const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {_171 const textarea = e.currentTarget;_171 const { left, top } = textarea.getBoundingClientRect();_171 const cursorPos = textarea.selectionStart;_171 const lines = textarea.value.substring(0, cursorPos).split("\n");_171_171 const lineNum = lines.length;_171 const colNum = lines[lines.length - 1].length;_171_171 // You may need to adjust these based on your specific styling_171 const charWidth = 9.65; // Approximate width of a character in pixels_171 const lineHeight = 20; // Approximate line height in pixels_171_171 const x = left + colNum * charWidth;_171 const y = top + (lineNum - 1) * lineHeight;_171_171 spawnParticles(x, y);_171 };_171_171 return (_171 <div_171 className={_171 "rounded-[calc(1.5rem-1px)] p-px bg-gradient-to-tr from-blue-400 to-cyan-500"_171 }_171 >_171 <div_171 className={_171 "rounded-[calc(1.5rem-1px)] p-10 bg-gradient-to-tr from-blue-900 to-cyan-900"_171 }_171 >_171 <textarea_171 onInput={handleInput}_171 id={"haikuWindow"}_171 value={haiku}_171 onChange={onChange}_171 rows={5}_171 cols={50}_171 className={_171 "font-mono h-32 leading-5 w-full z-index-10 border-none outline-none resize-none bg-transparent text-white"_171 }_171 />_171 {safeToRender && (_171 <canvas_171 ref={canvasRef}_171 style={{_171 position: "absolute",_171 top: 0,_171 left: 0,_171 pointerEvents: "none",_171 }}_171 width={window.innerWidth}_171 height={window.innerHeight}_171 />)}_171 </div>_171 </div>_171 )_171};