1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8" /> 5<meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6<title>Motive11</title> 7<meta name="description" content="Motive11 is the entity. Mark is the operator. One person plus AI, building web systems and automations from Pittsburgh." /> 8<meta name="theme-color" content="#1E1E1F" /> 9<meta http-equiv="Content-Language" content="en" /> 10<meta name="google" content="notranslate" /> 11 12<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32.png" /> 13<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" /> 14<link rel="manifest" href="/site.webmanifest" /> 15 16<meta property="og:type" content="website" /> 17<meta property="og:url" content="https://motive11.com/" /> 18<meta property="og:title" content="Motive11" /> 19<meta property="og:description" content="Motive11 is the entity. Mark is the operator." /> 20<meta property="og:image" content="https://motive11.com/images/icon-512.png" /> 21 22<meta name="twitter:card" content="summary_large_image" /> 23<meta name="twitter:title" content="Motive11" /> 24<meta name="twitter:description" content="Motive11 is the entity. Mark is the operator." /> 25<meta name="twitter:image" content="https://motive11.com/images/icon-512.png" /> 26 27<link rel="preconnect" href="https://api.fontshare.com"> 28<link rel="preconnect" href="https://cdn.fontshare.com" crossorigin> 29<!-- Satoshi: distinctive geometric sans, not Inter, not General Sans --> 30<link href="https://api.fontshare.com/v2/css?f[]=satoshi@900,700,500,400&display=swap" rel="stylesheet"> 31 32<!-- Fathom Analytics (motive11.com). Source of truth is AIOS site_traffic; see 33 pipelines/site-analytics. -->
34<script src="https://cdn.usefathom.com/script.js" data-excluded-domains="localhost,127.0.0.1" data-site="UYCRGPKV" defer></script>
34 35 36<style> 37 :root { 38 --bg: #1E1E1F; 39 --fg: #F4F3EF; 40 --fg-dim: rgba(244, 243, 239, 0.55); 41 --fg-faint: rgba(244, 243, 239, 0.18); 42 --orange: #d64916; 43 --sans: "Satoshi", -apple-system, BlinkMacSystemFont, system-ui, sans-serif; 44 } 45 46 * { box-sizing: border-box; margin: 0; padding: 0; } 47 48 html, body { 49 width: 100%; 50 height: 100%; 51 overflow: hidden; 52 background: var(--bg); 53 color: var(--fg); 54 font-family: var(--sans); 55 -webkit-font-smoothing: antialiased; 56 -moz-osx-font-smoothing: grayscale; 57 } 58 59 body::before { 60 content: ""; 61 position: fixed; 62 inset: 0; 63 pointer-events: none; 64 z-index: 10; 65 opacity: 0.05; 66 mix-blend-mode: overlay; 67 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); 68 } 69 70 body::after { 71 content: ""; 72 position: fixed; 73 inset: 0; 74 pointer-events: none; 75 z-index: 9; 76 background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.55) 100%); 77 } 78 79 #scene { 80 position: fixed; 81 inset: 0; 82 z-index: 0; 83 } 84 85 .progress { 86 position: fixed; 87 top: 0; 88 left: 0; 89 height: 4px; 90 width: 0%; 91 background: var(--orange); 92 z-index: 20; 93 box-shadow: 0 0 12px var(--orange); 94 } 95 96 .station, 97 .footer-mark, 98 .footer-mark-right { 99 position: fixed; 100 z-index: 15; 101 font-size: 10px; 102 letter-spacing: 0.3em; 103 text-transform: uppercase; 104 color: var(--fg-dim); 105 font-weight: 500; 106 } 107 .station { 108 top: 24px; 109 left: 50%; 110 transform: translateX(-50%); 111 } 112 .footer-mark { bottom: 24px; left: 28px; color: var(--fg-faint); } 113 .footer-mark-right { bottom: 24px; right: 28px; color: var(--fg-faint); } 114 115 .footer-mark { 116 display: flex; 117 align-items: center; 118 gap: 14px; 119 } 120 .footer-mark a { 121 color: var(--fg-dim); 122 text-decoration: none; 123 transition: color 0.3s ease; 124 } 125 .footer-mark a:hover, 126 .footer-mark a:focus-visible { 127 color: var(--orange); 128 } 129 130 .pause-btn { 131 position: fixed; 132 right: 28px; 133 bottom: 56px; 134 z-index: 16; 135 width: 40px; 136 height: 40px; 137 display: flex; 138 align-items: center; 139 justify-content: center; 140 background: transparent; 141 border: 1px solid var(--fg-faint); 142 border-radius: 50%; 143 color: var(--orange); 144 cursor: pointer; 145 opacity: 0; 146 transition: opacity 1.2s ease, border-color 0.3s ease, background 0.3s ease; 147 padding: 0; 148 font-family: inherit; 149 } 150 151 .pause-btn.visible { 152 opacity: 0.7; 153 } 154 155 .pause-btn:hover, 156 .pause-btn:focus-visible { 157 opacity: 1; 158 border-color: var(--fg-dim); 159 background: rgba(214, 73, 22, 0.08); 160 outline: none; 161 } 162 163 .pause-btn svg { 164 width: 14px; 165 height: 14px; 166 display: block; 167 } 168 169 .pause-btn .icon-play { display: none; } 170 .pause-btn.paused .icon-play { display: block; } 171 .pause-btn.paused .icon-pause { display: none; } 172 173 .station a { 174 display: block; 175 line-height: 0; 176 } 177 178 .station .logo { 179 display: block; 180 height: 22px; 181 width: auto; 182 opacity: 0.85; 183 transition: opacity 0.3s ease; 184 } 185 186 .station a:hover .logo, 187 .station a:focus-visible .logo { 188 opacity: 1; 189 } 190 191 .station a:focus-visible { 192 outline: 1px solid var(--fg-faint); 193 outline-offset: 6px; 194 border-radius: 2px; 195 } 196 197 .stage { 198 position: fixed; 199 inset: 0; 200 z-index: 5; 201 } 202 203 .slide { 204 position: absolute; 205 inset: 0; 206 display: flex; 207 align-items: center; 208 justify-content: center; 209 padding: 80px 6vw; 210 opacity: 0; 211 pointer-events: none; 212 transition: opacity 0.9s cubic-bezier(0.22, 1, 0.36, 1); 213 } 214 215 .slide.active { 216 opacity: 1; 217 pointer-events: auto; 218 } 219 220 .slide-inner { 221 width: 100%; 222 max-width: 1400px; 223 text-align: left; 224 } 225 226 .big { 227 font-family: var(--sans); 228 font-weight: 500; 229 font-size: clamp(48px, 9vw, 140px); 230 line-height: 1.05; 231 letter-spacing: -0.045em; 232 color: var(--fg); 233 } 234 235 .big.heavy { font-weight: 900; line-height: 0.95; } 236 .big.light { font-weight: 400; line-height: 1.15; } 237 238 .big .orange { color: var(--orange); } 239 .big .dim { color: var(--fg-dim); } 240 241 .slide.url-slide .big { 242 white-space: nowrap; 243 font-size: clamp(36px, 9vw, 140px); 244 letter-spacing: -0.05em; 245 } 246 247 .word { 248 display: inline-block; 249 opacity: 0; 250 transform: translateY(0.4em); 251 transition: opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1), 252 transform 0.7s cubic-bezier(0.22, 1, 0.36, 1); 253 white-space: pre; 254 } 255 256 .word.in { 257 opacity: 1; 258 transform: translateY(0); 259 } 260 261 .char { 262 display: inline-block; 263 opacity: 0; 264 transform: translateY(0.3em); 265 transition: opacity 0.3s ease, transform 0.4s cubic-bezier(0.22, 1, 0.36, 1); 266 white-space: pre; 267 } 268 269 .char.in { 270 opacity: 1; 271 transform: translateY(0); 272 } 273
274 .cta-slide .big { 275 font-size: clamp(40px, 7vw, 100px); 276 } 277 278 .cta-link { 279 display: inline-block; 280 color: var(--orange); 281 text-decoration: none; 282 position: relative; 283 padding-bottom: 8px; 284 } 285 286 .cta-link::after { 287 content: ""; 288 position: absolute; 289 left: 0; 290 bottom: 0; 291 width: 0; 292 height: 3px; 293 background: var(--orange); 294 transition: width 1.4s cubic-bezier(0.22, 1, 0.36, 1) 0.3s; 295 } 296 297 .cta-link.drawn::after { 298 width: 100%; 299 } 300 301 /* Greeting slide: portrait + text */ 302 .slide.greeting-slide .slide-inner { 303 display: flex; 304 flex-direction: row; 305 align-items: center; 306 gap: 40px; 307 text-align: left; 308 } 309 310 .portrait { 311 flex-shrink: 0; 312 width: 140px; 313 height: 140px; 314 border-radius: 50%; 315 object-fit: cover; 316 opacity: 0; 317 transform: scale(0.94); 318 transition: opacity 0.9s cubic-bezier(0.22, 1, 0.36, 1), 319 transform 1.2s cubic-bezier(0.22, 1, 0.36, 1); 320 box-shadow: 0 0 0 1px rgba(244, 243, 239, 0.18), 321 0 0 60px rgba(0, 0, 0, 0.5); 322 } 323 324 .portrait.in { 325 opacity: 1; 326 transform: scale(1); 327 } 328 329 .greeting-text { 330 font-family: var(--sans); 331 font-weight: 500; 332 font-size: clamp(36px, 6vw, 84px); 333 line-height: 1.05; 334 letter-spacing: -0.035em; 335 color: var(--fg); 336 } 337 338 .greeting-text .line { 339 display: block; 340 } 341 342 .greeting-text .wave { 343 display: inline-block; 344 margin-right: 0.15em; 345 } 346 347 @media (max-width: 640px) { 348 .portrait { width: 96px; height: 96px; } 349 .slide.greeting-slide .slide-inner { 350 flex-direction: column; 351 align-items: flex-start; 352 gap: 20px; 353 } 354 .greeting-text { font-size: clamp(32px, 9vw, 56px); } 355 .slide { padding: 100px 24px; } 356 .big { font-size: clamp(40px, 13vw, 80px); } 357 .cta-slide .big { font-size: clamp(32px, 10vw, 60px); } 358 .footer-mark, .footer-mark-right { font-size: 9px; letter-spacing: 0.2em; } 359 .station { top: 18px; } 360 .station .logo { height: 18px; } 361 .footer-mark { bottom: 18px; left: 18px; } 362 .footer-mark-right { bottom: 18px; right: 18px; } 363 } 364 365 @media (max-width: 420px) { 366 .footer-mark-right { display: none; } 367 } 368</style> 369</head> 370<body> 371 372<canvas id="scene"></canvas> 373 374<div class="progress" id="progress"></div> 375 376<div class="station"> 377 <a href="/" aria-label="Replay from the start"> 378 <img class="logo" src="/images/logo-light.svg" alt="Motive11" /> 379 </a> 380</div> 381<div class="footer-mark"> 382 <span>Motive11, LLC</span> 383 <a href="/terms">Terms</a> 384 <a href="/ai-policy">AI Policy</a> 385</div> 386<div class="footer-mark-right">Est. 2019</div> 387<button class="pause-btn" id="pauseBtn" aria-label="Pause" type="button"> 388 <svg class="icon-pause" viewBox="0 0 12 14" fill="currentColor" aria-hidden="true"><rect x="1" y="1" width="3" height="12" rx="0.5"/><rect x="8" y="1" width="3" height="12" rx="0.5"/></svg> 389 <svg class="icon-play" viewBox="0 0 12 14" fill="currentColor" aria-hidden="true"><path d="M2 1.5 L11 7 L2 12.5 Z"/></svg> 390</button> 391 392<div class="stage" id="stage"></div> 393
394<script type="importmap"> 395{ 396 "imports": { 397 "three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.module.js" 398 } 399} 400</script>
400 401
402<script type="module"> 403import * as THREE from "three"; 404 405// ============ BACKGROUND ============ 406const canvas = document.getElementById("scene"); 407const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); 408renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); 409renderer.setSize(window.innerWidth, window.innerHeight); 410 411const scene = new THREE.Scene(); 412scene.fog = new THREE.FogExp2(0x1E1E1F, 0.045); 413 414const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 100); 415camera.position.z = 8; 416 417const particleCount = window.innerWidth < 640 ? 1000 : 2500; 418const positions = new Float32Array(particleCount * 3); 419const colors = new Float32Array(particleCount * 3); 420const sizes = new Float32Array(particleCount); 421const velocities = new Float32Array(particleCount * 3); 422 423const orangeColor = new THREE.Color(0xd64916); 424const whiteColor = new THREE.Color(0xF4F3EF); 425 426for (let i = 0; i < particleCount; i++) { 427 const i3 = i * 3; 428 positions[i3] = (Math.random() - 0.5) * 45; 429 positions[i3 + 1] = (Math.random() - 0.5) * 30; 430 positions[i3 + 2] = (Math.random() - 0.5) * 35 - 8; 431 432 const isOrange = Math.random() < 0.05; 433 const c = isOrange ? orangeColor : whiteColor; 434 colors[i3] = c.r; 435 colors[i3 + 1] = c.g; 436 colors[i3 + 2] = c.b; 437 438 sizes[i] = isOrange ? 0.05 + Math.random() * 0.03 : 0.012 + Math.random() * 0.018; 439 440 velocities[i3] = (Math.random() - 0.5) * 0.0004; 441 velocities[i3 + 1] = (Math.random() - 0.5) * 0.0004; 442 velocities[i3 + 2] = (Math.random() - 0.5) * 0.0003; 443} 444 445const geometry = new THREE.BufferGeometry(); 446geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); 447geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); 448geometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1)); 449 450const particleMaterial = new THREE.ShaderMaterial({ 451 uniforms: {}, 452 vertexShader: ` 453 attribute float size; 454 varying vec3 vColor; 455 void main() { 456 vColor = color; 457 vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); 458 gl_PointSize = size * (450.0 / -mvPosition.z); 459 gl_Position = projectionMatrix * mvPosition; 460 } 461 `, 462 fragmentShader: ` 463 varying vec3 vColor; 464 void main() { 465 vec2 xy = gl_PointCoord.xy - vec2(0.5); 466 float r = length(xy); 467 if (r > 0.5) discard; 468 float alpha = smoothstep(0.5, 0.0, r); 469 gl_FragColor = vec4(vColor, alpha * 0.55); 470 } 471 `, 472 vertexColors: true, 473 transparent: true, 474 depthWrite: false, 475 blending: THREE.AdditiveBlending 476}); 477 478const particles = new THREE.Points(geometry, particleMaterial); 479scene.add(particles); 480 481const glowGeom = new THREE.PlaneGeometry(70, 45); 482const glowMat = new THREE.ShaderMaterial({ 483 uniforms: { time: { value: 0 } }, 484 vertexShader: ` 485 varying vec2 vUv; 486 void main() { 487 vUv = uv; 488 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 489 } 490 `, 491 fragmentShader: ` 492 uniform float time; 493 varying vec2 vUv; 494 void main() { 495 vec2 center = vec2(0.5 + sin(time * 0.08) * 0.2, 0.5 + cos(time * 0.06) * 0.12); 496 float d = distance(vUv, center); 497 float glow = smoothstep(0.6, 0.0, d) * 0.28; 498 vec3 col = vec3(0.839, 0.286, 0.086) * glow; 499 gl_FragColor = vec4(col, glow); 500 } 501 `, 502 transparent: true, 503 depthWrite: false, 504 blending: THREE.AdditiveBlending 505}); 506const glow = new THREE.Mesh(glowGeom, glowMat); 507glow.position.z = -22; 508scene.add(glow); 509 510const clock = new THREE.Clock(); 511function animate() { 512 const t = clock.getElapsedTime(); 513 const pos = geometry.attributes.position.array; 514 515 for (let i = 0; i < particleCount; i++) { 516 const i3 = i * 3; 517 pos[i3] += velocities[i3]; 518 pos[i3 + 1] += velocities[i3 + 1]; 519 pos[i3 + 2] += velocities[i3 + 2]; 520 521 if (pos[i3] > 22) pos[i3] = -22; 522 if (pos[i3] < -22) pos[i3] = 22; 523 if (pos[i3 + 1] > 15) pos[i3 + 1] = -15; 524 if (pos[i3 + 1] < -15) pos[i3 + 1] = 15; 525 if (pos[i3 + 2] > 18) pos[i3 + 2] = -18; 526 if (pos[i3 + 2] < -20) pos[i3 + 2] = 12; 527 } 528 geometry.attributes.position.needsUpdate = true; 529 530 particles.rotation.y = t * 0.012; 531 particles.rotation.x = Math.sin(t * 0.06) * 0.015; 532 533 glowMat.uniforms.time.value = t; 534 535 renderer.render(scene, camera); 536 requestAnimationFrame(animate); 537} 538animate(); 539 540window.addEventListener("resize", () => { 541 camera.aspect = window.innerWidth / window.innerHeight; 542 camera.updateProjectionMatrix(); 543 renderer.setSize(window.innerWidth, window.innerHeight); 544}); 545 546// ============ SLIDES ============ 547const slides = [ 548 { 549 isGreeting: true, 550 hold: 2400 551 }, 552 { 553 html: `Thanks for stopping by<br>motive11.com.`, 554 weight: "light", 555 hold: 1600, 556 reveal: "word", 557 revealSpeed: 85 558 }, 559 { 560 html: `I'll be quick.<br>I just wanna explain something.`, 561 weight: "light", 562 hold: 1800, 563 reveal: "word",
564 revealSpeed: 80 565 }, 566 { 567 html: `Motive11 <span class="dim">is</span> my company.`, 568 weight: "heavy", 569 hold: 1600, 570 reveal: "word", 571 revealSpeed: 100 572 }, 573 { 574 html: `Websites. Marketing systems.<br>AI automation. CRM builds.`, 575 weight: "light", 576 hold: 2000, 577 reveal: "word", 578 revealSpeed: 85 579 }, 580 { 581 html: `It's <span class="orange">real.</span><br>It gets <span class="orange">real</span> results.`, 582 weight: "heavy", 583 hold: 2200, 584 reveal: "word", 585 revealSpeed: 110 586 }, 587 { 588 html: `But I'm going to be<br>straight with you.`, 589 weight: "light", 590 hold: 1600, 591 reveal: "word", 592 revealSpeed: 90 593 }, 594 { 595 html: `It's just <span class="orange">me.</span>`, 596 weight: "heavy", 597 hold: 2200, 598 reveal: "word", 599 revealSpeed: 130 600 }, 601 { 602 html: `No 20-person agency.<br>No team page<br>with stock photos.`, 603 weight: "", 604 hold: 2200, 605 reveal: "word", 606 revealSpeed: 80 607 }, 608 { 609 html: `One person who's spent<br>10+ years figuring out<br>how all this actually connects.`, 610 weight: "light", 611 hold: 2400, 612 reveal: "word", 613 revealSpeed: 80 614 }, 615 { 616 html: `Plus <span class="orange">AI,</span> doing the work<br>of many.`, 617 weight: "heavy", 618 hold: 2200, 619 reveal: "word", 620 revealSpeed: 110 621 }, 622 { 623 html: `Motive11 <span class="dim">is</span> the vehicle.`, 624 weight: "heavy", 625 hold: 1800, 626 reveal: "word", 627 revealSpeed: 110 628 }, 629 { 630 html: `I'm <span class="orange">the driver.</span>`, 631 weight: "heavy", 632 hold: 2200, 633 reveal: "word", 634 revealSpeed: 120 635 }, 636 { 637 html: `I run everything<br>through my personal brand.`, 638 weight: "light", 639 hold: 1800, 640 reveal: "word", 641 revealSpeed: 80 642 }, 643 { 644 html: `My content. My offers.<br>My YouTube. All of it.`, 645 weight: "light", 646 hold: 2000,
647 reveal: "word", 648 revealSpeed: 80 649 }, 650 { 651 html: `Rather than duplicate<br>all of it here,`, 652 weight: "light", 653 hold: 1600, 654 reveal: "word", 655 revealSpeed: 85 656 }, 657 { 658 html: `I'm sending you<br>to <span class="orange">the source.</span>`, 659 weight: "heavy", 660 hold: 2200, 661 reveal: "word", 662 revealSpeed: 110 663 }, 664 { 665 html: `If you're here<br>to work with me,`, 666 weight: "light", 667 hold: 1500, 668 reveal: "word", 669 revealSpeed: 75 670 }, 671 { 672 html: `<span class="orange">markszymanski.co</span>`, 673 weight: "heavy", 674 hold: 1800, 675 reveal: "char", 676 revealSpeed: 42, 677 isURL: true 678 }, 679 { 680 html: `<a class="cta-link" id="ctaLink" href="https://markszymanski.co/work-with-me" target="_top" rel="noopener">Find me there â</a>`, 681 weight: "", 682 hold: 999999, 683 reveal: "word", 684 revealSpeed: 140, 685 isCTA: true 686 } 687]; 688 689const stage = document.getElementById("stage"); 690const progressEl = document.getElementById("progress"); 691const pauseBtn = document.getElementById("pauseBtn"); 692 693// Fade the pause button in after the first slide has settled 694setTimeout(() => pauseBtn.classList.add("visible"), 2200); 695 696window.addEventListener("keydown", (e) => { 697 if (e.key === " " || e.code === "Space") { 698 e.preventDefault(); 699 togglePause(); 700 } 701}); 702 703// ============ PAUSE STATE ============ 704let paused = false; 705let pauseStartedAt = 0; 706const pauseWaiters = new Set(); 707 708function togglePause() { 709 if (paused) resume(); 710 else pause(); 711} 712 713function pause() { 714 if (paused) return; 715 paused = true; 716 pauseStartedAt = performance.now(); 717 pauseBtn.classList.add("paused"); 718 pauseBtn.setAttribute("aria-label", "Play"); 719 720 // Cancel every pending timer, keep remaining ms for resume 721 for (const w of pauseWaiters) { 722 clearTimeout(w.timerId); 723 w.timerId = null; 724 w.remaining = Math.max(0, w.fireAt - pauseStartedAt); 725 } 726} 727 728function resume() { 729 if (!paused) return; 730 const pausedFor = performance.now() - pauseStartedAt; 731 paused = false; 732 pauseBtn.classList.remove("paused"); 733 pauseBtn.setAttribute("aria-label", "Pause"); 734 735 // Shift progress bar's elapsed-time anchor 736 progressStart += pausedFor; 737 738 // Re-schedule every waiter with its remaining time 739 const now = performance.now(); 740 for (const w of pauseWaiters) { 741 w.fireAt = now + w.remaining; 742 w.timerId = setTimeout(w.fire, w.remaining); 743 } 744 745 if (progressRunning) requestAnimationFrame(tickProgress); 746} 747 748pauseBtn.addEventListener("click", togglePause); 749 750function pausableSleep(ms) { 751 return new Promise((resolve) => { 752 const waiter = { 753 fireAt: performance.now() + ms, 754 remaining: ms, 755 timerId: null, 756 fire: () => { 757 pauseWaiters.delete(waiter); 758 resolve(); 759 } 760 }; 761 pauseWaiters.add(waiter); 762 if (!paused) { 763 waiter.timerId = setTimeout(waiter.fire, ms); 764 } 765 // If paused, waiter just sits in the set; resume() will schedule it. 766 }); 767} 768 769function splitForReveal(html, mode) { 770 const tmp = document.createElement("div"); 771 tmp.innerHTML = html; 772 const output = document.createElement("div"); 773 774 function processNode(node, parent) { 775 if (node.nodeType === Node.TEXT_NODE) { 776 const text = node.nodeValue; 777 if (mode === "word") { 778 const parts = text.split(/(\s+)/); 779 for (const p of parts) { 780 if (p.length === 0) continue; 781 if (/^\s+$/.test(p)) { 782 parent.appendChild(document.createTextNode(p)); 783 } else { 784 const span = document.createElement("span"); 785 span.className = "word"; 786 span.textContent = p; 787 parent.appendChild(span); 788 } 789 } 790 } else if (mode === "char") { 791 for (const ch of text) { 792 if (ch === " ") { 793 parent.appendChild(document.createTextNode(" ")); 794 } else { 795 const span = document.createElement("span"); 796 span.className = "char"; 797 span.textContent = ch; 798 parent.appendChild(span); 799 } 800 } 801 } 802 } else if (node.nodeType === Node.ELEMENT_NODE) { 803 if (node.tagName === "BR") { 804 parent.appendChild(document.createElement("br")); 805 } else { 806 const clone = node.cloneNode(false); 807 parent.appendChild(clone); 808 for (const child of Array.from(node.childNodes)) { 809 processNode(child, clone); 810 } 811 } 812 } 813 } 814 815 for (const child of Array.from(tmp.childNodes)) { 816 processNode(child, output); 817 } 818 819 return output; 820} 821 822function createSlide(slide, index) { 823 const el = document.createElement("div"); 824 el.className = "slide" 825 + (slide.isCTA ? " cta-slide" : "") 826 + (slide.isGreeting ? " greeting-slide" : "") 827 + (slide.isURL ? " url-slide" : ""); 828 829 const inner = document.createElement("div"); 830 inner.className = "slide-inner"; 831 832 if (slide.isGreeting) { 833 const img = document.createElement("img"); 834 img.className = "portrait"; 835 img.src = "/images/mark-szymanski.webp"; 836 img.alt = "Mark Szymanski"; 837 838 const text = document.createElement("div"); 839 text.className = "greeting-text"; 840 text.innerHTML = `
841 <span class="line"><span class="word"><span class="wave">ð</span>Hey there.</span></span> 842 <span class="line"><span class="word">I'm Mark Szymanski.</span></span> 843 `; 844 845 inner.appendChild(img); 846 inner.appendChild(text); 847 el.appendChild(inner); 848 return el; 849 } 850 851 const big = document.createElement("div"); 852 big.className = "big" + (slide.weight ? " " + slide.weight : ""); 853 854 const revealContainer = splitForReveal(slide.html, slide.reveal); 855 while (revealContainer.firstChild) { 856 big.appendChild(revealContainer.firstChild); 857 } 858 859 inner.appendChild(big); 860 el.appendChild(inner); 861 return el; 862} 863 864async function revealSlide(slideEl, mode, speed) { 865 const units = slideEl.querySelectorAll(mode === "word" ? ".word" : ".char"); 866 if (units.length === 0) return; 867 for (let i = 0; i < units.length; i++) { 868 units[i].classList.add("in"); 869 if (i < units.length - 1) await pausableSleep(speed); 870 } 871 await pausableSleep(300); 872} 873 874async function revealGreeting(slideEl) { 875 const img = slideEl.querySelector(".portrait"); 876 const words = slideEl.querySelectorAll(".greeting-text .word"); 877 if (img) requestAnimationFrame(() => img.classList.add("in")); 878 await pausableSleep(400); 879 for (let i = 0; i < words.length; i++) { 880 words[i].classList.add("in"); 881 if (i < words.length - 1) await pausableSleep(450); 882 } 883 await pausableSleep(400); 884} 885 886// All timing in the sequencer goes through pausableSleep 887const sleep = pausableSleep; 888 889function countRevealUnits(html, mode) { 890 if (!html) return 0; 891 const tmp = document.createElement("div"); 892 tmp.innerHTML = html; 893 const text = tmp.textContent || ""; 894 if (mode === "word") { 895 return text.trim().split(/\s+/).filter(Boolean).length; 896 } 897 return text.replace(/\s+/g, "").length; 898} 899 900function estimateSlideDuration(slide) { 901 const PRE = 300; 902 const REVEAL_BUFFER = 300; 903 const FADE_OUT = 900; 904 if (slide.isGreeting) { 905 // 400ms initial delay + 2 lines à 450ms stagger + 400ms trailing buffer 906 return PRE + 400 + 2 * 450 + 400 + slide.hold + FADE_OUT; 907 } 908 const units = countRevealUnits(slide.html, slide.reveal); 909 const revealTime = Math.max(0, units - 1) * (slide.revealSpeed || 0); 910 return PRE + revealTime + REVEAL_BUFFER + slide.hold + FADE_OUT; 911} 912 913let progressStart = 0; 914let progressTotal = 0; 915let progressRunning = false; 916 917function tickProgress() { 918 if (!progressRunning || paused) return; 919 const elapsed = performance.now() - progressStart; 920 const pct = Math.min(100, (elapsed / progressTotal) * 100); 921 progressEl.style.width = pct + "%"; 922 if (pct < 100) requestAnimationFrame(tickProgress); 923} 924 925async function run() { 926 // total runtime = everything up to the moment the CTA appears 927 progressTotal = slides 928 .filter(s => !s.isCTA) 929 .reduce((sum, s) => sum + estimateSlideDuration(s), 0); 930 // add the pre-run sleep and the CTA's pre-reveal/fade-in so the bar 931 // finishes as the CTA text starts revealing 932 progressTotal += 700 + 300; 933 934 await sleep(700); 935 936 progressStart = performance.now(); 937 progressRunning = true; 938 requestAnimationFrame(tickProgress); 939 940 for (let i = 0; i < slides.length; i++) { 941 const slide = slides[i]; 942 const el = createSlide(slide, i); 943 stage.appendChild(el); 944 945 void el.offsetHeight; 946 947 el.classList.add("active"); 948 949 await sleep(300); 950 if (slide.isGreeting) { 951 await revealGreeting(el); 952 } else { 953 await revealSlide(el, slide.reveal, slide.revealSpeed); 954 } 955 956 if (slide.isCTA) { 957 progressRunning = false; 958 progressEl.style.width = "100%"; 959 const link = el.querySelector(".cta-link"); 960 if (link) { 961 setTimeout(() => link.classList.add("drawn"), 400); 962 } 963 } 964 965 await sleep(slide.hold); 966 967 if (!slide.isCTA) { 968 el.classList.remove("active"); 969 await sleep(900); 970 stage.removeChild(el); 971 } 972 } 973} 974 975run(); 976</script>
976 977
978<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4bc70e2c01a94c73b74392e4234840661791215815920" integrity="sha512-L0ha0OXavK/8okipN9F8BtP84dg9DUhPERbBXzwI6dgTA55d2+yweo3pn5CSFYs45/r8md2+xvUPtTdvTNRfjA==" data-cf-beacon='{"version":"2024.11.0","token":"300aae7c47024b3092c1b46969bd2ba9","r":1,"spa":2}' crossorigin="anonymous"></script>
978 979</body> 980</html>
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.