Whoozy Visuals
---
format: typebulb/v1
name: Sick
---
**code.tsx**
```tsx
import { App, Component, div, canvas } from "domeleon"
interface Particle {
x: number
y: number
vx: number
vy: number
life: number
maxLife: number
hue: number
size: number
}
class SickViz extends Component {
#canvas: HTMLCanvasElement
#ctx: CanvasRenderingContext2D
#animationId: number
#time = 0
#particles: Particle[] = []
#mouseX = 0
#mouseY = 0
#isMouseDown = false
view() {
return div({ class: "viz-container" },
canvas({
width: window.innerWidth,
height: window.innerHeight,
onMounted: (el) => {
this.#canvas = el as HTMLCanvasElement
this.#ctx = this.#canvas.getContext("2d", { alpha: false })
this.#canvas.addEventListener("mousemove", this.handleMouseMove)
this.#canvas.addEventListener("mousedown", this.handleMouseDown)
this.#canvas.addEventListener("mouseup", this.handleMouseUp)
this.#canvas.addEventListener("touchmove", this.handleTouchMove)
window.addEventListener("resize", this.handleResize)
this.startAnimation()
return () => {
cancelAnimationFrame(this.#animationId)
this.#canvas.removeEventListener("mousemove", this.handleMouseMove)
this.#canvas.removeEventListener("mousedown", this.handleMouseDown)
this.#canvas.removeEventListener("mouseup", this.handleMouseUp)
this.#canvas.removeEventListener("touchmove", this.handleTouchMove)
window.removeEventListener("resize", this.handleResize)
}
}
})
)
}
handleMouseMove = (e: MouseEvent) => {
this.#mouseX = e.clientX
this.#mouseY = e.clientY
}
handleMouseDown = () => {
this.#isMouseDown = true
}
handleMouseUp = () => {
this.#isMouseDown = false
}
handleTouchMove = (e: TouchEvent) => {
e.preventDefault()
this.#mouseX = e.touches[0].clientX
this.#mouseY = e.touches[0].clientY
}
handleResize = () => {
this.#canvas.width = window.innerWidth
this.#canvas.height = window.innerHeight
}
startAnimation() {
const animate = () => {
this.draw()
this.#animationId = requestAnimationFrame(animate)
}
animate()
}
draw() {
const ctx = this.#ctx
const w = this.#canvas.width
const h = this.#canvas.height
this.#time += 0.01
// Fade effect for trails
ctx.fillStyle = "rgba(0, 0, 0, 0.08)"
ctx.fillRect(0, 0, w, h)
// Generate particles based on algorithmic patterns
if (this.#particles.length < 500) {
const count = this.#isMouseDown ? 5 : 1
for (let i = 0; i < count; i++) {
this.spawnParticle()
}
}
// Draw geometric grid with modulation
this.drawModulatedGrid(ctx, w, h)
// Update and draw particles
this.updateParticles(ctx)
// Draw complex polyrhythmic circles
this.drawPolyrhythmicCircles(ctx, w, h)
}
spawnParticle() {
const angle = Math.random() * Math.PI * 2
const speed = Math.random() * 2 + 1
this.#particles.push({
x: this.#mouseX || this.#canvas.width / 2,
y: this.#mouseY || this.#canvas.height / 2,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1,
maxLife: Math.random() * 100 + 50,
hue: (this.#time * 50 + Math.random() * 60) % 360,
size: Math.random() * 3 + 1
})
}
updateParticles(ctx: CanvasRenderingContext2D) {
for (let i = this.#particles.length - 1; i >= 0; i--) {
const p = this.#particles[i]
p.x += p.vx
p.y += p.vy
p.life--
if (p.life <= 0) {
this.#particles.splice(i, 1)
continue
}
const alpha = p.life / p.maxLife
ctx.fillStyle = `hsla(${p.hue}, 80%, 60%, ${alpha})`
ctx.fillRect(p.x, p.y, p.size, p.size)
}
}
drawModulatedGrid(ctx: CanvasRenderingContext2D, w: number, h: number) {
ctx.strokeStyle = `hsla(${(this.#time * 30) % 360}, 70%, 50%, 0.15)`
ctx.lineWidth = 1
const gridSize = 40
for (let x = 0; x < w; x += gridSize) {
const offset = Math.sin(this.#time + x * 0.01) * 20
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x + offset, h)
ctx.stroke()
}
for (let y = 0; y < h; y += gridSize) {
const offset = Math.cos(this.#time + y * 0.01) * 20
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(w, y + offset)
ctx.stroke()
}
}
drawPolyrhythmicCircles(ctx: CanvasRenderingContext2D, w: number, h: number) {
const cx = w / 2
const cy = h / 2
// Responsive: more circles on larger screens, fewer on smaller ones.
// Keeps the existing circles identical (same formula), just adjusts how many are drawn.
const maxRadius = Math.min(w, h) / 2 - 20
const maxIndex = Math.floor((maxRadius - 80) / 60) // 80 = 50 base + 30 modulation
const circleCount = Math.max(1, maxIndex + 1)
for (let i = 0; i < circleCount; i++) {
const radius = 50 + i * 60 + Math.sin(this.#time * (1 + i * 0.3)) * 30
const hue = (this.#time * 20 + i * 72) % 360
ctx.strokeStyle = `hsla(${hue}, 90%, 60%, 0.3)`
ctx.lineWidth = 2
ctx.beginPath()
ctx.arc(cx, cy, radius, 0, Math.PI * 2)
ctx.stroke()
}
}
}
new App({ root: new SickViz(), id: "app" })
```
**styles.css**
```css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
html[data-theme="dark"] {
color-scheme: dark;
--bg-color: #000000;
--primary-hue: 200;
}
html[data-theme="light"] {
color-scheme: light;
--bg-color: #0a0a0a;
--primary-hue: 280;
}
body {
background: var(--bg-color);
font-family: system-ui, -apple-system, sans-serif;
}
.viz-container {
width: 100%;
height: 100%;
cursor: crosshair;
}
canvas {
display: block;
}
```
**index.html**
```html
<div id="app"/>
```
**config.json**
```json
{
"dependencies": {
"domeleon": "^0.5.2"
},
"description": "Whoozy Visuals"
}
```
**notes.md**
````md
# Autechre-Inspired Visualization
A mesmerizing, algorithmic visualization inspired by Autechre's aesthetic.
## Features
- **Particle System**: Spawns particles that create flowing trails
- **Modulated Grid**: Sine/cosine wave distortions on grid lines
- **Polyrhythmic Circles**: Concentric circles pulsing at different frequencies
- **Interactive**: Mouse movement influences particle spawn position
- **Click/Hold**: Spawns more particles when mouse is held down
- **Fade Trails**: Creates ghosting effect for smooth motion blur
## Interaction
- Move mouse to change particle spawn location
- Click and hold to spawn more particles
- Touch-enabled for mobile devices
## Visual Style
Complex, glitchy, mathematical patterns reminiscent of Autechre's generative and algorithmic approach to electronic music. The visualization evolves continuously with layered rhythms and geometric precision.
## Example of Using Domeleon
```ts
import { App, Component, div, button, formField, inputRange, canvas } from "domeleon"
class Counter extends Component {
count: number
role: string
#canvasEl: HTMLCanvasElement
#canvasCtx: CanvasRenderingContext2D
constructor(initial: number, role: string) {
super()
this.count = initial
this.role = role
}
view() {
return div({ class: "counter" },
formField ({ // databound input
target: this,
inputFn: inputRange, // inputSelect, inputText, etc. (import "domeleon/maskito" for inputNumber)
prop: () => this.count,
label: this.role,
fieldAttrs: { class: "field-class"},
labelAttrs: { class: "label-class"},
inputProps: { attrs: { min: 100, max: 250, step: 1 } }
}),
button({ onClick: () => this.inc(+1) }, "+"), // always use closure to bind to preserve "this"
button({ onClick: () => this.inc(-1) }, "-"),
this.count,
canvas({
width: 400,
height: 20,
onMounted: el => {
this.#canvasEl = el as HTMLCanvasElement
this.#canvasCtx = this.#canvasEl.getContext("2d")
return () => { // dispose
this.#canvasEl = undefined
this.#canvasCtx = undefined
}
}
})
)
}
inc(x: number) {
this.count+=x
this.update() // triggers render on app
}
// `onRendered` called after VDOM has created/updated DOM elements
// useful for operations that require raw DOM access, such as canvas operations
override onRendered () {
this.drawOnCanvas()
}
drawOnCanvas() {
const canvas = this.#canvasEl
const cc = this.#canvasCtx
cc.clearRect(0, 0, canvas.width, canvas.height)
const barWidth = (this.count / 250) * canvas.width
cc.fillStyle = "#4a90e2"
cc.fillRect(0, 0, barWidth, canvas.height)
}
}
class Game extends Component {
wins = new Counter(10, "wins")
losses = new Counter(5, "losses")
view() {
return div(
this.wins.view(),
this.losses.view()
)
}
}
new App({ root: new Game(), id: "app"})
```
````