| 1 |
(function () { |
| 2 |
|
| 3 |
var _typoKeys = { |
| 4 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 5 |
decoration:'text-decoration', transform:'text-transform', |
| 6 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 7 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 8 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 9 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 10 |
}; |
| 11 |
function typoCssVarsForEl(el, obj, prefix) { |
| 12 |
if (!obj || typeof obj !== 'object') return; |
| 13 |
Object.keys(_typoKeys).forEach(function (k) { |
| 14 |
var v = obj[k]; |
| 15 |
if (v === undefined || v === '' || v === null) return; |
| 16 |
if (k === 'sizeDesktop' || k === 'sizeTablet' || k === 'sizeMobile') v = v + (obj.sizeUnit || 'px'); |
| 17 |
else if (k === 'lineHeightDesktop' || k === 'lineHeightTablet' || k === 'lineHeightMobile') v = v + (obj.lineHeightUnit || ''); |
| 18 |
else if (k === 'letterSpacingDesktop' || k === 'letterSpacingTablet' || k === 'letterSpacingMobile') v = v + (obj.letterSpacingUnit || 'px'); |
| 19 |
else if (k === 'wordSpacingDesktop' || k === 'wordSpacingTablet' || k === 'wordSpacingMobile') v = v + (obj.wordSpacingUnit || 'px'); |
| 20 |
el.style.setProperty(prefix + _typoKeys[k], String(v)); |
| 21 |
}); |
| 22 |
} |
| 23 |
|
| 24 |
function easeOut(t) { return 1 - Math.pow(1 - t, 3); } |
| 25 |
function easeIn(t) { return t * t * t; } |
| 26 |
function linear(t) { return t; } |
| 27 |
function spring(t) { var c4 = (2*Math.PI)/3; return t===0?0:t===1?1:Math.pow(2,-10*t)*Math.sin((t*10-0.75)*c4)+1; } |
| 28 |
|
| 29 |
function getEaseFn(name) { |
| 30 |
return { 'ease-out':easeOut, 'ease-in':easeIn, 'linear':linear, 'spring':spring }[name] || easeOut; |
| 31 |
} |
| 32 |
|
| 33 |
function hexToRgb(hex) { |
| 34 |
var r=parseInt((hex||'#7c3aed').replace('#','').substr(0,2),16); |
| 35 |
var g=parseInt((hex||'#7c3aed').replace('#','').substr(2,2),16); |
| 36 |
var b=parseInt((hex||'#7c3aed').replace('#','').substr(4,2),16); |
| 37 |
return [r,g,b]; |
| 38 |
} |
| 39 |
|
| 40 |
function lerpRgb(c1, c2, t) { |
| 41 |
return [ |
| 42 |
Math.round(c1[0]+(c2[0]-c1[0])*t), |
| 43 |
Math.round(c1[1]+(c2[1]-c1[1])*t), |
| 44 |
Math.round(c1[2]+(c2[2]-c1[2])*t) |
| 45 |
]; |
| 46 |
} |
| 47 |
|
| 48 |
function initParticleText(appEl) { |
| 49 |
var opts; |
| 50 |
try { opts = JSON.parse(appEl.getAttribute('data-opts') || '{}'); } catch(e){ opts={}; } |
| 51 |
|
| 52 |
var a = { |
| 53 |
text: opts.text || 'BLOCKENBERG', |
| 54 |
subtext: opts.subtext || '', |
| 55 |
sampleFontSize:opts.sampleFontSize|| 100, |
| 56 |
fontFamily: opts.fontFamily || 'system-ui, sans-serif', |
| 57 |
fontWeight: opts.fontWeight || '900', |
| 58 |
particleSize: opts.particleSize || 2, |
| 59 |
particleGap: opts.particleGap || 4, |
| 60 |
particleColor: opts.particleColor || '#7c3aed', |
| 61 |
particleColor2:opts.particleColor2||'#ec4899', |
| 62 |
useGradient: opts.useGradient !== false, |
| 63 |
bgColor: opts.bgColor || '#0a0a0f', |
| 64 |
subtextColor: opts.subtextColor || '#94a3b8', |
| 65 |
subtextSize: opts.subtextSize || 18, |
| 66 |
canvasHeight: opts.canvasHeight || 300, |
| 67 |
scatterRadius: opts.scatterRadius || 500, |
| 68 |
enterDuration: opts.enterDuration || 1200, |
| 69 |
easing: opts.easing || 'ease-out', |
| 70 |
hoverEffect: opts.hoverEffect !== false, |
| 71 |
hoverRadius: opts.hoverRadius || 80, |
| 72 |
paddingTop: opts.paddingTop || 80, |
| 73 |
paddingBottom: opts.paddingBottom || 80, |
| 74 |
subtextTypo: opts.subtextTypo || {} |
| 75 |
}; |
| 76 |
|
| 77 |
/* Section */ |
| 78 |
var section = document.createElement('section'); |
| 79 |
section.className = 'bkbg-pt-section'; |
| 80 |
section.style.setProperty('--bkbg-pt-bg', a.bgColor); |
| 81 |
section.style.setProperty('--bkbg-pt-pt', a.paddingTop+'px'); |
| 82 |
section.style.setProperty('--bkbg-pt-pb', a.paddingBottom+'px'); |
| 83 |
section.style.setProperty('--bkbg-pt-sub-size', a.subtextSize+'px'); |
| 84 |
section.style.setProperty('--bkbg-pt-sub-color',a.subtextColor); |
| 85 |
|
| 86 |
typoCssVarsForEl(section, a.subtextTypo, '--bkbg-pt-st-'); |
| 87 |
|
| 88 |
var wrap = document.createElement('div'); |
| 89 |
wrap.className = 'bkbg-pt-canvas-wrap'; |
| 90 |
|
| 91 |
var canvas = document.createElement('canvas'); |
| 92 |
canvas.className = 'bkbg-pt-canvas'; |
| 93 |
canvas.style.height = a.canvasHeight + 'px'; |
| 94 |
|
| 95 |
wrap.appendChild(canvas); |
| 96 |
section.appendChild(wrap); |
| 97 |
|
| 98 |
var subtextEl = null; |
| 99 |
if (a.subtext) { |
| 100 |
subtextEl = document.createElement('p'); |
| 101 |
subtextEl.className = 'bkbg-pt-subtext'; |
| 102 |
subtextEl.textContent = a.subtext; |
| 103 |
section.appendChild(subtextEl); |
| 104 |
} |
| 105 |
|
| 106 |
appEl.replaceWith(section); |
| 107 |
|
| 108 |
/* ------- Canvas setup ------- */ |
| 109 |
var ctx = canvas.getContext('2d'); |
| 110 |
var pixelRatio = window.devicePixelRatio || 1; |
| 111 |
var W, H; |
| 112 |
var particles = []; |
| 113 |
var easeFn = getEaseFn(a.easing); |
| 114 |
var c1 = hexToRgb(a.particleColor); |
| 115 |
var c2 = hexToRgb(a.particleColor2); |
| 116 |
var rafId = null; |
| 117 |
var startTime = null; |
| 118 |
var assembled = false; |
| 119 |
var mx = -9999, my = -9999; |
| 120 |
|
| 121 |
function sampleText() { |
| 122 |
/* Offscreen canvas to sample text pixels */ |
| 123 |
var off = document.createElement('canvas'); |
| 124 |
var fs = a.sampleFontSize; |
| 125 |
off.width = Math.max(800, W); |
| 126 |
off.height = fs * 2.5; |
| 127 |
var oc = off.getContext('2d'); |
| 128 |
oc.clearRect(0, 0, off.width, off.height); |
| 129 |
oc.font = a.fontWeight + ' ' + fs + 'px ' + a.fontFamily; |
| 130 |
oc.fillStyle = '#fff'; |
| 131 |
oc.textAlign = 'center'; |
| 132 |
oc.textBaseline = 'middle'; |
| 133 |
oc.fillText(a.text, off.width / 2, off.height / 2); |
| 134 |
|
| 135 |
var data = oc.getImageData(0, 0, off.width, off.height).data; |
| 136 |
var pts = []; |
| 137 |
var gap = a.particleGap; |
| 138 |
for (var y = 0; y < off.height; y += gap) { |
| 139 |
for (var x = 0; x < off.width; x += gap) { |
| 140 |
var idx = (y * off.width + x) * 4; |
| 141 |
if (data[idx + 3] > 128) { |
| 142 |
/* Map from off-canvas coords to main canvas coords */ |
| 143 |
var cx2 = (x / off.width) * W; |
| 144 |
var cy2 = (y / off.height) * H; |
| 145 |
pts.push({ tx: cx2, ty: cy2 }); |
| 146 |
} |
| 147 |
} |
| 148 |
} |
| 149 |
return pts; |
| 150 |
} |
| 151 |
|
| 152 |
function buildParticles() { |
| 153 |
var targets = sampleText(); |
| 154 |
particles = targets.map(function(p) { |
| 155 |
var angle = Math.random() * Math.PI * 2; |
| 156 |
var dist = Math.random() * a.scatterRadius + 50; |
| 157 |
return { |
| 158 |
tx: p.tx, ty: p.ty, |
| 159 |
x: p.tx + Math.cos(angle) * dist, |
| 160 |
y: p.ty + Math.sin(angle) * dist, |
| 161 |
ox: p.tx, oy: p.ty, /* original target */ |
| 162 |
scatterX: p.tx + Math.cos(angle) * dist, |
| 163 |
scatterY: p.ty + Math.sin(angle) * dist, |
| 164 |
color: Math.random() |
| 165 |
}; |
| 166 |
}); |
| 167 |
} |
| 168 |
|
| 169 |
function resize() { |
| 170 |
W = wrap.clientWidth || 800; |
| 171 |
H = a.canvasHeight; |
| 172 |
canvas.width = W * pixelRatio; |
| 173 |
canvas.height = H * pixelRatio; |
| 174 |
canvas.style.width = W + 'px'; |
| 175 |
canvas.style.height = H + 'px'; |
| 176 |
ctx.scale(pixelRatio, pixelRatio); |
| 177 |
buildParticles(); |
| 178 |
} |
| 179 |
|
| 180 |
function drawFrame(now) { |
| 181 |
ctx.clearRect(0, 0, W, H); |
| 182 |
|
| 183 |
var elapsed = now - (startTime || now); |
| 184 |
var t = Math.min(1, elapsed / a.enterDuration); |
| 185 |
var et = easeFn(t); |
| 186 |
|
| 187 |
for (var i = 0; i < particles.length; i++) { |
| 188 |
var p = particles[i]; |
| 189 |
|
| 190 |
if (!assembled) { |
| 191 |
p.x = p.scatterX + (p.tx - p.scatterX) * et; |
| 192 |
p.y = p.scatterY + (p.ty - p.scatterY) * et; |
| 193 |
} |
| 194 |
|
| 195 |
/* Hover repulsion */ |
| 196 |
if (a.hoverEffect && assembled) { |
| 197 |
var dx = p.x - mx, dy = p.y - my; |
| 198 |
var dist = Math.sqrt(dx*dx + dy*dy); |
| 199 |
if (dist < a.hoverRadius) { |
| 200 |
var force = (a.hoverRadius - dist) / a.hoverRadius; |
| 201 |
p.x += (dx / dist) * force * 30; |
| 202 |
p.y += (dy / dist) * force * 30; |
| 203 |
} else { |
| 204 |
p.x += (p.tx - p.x) * 0.12; |
| 205 |
p.y += (p.ty - p.y) * 0.12; |
| 206 |
} |
| 207 |
} |
| 208 |
|
| 209 |
var rgb = a.useGradient ? lerpRgb(c1, c2, p.color) : c1; |
| 210 |
var alpha = assembled ? 1 : (0.4 + et * 0.6); |
| 211 |
ctx.fillStyle = 'rgba('+rgb[0]+','+rgb[1]+','+rgb[2]+','+alpha+')'; |
| 212 |
ctx.fillRect(p.x - a.particleSize/2, p.y - a.particleSize/2, a.particleSize, a.particleSize); |
| 213 |
} |
| 214 |
|
| 215 |
if (!assembled && t >= 1) { |
| 216 |
assembled = true; |
| 217 |
if (subtextEl) subtextEl.classList.add('bkbg-pt-visible'); |
| 218 |
} |
| 219 |
} |
| 220 |
|
| 221 |
function loop(now) { |
| 222 |
if (!startTime) startTime = now; |
| 223 |
drawFrame(now); |
| 224 |
rafId = requestAnimationFrame(loop); |
| 225 |
} |
| 226 |
|
| 227 |
/* Start when visible */ |
| 228 |
resize(); |
| 229 |
window.addEventListener('resize', function() { resize(); if (assembled) { assembled = false; startTime = null; } }); |
| 230 |
|
| 231 |
if ('IntersectionObserver' in window) { |
| 232 |
var obs = new IntersectionObserver(function(entries) { |
| 233 |
if (entries[0].isIntersecting) { |
| 234 |
if (!rafId) { startTime = null; assembled = false; rafId = requestAnimationFrame(loop); } |
| 235 |
} else { |
| 236 |
if (rafId) { cancelAnimationFrame(rafId); rafId = null; } |
| 237 |
} |
| 238 |
}, { threshold: 0.1 }); |
| 239 |
obs.observe(section); |
| 240 |
} else { |
| 241 |
rafId = requestAnimationFrame(loop); |
| 242 |
} |
| 243 |
|
| 244 |
/* Mouse tracking for hover effect */ |
| 245 |
if (a.hoverEffect) { |
| 246 |
canvas.addEventListener('mousemove', function(e) { |
| 247 |
var rect = canvas.getBoundingClientRect(); |
| 248 |
mx = e.clientX - rect.left; |
| 249 |
my = e.clientY - rect.top; |
| 250 |
}); |
| 251 |
canvas.addEventListener('mouseleave', function() { mx = -9999; my = -9999; }); |
| 252 |
} |
| 253 |
} |
| 254 |
|
| 255 |
document.querySelectorAll('.bkbg-pt-app').forEach(initParticleText); |
| 256 |
})(); |
| 257 |
|