| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var svgNS = 'http://www.w3.org/2000/svg'; |
| 5 |
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; |
| 6 |
|
| 7 |
document.querySelectorAll('.bkbg-hw-app').forEach(function (app) { |
| 8 |
var opts = {}; |
| 9 |
try { opts = JSON.parse(app.dataset.opts || '{}'); } catch (e) { } |
| 10 |
|
| 11 |
var text = opts.text || ''; |
| 12 |
var fontFamily = opts.fontFamily || "'Dancing Script', cursive"; |
| 13 |
var fontSize = opts.fontSize || 72; |
| 14 |
var fontWeight = opts.fontWeight || '700'; |
| 15 |
var svgWidth = opts.svgWidth || 900; |
| 16 |
var svgHeight = opts.svgHeight || 140; |
| 17 |
var textX = opts.textX || 50; |
| 18 |
var textY = opts.textY || 110; |
| 19 |
var textAnchor = opts.textAnchor || 'middle'; |
| 20 |
var strokeWidth = opts.strokeWidth || 2; |
| 21 |
var duration = opts.duration || 3; |
| 22 |
var delay = opts.delay || 0.3; |
| 23 |
var trigger = opts.trigger || 'scroll'; |
| 24 |
var fillOnComplete = opts.fillOnComplete !== false; |
| 25 |
var fillDuration = opts.fillDuration || 0.8; |
| 26 |
var repeatAnim = opts.repeatAnimation || false; |
| 27 |
var easing = opts.easing || 'ease-in-out'; |
| 28 |
var textColor = opts.textColor || '#0f172a'; |
| 29 |
var strokeColor = opts.strokeColor || '#6366f1'; |
| 30 |
var align2 = opts.align2 || 'center'; |
| 31 |
var sectionBg = opts.sectionBg || ''; |
| 32 |
var showUnderline = opts.showUnderline || false; |
| 33 |
var underlineColor = opts.underlineColor || '#6366f1'; |
| 34 |
var underlineWidth = opts.underlineWidth || 4; |
| 35 |
var underlineOffsetY = opts.underlineOffsetY || 12; |
| 36 |
var underlineDuration = opts.underlineDuration || 0.6; |
| 37 |
|
| 38 |
if (sectionBg) app.style.background = sectionBg; |
| 39 |
|
| 40 |
/* typography CSS vars */ |
| 41 |
if (window.typoCssVarsForEl) window.typoCssVarsForEl(opts.typoText, '--bkbg-hw-tt-', app); |
| 42 |
|
| 43 |
// Build SVG |
| 44 |
var svg = document.createElementNS(svgNS, 'svg'); |
| 45 |
svg.setAttribute('viewBox', '0 0 ' + svgWidth + ' ' + svgHeight); |
| 46 |
svg.setAttribute('width', '100%'); |
| 47 |
svg.style.cssText = 'display:block;overflow:visible;max-width:' + svgWidth + 'px;' + |
| 48 |
'margin:' + (align2 === 'center' ? '0 auto' : align2 === 'right' ? '0 0 0 auto' : '0'); |
| 49 |
|
| 50 |
// Text element |
| 51 |
var textEl = document.createElementNS(svgNS, 'text'); |
| 52 |
textEl.className.baseVal = 'bkbg-hw-text'; |
| 53 |
textEl.setAttribute('x', textX + '%'); |
| 54 |
textEl.setAttribute('y', textY); |
| 55 |
textEl.setAttribute('text-anchor', textAnchor); |
| 56 |
textEl.setAttribute('stroke', strokeColor); |
| 57 |
textEl.setAttribute('stroke-width', strokeWidth); |
| 58 |
textEl.textContent = text; |
| 59 |
|
| 60 |
svg.appendChild(textEl); |
| 61 |
|
| 62 |
var wrap = document.createElement('div'); |
| 63 |
wrap.className = 'bkbg-hw-wrap'; |
| 64 |
wrap.style.textAlign = align2; |
| 65 |
wrap.appendChild(svg); |
| 66 |
app.appendChild(wrap); |
| 67 |
|
| 68 |
// Need text in DOM before measuring |
| 69 |
// Use requestAnimationFrame to let browser render |
| 70 |
requestAnimationFrame(function () { |
| 71 |
var len = 5000; // large fallback |
| 72 |
try { len = textEl.getComputedTextLength() || 5000; } catch (e) { } |
| 73 |
len = Math.max(len, textEl.getTotalLength ? textEl.getTotalLength() : len); |
| 74 |
|
| 75 |
// Also build underline path if requested |
| 76 |
var ulEl = null; |
| 77 |
var ulLen = 0; |
| 78 |
if (showUnderline) { |
| 79 |
// Get bounding box to draw underline across actual text width |
| 80 |
var bbox = { x: 0, width: svgWidth }; |
| 81 |
try { bbox = textEl.getBBox(); } catch (e) { } |
| 82 |
var ulY = parseFloat(textY) + underlineOffsetY; |
| 83 |
var ulX1 = bbox.x; |
| 84 |
var ulX2 = bbox.x + bbox.width; |
| 85 |
|
| 86 |
ulEl = document.createElementNS(svgNS, 'path'); |
| 87 |
ulEl.className.baseVal = 'bkbg-hw-underline'; |
| 88 |
ulEl.setAttribute('d', 'M ' + ulX1 + ' ' + ulY + ' L ' + ulX2 + ' ' + ulY); |
| 89 |
ulEl.setAttribute('stroke', underlineColor); |
| 90 |
ulEl.setAttribute('stroke-width', underlineWidth); |
| 91 |
ulEl.setAttribute('stroke-linecap', 'round'); |
| 92 |
svg.appendChild(ulEl); |
| 93 |
|
| 94 |
ulLen = (ulX2 - ulX1) || svgWidth; |
| 95 |
} |
| 96 |
|
| 97 |
function setupInitialState() { |
| 98 |
if (reducedMotion) { |
| 99 |
// Show immediately |
| 100 |
textEl.setAttribute('fill', textColor); |
| 101 |
textEl.setAttribute('stroke', 'none'); |
| 102 |
if (ulEl) { |
| 103 |
ulEl.style.strokeDasharray = ''; |
| 104 |
ulEl.style.strokeDashoffset = ''; |
| 105 |
} |
| 106 |
return; |
| 107 |
} |
| 108 |
|
| 109 |
// Set dash = full length = hidden |
| 110 |
textEl.style.strokeDasharray = len; |
| 111 |
textEl.style.strokeDashoffset = len; |
| 112 |
textEl.setAttribute('fill', 'transparent'); |
| 113 |
textEl.style.transition = 'none'; |
| 114 |
|
| 115 |
if (ulEl) { |
| 116 |
ulEl.style.strokeDasharray = ulLen; |
| 117 |
ulEl.style.strokeDashoffset = ulLen; |
| 118 |
ulEl.style.transition = 'none'; |
| 119 |
} |
| 120 |
} |
| 121 |
|
| 122 |
function startAnimation() { |
| 123 |
requestAnimationFrame(function () { |
| 124 |
requestAnimationFrame(function () { |
| 125 |
// Draw stroke |
| 126 |
textEl.style.transition = 'stroke-dashoffset ' + duration + 's ' + easing + ' ' + delay + 's'; |
| 127 |
textEl.style.strokeDashoffset = 0; |
| 128 |
|
| 129 |
// Underline draws after text |
| 130 |
if (ulEl) { |
| 131 |
var ulDelay = delay + duration * 0.9; |
| 132 |
ulEl.style.transition = 'stroke-dashoffset ' + underlineDuration + 's ease-out ' + ulDelay + 's'; |
| 133 |
ulEl.style.strokeDashoffset = 0; |
| 134 |
} |
| 135 |
|
| 136 |
// Fill text after stroke completes |
| 137 |
if (fillOnComplete) { |
| 138 |
var fillDelay = delay + duration; |
| 139 |
setTimeout(function () { |
| 140 |
textEl.classList.add('bkbg-hw-filled'); |
| 141 |
textEl.style.fill = textColor; |
| 142 |
textEl.style.fillOpacity = '1'; |
| 143 |
textEl.style.transitionProperty = 'fill'; |
| 144 |
textEl.style.transitionDuration = fillDuration + 's'; |
| 145 |
textEl.style.transitionTimingFunction = 'ease-in-out'; |
| 146 |
}, fillDelay * 1000); |
| 147 |
} |
| 148 |
}); |
| 149 |
}); |
| 150 |
} |
| 151 |
|
| 152 |
function resetAnimation() { |
| 153 |
textEl.style.transition = 'none'; |
| 154 |
textEl.style.strokeDashoffset = len; |
| 155 |
textEl.setAttribute('fill', 'transparent'); |
| 156 |
textEl.classList.remove('bkbg-hw-filled'); |
| 157 |
if (ulEl) { |
| 158 |
ulEl.style.transition = 'none'; |
| 159 |
ulEl.style.strokeDashoffset = ulLen; |
| 160 |
} |
| 161 |
} |
| 162 |
|
| 163 |
setupInitialState(); |
| 164 |
|
| 165 |
if (reducedMotion) return; // Don't animate for reduced motion users |
| 166 |
|
| 167 |
if (trigger === 'load') { |
| 168 |
startAnimation(); |
| 169 |
} else if (trigger === 'click') { |
| 170 |
app.style.cursor = 'pointer'; |
| 171 |
var played = false; |
| 172 |
app.addEventListener('click', function () { |
| 173 |
if (played) resetAnimation(); |
| 174 |
setTimeout(startAnimation, played ? 50 : 0); |
| 175 |
played = true; |
| 176 |
}); |
| 177 |
} else { |
| 178 |
// scroll (IntersectionObserver) |
| 179 |
var played2 = false; |
| 180 |
var io = new IntersectionObserver(function (entries) { |
| 181 |
entries.forEach(function (entry) { |
| 182 |
if (entry.isIntersecting && !played2) { |
| 183 |
played2 = true; |
| 184 |
startAnimation(); |
| 185 |
if (!repeatAnim) io.unobserve(app); |
| 186 |
} else if (!entry.isIntersecting && repeatAnim && played2) { |
| 187 |
played2 = false; |
| 188 |
resetAnimation(); |
| 189 |
} |
| 190 |
}); |
| 191 |
}, { threshold: 0.3 }); |
| 192 |
io.observe(app); |
| 193 |
} |
| 194 |
}); |
| 195 |
}); |
| 196 |
})(); |
| 197 |
|