0x77b35947…cc7dsent to0xc3e1f0dd…9515·#25,613,198·view on Etherscan
g.rect(0, 0, r.width, r.height)
g.pop()
}
}
isDone() {
if (this.rectangles.length === 0) return true
return this.fadedCount >= this.rectangles.length
}
averageOpacity() {
if (this.startedCount === 0) return 100
let sum = 0
for (let r of this.rectangles) {
if (r.started) sum += r.opacity
}
return sum / this.startedCount
}
}
class Shockwave extends FillAnimation {
constructor(x, y, palette, traits) {
super(x, y, palette, traits)
this.palette = palette
this.rectangles = []
this._initRects(x, y, palette)
}
_initRects(x, y, palette) {
const numRects = floor(random(3, 30))
for (let i = 0; i < numRects; i++) {
const finalWidth = random(150, 1100) * _designScale
const finalHeight = random(150, 1100) * _designScale
const duration = random(80, 250) / FILL_ANIMATION_SPEED_MULT
const startDelay = floor(random(0, 80))
const originalSW = random(8, 150) * _designScale
this.rectangles.push({
x: x,
y: y,
width: 0,
height: 0,
targetWidth: finalWidth,
targetHeight: finalHeight,
growthRateWidth: finalWidth / duration,
growthRateHeight: finalHeight / duration,
color: random(palette),
sw: originalSW,
originalSW: originalSW,
delay: startDelay,
started: false,
done: false,
blendMode: this.getBlend(random() < 0.2 ? DIFFERENCE : BLEND),
})
}
}
reset(x, y, palette, traits) {
super.reset(x, y, palette, traits)
this.palette = palette
this.rectangles = []
this._initRects(x, y, palette)
}
update() {
super.update()
for (let r of this.rectangles) {
if (this.age < r.delay) continue
if (!r.started) r.started = true
r.width += r.growthRateWidth
r.height += r.growthRateHeight
const sizeProgress = max(r.width / r.targetWidth, r.height / r.targetHeight)
if (sizeProgress >= 0.6) {
const ease = pow((sizeProgress - 0.6) / 0.4, 2)
r.sw = max(0, r.originalSW * (1 - ease))
}
if (!r.done && r.width >= r.targetWidth && r.height >= r.targetHeight && r.sw <= 0) {
r.done = true
}
}
}
display(g) {
for (let r of this.rectangles) {
if (!r.started || r.sw <= 0) continue
g.push()
g.blendMode(r.blendMode)
g.stroke(r.color)
g.strokeWeight(r.sw)
g.noFill()
g.rectMode(CENTER)
const adjW = max(0, r.width - r.sw)
const adjH = max(0, r.height - r.sw)
g.rect(r.x, r.y, adjW, adjH)
g.pop()
}
}
isDone() {
if (this.rectangles.length === 0) return true
return this.rectangles.every(r => r.done)
}
averageOpacity() {
if (this.rectangles.length === 0) return 0
let visible = 0
for (let r of this.rectangles) {
if (r.started && r.sw > 0) visible++
}
return (visible / this.rectangles.length) * 100
}
}
class ExpandingCircles extends FillAnimation {
constructor(x, y, palette, traits) {
super(x, y, palette, traits)
this.circles = []
const count = floor(random(1, 7))
for (let i = 0; i < count; i++) {
this.circles.push({
radius: random(10, 30) * _designScale,
maxRadius: random(500, 900) * _designScale,
speed: random(0.375, 3.0) * FILL_ANIMATION_SPEED_MULT,
strokeWeight: random(2, 6) * _designScale,
color: random(palette),
opacity: 100,
blendMode: this.getBlend(i % 2 === 0 ? DIFFERENCE : ADD),
maxLife: floor(random(60, 350)),
age: 0
})
}
}
reset(x, y, palette, traits) {
super.reset(x, y, palette, traits)
this.circles = []
const count = floor(random(1, 3))
for (let i = 0; i < count; i++) {
this.circles.push({
radius: random(10, 20) * _designScale,
maxRadius: random(500, 900) * _designScale,
speed: random(0.375, 3.0) * FILL_ANIMATION_SPEED_MULT,
strokeWeight: random(2, 6) * _designScale,
color: random(palette),
opacity: 100,
blendMode: this.getBlend(i % 2 === 0 ? DIFFERENCE : ADD),
maxLife: floor(random(60, 350)),
age: 0
})
}
}
update() {
super.update()
for (let c of this.circles) {
c.age++
const selfFading = c.age > c.maxLife
c.radius += c.speed * (selfFading ? 2 : 1)
const fadeProgress = c.radius / c.maxRadius
const t = constrain(map(fadeProgress, 0.2, 1.1, 0, 1), 0, 1)
c.opacity = max(0, 100 * (1 - t))
}
}
display(g) {
g.push()
g.noFill()
for (let c of this.circles) {
if (c.opacity <= 0) continue
g.push()
g.blendMode(c.blendMode)
const col = g.color(c.color)
col.setAlpha(c.opacity)
g.stroke(col)
g.strokeWeight(c.strokeWeight)
g.noFill()
g.circle(this.x, this.y, c.radius * 2)
g.pop()
}
g.pop()
}
isDone() {
if (this.circles.length === 0) return true
return this.circles.every(c => c.opacity <= 0)
}
averageOpacity() {
if (this.circles.length === 0) return 0
return this.circles.reduce((sum, c) => sum + c.opacity, 0) / this.circles.length
}
}
class ExpandingSquares extends FillAnimation {
constructor(x, y, palette, traits) {
super(x, y, palette, traits)
this.fadeInDuration = 60
this.squares = []
this.persistMode = (traits.expandingPersist === 'persist')
this.persistentLayer = null
this.rotationMode = traits.expandingRotation || 'rotating'
this.fixedRotation = this.rotationMode === 'static' ? random([0, QUARTER_PI, HALF_PI, QUARTER_PI * 3]) : 0
const count = floor(random(1, 7))
for (let i = 0; i < count; i++) {
this.squares.push({
size: random(10, 30) * _designScale,
maxSize: random(500, 900) * _designScale,
speed: random(0.375, 3.0) * FILL_ANIMATION_SPEED_MULT,
strokeWeight: random(2, 6) * _designScale,
width: random(0.5, 1.5),
height: random(0.5, 1.5),
rotation: this.rotationMode === 'rotating' ? random(TWO_PI) : this.fixedRotation,
rotSpeed: this.rotationMode === 'rotating' ? random(-0.02, 0.02) : 0,
color: random(palette),
opacity: 100,
blendMode: this.getBlend(i % 2 === 0 ? DIFFERENCE : ADD),
maxLife: floor(random(60, 350)),
age: 0
})
}
}
reset(x, y, palette, traits) {
super.reset(x, y, palette, traits)
this.squares = []
this.persistMode = (traits.expandingPersist === 'persist')
this.rotationMode = traits.expandingRotation || 'rotating'
this._fadeoutStart = undefined
if (!this.fixedRotation && this.rotationMode === 'static') {
this.fixedRotation = random([0, QUARTER_PI, HALF_PI, QUARTER_PI * 3])
}
const count = floor(random(1, 3))
if (this.persistMode && !this.persistentLayer) {
this.persistentLayer = createGraphics(width, height)
this.persistentLayer.clear()
} else if (!this.persistMode && this.persistentLayer) {
this.persistentLayer.remove()
this.persistentLayer = null
} else if (this.persistMode && this.persistentLayer) {
this.persistentLayer.clear()
}
for (let i = 0; i < count; i++) {
this.squares.push({
size: random(10, 20),
maxSize: random(500, 900) * _designScale,
speed: random(0.375, 3.0) * FILL_ANIMATION_SPEED_MULT,
strokeWeight: random(2, 6) * _designScale,
width: random(0.5, 1.5),
height: random(0.5, 1.5),
rotation: this.rotationMode === 'rotating' ? random(TWO_PI) : this.fixedRotation,
rotSpeed: this.rotationMode === 'rotating' ? random(-0.02, 0.02) : 0,
color: random(palette),
opacity: 100,
blendMode: this.getBlend(i % 2 === 0 ? DIFFERENCE : ADD),
maxLife: floor(random(60, 350)),
age: 0
})
}
}
update() {
super.update()
for (let s of this.squares) {
s.age++
const selfFading = s.age > s.maxLife
s.size += s.speed * (selfFading ? 2 : 1)
const fadeProgress = s.size / s.maxSize
const t = constrain(map(fadeProgress, 0.2, 1.1, 0, 1), 0, 1)
s.opacity = max(0, 100 * (1 - t))
if (this.persistMode && s.size >= s.maxSize) {
s.opacity = max(0, s.opacity - (selfFading ? 0.8 : 0.5))
}
}
}
display(g) {
g.push()
const visualOpacity = this.getVisualOpacity()
if (this.persistMode && this.persistentLayer) {
if (visualOpacity < 255) {
g.tint(255, visualOpacity)
}
g.image(this.persistentLayer, 0, 0)
}
g.noFill()
for (let s of this.squares) {
if (s.opacity <= 0) continue
const targetBuffer = (this.persistMode && this.persistentLayer) ? this.persistentLayer : g
targetBuffer.push()
targetBuffer.blendMode(s.blendMode)
targetBuffer.stroke(s.color)
targetBuffer.strokeWeight(s.strokeWeight)
targetBuffer.noFill()
if (!this.persistMode) {
const c = color(s.color)
c.setAlpha(s.opacity * (visualOpacity / 255))
targetBuffer.stroke(c)
}
targetBuffer.translate(this.x, this.y)
targetBuffer.rotate(s.rotation)
targetBuffer.rectMode(CENTER)
targetBuffer.rect(0, 0, s.size * s.width * 2, s.size * s.height * 2)
targetBuffer.pop()
if (this.persistMode && this.persistentLayer && targetBuffer === this.persistentLayer) {
g.push()
g.blendMode(s.blendMode)
const c = color(s.color)
c.setAlpha(visualOpacity)
g.stroke(c)
g.strokeWeight(s.strokeWeight)
g.noFill()
g.translate(this.x, this.y)
g.rotate(s.rotation)
g.rectMode(CENTER)
g.rect(0, 0, s.size * s.width * 2, s.size * s.height * 2)
g.pop()
}
}
g.pop()
}
getVisualOpacity() {
const anyAlive = this.squares.some(s => s.opacity > 0)
if (anyAlive) {
this._fadeoutStart = undefined
return 255
}
if (!this._fadeoutStart) this._fadeoutStart = this.age
const elapsed = this.age - this._fadeoutStart
const fadeDuration = this.persistMode ? 100 : 1 // Only persistent layer needs fadeout
return max(0, map(elapsed, 0, fadeDuration, 255, 0))
}
isDone() {
return this.getVisualOpacity() <= 0
}
averageOpacity() {
const squareOpacity = this.squares.length === 0 ? 0 :
this.squares.reduce((sum, s) => sum + s.opacity, 0) / this.squares.length
if (squareOpacity > 0) return squareOpacity
return this.getVisualOpacity() / 255 * 100
}
}
class VerticalStripes extends FillAnimation {
constructor(x, y, palette, traits) {
super(x, y, palette, traits)
this.stripes = []
this.reset(x, y, palette, traits)
}
reset(x, y, palette, traits) {
super.reset(x, y, palette, traits)
this.stripes = []
const count = floor(random(4, 10))
const spacing = width / count
for (let i = 0; i < count; i++) {
this.stripes.push({
x: i * spacing + random(-spacing * 0.3, spacing * 0.3),
y: random(-height * 0.2, height * 0.2),
width: random(spacing * 0.05, spacing * 0.5),
height: 0,
maxHeight: height * random(1, 7),
speed: random(3, 10) * FILL_ANIMATION_SPEED_MULT,
color: random(palette),
opacity: 100,
blendMode: this.getBlend([DIFFERENCE, ADD, SCREEN][i % 3]),
maxLife: floor(random(60, 300)),
age: 0,
selfFading: false
})
}
}
update() {
super.update()
for (let s of this.stripes) {
s.age++
if (!s.selfFading && s.age > s.maxLife) {
s.selfFading = true
}
s.height = min(s.height + s.speed * (s.selfFading ? 1.5 : 1), s.maxHeight)
if (s.selfFading || s.height >= s.maxHeight) {
s.opacity = max(0, s.opacity - (s.selfFading ? 0.8 : 0.3))
}
}
}
display(g) {
g.push()
g.noStroke()
for (let s of this.stripes) {
if (s.opacity <= 0) continue
g.push()
g.blendMode(s.blendMode)
const c = g.color(s.color)
c.setAlpha(s.opacity)
g.fill(c)
g.rectMode(CENTER)
g.rect(s.x, s.y, s.width, s.height)
g.pop()
}
g.pop()
}
isDone() {
if (this.stripes.length === 0) return true
return this.stripes.every(s => s.opacity <= 0)
}
averageOpacity() {
if (this.stripes.length === 0) return 0
return this.stripes.reduce((sum, s) => sum + s.opacity, 0) / this.stripes.length
}
}
class HorizontalStripes extends FillAnimation {
constructor(x, y, palette, traits) {
super(x, y, palette, traits)
this.stripes = []
this.reset(x, y, palette, traits)
}
reset(x, y, palette, traits) {
super.reset(x, y, palette, traits)
this.stripes = []
const verticalBuffer = 0.25
const baseHeight = globalBounds ? (globalBounds.maxY - globalBounds.minY) : height
const baseMinY = globalBounds ? globalBounds.minY : 0
const baseMaxY = globalBounds ? globalBounds.maxY : height
const maskMinY = baseMinY - baseHeight * verticalBuffer
const maskMaxY = baseMaxY + baseHeight * verticalBuffer
const maskHeight = maskMaxY - maskMinY
const maskMinX = globalBounds ? globalBounds.minX : 0
const maskMaxX = globalBounds ? globalBounds.maxX : width
const maskWidth = maskMaxX - maskMinX
const count = floor(random(5, 15))
const spacing = maskHeight / count
const widthMode = random(['wide', 'narrow', 'variable'])
for (let i = 0; i < count; i++) {
let stripeHeight
if (widthMode === 'wide') {
stripeHeight = spacing * random(0.7, 1.2)
} else if (widthMode === 'narrow') {
stripeHeight = spacing * random(0.05, 0.25)
} else {
stripeHeight = random(spacing * 0.1, spacing * 1.3)
}
this.stripes.push({
x: random(maskMinX - maskWidth * 0.3, maskMinX + maskWidth * 1.5),
y: maskMinY + i * spacing + random(-spacing * 0.3, spacing * 0.3),
width: 0,
maxWidth: maskWidth * random(1.2, 2.0),
height: stripeHeight,
speed: random(2, 4.5) * FILL_ANIMATION_SPEED_MULT,
color: random(palette),
opacity: 100,
blendMode: this.getBlend([DIFFERENCE, ADD, MULTIPLY][i % 3]),
maxLife: floor(random(60, 300)),
age: 0,
selfFading: false
})
}
}
update() {
super.update()
for (let s of this.stripes) {
s.age++
if (!s.selfFading && s.age > s.maxLife) {
s.selfFading = true
}
s.width = min(s.width + s.speed * (s.selfFading ? 1.5 : 1), s.maxWidth)
if (s.selfFading || s.width >= s.maxWidth) {
s.opacity = max(0, s.opacity - (s.selfFading ? 0.8 : 0.3))
}
}
}
display(g) {
g.push()
g.noStroke()
for (let s of this.stripes) {
if (s.opacity <= 0) continue
g.push()
g.blendMode(s.blendMode)
const c = g.color(s.color)
c.setAlpha(s.opacity)
g.fill(c)
g.rectMode(CENTER)
g.rect(s.x, s.y, s.width, s.height)
g.pop()
}
g.pop()
}
isDone() {
if (this.stripes.length === 0) return true
return this.stripes.every(s => s.opacity <= 0)
}
averageOpacity() {
if (this.stripes.length === 0) return 0
return this.stripes.reduce((sum, s) => sum + s.opacity, 0) / this.stripes.length
}
}
class GradientFill extends FillAnimation {
constructor(x, y, palette, traits) {
super(x, y, palette, traits)
this.fadeInDuration = 60
this.radius = 0
this.prevRadius = 0
this.maxRadius = max(width, height) * 1.5
this.speed = random(1.5, 3) * FILL_ANIMATION_SPEED_MULT
this.colors = []
this.persistentLayer = createGraphics(width, height)
this.angle = random(TWO_PI)
this.lifetime = 0
this.fadeStartTime = null
const colorCount = floor(random(3, 6))
for (let i = 0; i < colorCount; i++) {
this.colors.push(random(palette))
}
}
reset(x, y, palette, traits) {
super.reset(x, y, palette, traits)
this.radius = 0
this.prevRadius = 0
this.maxRadius = max(width, height) * 1.5
this.speed = random(1.5, 3) * FILL_ANIMATION_SPEED_MULT
this.colors = []
this.angle = random(TWO_PI)
this.lifetime = 0
this.fadeStartTime = null
if (!this.persistentLayer) {
this.persistentLayer = createGraphics(width, height)
}
this.persistentLayer.clear()
const colorCount = floor(random(3, 20))
for (let i = 0; i < colorCount; i++) {
this.colors.push(random(palette))
}
}
update() {
super.update()
this.lifetime++
this.prevRadius = this.radius
this.radius = min(this.radius + this.speed, this.maxRadius)
if (this.radius >= this.maxRadius && this.fadeStartTime === null) {
this.fadeStartTime = this.lifetime
}
if (this.radius > this.prevRadius && this.radius < this.maxRadius) {
const pl = this.persistentLayer
pl.push()
pl.noStroke()
pl.blendMode(BLEND)
const r = this.radius
const t = r / this.maxRadius
const colorIndex = floor(t * (this.colors.length - 1))
const nextColorIndex = min(colorIndex + 1, this.colors.length - 1)
const localT = (t * (this.colors.length - 1)) % 1
const c = lerpColor(color(this.colors[colorIndex]), color(this.colors[nextColorIndex]), localT)
pl.noFill()
pl.stroke(c)
pl.strokeWeight(this.speed + 1)
pl.circle(this.x, this.y, r * 2)
pl.pop()
}
}
getVisualOpacity() {
if (this.fadeStartTime === null) {
return 255
}
const fadeTime = 150
const timeSinceFade = this.lifetime - this.fadeStartTime
return max(0, map(timeSinceFade, 0, fadeTime, 255, 0))
}
display(g) {
g.push()
const opacity = this.getVisualOpacity()
if (opacity < 255) {
g.tint(255, opacity)
}
g.image(this.persistentLayer, 0, 0)
g.pop()
}
isDone() {
return this.getVisualOpacity() <= 0
}
averageOpacity() {
return this.getVisualOpacity() / 255 * 100
}
}
class WavyLines extends FillAnimation {
constructor(x, y, palette, traits) {
super(x, y, palette, traits)
this.fadeInDuration = 60
this.lines = []
this.trailLayer = null // Decaying trail buffer — always active
this.directionMode = traits.wavyDirection || 'chaos'
this.noiseOffset = random(100) // Unique noise offset per instance
this.diagonalAngle = null // Locked per instance for diagonal mode
const lineCount = floor(random(10, 24))
for (let i = 0; i < lineCount; i++) {
const orientation = this.pickOrientation()
this.lines.push(this.createLine(orientation, palette, i))
}
}
pickOrientation() {
const mode = this.directionMode
if (mode === 'horizontal-clash') {
return random() < 0.5 ? 'horizontal-right' : 'horizontal-left'
}
if (mode === 'chaos') {
const dirs = ['horizontal-right', 'horizontal-left', 'vertical-down', 'vertical-up',
'diagonal-down-right', 'diagonal-down-left', 'diagonal-up-right', 'diagonal-up-left']
return random(dirs)
}
return mode
}
createLine(orientation, palette, index) {
const fullSpan = max(width, height) * 1.5 // Extended for diagonal lines
const crossSpan = orientation === 'horizontal' ? height : width
const spanType = random()
let startT, endT
if (spanType < 0.3) {
startT = 0
endT = 1
} else if (spanType < 0.6) {
startT = 0
endT = random(0.3, 0.8)
} else if (spanType < 0.8) {
startT = random(0.2, 0.7)
endT = 1
} else {
startT = random(0.1, 0.4)
endT = random(0.6, 0.9)
}
const baseFreq = random(1.5, 4)
const harmonics = [
{ freq: baseFreq, amp: random(20, 10) * _designScale, phase: random(TWO_PI) },
{ freq: baseFreq * random(1.8, 2.5), amp: random(8, 25) * _designScale, phase: random(TWO_PI) },
{ freq: baseFreq * random(0.3, 0.6), amp: random(15, 40) * _designScale, phase: random(TWO_PI) },
]
const noiseSeedX = random(100)
const noiseSeedY = random(1000)
const numSegments = 40
const baseOffset = -random(100, 300)
const baseSpeed = random(0.2, 2.8) * FILL_ANIMATION_SPEED_MULT
const segments = []
for (let i = 0; i <= numSegments; i++) {
const t = i / numSegments
const speedVariation = random(0.4, 1.6) // Some segments faster, some slower
segments.push({
offset: baseOffset,
speed: baseSpeed * speedVariation,
speedNoiseSeed: random(1000),
})
}
let angle, moveAngle
if (orientation === 'horizontal-right') {
angle = 0
moveAngle = PI / 2 // Move downward (perpendicular)
} else if (orientation === 'horizontal-left') {
angle = 0
moveAngle = -PI / 2 // Move upward (perpendicular)
} else if (orientation === 'vertical-down') {
angle = PI / 2
moveAngle = 0 // Move rightward (perpendicular)
} else if (orientation === 'vertical-up') {
angle = PI / 2
moveAngle = PI // Move leftward (perpendicular)
} else if (orientation === 'diagonal-down-right') {
angle = random(PI / 6, PI / 3) // 30-60 degrees
moveAngle = angle + PI / 2 // Perpendicular movement
} else if (orientation === 'diagonal-down-left') {
angle = random(2 * PI / 3, 5 * PI / 6) // 120-150 degrees
moveAngle = angle + PI / 2
} else if (orientation === 'diagonal-up-right') {
angle = random(-PI / 3, -PI / 6) // -30 to -60 degrees
moveAngle = angle + PI / 2
} else if (orientation === 'diagonal-up-left') {
angle = random(-5 * PI / 6, -2 * PI / 3) // -120 to -150 degrees
moveAngle = angle + PI / 2
} else {
angle = random(TWO_PI)
moveAngle = angle + PI / 2
}
const startX = random(-width * 0.3, width * 1.3)
const startY = random(-height * 0.3, height * 1.3)
return {
startX: startX,
startY: startY,
angle: angle, // Direction the line extends
moveAngle: moveAngle, // Direction the line travels
baseOffset: baseOffset,
maxOffset: random(800, 2000) * _designScale, // Travel distance before starting to check exit
baseSpeed: baseSpeed,
segments: segments,
harmonics: harmonics,
thickness: random(3, 25) * _designScale,
thicknessVariation: random(0.3, 10.8) * _designScale,
color: random(palette),
opacity: 100,
isFading: false, // Once true, line commits to fading out (no flicker)
blendMode: this.getBlend([DIFFERENCE, ADD, MULTIPLY][index % 3]),
orientation: orientation, // Keep for reference
startT: startT,
endT: endT,
noiseSeedX: noiseSeedX,
noiseSeedY: noiseSeedY,
noiseScale: random(0.003, 0.08),
noiseAmp: random(15, 40) * _designScale,
dripFactor: random(0.5, 1.5),
phaseSpeed: random(0.01, 0.04),
birthTime: this.age || 0,
maxLife: floor(random(300, 700)), // 12-29 seconds at 24fps
fadeInDuration: random(30, 60),
speedEvolution: random(0.005, 0.02),
speedNoiseScale: random(0.01, 0.03),
lineLength: random(width * 0.8, width * 1.4), // How long the line extends
}
}
reset(x, y, palette, traits) {
super.reset(x, y, palette, traits)
this.lines = []
this.directionMode = traits.wavyDirection || 'chaos'
this.noiseOffset = random(1000)
if (!this.trailLayer) {
this.trailLayer = createGraphics(width, height)
this.trailLayer.colorMode(HSB, 360, 100, 100, 100)
}
this.trailLayer.clear()
this._trailDecayStart = undefined
this._fadeoutStart = undefined
const lineCount = floor(random(10, 24))
for (let i = 0; i < lineCount; i++) {
const orientation = this.pickOrientation()
this.lines.push(this.createLine(orientation, palette, i))
}
}
getWaveDisplacement(l, t, time) {
const pos = t * l.lineLength
let wave = 0
for (const h of l.harmonics) {
const animatedPhase = h.phase + time * l.phaseSpeed
wave += sin(pos * 0.02 * h.freq + animatedPhase) * h.amp
}
const noiseVal = noise(
l.noiseSeedX + pos * l.noiseScale,
l.noiseSeedY + time * 0.01 + this.noiseOffset
)
const noiseDisplacement = (noiseVal - 0.5) * 2 * l.noiseAmp
const ampModulation = noise(l.noiseSeedX + 100, t * 3) * 0.5 + 0.5
return (wave * ampModulation + noiseDisplacement) * l.dripFactor
}
getThicknessAt(l, t) {
const thickNoise = noise(l.noiseSeedX + 200, t * 4)
const variation = 1 - l.thicknessVariation + thickNoise * l.thicknessVariation * 2
return l.thickness * variation
}
update() {
super.update()
const time = this.age
const padding = 100
const minX = -padding
const maxX = width + padding
const minY = -padding
const maxY = height + padding
for (let l of this.lines) {
let maxSegmentOffset = -Infinity
let minSegmentOffset = Infinity
for (let i = 0; i < l.segments.length; i++) {
const seg = l.segments[i]
const t = i / (l.segments.length - 1)
const speedNoise = noise(
seg.speedNoiseSeed + time * l.speedEvolution,
t * 3
)
const dynamicSpeedMult = 0.3 + speedNoise * 1.4
seg.offset += seg.speed * dynamicSpeedMult
maxSegmentOffset = max(maxSegmentOffset, seg.offset)
minSegmentOffset = min(minSegmentOffset, seg.offset)
}
l.currentMaxOffset = maxSegmentOffset
l.currentMinOffset = minSegmentOffset
let isOffCanvas = true
const numSegments = l.segments.length - 1
for (let i = 0; i <= numSegments && isOffCanvas; i++) {
const t = i / numSegments
if (t < l.startT || t > l.endT) continue
const seg = l.segments[i]
const wave = this.getWaveDisplacement(l, t, time)
const alongLine = t * l.lineLength - l.lineLength / 2
const baseX = l.startX + cos(l.angle) * alongLine
const baseY = l.startY + sin(l.angle) * alongLine
const px = baseX + cos(l.moveAngle) * (seg.offset + wave)
const py = baseY + sin(l.moveAngle) * (seg.offset + wave)
if (px >= minX && px <= maxX && py >= minY && py <= maxY) {
isOffCanvas = false
}
}
const lineAge = this.age - l.birthTime
const pastMaxLife = lineAge > l.maxLife
if (l.isFading) {
l.opacity = max(0, l.opacity - 0.15)
} else if (isOffCanvas && maxSegmentOffset > 0) {
l.isFading = true
l.opacity = max(0, l.opacity - 0.15)
} else if (pastMaxLife) {
l.isFading = true
l.opacity = max(0, l.opacity - 0.25)
}
}
}
isDone() {
return this.getVisualOpacity() <= 0
}
getVisualOpacity() {
const anyAlive = this.lines.some(l => l.opacity > 0)
if (anyAlive) {
this._fadeoutStart = undefined
return 255
}
if (!this._fadeoutStart) this._fadeoutStart = this.age
const elapsed = this.age - this._fadeoutStart
const fadeDuration = 150 // ~6 seconds at 24fps
return max(0, map(elapsed, 0, fadeDuration, 255, 0))
}
averageOpacity() {
return this.getVisualOpacity() / 255 * 100
}
display(g) {
g.push()
const visualOpacity = this.getVisualOpacity()
if (this.trailLayer) {
this.trailLayer.push()
this.trailLayer.blendMode(BLEND)
this.trailLayer.noStroke()
this.trailLayer.fill(0, 0, 0, 3) // ~3% decay per frame
this.trailLayer.rect(0, 0, width, height)
this.trailLayer.pop()
}
const time = this.age
for (let l of this.lines) {
const lineAge = time - l.birthTime
const fadeInAlpha = min(1, lineAge / l.fadeInDuration)
const effectiveOpacity = l.opacity * fadeInAlpha * (visualOpacity / 255)
if (effectiveOpacity <= 0) continue
const numSegments = l.segments.length - 1
const segmentData = []
for (let i = 0; i < numSegments; i++) {
const t1 = i / numSegments
const t2 = (i + 1) / numSegments
if (t2 < l.startT || t1 > l.endT) continue
const segT1 = max(t1, l.startT)
const segT2 = min(t2, l.endT)
let edgeFade = 1
const edgeWidth = 0.1
if (segT1 < l.startT + edgeWidth) {
edgeFade *= map(segT1, l.startT, l.startT + edgeWidth, 0, 1)
}
if (segT2 > l.endT - edgeWidth) {
edgeFade *= map(segT2, l.endT - edgeWidth, l.endT, 1, 0)
}
const midT = (segT1 + segT2) / 2
const thickness = this.getThicknessAt(l, midT)
const segIdx1 = floor(t1 * numSegments)
const segIdx2 = min(segIdx1 + 1, numSegments)
const localT = (t1 * numSegments) - segIdx1
const offset1 = lerp(l.segments[segIdx1].offset, l.segments[segIdx2].offset, localT)
const segIdx3 = floor(t2 * numSegments)
const segIdx4 = min(segIdx3 + 1, numSegments)
const localT2 = (t2 * numSegments) - segIdx3
const offset2 = lerp(l.segments[segIdx3].offset, l.segments[segIdx4].offset, localT2)
const alongLine1 = segT1 * l.lineLength - l.lineLength / 2
const alongLine2 = segT2 * l.lineLength - l.lineLength / 2
const baseX1 = l.startX + cos(l.angle) * alongLine1
const baseY1 = l.startY + sin(l.angle) * alongLine1
const baseX2 = l.startX + cos(l.angle) * alongLine2
const baseY2 = l.startY + sin(l.angle) * alongLine2
const wave1 = this.getWaveDisplacement(l, segT1, time)
const wave2 = this.getWaveDisplacement(l, segT2, time)
const x1 = baseX1 + cos(l.moveAngle) * (offset1 + wave1)
const y1 = baseY1 + sin(l.moveAngle) * (offset1 + wave1)
const x2 = baseX2 + cos(l.moveAngle) * (offset2 + wave2)
const y2 = baseY2 + sin(l.moveAngle) * (offset2 + wave2)
segmentData.push({ x1, y1, x2, y2, thickness, edgeFade })
}
if (this.trailLayer) {
this.trailLayer.push()
this.trailLayer.blendMode(l.blendMode)
this.trailLayer.noFill()
for (const seg of segmentData) {
const trailColor = color(l.color)
trailColor.setAlpha(effectiveOpacity * seg.edgeFade * 0.5) // Half opacity for trail
this.trailLayer.stroke(trailColor)
this.trailLayer.strokeWeight(seg.thickness)
this.trailLayer.line(seg.x1, seg.y1, seg.x2, seg.y2)
}
this.trailLayer.pop()
}
g.push()
g.blendMode(l.blendMode)
g.noFill()
for (const seg of segmentData) {
const coreColor = color(l.color)
coreColor.setAlpha(effectiveOpacity * seg.edgeFade)
g.stroke(coreColor)
g.strokeWeight(seg.thickness)
g.line(seg.x1, seg.y1, seg.x2, seg.y2)
}
g.pop()
}
if (this.trailLayer) {
g.push()
if (visualOpacity < 255) {
g.tint(255, visualOpacity)
}
g.image(this.trailLayer, 0, 0)
g.pop()
}
g.pop()
}
}
class Sparkles extends FillAnimation {
constructor(x, y, palette, traits) {
super(x, y, palette, traits)
this.fadeInDuration = 60
this.sparklePaths = []
this.lifetime = 0
this.maxLifetime = random(120, 250)
this.maxActivePaths = 40
this.palette = palette
this.persistentLayer = null
}
reset(x, y, palette, traits) {
super.reset(x, y, palette, traits)
this.sparklePaths = []
this.lifetime = 0
this.maxLifetime = random(120, 250)
this.palette = palette
if (!this.persistentLayer) {
this.persistentLayer = createGraphics(width, height)
}
this.persistentLayer.clear()
for (let i = 0; i < this.maxActivePaths; i++) {
this.sparklePaths.push(this.createSparkle())
}
}
createSparkle() {
const angle = random(TWO_PI)
const speed = random(1, 3) * FILL_ANIMATION_SPEED_MULT
return {
x: this.x,
y: this.y,
vx: cos(angle) * speed,
vy: sin(angle) * speed,
size: random(10, 50) * _designScale,
color: random(this.palette),
life: random(0, 80),
maxLife: random(60, 180)
}
}
resetSparkle(s) {
const angle = random(TWO_PI)
const speed = random(1, 3) * FILL_ANIMATION_SPEED_MULT
s.x = this.x
s.y = this.y
s.vx = cos(angle) * speed
s.vy = sin(angle) * speed
s.size = random(10, 50) * _designScale
s.color = random(this.palette)
s.life = 0
s.maxLife = random(60, 180)
}
update() {
super.update()
this.lifetime++
const stillSpawning = this.lifetime < this.maxLifetime
const pl = this.persistentLayer
pl.push()
pl.noStroke()
pl.blendMode(BLEND)
const bounds = globalBounds
const hasB = !!bounds
const bounce = 0.7
for (let i = 0; i < this.sparklePaths.length; i++) {
const s = this.sparklePaths[i]
pl.fill(s.color)
pl.circle(s.x, s.y, s.size)
let x = s.x + s.vx
let y = s.y + s.vy
if (hasB) {
if (x < bounds.minX) { x = bounds.minX; s.vx = Math.abs(s.vx) * bounce }
else if (x > bounds.maxX) { x = bounds.maxX; s.vx = -Math.abs(s.vx) * bounce }
if (y < bounds.minY) { y = bounds.minY; s.vy = Math.abs(s.vy) * bounce }
else if (y > bounds.maxY) { y = bounds.maxY; s.vy = -Math.abs(s.vy) * bounce }
}
s.x = x
s.y = y
s.life++
if (s.life > s.maxLife) {
if (stillSpawning) {
this.resetSparkle(s)
}
}
}
pl.pop()
}
display(g) {
g.push()
const visualOpacity = this.getVisualOpacity()
if (visualOpacity < 255) {
g.tint(255, visualOpacity)
}
g.image(this.persistentLayer, 0, 0)
g.pop()
}
getVisualOpacity() {
if (this.lifetime < this.maxLifetime) {
return 255
}
const fadeTime = 150
const timeSinceEnd = this.lifetime - this.maxLifetime
return max(0, map(timeSinceEnd, 0, fadeTime, 255, 0))
}
isDone() {
return this.getVisualOpacity() <= 0
}
averageOpacity() {
return this.getVisualOpacity() / 255 * 100
}
}
class VoronoiCells extends FillAnimation {
constructor(x, y, palette, traits) {
super(x, y, palette, traits)
this.cells = []
const cellCount = floor(random(4, 10))
for (let i = 0; i < cellCount; i++) {
const angle = random(TWO_PI)
const dist = random(20, 80) * _designScale
this.cells.push({
x: this.x + cos(angle) * dist,
y: this.y + sin(angle) * dist,
vx: cos(angle) * random(0.1, 0.4) * FILL_ANIMATION_SPEED_MULT,
vy: sin(angle) * random(0.1, 0.4) * FILL_ANIMATION_SPEED_MULT,
radius: random(15, 35) * _designScale,
targetRadius: random(60, 120) * _designScale,
color: random(palette),
opacity: 100,
blendMode: this.getBlend([DIFFERENCE, ADD, MULTIPLY][i % 3]),
growthSpeed: random(0.3, 0.8) * FILL_ANIMATION_SPEED_MULT,
maxLife: floor(random(80, 400)),
age: 0,
selfFading: false
})
}
}
reset(x, y, palette, traits) {
super.reset(x, y, palette, traits)
this.cells = []
const cellCount = floor(random(4, 10))
for (let i = 0; i < cellCount; i++) {
const angle = random(TWO_PI)
const dist = random(20, 80) * _designScale
this.cells.push({
x: this.x + cos(angle) * dist,
y: this.y + sin(angle) * dist,
vx: cos(angle) * random(0.1, 0.4) * FILL_ANIMATION_SPEED_MULT,
vy: sin(angle) * random(0.1, 0.4) * FILL_ANIMATION_SPEED_MULT,
radius: random(15, 35) * _designScale,
targetRadius: random(60, 120) * _designScale,
color: random(palette),
opacity: 100,
blendMode: this.getBlend([DIFFERENCE, ADD, MULTIPLY][i % 3]),
growthSpeed: random(0.3, 0.8) * FILL_ANIMATION_SPEED_MULT,
maxLife: floor(random(80, 400)),
age: 0,
selfFading: false
})
}
}
update() {
super.update()
for (let c of this.cells) {
c.age++
if (!c.selfFading && c.age > c.maxLife) {
c.selfFading = true
}
if (c.radius < c.targetRadius) {
c.radius += c.growthSpeed
}
let newX = c.x + c.vx
let newY = c.y + c.vy
const constrained = constrainToMaskBounds(newX, newY, c.vx, c.vy)
c.x = constrained.x
c.y = constrained.y
c.vx = constrained.vx
c.vy = constrained.vy
const pulse = sin(this.age * 0.05) * 3
c.displayRadius = c.radius + pulse
if (c.selfFading) {
c.opacity = max(0, c.opacity - 0.6)
}
}
}
display(g) {
for (let c of this.cells) {
if (c.opacity <= 0) continue
g.push()
g.blendMode(c.blendMode)
const col = g.color(c.color)
col.setAlpha(c.opacity)
g.fill(col)
g.noStroke()
g.circle(c.x, c.y, c.displayRadius * 2)
g.pop()
}
}
isDone() {
if (this.cells.length === 0) return true
return this.cells.every(c => c.opacity <= 0)
}
averageOpacity() {
if (this.cells.length === 0) return 0
return this.cells.reduce((sum, c) => sum + c.opacity, 0) / this.cells.length
}
}
class AnimationPool {
constructor() {
this.pools = {
Supernova: [],
ExpandingCircles: [],
ExpandingSquares: [],
VerticalStripes: [],
HorizontalStripes: [],
GradientFill: [],
WavyLines: [],
Sparkles: [],
VoronoiCells: [],
Shockwave: [],
}
this.maxPoolSize = 100
}
createNew(type) {
switch (type) {
case 'Supernova':
return new Supernova(0, 0, [], 0, [0, 0], {})
case 'ExpandingCircles':
return new ExpandingCircles(0, 0, [], {})
case 'ExpandingSquares':
return new ExpandingSquares(0, 0, [], {})
case 'VerticalStripes':
return new VerticalStripes(0, 0, [], {})
case 'HorizontalStripes':
return new HorizontalStripes(0, 0, [], {})
case 'GradientFill':
return new GradientFill(0, 0, [], {})
case 'WavyLines':
return new WavyLines(0, 0, [], {})
case 'Sparkles':
return new Sparkles(0, 0, [], {})
case 'VoronoiCells':
return new VoronoiCells(0, 0, [], {})
case 'Shockwave':
return new Shockwave(0, 0, [], {})
default:
return null
}
}
acquire(type, x, y, palette, ...extraArgs) {
const pool = this.pools[type]
let obj = pool.pop()
if (!obj) {
obj = this.createNew(type)
}
obj.reset(x, y, palette, ...extraArgs)
return obj
}
release(obj) {
const type = obj.constructor.name
const pool = this.pools[type]
if (pool && pool.length < this.maxPoolSize) {
if (obj.persistentLayer) {
obj.persistentLayer.clear()
}
if (obj.trailLayer) {
obj.trailLayer.clear()
}
pool.push(obj)
} else {
if (obj.persistentLayer) {
obj.persistentLayer.remove()
obj.persistentLayer = null
}
if (obj.trailLayer) {
obj.trailLayer.remove()
obj.trailLayer = null
}
}
}
clear() {
Object.keys(this.pools).forEach(key => {
this.pools[key] = []
})
}
}
let animationPool
let TRAITS, PARAMS
let fillAnimations = []
let orbitingElements = []
let starburstRotation = 0
let nextSpawnFrame = 0
let originCenters = []
let tidalAngle = 0
let eyesFlickerStart = 30
let eyesFlickerDuration = 35
let eyesFullyOn = false
let regenStartFrame = 0
let staticLayerCache
let formPart1Cache
let formPart2Cache
let maskSilhouetteCache
let fillBuffer
let fillPersistentLayer // Composites all current fill animations per frame
let eyesCache
let eyesBlockingCache
let omniaEyesBase
let orbitBuffer
let meltBuffer
let corruptBuffer
let finalComposite
let finalShadedBuffer
let asciiShader
let clipBuffer // temp buffer for mask clipping compositing
let masks = []
let maskBoundsCache = []
let globalBounds = null
let isDirty = true
function breathWave(t, hz) {
const cycleTime = 1.0 / hz
const phase = (t % cycleTime) / cycleTime
if (phase < 0.65) {
const p = phase / 0.65
const sine = Math.sin(p * Math.PI)
return Math.pow(sine, 0.7)
} else {
return 0.0
}
}
function getBreathTime() {
if (_breathStartMillis === null) _breathStartMillis = millis()
return (millis() - _breathStartMillis) / 1000.0
}
function formBreathOffsetPart2() {
const t = getBreathTime()
const wave = breathWave(t, FORM_BREATH_HZ)
const amp = TRAITS.form === "Armatus" ? FORM_BREATH_AMP * 0.5 : FORM_BREATH_AMP
return wave * amp
}
function formBreathOffsetPart1() {
const t = getBreathTime()
const delayedT = t - BREATH_DELAY_PART1
const wave = breathWave(delayedT, FORM_BREATH_HZ)
const amp = TRAITS.form === "Armatus" ? FORM_BREATH_AMP * 0.5 : FORM_BREATH_AMP
return wave * amp
}
function maskBreathOffset() {
const t = getBreathTime()
const delayedT = t - BREATH_DELAY_MASK
const wave = breathWave(delayedT, FORM_BREATH_HZ)
const amp = TRAITS.form === "Armatus" ? FORM_BREATH_AMP * 0.5 : FORM_BREATH_AMP
return wave * amp
}
function getEyeOpacity() {
if (eyesFullyOn) return 1.0
const relativeFrame = frameCount - regenStartFrame
const flickerEnd = eyesFlickerStart + eyesFlickerDuration
if (relativeFrame < eyesFlickerStart) {
return 0.0
} else if (relativeFrame >= flickerEnd) {
eyesFullyOn = true
return 1.0
} else {
const flickerProgress = (relativeFrame - eyesFlickerStart) / eyesFlickerDuration
const flickerSpeed = map(flickerProgress, 0, 1, 8, 2)
const baseOpacity = map(flickerProgress, 0, 1, 0.3, 1.0)
const flicker = noise(frameCount * flickerSpeed) > 0.4 ? 1.0 : 0.0
return baseOpacity * flicker
}
}
function calculateMaskBounds() {
maskBoundsCache = []
for (const m of masks) {
const padding = 20
const bounds = {
minX: m.x - m.w / 2 + padding,
maxX: m.x + m.w / 2 - padding,
minY: m.y - m.h / 2 + padding,
maxY: m.y + m.h / 2 - padding,
centerX: m.x,
centerY: m.y,
w: m.w,
h: m.h,
mask: m
}
maskBoundsCache.push(bounds)
}
calculateGlobalBounds()
}
function calculateGlobalBounds() {
if (maskBoundsCache.length === 0) {
globalBounds = null
return
}
let minX = Infinity, maxX = -Infinity
let minY = Infinity, maxY = -Infinity
for (const bounds of maskBoundsCache) {
minX = min(minX, bounds.minX)
maxX = max(maxX, bounds.maxX)
minY = min(minY, bounds.minY)
maxY = max(maxY, bounds.maxY)
}
globalBounds = {
minX: minX,
maxX: maxX,
minY: minY,
maxY: maxY,
centerX: (minX + maxX) / 2,
centerY: (minY + maxY) / 2
}
}
function getRandomPointInMasks() {
if (maskBoundsCache.length === 0) return { x: width / 2, y: height / 2 }
const bounds = random(maskBoundsCache)
return {
x: random(bounds.minX, bounds.maxX),
y: random(bounds.minY, bounds.maxY)
}
}
function co