1/* familyRevealFull: WebGL + GSAP for #mc-family-intro. */ 2if (typeof window !== 'undefined') { 3 window._familyRevealCleanup = window._familyRevealCleanup || function() {}; 4} 5function __mcRunFamilyRevealCore() { 6 if (window.__mcFRDidInit) return; 7 var canvas = document.getElementById('family-reveal-canvas'); 8 if (!canvas) return; 9 var gl = canvas.getContext('webgl2'); 10 var isWebGL2 = !!gl; 11 if (!gl) gl = canvas.getContext('webgl'); 12 if (!gl) return; 13 14 /* ââ Shader sources ââ */ 15 16 var VS = [ 17 'attribute vec2 aPos;', 18 'varying vec2 vUv;', 19 'void main() {', 20 ' vUv = aPos * 0.5 + 0.5;', 21 ' gl_Position = vec4(aPos, 0.0, 1.0);', 22 '}' 23 ].join('\n'); 24 25 var FS = 'precision highp float;\n' + 26 'varying vec2 vUv;\n' + 27 'uniform sampler2D uTexLines;\n' + 28 'uniform sampler2D uTexSat;\n' + 29 'uniform float uRevealLines;\n' + 30 'uniform float uRevealSat;\n' + 31 'uniform float uDepixLines;\n' + 32 'uniform float uDepixSat;\n' + 33 'uniform float uScale;\n' + 34 'uniform float uOpacity;\n' + 35 'uniform vec2 uResolution;\n' + 36 'uniform float uBlockSize;\n' + 37 'uniform float uGridAlpha;\n' + 38 'uniform float uLinesExit;\n' + 39 'uniform float uFamilyNGTime;\n' + 40 'uniform float uFamilyNGFx;\n' + 41 'uniform float uFamilyNGFlashHz;\n' + 42 'uniform float uFamilyNGDensity;\n' + 43 'uniform float uFamilyNGStrength;\n' + 44 'uniform float uFamilyNGLineW;\n' + 45 'uniform float uFamilyNGChaos;\n' + 46 'uniform float uTexAspectLines;\n' + 47 'uniform float uTexAspectSat;\n' + 48 '\n' + 49 '/* object-fit: cover â top-anchored when ca>ta (fill width, crop bottom). ca<=ta: fill height, crop sides (centered). Textures use UNPACK_FLIP_Y_WEBGL. */\n' + 50 'vec2 canvasUvToTexCover(vec2 uvCanvas, float texAsp) {\n' + 51 ' float ta = max(texAsp, 0.0001);\n' + 52 ' float ca = uResolution.x / max(uResolution.y, 1.0);\n' + 53 ' if (ca > ta) {\n' + 54 ' return vec2(uvCanvas.x, uvCanvas.y * (ta / ca) + (1.0 - ta / ca));\n' + 55 ' }\n' + 56 ' return vec2((uvCanvas.x - 0.5) * (ca / ta) + 0.5, uvCanvas.y);\n' + 57 '}\n' + 58 '\n' + 59 'float hash21(vec2 p) {\n' + 60 ' p = fract(p * vec2(233.34, 851.73));\n' + 61 ' p += dot(p, p + 23.45);\n' + 62 ' return fract(p.x * p.y);\n' + 63 '}\n' + 64 '\n' + 65 'float bayer8(vec2 pos) {\n' + 66 ' int x = int(mod(pos.x, 8.0));\n' + 67 ' int y = int(mod(pos.y, 8.0));\n' + 68 ' int idx = x + y * 8;\n' + 69 ' float val = 0.0;\n' + 70 ' if(idx==0) val=0.0; else if(idx==1) val=32.0; else if(idx==2) val=8.0; else if(idx==3) val=40.0;\n' + 71 ' else if(idx==4) val=2.0; else if(idx==5) val=34.0; else if(idx==6) val=10.0; else if(idx==7) val=42.0;\n' + 72 ' else if(idx==8) val=48.0; else if(idx==9) val=16.0; else if(idx==10) val=56.0; else if(idx==11) val=24.0;\n' + 73 ' else if(idx==12) val=50.0; else if(idx==13) val=18.0; else if(idx==14) val=58.0; else if(idx==15) val=26.0;\n' + 74 ' else if(idx==16) val=12.0; else if(idx==17) val=44.0; else if(idx==18) val=4.0; else if(idx==19) val=36.0;\n' + 75 ' else if(idx==20) val=14.0; else if(idx==21) val=46.0; else if(idx==22) val=6.0; else if(idx==23) val=38.0;\n' + 76 ' else if(idx==24) val=60.0; else if(idx==25) val=28.0; else if(idx==26) val=52.0; else if(idx==27) val=20.0;\n' + 77 ' else if(idx==28) val=62.0; else if(idx==29) val=30.0; else if(idx==30) val=54.0; else if(idx==31) val=22.0;\n' + 78 ' else if(idx==32) val=3.0; else if(idx==33) val=35.0; else if(idx==34) val=11.0; else if(idx==35) val=43.0;\n' + 79 ' else if(idx==36) val=1.0; else if(idx==37) val=33.0; else if(idx==38) val=9.0; else if(idx==39) val=41.0;\n' + 80 ' else if(idx==40) val=51.0; else if(idx==41) val=19.0; else if(idx==42) val=59.0; else if(idx==43) val=27.0;\n' + 81 ' else if(idx==44) val=49.0; else if(idx==45) val=17.0; else if(idx==46) val=57.0; else if(idx==47) val=25.0;\n' + 82 ' else if(idx==48) val=15.0; else if(idx==49) val=47.0; else if(idx==50) val=7.0; else if(idx==51) val=39.0;\n' + 83 ' else if(idx==52) val=13.0; else if(idx==53) val=45.0; else if(idx==54) val=5.0; else if(idx==55) val=37.0;\n' + 84 ' else if(idx==56) val=63.0; else if(idx==57) val=31.0; else if(idx==58) val=55.0; else if(idx==59) val=23.0;\n' + 85 ' else if(idx==60) val=61.0; else if(idx==61) val=29.0; else if(idx==62) val=53.0; else if(idx==63) val=21.0;\n' + 86 ' return val / 64.0;\n' + 87 '}\n' + 88 '\n' + 89 /* Radial threshold: distance from center + noise â center-out expansion. 90 Heavily distance-weighted so reveal stays tight to center. */ 91 'float radialThresh(vec2 bUv, vec2 grid, float seed1, float seed2) {\n' + 92 ' vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);\n' + 93 ' float dist = length((bUv - 0.5) * aspect) / length(aspect * 0.5);\n' +
94 ' float n = hash21(grid * seed1 + seed2);\n' + 95 ' return dist * 0.85 + n * 0.15;\n' + 96 '}\n' + 97 '\n' + 98 /* Snap to power-of-2 tier so blocks nest in a clean hierarchical grid */ 99 'float tierMul(float reveal, float thresh) {\n' + 100 ' float age = reveal - thresh;\n' + 101 ' float t = clamp(age / 0.25, 0.0, 1.0);\n' + 102 ' float raw = mix(1.0, 8.0, t * t);\n' + 103 ' if (raw >= 6.0) return 8.0;\n' + 104 ' if (raw >= 3.0) return 4.0;\n' + 105 ' if (raw >= 1.5) return 2.0;\n' + 106 ' return 1.0;\n' + 107 '}\n' + 108 '\n' + 109 /* Given a tier multiplier, compute block-snapped UV and cell-local coords */ 110 'vec4 blockAt(float mul, float bs, vec2 uv, vec2 res) {\n' + 111 ' float eff = bs * mul;\n' + 112 ' vec2 g = floor(uv * res / eff);\n' + 113 ' vec2 bUv = (g + 0.5) * eff / res;\n' + 114 ' return vec4(bUv, fract(uv * res / eff));\n' + 115 '}\n' + 116 '\n' + 117 /* Lines pixel-out: fine (1Ã) first, then grow to 16Ã right before cell pops off. */ 118 'float exitBlockMul(float t) {\n' + 119 ' float tt = clamp(t, 0.0, 1.0);\n' + 120 ' float raw = mix(1.0, 16.0, tt * tt);\n' + 121 ' if (raw < 1.5) return 1.0;\n' + 122 ' if (raw < 3.0) return 2.0;\n' + 123 ' if (raw < 6.0) return 4.0;\n' + 124 ' if (raw < 12.0) return 8.0;\n' + 125 ' return 16.0;\n' + 126 '}\n' + 127 '\n' + 128 'void main() {\n' + 129 ' float bs = uBlockSize;\n' + 130 ' vec2 pixCoord = vUv * uResolution;\n' + 131 '\n' + 132 /* Use the 1x grid for all threshold / visibility decisions */ 133 ' vec2 smGrid = floor(pixCoord / bs);\n' + 134 ' vec2 smUv = (smGrid + 0.5) * bs / uResolution;\n' + 135 '\n' + 136 ' float lRevThresh = radialThresh(smUv, smGrid, 0.37, 7.13);\n' + 137 ' float lRevVisible = step(lRevThresh, uRevealLines);\n' + 138 ' float lDepThresh = radialThresh(smUv, smGrid, 0.41, 11.3);\n' + 139 ' float lDepClean = step(lDepThresh, uDepixLines);\n' + 140 ' float sRevThresh = radialThresh(smUv, smGrid, 0.53, 13.7);\n' + 141 ' float sRevVisible = step(sRevThresh, uRevealSat);\n' + 142 ' float sDepThresh = radialThresh(smUv, smGrid, 0.59, 17.3);\n' + 143 ' float sDepClean = step(sDepThresh, uDepixSat);\n' + 144 '\n' + 145 /* Tier multiplier: 1x, 2x, 4x, or 8x â snapped so grids always align */ 146 ' float lMul = lDepClean > 0.5 ? 1.0 : tierMul(uRevealLines, lRevThresh);\n' + 147 ' float sMul = sDepClean > 0.5 ? 1.0 : tierMul(uRevealSat, sRevThresh);\n' + 148 '\n' + 149 /* Block-snap at the chosen tier */ 150 ' vec4 lB = blockAt(lMul, bs, vUv, uResolution);\n' + 151 ' vec2 lBlockUv = lB.xy;\n' + 152 ' vec2 lCellLocal = lB.zw;\n' + 153 ' float lBlockEff = bs * lMul;\n' + 154 ' vec2 linesUv = mix(lBlockUv, vUv, lDepClean);\n' + 155 '\n' + 156 ' vec4 sB = blockAt(sMul, bs, vUv, uResolution);\n' + 157 ' vec2 sBlockUv = sB.xy;\n' + 158 ' vec2 sCellLocal = sB.zw;\n' + 159 ' float sBlockEff = bs * sMul;\n' + 160 ' vec2 satSampleUv = mix(sBlockUv, vUv, sDepClean);\n' + 161 '\n' + 162 ' vec2 linesScaled = (linesUv - 0.5) / uScale + 0.5;\n' + 163 ' vec2 satScaled = (satSampleUv - 0.5) / uScale + 0.5;\n' + 164 ' vec2 boundsCheck = (smUv - 0.5) / uScale + 0.5;\n' + 165 ' bool inBounds = boundsCheck.x >= 0.0 && boundsCheck.x <= 1.0 && boundsCheck.y >= 0.0 && boundsCheck.y <= 1.0;\n' + 166 '\n' + 167 ' vec2 linesTexUv = canvasUvToTexCover(linesScaled, uTexAspectLines);\n' + 168 ' vec2 satTexUv = canvasUvToTexCover(satScaled, uTexAspectSat);\n' + 169 ' vec3 linesCol = vec3(0.0);\n' + 170 ' vec3 satCol = vec3(0.0);\n' + 171 ' if (inBounds) {\n' + 172 ' linesCol = texture2D(uTexLines, linesTexUv).rgb;\n' + 173 ' satCol = texture2D(uTexSat, satTexUv).rgb;\n' + 174 ' }\n' + 175 '\n' + 176 ' vec2 pixelPos = vUv * uResolution;\n' + 177 ' float dither = bayer8(pixelPos);\n' + 178 ' float lum = dot(linesCol, vec3(0.299, 0.587, 0.114));\n' + 179 ' float levels = 6.0;\n' + 180 ' float quantized = floor(lum * levels + dither) / levels;\n' + 181 ' vec3 ditheredLines = vec3(quantized);\n' + 182 ' vec3 linesDisplay = mix(ditheredLines, linesCol, lDepClean);\n' + 183 ' vec3 linesNoisy = linesDisplay;\n' + 184 '\n' + 185 /* Grid lines: use effective cell, fade on clean blocks */ 186 ' float lEdge = 1.5 / lBlockEff;\n' + 187 ' float lGridLine = 0.0;\n' + 188 ' if (lCellLocal.x < lEdge || lCellLocal.x > 1.0 - lEdge || lCellLocal.y < lEdge || lCellLocal.y > 1.0 - lEdge) {\n' + 189 ' lGridLine = 1.0;\n' + 190 ' }\n' + 191 ' float sEdge = 1.5 / sBlockEff;\n' + 192 ' float sGridLine = 0.0;\n' + 193 ' if (sCellLocal.x < sEdge || sCellLocal.x > 1.0 - sEdge || sCellLocal.y < sEdge || sCellLocal.y > 1.0 - sEdge) {\n' +
194 ' sGridLine = 1.0;\n' + 195 ' }\n' + 196 '\n' + 197 ' vec3 bg = vec3(0.0);\n' + 198 ' float alpha = 0.0;\n' + 199 '\n' + 200 /* Base layer: satellite image (underneath) */ 201 ' if (inBounds && sRevVisible > 0.5) {\n' + 202 ' bg = satCol;\n' + 203 ' bg += vec3(sGridLine * uGridAlpha * 0.4 * (1.0 - sDepClean));\n' + 204 ' alpha = 1.0;\n' + 205 ' }\n' + 206 '\n' + 207 /* Terrain: random 1px white âdifferenceâ grid (flashing). JS ramps uFamilyNGFx from intro start. 208 On satellite once visible; on lines texture during early pixel intro so squares read from the first frames. */ 209 ' if (uFamilyNGFx > 0.001 && inBounds) {\n' + 210 ' float fnw = max(uFamilyNGLineW, 1.0);\n' + 211 ' vec2 pcg = pixCoord;\n' + 212 ' vec2 anch = floor(pcg / (bs * 16.0));\n' + 213 ' float hp = hash21(anch * 0.37 + vec2(12.91, 55.3));\n' + 214 ' float zi = floor(hp * 5.0);\n' + 215 ' float fmul = 1.0;\n' + 216 ' if (zi > 0.5) fmul = 2.0;\n' + 217 ' if (zi > 1.5) fmul = 4.0;\n' + 218 ' if (zi > 2.5) fmul = 8.0;\n' + 219 ' if (zi > 3.5) fmul = 16.0;\n' + 220 ' float effn = bs * fmul;\n' + 221 ' vec2 gcel = floor(pcg / effn);\n' + 222 ' float hAllow = hash21(gcel + vec2(3.22, 8.77));\n' + 223 ' float allow = step(1.0 - uFamilyNGDensity, hAllow);\n' + 224 ' float tk = floor(uFamilyNGTime * uFamilyNGFlashHz + hash21(gcel) * 6.0);\n' + 225 ' float flash = step(0.5, hash21(gcel * 0.19 + vec2(tk * 17.1, tk * 0.93)));\n' + 226 ' float micro = step(0.55, hash21(gcel + floor(uFamilyNGTime * uFamilyNGFlashHz * 2.7)));\n' + 227 ' float flick = mix(1.0, micro, uFamilyNGChaos);\n' + 228 ' vec2 locn = fract(pcg / effn) * effn;\n' + 229 ' float ev = step(locn.x, fnw) + step(effn - fnw, locn.x);\n' + 230 ' float eh = step(locn.y, fnw) + step(effn - fnw, locn.y);\n' + 231 ' float edge = clamp(ev + eh, 0.0, 1.0);\n' + 232 ' float mask = allow * flash * flick * edge * uFamilyNGFx * uFamilyNGStrength;\n' + 233 ' float onSat = step(0.5, sRevVisible) * step(0.5, alpha);\n' + 234 ' float onLines = (1.0 - onSat) * step(0.012, lRevVisible);\n' + 235 ' if (onSat > 0.5) {\n' + 236 ' vec3 diffc = abs(bg - vec3(1.0));\n' + 237 ' bg = mix(bg, diffc, mask);\n' + 238 ' } else if (onLines > 0.5) {\n' + 239 ' vec3 diffcL = abs(linesNoisy - vec3(1.0));\n' + 240 ' linesNoisy = mix(linesNoisy, diffcL, mask);\n' + 241 ' }\n' + 242 ' }\n' + 243 '\n' + 244 /* Lines exit wave: uLinesExit is 0 until JS finishes reveal+hold â then 0â1. */ 245 ' float lExitDist = length((smUv - 0.5) * vec2(uResolution.x / uResolution.y, 1.0)) / length(vec2(uResolution.x / uResolution.y, 1.0) * 0.5);\n' + 246 ' float lExitNoise = hash21(smGrid * 0.71 + 19.3);\n' + 247 ' float lExitThresh = lExitDist * 0.85 + lExitNoise * 0.15;\n' + 248 ' float lExitGone = step(lExitThresh, uLinesExit);\n' + 249 ' float EXIT_REPIX_W = 0.26;\n' + 250 ' float repixStart = max(0.0, lExitThresh - EXIT_REPIX_W);\n' + 251 ' float repixDenom = max(lExitThresh - repixStart, 0.0001);\n' + 252 ' float lExitT = clamp((uLinesExit - repixStart) / repixDenom, 0.0, 1.0);\n' + 253 ' float exitLinesMul = exitBlockMul(lExitT);\n' + 254 ' vec4 exB = blockAt(exitLinesMul, bs, vUv, uResolution);\n' + 255 ' vec2 exitBlockUv = exB.xy;\n' + 256 ' float exEff = bs * exitLinesMul;\n' + 257 ' float exEdge = 1.5 / exEff;\n' + 258 ' float exGrid = 0.0;\n' + 259 ' if (exB.z < exEdge || exB.z > 1.0 - exEdge || exB.w < exEdge || exB.w > 1.0 - exEdge) {\n' + 260 ' exGrid = 1.0;\n' + 261 ' }\n' + 262 '\n' + 263 /* Top layer: lines with multiply blend. uLinesExit==0 â pure linesDisplay (pixel-on unchanged). */ 264 ' if (inBounds && lRevVisible > 0.5 && lExitGone < 0.5) {\n' + 265 ' vec3 lFinal;\n' + 266 ' if (uLinesExit < 0.0001) {\n' + 267 ' lFinal = linesNoisy;\n' + 268 ' } else {\n' + 269 ' vec2 exitScaled = (exitBlockUv - 0.5) / uScale + 0.5;\n' + 270 ' vec2 exitTexUv = canvasUvToTexCover(exitScaled, uTexAspectLines);\n' + 271 ' vec3 exitCol = texture2D(uTexLines, exitTexUv).rgb;\n' + 272 ' float exitBlend = smoothstep(0.0, 0.2, lExitT);\n' + 273 ' lFinal = mix(linesNoisy, exitCol, exitBlend);\n' + 274 ' float exitGridWt = smoothstep(0.12, 0.4, lExitT) * (1.0 - smoothstep(0.9, 1.0, lExitT));\n' + 275 ' lFinal += vec3(exGrid * uGridAlpha * 0.85 * exitGridWt);\n' + 276 ' }\n' + 277 ' if (alpha > 0.5) {\n' + 278 ' bg = bg * lFinal;\n' + 279 ' } else {\n' + 280 ' bg = lFinal;\n' + 281 ' alpha = 1.0;\n' + 282 ' }\n' + 283 ' }\n' + 284 '\n' + 285 ' gl_FragColor = vec4(bg, alpha * uOpacity);\n' + 286 '}\n'; 287 288 /* ââ Compile helpers ââ */ 289 function compileShader(src, type) { 290 var s = gl.createShader(type); 291 gl.shaderSource(s, src); 292 gl.compileShader(s); 293 if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { 294 console.error('Family shader error:', gl.getShaderInfoLog(s)); 295 gl.deleteShader(s); 296 return null; 297 } 298 return s; 299 } 300 301 var vs = compileShader(VS, gl.VERTEX_SHADER); 302 var fs = compileShader(FS, gl.FRAGMENT_SHADER); 303 if (!vs || !fs) return; 304 305 var program = gl.createProgram(); 306 gl.attachShader(program, vs); 307 gl.attachShader(program, fs); 308 gl.bindAttribLocation(program, 0, 'aPos'); 309 gl.linkProgram(program); 310 if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 311 console.error('Family program link error:', gl.getProgramInfoLog(program)); 312 return; 313 } 314 gl.useProgram(program); 315 316 /* ââ Fullscreen quad ââ */ 317 var quad = gl.createBuffer(); 318 gl.bindBuffer(gl.ARRAY_BUFFER, quad); 319 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW); 320 gl.enableVertexAttribArray(0); 321 gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); 322 323 /* ââ Uniform locations ââ */ 324 var uTexLines = gl.getUniformLocation(program, 'uTexLines'); 325 var uTexSat = gl.getUniformLocation(program, 'uTexSat'); 326 var uRevealLines = gl.getUniformLocation(program, 'uRevealLines'); 327 var uRevealSat = gl.getUniformLocation(program, 'uRevealSat'); 328 var uDepixLines = gl.getUniformLocation(program, 'uDepixLines'); 329 var uDepixSat = gl.getUniformLocation(program, 'uDepixSat'); 330 var uScale = gl.getUniformLocation(program, 'uScale'); 331 var uOpacity = gl.getUniformLocation(program, 'uOpacity'); 332 var uResolution = gl.getUniformLocation(program, 'uResolution'); 333 var uBlockSize = gl.getUniformLocation(program, 'uBlockSize'); 334 var uGridAlpha = gl.getUniformLocation(program, 'uGridAlpha'); 335 var uLinesExit = gl.getUniformLocation(program, 'uLinesExit'); 336 var uFamilyNGTime = gl.getUniformLocation(program, 'uFamilyNGTime'); 337 var uFamilyNGFx = gl.getUniformLocation(program, 'uFamilyNGFx'); 338 var uFamilyNGFlashHz = gl.getUniformLocation(program, 'uFamilyNGFlashHz'); 339 var uFamilyNGDensity = gl.getUniformLocation(program, 'uFamilyNGDensity'); 340 var uFamilyNGStrength = gl.getUniformLocation(program, 'uFamilyNGStrength'); 341 var uFamilyNGLineW = gl.getUniformLocation(program, 'uFamilyNGLineW'); 342 var uFamilyNGChaos = gl.getUniformLocation(program, 'uFamilyNGChaos'); 343 var uTexAspectLines = gl.getUniformLocation(program, 'uTexAspectLines'); 344 var uTexAspectSat = gl.getUniformLocation(program, 'uTexAspectSat'); 345 346 /* ââ Texture loading ââ */ 347 var texLines = gl.createTexture(); 348 var texSat = gl.createTexture(); 349 var texturesReady = 0; 350 var aspectLines = 1.0; 351 var aspectSat = 1.0; 352 353 function pushTexAspectUniforms() { 354 gl.useProgram(program); 355 gl.uniform1f(uTexAspectLines, aspectLines); 356 gl.uniform1f(uTexAspectSat, aspectSat); 357 } 358 359 function initTexture(tex, unit) { 360 gl.activeTexture(gl.TEXTURE0 + unit); 361 gl.bindTexture(gl.TEXTURE_2D, tex); 362 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 363 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 364 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 365 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 366 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0,0,0,255])); 367 }
368 initTexture(texLines, 0); 369 initTexture(texSat, 1); 370 371 function uploadTexture(img, tex, unit) { 372 gl.activeTexture(gl.TEXTURE0 + unit); 373 gl.bindTexture(gl.TEXTURE_2D, tex); 374 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 375 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); 376 texturesReady++; 377 } 378 379 var imgLines = new Image(); 380 imgLines.crossOrigin = 'anonymous'; 381 imgLines.onload = function() { 382 uploadTexture(imgLines, texLines, 0); 383 aspectLines = imgLines.naturalWidth / Math.max(1, imgLines.naturalHeight); 384 pushTexAspectUniforms(); 385 }; 386 imgLines.src = (window.__MC_FAMILY_REVEAL_ASSETS && window.__MC_FAMILY_REVEAL_ASSETS.lines) || '/static/marketing/images/terrain/multicam_all_terrain_lines-2400.png'; 387 388 var imgSat = new Image(); 389 imgSat.crossOrigin = 'anonymous'; 390 imgSat.onload = function() { 391 uploadTexture(imgSat, texSat, 1); 392 aspectSat = imgSat.naturalWidth / Math.max(1, imgSat.naturalHeight); 393 pushTexAspectUniforms(); 394 }; 395 imgSat.src = (window.__MC_FAMILY_REVEAL_ASSETS && window.__MC_FAMILY_REVEAL_ASSETS.sat) || '/static/marketing/images/terrain/multicam_all_terrain-2400.avif'; 396 397 gl.uniform1i(uTexLines, 0); 398 gl.uniform1i(uTexSat, 1); 399 pushTexAspectUniforms(); 400 401 /* ââ Scroll state ââ */ 402 var scrollProgress = 0; 403 var BLOCK_PX = 24; 404 var familyNoiseGridFx = 0; 405 var geoOverlay = document.getElementById('family-geo-overlay'); 406 var logosOverlay = document.getElementById('family-logos-overlay'); 407 408 /* ââ Ring & logo scroll-driven animation ââ */ 409 /* Same stagger window as before (RING_END â RING_START); shifted later vs satellite reveal. */ 410 var RING_START = 0.5; 411 var RING_END = 0.7; 412 var RING_COUNT = 4; 413 var STAGGER = (RING_END - RING_START) / (RING_COUNT + 1); 414 var RING_DUR = STAGGER * 2.5; 415 var LOGO_DELAY = RING_DUR * 0.5; 416 var LOGO_DUR = STAGGER * 1.5; 417 418 /* ââ Terrain noise grid (WebGL): 1px white difference lines on sat, aligned to bsÃ{1,2,4,8,16}, random flash. 419 uFamilyNGFx ramps from scroll progress SCROLL_START (0 = visible from first pixel intro frames). */ 420 var FAMILY_NOISE_GRID = { 421 SCROLL_START: 0, 422 SCROLL_FADE_IN: 0.04, 423 FLASH_HZ: 0.12, 424 CELL_DENSITY: 0.1, 425 STRENGTH: 1, 426 LINE_WIDTH_PX: 1.0, 427 CHAOS: 0.5 428 }; 429 430 var rings = logosOverlay ? logosOverlay.querySelectorAll('.family-logo-ring') : []; 431 var ringCircles = logosOverlay ? logosOverlay.querySelectorAll('.family-logo-ring-circle') : []; 432 var logoImgs = logosOverlay ? logosOverlay.querySelectorAll('.family-logo-ring-img') : []; 433 434 /* One-shot hover-like flash per ring as each logo staggers in (order 0â3). */ 435 var RING_INTRO_PULSE_LOGO_T = 0.88; 436 var RING_INTRO_PULSE_HOLD_MS = 360; 437 var ringIntroPulsed = []; 438 var ringIntroPulseTimers = []; 439 var familyRingIntroPulseReduced = false; 440 try { 441 familyRingIntroPulseReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 442 } catch (ePulseRm) {} 443 444 function clearRingIntroPulse(i, resetFlag) { 445 if (ringIntroPulseTimers[i]) { 446 clearTimeout(ringIntroPulseTimers[i]); 447 ringIntroPulseTimers[i] = null; 448 } 449 if (rings[i]) rings[i].classList.remove('ring-intro-pulse'); 450 if (resetFlag) ringIntroPulsed[i] = false; 451 } 452 453 function clearAllRingIntroPulses(resetFlags) { 454 for (var pj = 0; pj < rings.length; pj++) { 455 clearRingIntroPulse(pj, !!resetFlags); 456 } 457 } 458 459 function triggerRingIntroPulse(i) { 460 if (familyRingIntroPulseReduced || ringIntroPulsed[i] || !rings[i]) return; 461 ringIntroPulsed[i] = true; 462 rings[i].classList.add('ring-intro-pulse'); 463 if (ringIntroPulseTimers[i]) clearTimeout(ringIntroPulseTimers[i]); 464 ringIntroPulseTimers[i] = setTimeout(function() { 465 ringIntroPulseTimers[i] = null; 466 if (rings[i]) rings[i].classList.remove('ring-intro-pulse'); 467 }, RING_INTRO_PULSE_HOLD_MS); 468 } 469 470 function easeOutBack(t) { 471 var c = 1.7; 472 return 1 + (--t) * t * ((c + 1) * t + c); 473 } 474 475 function updateRingsAndLogos(p) { 476 for (var i = 0; i < rings.length; i++) { 477 var order = parseInt(rings[i].getAttribute('data-ring-order') || i); 478 var ringStart = RING_START + order * STAGGER; 479 var ringEnd = ringStart + RING_DUR; 480 var ringT = Math.max(0, Math.min(1, (p - ringStart) / (ringEnd - ringStart))); 481 var ringScale = easeOutBack(ringT); 482 if (ringCircles[i]) { 483 ringCircles[i].style.transform = 'scale(' + ringScale + ')'; 484 } 485 var logoStart = ringStart + LOGO_DELAY; 486 var logoEnd = logoStart + LOGO_DUR; 487 var logoT = Math.max(0, Math.min(1, (p - logoStart) / (logoEnd - logoStart))); 488 if (logoImgs[i]) { 489 logoImgs[i].style.opacity = logoT > 0 ? '1' : '0'; 490 logoImgs[i].style.transform = 'scale(1)'; 491 } 492 if (logoT < 0.05 && ringIntroPulsed[i]) { 493 clearRingIntroPulse(i, true); 494 } else if (!ringIntroPulsed[i] && logoT >= RING_INTRO_PULSE_LOGO_T) { 495 triggerRingIntroPulse(i); 496 } 497 } 498 } 499 500 (function cacheBaseFilters() { 501 for (var i = 0; i < logoImgs.length; i++) { 502 logoImgs[i]._baseFilter = logoImgs[i].style.filter || ''; 503 } 504 })(); 505
506 function familyIsNarrow() { 507 return window.innerWidth <= 600; 508 } 509 /* Match updateRingsAndLogos: last ring logo finishes at ringStart + LOGO_DELAY + LOGO_DUR. */ 510 function familyRingsInteractive() { 511 var lastOrder = RING_COUNT - 1; 512 var lastRingStart = RING_START + lastOrder * STAGGER; 513 return scrollProgress >= lastRingStart + LOGO_DELAY + LOGO_DUR; 514 } 515 /* ââ Snappy text: geo ~0.78; copy band at RING_START (with rings) ââ */ 516 var geoTriggered = false; 517 var geoItems = geoOverlay ? geoOverlay.querySelectorAll('.family-geo-text-item') : []; 518 /* Last pointer inside #family-section â ring hover uses mouseenter, which does not refire if the 519 pointer was already over a logo when rings become interactive (scroll). */ 520 var familyLastClientX = -1; 521 var familyLastClientY = -1; 522 523 function syncRingHoverFromPointer() { 524 if (!familyRingsInteractive()) return; 525 if (familyLastClientX < 0 || familyLastClientY < 0) return; 526 var x = familyLastClientX; 527 var y = familyLastClientY; 528 document.querySelectorAll('.family-logo-ring-hitarea').forEach(function(hit) { 529 var ring = hit.closest('.family-logo-ring'); 530 if (!ring) return; 531 var r = hit.getBoundingClientRect(); 532 var over = x >= r.left && x <= r.right && y >= r.top && y <= r.bottom; 533 ring.classList.toggle('ring-hovered', over); 534 }); 535 } 536 537 function staggerReset(nodes, clearRings) { 538 for (var i = 0; i < nodes.length; i++) { 539 nodes[i].style.transition = 'opacity 0.25s ease, transform 0.25s ease'; 540 nodes[i].style.opacity = '0'; 541 var id = nodes[i].id || ''; 542 var yOut = (id === 'family-fake-longitude' || id === 'family-fake-latitude') ? '40px' : '30px'; 543 nodes[i].style.transform = 'translateY(' + yOut + ')'; 544 } 545 if (clearRings) { 546 clearAllRingIntroPulses(true); 547 document.querySelectorAll('.family-logo-ring.ring-hovered').forEach(function(r) { 548 r.classList.remove('ring-hovered'); 549 }); 550 } 551 } 552 553 function triggerGeoIn() { 554 if (geoTriggered) return; 555 geoTriggered = true; 556 for (var gi = 0; gi < geoItems.length; gi++) { 557 (function(el, delay) { 558 setTimeout(function() { 559 el.style.transition = 'opacity 0.45s cubic-bezier(0.16,1,0.3,1), transform 0.45s cubic-bezier(0.16,1,0.3,1)'; 560 el.style.opacity = '1'; 561 el.style.transform = 'translateY(0)'; 562 }, delay); 563 })(geoItems[gi], gi * 100); 564 } 565 } 566 567 function resetGeoOut() { 568 if (!geoTriggered) return; 569 geoTriggered = false; 570 staggerReset(geoItems, false); 571 } 572 573 function updateFromScroll(progress, pinIntroOpacity) { 574 /* Pin scrub: #mc-family-scroll-track min-height vs sticky #mc-family-pin-shell (min 100dvh, grows with 1:1 image). 575 Progress 0â1: lines/sat reveal, depix, lines exit, rings/cards (see phase comments below). */ 576 scrollProgress = progress; 577 578 /* ââ Phase layout across 0.0â1.0 (380vh scroll track, start: top 70%) ââ 579 uLinesExit stays 0 until reveal+depix complete and a sharp hold â then lines pixel-out wave. 580 0.00â0.405 (unchanged) lines/sat reveal + depix through sat depix segment 581 0.405â0.48 Lines revealL/depixL 0.85â1.0 (fully sharp) 582 0.48â0.52 Hold sharp lines; uLinesExit=0 583 0.52â1.0 uLinesExit 0â1 (1Ãâ16Ã exit in shader); text from ~0.78 584 */ 585 var LINES_FULL_END = 0.48; 586 var LINES_EXIT_START = 0.38; 587 var opacity = 1, revealL = 0, depixL = 0, revealS = 0, depixS = 0; 588 var scale = 1, gridAlpha = 0, linesExit = 0; 589 590 if (progress < 0.025) { 591 opacity = progress / 0.025; 592 scale = 0.4; 593 gridAlpha = 0.15; 594 } else if (progress < 0.115) { 595 var t = (progress - 0.025) / 0.09; 596 revealL = t * 0.30; 597 scale = 0.4 + t * 0.20; 598 gridAlpha = 0.15; 599 } else if (progress < 0.185) { 600 var t2 = (progress - 0.115) / 0.07; 601 revealL = 0.30 + t2 * 0.08; 602 depixL = t2 * 0.28; 603 scale = 0.60 + t2 * 0.12; 604 gridAlpha = 0.15 * (1.0 - t2 * 0.4); 605 } else if (progress < 0.265) { 606 var t3 = (progress - 0.185) / 0.08; 607 revealL = 0.38 + t3 * 0.12; 608 depixL = 0.28 + t3 * 0.14; 609 scale = 0.72 + t3 * 0.10; 610 gridAlpha = 0.09; 611 } else if (progress < 0.34) { 612 var t4 = (progress - 0.265) / 0.075; 613 revealL = 0.50 + t4 * 0.15; 614 depixL = 0.42 + t4 * 0.16; 615 revealS = t4; 616 scale = 0.82 + t4 * 0.10; 617 gridAlpha = 0.09 + 0.05 * t4; 618 } else if (progress < 0.405) { 619 var t5 = (progress - 0.34) / 0.065; 620 revealL = 0.65 + t5 * 0.20; 621 depixL = 0.58 + t5 * 0.27; 622 revealS = 1; 623 depixS = t5; 624 scale = 0.92 + t5 * 0.08; 625 gridAlpha = 0.14 * (1.0 - t5); 626 } else if (progress < LINES_FULL_END) { 627 var tf = (progress - 0.405) / (LINES_FULL_END - 0.405); 628 revealL = 0.85 + tf * 0.15; 629 depixL = 0.85 + tf * 0.15; 630 revealS = 1; 631 depixS = 1; 632 scale = 1.0; 633 gridAlpha = 0.10 * (1.0 - tf * 0.85); 634 } else if (progress < LINES_EXIT_START) { 635 revealL = 1; 636 depixL = 1; 637 revealS = 1; 638 depixS = 1; 639 scale = 1.0; 640 var th = (progress - LINES_FULL_END) / (LINES_EXIT_START - LINES_FULL_END); 641 gridAlpha = 0.015 * (1.0 - th); 642 } else { 643 revealL = 1; 644 depixL = 1; 645 revealS = 1; 646 depixS = 1; 647 scale = 1.0; 648 var t7 = (progress - LINES_EXIT_START) / (1 - LINES_EXIT_START); 649 gridAlpha = 0.002 * (1.0 - t7); 650 } 651 652 if (progress >= LINES_EXIT_START) { 653 linesExit = Math.min(1, (progress - LINES_EXIT_START) / (1 - LINES_EXIT_START)); 654 } 655 656 var pinOp = pinIntroOpacity == null ? 1 : pinIntroOpacity; 657 pinOp = pinOp < 0 ? 0 : (pinOp > 1 ? 1 : pinOp); 658 659 /** Lines-only intro: overlay blend on #family-canvas-group vs heading + section bg. 660 Heading fades out over a long revealS window; switch to normal blend only after 661 the heading is fully gone (not while text is still visible). */ 662 var cvgEl = document.getElementById('family-canvas-group'); 663 var h2FadeEl = document.getElementById('mc-family-h2'); 664 var TEXT_FADE_S_START = 0.02; 665 var TEXT_FADE_S_END = 0.86; 666 var BLEND_OFF_S = TEXT_FADE_S_END + 0.06; 667 var diffOn = pinOp >= 0.99 && revealL > 0.04 && revealS < BLEND_OFF_S; 668 if (cvgEl) cvgEl.style.mixBlendMode = diffOn ? 'overlay' : ''; 669 if (h2FadeEl && typeof gsap !== 'undefined') { 670 var h2Op = 1; 671 if (revealS <= TEXT_FADE_S_START) { 672 h2Op = 1; 673 } else if (revealS < TEXT_FADE_S_END) { 674 var fadeSpan = TEXT_FADE_S_END - TEXT_FADE_S_START; 675 h2Op = 1 - (revealS - TEXT_FADE_S_START) / Math.max(0.0001, fadeSpan); 676 } else { 677 h2Op = 0; 678 } 679 if (h2Op < 0) h2Op = 0; 680 if (h2Op > 1) h2Op = 1; 681 gsap.set(h2FadeEl, { opacity: h2Op }); 682 } 683 684 var fng = FAMILY_NOISE_GRID; 685 var fngStart = typeof fng.SCROLL_START === 'number' ? fng.SCROLL_START : RING_START; 686 if (progress < fngStart) { 687 familyNoiseGridFx = 0; 688 } else { 689 familyNoiseGridFx = Math.min(1, (progress - fngStart) / Math.max(0.001, fng.SCROLL_FADE_IN)); 690 } 691 692 gl.useProgram(program); 693 gl.uniform1f(uRevealLines, revealL); 694 gl.uniform1f(uRevealSat, revealS); 695 gl.uniform1f(uDepixLines, depixL); 696 gl.uniform1f(uDepixSat, depixS); 697 gl.uniform1f(uScale, scale); 698 gl.uniform1f(uOpacity, opacity * pinOp); 699 gl.uniform1f(uBlockSize, BLOCK_PX); 700 gl.uniform1f(uGridAlpha, gridAlpha); 701 gl.uniform1f(uLinesExit, linesExit); 702 703 updateRingsAndLogos(progress); 704 705 if (progress >= LINES_EXIT_START && progress > 0.78) triggerGeoIn(); 706 else resetGeoOut(); 707 } 708 709 /* ââ Resize ââ */ 710 var MAX_CANVAS_DIM = 4096; 711 function resize() { 712 var dpr = Math.min(window.devicePixelRatio || 1, 2); 713 var w = Math.min(canvas.clientWidth * dpr, MAX_CANVAS_DIM); 714 var h = Math.min(canvas.clientHeight * dpr, MAX_CANVAS_DIM); 715 if (canvas.width !== w || canvas.height !== h) { 716 canvas.width = w; 717 canvas.height = h; 718 gl.viewport(0, 0, w, h); 719 gl.useProgram(program); 720 gl.uniform2f(uResolution, w, h); 721 } 722 /* titles removed */ 723 } 724 resize(); 725 window.addEventListener('resize', resize); 726 727 /* ââ Render loop ââ */ 728 var rafId = null; 729 var isRunning = false;
730 731 function render() { 732 if (!isRunning) return; 733 if (texturesReady < 2) { 734 rafId = requestAnimationFrame(render); 735 return; 736 } 737 resize(); 738 gl.enable(gl.BLEND); 739 gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 740 gl.clearColor(0, 0, 0, 0); 741 gl.clear(gl.COLOR_BUFFER_BIT); 742 gl.useProgram(program); 743 var fng = FAMILY_NOISE_GRID; 744 gl.uniform1f(uFamilyNGTime, performance.now() * 0.001); 745 gl.uniform1f(uFamilyNGFx, familyNoiseGridFx); 746 gl.uniform1f(uFamilyNGFlashHz, fng.FLASH_HZ); 747 gl.uniform1f(uFamilyNGDensity, fng.CELL_DENSITY); 748 gl.uniform1f(uFamilyNGStrength, fng.STRENGTH); 749 gl.uniform1f(uFamilyNGLineW, fng.LINE_WIDTH_PX); 750 gl.uniform1f(uFamilyNGChaos, fng.CHAOS); 751 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 752 rafId = requestAnimationFrame(render); 753 } 754 755 function startLoop() { 756 if (isRunning) return; 757 isRunning = true; 758 render(); 759 } 760 761 function stopLoop() { 762 isRunning = false; 763 if (rafId) { cancelAnimationFrame(rafId); rafId = null; } 764 } 765 766 /* ââ Pinned main driver (canvas group + shader); H1 intro ST; cards scrub on pin track ââ */ 767 var familyPinST = null; 768 var familyH1IntroST = null; 769 var familyCardsRevealST = null; 770 var familyCardsParallaxCtx = null; 771 var familyCardsBelowEl = null; 772 773 var FAMILY_CARDS_BASE_Y_VH = 0.60; 774 var FAMILY_CARDS_TOTAL_DRIFT_VH = 0.80; 775 776 function getFamilyH2Lines() { 777 var h2 = document.getElementById('mc-family-h2'); 778 return h2 ? h2.querySelectorAll('.mc-family-h2-line') : []; 779 } 780 781 function applyFamilyH1IntroProgress(u) { 782 var lines = getFamilyH2Lines(); 783 if (!lines.length || typeof gsap === 'undefined') return; 784 var scrub = window.marketingReveal && window.marketingReveal.scrubStagger; 785 var yMax = familyIsNarrow() ? 22 : 36; 786 for (var li = 0; li < lines.length; li++) { 787 var st = scrub 788 ? scrub(u, lines.length, li, { yMax: yMax, span: 0.52 }) 789 : { opacity: 1, y: 0 }; 790 gsap.set(lines[li], { autoAlpha: st.opacity, y: st.y, visibility: 'visible' }); 791 } 792 } 793 function onFamilyPinUpdate(self) { 794 var p = self.progress; 795 var vh = typeof window !== 'undefined' && window.innerHeight ? window.innerHeight : 800; 796 var h2Wrap = document.getElementById('mc-family-h2-wrap'); 797 if (h2Wrap) { 798 /** Subtle parallax vs pin progress while the terrain block is pinned. */ 799 var h2ParallaxPx = p * vh * 0.028; 800 h2Wrap.style.transform = 'translateY(calc(-50% + ' + h2ParallaxPx.toFixed(2) + 'px))'; 801 } 802 var cvg = document.getElementById('family-canvas-group'); 803 if (cvg) { 804 cvg.style.opacity = '1'; 805 } 806 var pinIntroOpacity = 1; 807 if (p < 0.03) pinIntroOpacity = p / 0.03; 808 var shaderP = p; 809 updateFromScroll(shaderP, pinIntroOpacity); 810 } 811 812 var familyBelowEls = null; 813 function getFamilyBelowEls() { 814 if (familyBelowEls) return familyBelowEls; 815 familyBelowEls = [ 816 document.getElementById('family-tagline'), 817 document.getElementById('family-heading-cell'), 818 document.getElementById('family-icon-cell'), 819 document.getElementById('family-body-cell'), 820 document.getElementById('family-map-cell'), 821 ].filter(Boolean); 822 return familyBelowEls; 823 } 824 function applyFamilyCards(p) { 825 var els = getFamilyBelowEls(); 826 if (!els.length) return; 827 var staggerOffsets = [0, 0.04, 0.06, 0.08, 0.1]; 828 for (var i = 0; i < els.length; i++) { 829 var onset = 0.65 + (staggerOffsets[i] || 0); 830 var dur = 0.18; 831 var t = (p - onset) / dur; 832 t = t < 0 ? 0 : (t > 1 ? 1 : t); 833 var ease = t * t * (3 - 2 * t); 834 els[i].style.opacity = String(ease); 835 els[i].style.transform = 'none'; 836 } 837 } 838 839 var familySectionEl = document.getElementById('mc-family-intro'); 840 var familyLastStacked = familyIsNarrow(); 841 var familyLayoutResizeHandler = null; 842 843 function ensureStackedFamilyCardsDom() { 844 var track = document.getElementById('mc-family-scroll-track'); 845 var shell = document.getElementById('mc-family-pin-shell'); 846 var cards = document.getElementById('family-cards-below'); 847 if (!cards || !track || !shell) return; 848 if (familyIsNarrow()) { 849 if (familySectionEl && (cards.parentNode !== familySectionEl || cards.previousElementSibling !== track)) { 850 familySectionEl.appendChild(cards); 851 } 852 } else if (cards.parentNode !== shell) { 853 shell.appendChild(cards); 854 } 855 } 856 857 function lockStackedFamilyCardsVisible() { 858 if (!familyIsNarrow()) return; 859 var els = getFamilyBelowEls(); 860 var ci; 861 for (ci = 0; ci < els.length; ci++) { 862 els[ci].style.opacity = '1'; 863 els[ci].style.transform = 'none'; 864 els[ci].style.removeProperty('transition'); 865 } 866 if (familyCardsBelowEl) { 867 familyCardsBelowEl.style.removeProperty('transform'); 868 familyCardsBelowEl.style.removeProperty('will-change'); 869 } 870 } 871
872 function teardownFamilyCardsParallax() { 873 if (familyCardsParallaxCtx) { 874 try { familyCardsParallaxCtx.revert(); } catch (ePxRev) {} 875 familyCardsParallaxCtx = null; 876 } 877 if (familyCardsBelowEl) { 878 familyCardsBelowEl.style.removeProperty('transform'); 879 familyCardsBelowEl.style.removeProperty('will-change'); 880 } 881 } 882 883 function setupFamilyCardsParallax(pinTrack, pinShell) { 884 if (familyCardsParallaxCtx || !familyCardsBelowEl || !pinTrack || !pinShell || familyIsNarrow()) return; 885 familyCardsBelowEl.style.willChange = 'transform'; 886 familyCardsParallaxCtx = gsap.context(function () { 887 gsap.fromTo( 888 familyCardsBelowEl, 889 { y: function () { return window.innerHeight * FAMILY_CARDS_BASE_Y_VH; } }, 890 { 891 y: function () { 892 return window.innerHeight * (FAMILY_CARDS_BASE_Y_VH - FAMILY_CARDS_TOTAL_DRIFT_VH); 893 }, 894 ease: 'none', 895 scrollTrigger: { 896 id: 'mc-family-cards-parallax', 897 trigger: pinTrack, 898 start: 'top top', 899 end: 'bottom top', 900 scrub: 0.35, 901 invalidateOnRefresh: true, 902 }, 903 } 904 ); 905 }, familyCardsBelowEl); 906 } 907 908 function teardownFamilyCardsRevealIO() { 909 if (familyCardsRevealST && familyCardsRevealST._io) { 910 try { familyCardsRevealST._io.disconnect(); } catch (eIo) {} 911 } 912 familyCardsRevealST = null; 913 } 914 915 function setupFamilyCardsRevealIO() { 916 if (!familyCardsBelowEl || familyIsNarrow()) return; 917 teardownFamilyCardsRevealIO(); 918 var cardsRevealDone = false; 919 var cardsRevealIO = new IntersectionObserver(function(entries) { 920 if (entries[0].isIntersecting && !cardsRevealDone) { 921 cardsRevealDone = true; 922 cardsRevealIO.disconnect(); 923 var els = getFamilyBelowEls(); 924 var STAGGER_MS = 140; 925 var EASE = 'cubic-bezier(0.16,1,0.3,1)'; 926 for (var ci = 0; ci < els.length; ci++) { 927 (function(el, delay) { 928 setTimeout(function() { 929 el.style.transition = 'opacity 0.7s ' + EASE; 930 el.style.opacity = '1'; 931 }, delay); 932 })(els[ci], ci * STAGGER_MS); 933 } 934 } 935 }, { threshold: 0.1 }); 936 cardsRevealIO.observe(familyCardsBelowEl); 937 familyCardsRevealST = { _io: cardsRevealIO }; 938 } 939 940 ensureStackedFamilyCardsDom(); 941 lockStackedFamilyCardsVisible(); 942 943 if (typeof gsap !== 'undefined' && typeof ScrollTrigger !== 'undefined') { 944 gsap.registerPlugin(ScrollTrigger); 945 var pinTrack = document.getElementById('mc-family-scroll-track'); 946 var pinShell = document.getElementById('mc-family-pin-shell'); 947 familyCardsBelowEl = document.getElementById('family-cards-below'); 948 setupFamilyCardsParallax(pinTrack, pinShell); 949 if (pinTrack && pinShell) { 950 var h1init = document.getElementById('mc-family-h2'); 951 var h1lines = getFamilyH2Lines(); 952 if (h1lines.length && typeof gsap !== 'undefined') { 953 gsap.set(h1lines, { autoAlpha: 0, y: 36 }); 954 } else if (h1init && typeof gsap !== 'undefined') { 955 gsap.set(h1init, { autoAlpha: 0, y: 36 }); 956 } 957 /* H1 blur-in runs while pinShell still scrolls (before pin lock), under canvas stack via z-0 in markup. */ 958 familyH1IntroST = ScrollTrigger.create({ 959 id: 'mc-family-h2-intro', 960 trigger: pinShell, 961 start: 'top 90%', 962 end: 'top top', 963 scrub: 0.35, 964 onUpdate: function (sub) { 965 applyFamilyH1IntroProgress(sub.progress); 966 }, 967 }); 968 if (familyH1IntroST && typeof familyH1IntroST.progress === 'number') { 969 applyFamilyH1IntroProgress(familyH1IntroST.progress); 970 } 971 familyPinST = ScrollTrigger.create({ 972 id: 'mc-family-intro-pin', 973 trigger: pinTrack, 974 start: 'top top', 975 end: function() { 976 return '+=' + (pinTrack.offsetHeight - pinShell.offsetHeight); 977 }, 978 scrub: 0.35, 979 onUpdate: onFamilyPinUpdate, 980 }); 981 onFamilyPinUpdate({ progress: familyPinST && typeof familyPinST.progress === 'number' ? familyPinST.progress : 0 }); 982 if (ScrollTrigger.refresh) { 983 ScrollTrigger.refresh(); 984 } 985 } 986 if (familyIsNarrow()) { 987 lockStackedFamilyCardsVisible(); 988 } else { 989 setupFamilyCardsRevealIO(); 990 } 991 if (familyCardsParallaxCtx && ScrollTrigger.refresh) { 992 ScrollTrigger.refresh(); 993 } 994 995 familyLayoutResizeHandler = function onFamilyLayoutResize() { 996 var stacked = familyIsNarrow(); 997 if (stacked === familyLastStacked) return; 998 familyLastStacked = stacked; 999 ensureStackedFamilyCardsDom(); 1000 if (stacked) {
1001 teardownFamilyCardsParallax(); 1002 teardownFamilyCardsRevealIO(); 1003 lockStackedFamilyCardsVisible(); 1004 } else { 1005 setupFamilyCardsParallax(pinTrack, pinShell); 1006 setupFamilyCardsRevealIO(); 1007 } 1008 if (typeof ScrollTrigger !== 'undefined' && ScrollTrigger.refresh) { 1009 ScrollTrigger.refresh(); 1010 } 1011 }; 1012 window.addEventListener('resize', familyLayoutResizeHandler); 1013 } 1014 1015 /* ââ IntersectionObserver ââ */ 1016 var observer = new IntersectionObserver(function(entries) { 1017 entries.forEach(function(e) { 1018 if (e.isIntersecting) startLoop(); 1019 else stopLoop(); 1020 }); 1021 }, { threshold: 0.05 }); 1022 observer.observe(canvas); 1023 1024 /* ââ Parallax â translate layers at different speeds ââ */ 1025 var fMouseX = 0, fMouseY = 0, fSmoothX = 0, fSmoothY = 0; 1026 var fParallaxActive = false; 1027 var canvasShift = 10; 1028 var ringShifts = { 0: 22, 1: 22, 2: 22, 3: 42 }; 1029 var logoRings = document.querySelectorAll('.family-logo-ring'); 1030 1031 var familySection = document.getElementById('mc-family-pin-shell'); 1032 var fakeLonEl = document.getElementById('family-fake-longitude'); 1033 var fakeLatEl = document.getElementById('family-fake-latitude'); 1034 var chHudEl = document.getElementById('family-crosshair-hud'); 1035 var chCornersEl = chHudEl ? chHudEl.querySelector('.family-ch-corners') : null; 1036 var chTargetX = 0, chTargetY = 0, chSmoothX = 0, chSmoothY = 0; 1037 var chPointerInside = false;
1038 var CH_CORNER_LERP = 0.055; 1039 var CH_GAP_INTRO_START = 0.38; 1040 var CH_GAP_INTRO_END = 0.83; 1041 var CH_CORNER_PAST_END = 0.12; 1042 1043 function chGapStartPx() { 1044 /* gapHalf must be >= max(sat w,h) so both arms vanish at every cursor position inside the sat viewport. */ 1045 var sat = document.getElementById('mc-family-pin-shell'); 1046 var r = sat && sat.getBoundingClientRect ? sat.getBoundingClientRect() : null; 1047 if (r && r.width > 0 && r.height > 0) { 1048 return Math.max(r.width, r.height) + 64; 1049 } 1050 return Math.max(window.innerWidth, window.innerHeight) + 64; 1051 } 1052 1053 function chEaseOutCubic(t) { 1054 return 1 - Math.pow(1 - t, 3); 1055 } 1056 1057 function chEaseOutBack(t) { 1058 var c1 = 1.525; 1059 var c3 = c1 + 1; 1060 return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); 1061 } 1062 1063 function chIntroGapHalf() { 1064 var p = scrollProgress; 1065 var gs = chGapStartPx(); 1066 var lineT = 0; 1067 if (p >= CH_GAP_INTRO_END) lineT = 1; 1068 else if (p > CH_GAP_INTRO_START) { 1069 lineT = (p - CH_GAP_INTRO_START) / (CH_GAP_INTRO_END - CH_GAP_INTRO_START); 1070 } 1071 lineT = chEaseOutCubic(Math.max(0, Math.min(1, lineT))); 1072 return gs + (50 - gs) * lineT; 1073 } 1074 1075 function chIntroCornerScale() { 1076 var p = scrollProgress; 1077 var span = CH_GAP_INTRO_END - CH_GAP_INTRO_START; 1078 var cornerStart = CH_GAP_INTRO_START + 0.8 * span; 1079 var cornerEnd = Math.min(1, CH_GAP_INTRO_END + CH_CORNER_PAST_END); 1080 if (p <= cornerStart) return 0; 1081 if (p >= cornerEnd) return 1; 1082 var ct = (p - cornerStart) / (cornerEnd - cornerStart); 1083 return chEaseOutBack(Math.max(0, Math.min(1, ct))); 1084 } 1085 1086 function chHudAllowed() { 1087 try { 1088 return !window.matchMedia('(pointer: coarse)').matches && 1089 !window.matchMedia('(prefers-reduced-motion: reduce)').matches; 1090 } catch (err) { 1091 return true; 1092 } 1093 } 1094 1095 /* Pointer maps ~Brooklyn-centered (mid viewport â 73.95°W): west â eastern PA, east â Boston. */ 1096 function formatFakeLongitude(nx) { 1097 nx = Math.max(0, Math.min(1, nx)); 1098 var deg = 76.8 - nx * 5.7; 1099 return 'LONG ' + deg.toFixed(6) + '° W'; 1100 } 1101 1102 function formatFakeLatitude(ny) { 1103 ny = Math.max(0, Math.min(1, ny)); 1104 var deg = 41.18 - ny * 1.06; 1105 return 'LAT ' + deg.toFixed(6) + '° N'; 1106 } 1107 1108 function updateFakeGeoLabels(clientX, clientY) { 1109 if (!fakeLonEl || !fakeLatEl) return; 1110 var satEl = document.getElementById('mc-family-pin-shell'); 1111 var rect = satEl && satEl.getBoundingClientRect ? satEl.getBoundingClientRect() : null; 1112 if (!rect || !rect.width) { 1113 if (!familySection) return; 1114 rect = familySection.getBoundingClientRect(); 1115 } 1116 var w = Math.max(1, rect.width); 1117 var h = Math.max(1, rect.height);
1118 var nx = (clientX - rect.left) / w; 1119 var ny = (clientY - rect.top) / h; 1120 fakeLonEl.textContent = formatFakeLongitude(nx); 1121 fakeLatEl.textContent = formatFakeLatitude(ny); 1122 } 1123 1124 function onFamilyMouseEnter(e) { 1125 familyLastClientX = e.clientX; 1126 familyLastClientY = e.clientY; 1127 updateFakeGeoLabels(e.clientX, e.clientY); 1128 if (familyRingsInteractive()) syncRingHoverFromPointer(); 1129 if (!chHudAllowed()) return; 1130 chPointerInside = true; 1131 chTargetX = e.clientX; 1132 chTargetY = e.clientY; 1133 chSmoothX = e.clientX; 1134 chSmoothY = e.clientY; 1135 } 1136 1137 function onFamilyMouseMove(e) { 1138 familyLastClientX = e.clientX; 1139 familyLastClientY = e.clientY; 1140 fMouseX = (e.clientX / window.innerWidth - 0.5) * 2; 1141 fMouseY = (e.clientY / window.innerHeight - 0.5) * 2; 1142 updateFakeGeoLabels(e.clientX, e.clientY); 1143 if (chHudAllowed()) { 1144 chPointerInside = true; 1145 chTargetX = e.clientX; 1146 chTargetY = e.clientY; 1147 } 1148 if (familyRingsInteractive()) syncRingHoverFromPointer(); 1149 } 1150 1151 function onFamilyMouseLeave() { 1152 familyLastClientX = -1; 1153 familyLastClientY = -1; 1154 document.querySelectorAll('.family-logo-ring.ring-hovered').forEach(function(r) { 1155 r.classList.remove('ring-hovered'); 1156 }); 1157 chPointerInside = false;
1158 if (chHudEl) { 1159 chHudEl.style.opacity = '0'; 1160 chHudEl.style.visibility = 'hidden'; 1161 } 1162 var satEl = document.getElementById('mc-family-pin-shell'); 1163 if (satEl && satEl.getBoundingClientRect) { 1164 var r = satEl.getBoundingClientRect(); 1165 updateFakeGeoLabels(r.left + r.width * 0.5, r.top + r.height * 0.5); 1166 } else if (familySection) { 1167 var r2 = familySection.getBoundingClientRect(); 1168 updateFakeGeoLabels(r2.left + r2.width * 0.5, r2.top + r2.height * 0.5); 1169 } 1170 } 1171 1172 if (familySection) { 1173 familySection.addEventListener('mousemove', onFamilyMouseMove); 1174 familySection.addEventListener('mouseenter', onFamilyMouseEnter); 1175 familySection.addEventListener('mouseleave', onFamilyMouseLeave); 1176 } 1177 1178 function updateParallax() { 1179 fSmoothX += (fMouseX - fSmoothX) * 0.08; 1180 fSmoothY += (fMouseY - fSmoothY) * 0.08; 1181 1182 canvas.style.transform = 'translate(' + (fSmoothX * canvasShift) + 'px,' + (fSmoothY * canvasShift) + 'px)'; 1183 1184 logoRings.forEach(function(ring) { 1185 var order = parseInt(ring.getAttribute('data-ring-order'), 10); 1186 var shift = ringShifts[order] || 22; 1187 ring.style.marginLeft = (fSmoothX * shift) + 'px'; 1188 ring.style.marginTop = (fSmoothY * shift) + 'px'; 1189 }); 1190 1191 var showCh = fParallaxActive && chPointerInside && chHudAllowed() && chHudEl && chCornersEl; 1192 if (showCh) { 1193 var satEl = document.getElementById('mc-family-pin-shell'); 1194 var r = satEl && satEl.getBoundingClientRect ? satEl.getBoundingClientRect() : null; 1195 if (r && r.width > 0 && r.height > 0) { 1196 var lx = chTargetX - r.left; 1197 var ly = chTargetY - r.top; 1198 chHudEl.style.setProperty('--ch-x', lx + 'px'); 1199 chHudEl.style.setProperty('--ch-y', ly + 'px'); 1200 chHudEl.style.setProperty('--ch-gap-half', chIntroGapHalf() + 'px'); 1201 chHudEl.style.setProperty('--ch-corner-scale', String(chIntroCornerScale())); 1202 chSmoothX += (chTargetX - chSmoothX) * CH_CORNER_LERP; 1203 chSmoothY += (chTargetY - chSmoothY) * CH_CORNER_LERP; 1204 chCornersEl.style.left = (chSmoothX - r.left) + 'px'; 1205 chCornersEl.style.top = (chSmoothY - r.top) + 'px'; 1206 chHudEl.style.opacity = '1'; 1207 chHudEl.style.visibility = 'visible'; 1208 } else { 1209 chHudEl.style.opacity = '0'; 1210 chHudEl.style.visibility = 'hidden'; 1211 } 1212 } else if (chHudEl) { 1213 chHudEl.style.opacity = '0'; 1214 chHudEl.style.visibility = 'hidden'; 1215 } 1216 1217 if (fParallaxActive) requestAnimationFrame(updateParallax); 1218 } 1219 1220 var parallaxObserver = new IntersectionObserver(function(entries) { 1221 fParallaxActive = entries[0].isIntersecting; 1222 if (!fParallaxActive) { 1223 chPointerInside = false;
1224 if (chHudEl) { 1225 chHudEl.style.opacity = '0'; 1226 chHudEl.style.visibility = 'hidden'; 1227 } 1228 } 1229 if (fParallaxActive) updateParallax(); 1230 }, { threshold: 0.1 }); 1231 if (familySection) parallaxObserver.observe(familySection); 1232 1233 /* ââ Logo ring hover fill ââ */ 1234 var hitareas = document.querySelectorAll('.family-logo-ring-hitarea'); 1235 hitareas.forEach(function(hit) { 1236 var ring = hit.closest('.family-logo-ring'); 1237 hit.addEventListener('mouseenter', function() { if (familyRingsInteractive()) ring.classList.add('ring-hovered'); }); 1238 hit.addEventListener('mouseleave', function() { ring.classList.remove('ring-hovered'); }); 1239 }); 1240 1241 /* ââ Cleanup (assigned below to window._familyRevealCleanup) ââ */ 1242 var __disposeFamily = function() { 1243 stopLoop(); 1244 if (familyCardsParallaxCtx) { 1245 try { familyCardsParallaxCtx.revert(); } catch (ePx) {} 1246 familyCardsParallaxCtx = null; 1247 } 1248 if (familyH1IntroST) { try { familyH1IntroST.kill(); } catch (e0) {} familyH1IntroST = null; } 1249 if (familyPinST) { try { familyPinST.kill(); } catch (e1) {} familyPinST = null; } 1250 if (familyCardsRevealST) { 1251 try { if (familyCardsRevealST._io) familyCardsRevealST._io.disconnect(); } catch (e2) {} 1252 familyCardsRevealST = null; 1253 } 1254 observer.disconnect(); 1255 parallaxObserver.disconnect(); 1256 if (familySection) { 1257 familySection.removeEventListener('mousemove', onFamilyMouseMove); 1258 familySection.removeEventListener('mouseenter', onFamilyMouseEnter); 1259 familySection.removeEventListener('mouseleave', onFamilyMouseLeave); 1260 } 1261 fParallaxActive = false; 1262 chPointerInside = false;
1263 if (chHudEl) { 1264 chHudEl.style.opacity = '0'; 1265 chHudEl.style.visibility = 'hidden'; 1266 } 1267 window.removeEventListener('resize', resize); 1268 if (familyLayoutResizeHandler) { 1269 window.removeEventListener('resize', familyLayoutResizeHandler); 1270 familyLayoutResizeHandler = null; 1271 } 1272 var shellRestore = document.getElementById('mc-family-pin-shell'); 1273 var cardsRestore = document.getElementById('family-cards-below'); 1274 if (shellRestore && cardsRestore && cardsRestore.parentNode !== shellRestore) { 1275 shellRestore.appendChild(cardsRestore); 1276 } 1277 var h1c = document.getElementById('mc-family-h2'); 1278 var h1cLines = getFamilyH2Lines(); 1279 if (typeof gsap !== 'undefined') { 1280 try { 1281 if (h1cLines.length) { 1282 gsap.set(h1cLines, { clearProps: 'opacity,visibility,transform,autoAlpha,y' }); 1283 } 1284 if (h1c) gsap.set(h1c, { clearProps: 'filter,autoAlpha,opacity' }); 1285 } catch (eH1) {} 1286 } 1287 var cvgC = document.getElementById('family-canvas-group'); 1288 if (cvgC) cvgC.style.removeProperty('opacity'); 1289 applyFamilyCards(0); 1290 if (familyCardsBelowEl) { 1291 familyCardsBelowEl.style.removeProperty('will-change'); 1292 } 1293 familyCardsBelowEl = null; 1294 var h2WrapC = document.getElementById('mc-family-h2-wrap'); 1295 if (h2WrapC) { 1296 h2WrapC.style.removeProperty('transform'); 1297 } 1298 ['family-tagline', 'family-heading-cell', 'family-icon-cell', 'family-body-cell', 'family-map-cell'].forEach(function (cid) { 1299 var c = document.getElementById(cid); 1300 if (!c) return; 1301 c.style.removeProperty('opacity'); 1302 c.style.removeProperty('transform'); 1303 c.style.removeProperty('transition'); 1304 }); 1305 familyBelowEls = null; 1306 clearAllRingIntroPulses(true); 1307 document.querySelectorAll('.family-logo-ring.ring-hovered').forEach(function(r) { 1308 r.classList.remove('ring-hovered'); 1309 }); 1310 canvas.style.transform = ''; 1311 canvas.style.removeProperty('mix-blend-mode'); 1312 var cvgDispose = document.getElementById('family-canvas-group'); 1313 if (cvgDispose) cvgDispose.style.removeProperty('mix-blend-mode'); 1314 gl.deleteTexture(texLines); 1315 gl.deleteTexture(texSat); 1316 gl.deleteBuffer(quad); 1317 gl.deleteProgram(program); 1318 gl.deleteShader(vs); 1319 gl.deleteShader(fs); 1320 var ext = gl.getExtension('WEBGL_lose_context'); 1321 if (ext) ext.loseContext(); 1322 if (typeof window !== 'undefined') { 1323 window.__mcFRDidInit = false; 1324 window._familyRevealCleanup = function() {}; 1325 } 1326 }; 1327 1328 window._familyRevealCleanup = __disposeFamily; 1329 if (typeof window !== 'undefined') { 1330 window.__mcFRDidInit = true; 1331 } 1332} 1333 1334if (typeof window !== 'undefined') { 1335 var __mcLenisFrRaf = 0; 1336 window.__mcFamilyIntroMount = function() { 1337 if (window.__mcFRDidInit) return; 1338 if (!document.getElementById('family-reveal-canvas')) return; 1339 if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 1340 var h1r = document.getElementById('mc-family-h2'); 1341 var h1rLines = h1r ? h1r.querySelectorAll('.mc-family-h2-line') : []; 1342 var cvgr = document.getElementById('family-canvas-group'); 1343 if (h1rLines.length && typeof gsap !== 'undefined') { 1344 gsap.set(h1rLines, { autoAlpha: 1, y: 0 }); 1345 } else if (h1r && typeof gsap !== 'undefined') { 1346 gsap.set(h1r, { autoAlpha: 1, y: 0 }); 1347 } else if (h1r) { 1348 h1r.style.opacity = '1'; 1349 h1r.style.transform = 'none'; 1350 } 1351 if (cvgr) cvgr.style.opacity = '1'; 1352 return; 1353 } 1354 if (typeof ScrollTrigger === 'undefined' || typeof gsap === 'undefined') { 1355 return; 1356 } 1357 function mcScrollSystemReady() { 1358 // Touch phones use native scroll (no Lenis); don't wait the full fallback for 1359 // a __marketingLenis that never appears â the native path is ready immediately. 1360 return ( 1361 typeof window.__marketingLenis !== 'undefined' || 1362 !!(window.__mcLenisTouchActive && window.__mcLenisTouchActive.mode === 'native-only') || 1363 document.documentElement.hasAttribute('data-mc-native-scroll') 1364 ); 1365 } 1366 function afterLenis() { 1367 if (!mcScrollSystemReady() && __mcLenisFrRaf < 400) { 1368 __mcLenisFrRaf += 1; 1369 requestAnimationFrame(afterLenis); 1370 return; 1371 } 1372 __mcLenisFrRaf = 0; 1373 __mcRunFamilyRevealCore(); 1374 } 1375 afterLenis(); 1376 }; 1377}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.