PageSourceSearch

https://multicampattern.com/static/marketing/js/familyRevealFull.fdcc4acb105d.js?v=2

js multicampattern.com collected 2026-09-26 11:22:13 UTC 54,710 bytes, 1,377 lines download raw bytes

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.