PageSourceSearch

https://www.kershaw.design/js/views/sharkninja.js

js kershaw.design collected 2026-10-10 01:41:26 UTC 9,542 bytes, 203 lines download raw bytes

1Router.register('sharkninja', {
2  mount(el) {
3    window.setPageTitle?.('SharkNinja');
4    el.classList.add('is-project');
5    window.setTerminalBack?.('#portfolio');
6    window.setProjectAccent?.('#e8362b');
7    this._el = el;
8
9    // 4 columns × 2 screens. Cols 1&3 start lower to match Figma stagger.
10    const colConfigs = [
11      { stagger: 120, speed: -80, screens: ['screen_1', 'screen_2'] },
12      { stagger:   0, speed:  65, screens: ['screen_3', 'screen_4'] },
13      { stagger: 120, speed: -70, screens: ['screen_5', 'screen_6'] },
14      { stagger:   0, speed:  90, screens: ['screen_7', 'screen_8'] },
15    ];
16
17    el.innerHTML = `
18      <div class="proj-page">
19
20        <div class="proj-scene">
21          <div class="proj-hero-wrap">
22            <video class="proj-hero" src="images/portfolio/sharkninja/hero.mp4" poster="images/portfolio/sharkninja/hero.webp" autoplay loop muted playsinline></video>
23          </div>
24          <div class="proj-header-overlay">
25            <section class="proj-header">
26              <div class="proj-header-left">
27                <h1>SharkNinja.com</h1>
28                <div class="proj-meta">
29                  <p>SharkNinja, USA</p>
30                </div>
31              </div>
32              <div class="proj-desc">
33                <ul class="proj-services">
34                  <li>Brand Design</li>
35                  <li>Web Design</li>
36                  <li>Ecommerce</li>
37                  <li>Creative Strategy</li>
38                </ul>
39                <p><strong>Reimagined SharkNinja's</strong> digital presence by uniting the Shark and Ninja brands on one cohesive eCommerce experience, designed to match the innovation and excellence of their physical products.</p>
40              </div>
41            </section>
42            <span class="proj-copyright">©2025-26</span>
43          </div>
44        </div>
45
46        <div class="proj-body">
47          <section class="proj-mobile-desc proj-reveal">
48            <p><strong>Reimagined SharkNinja's</strong> digital presence by uniting the Shark and Ninja brands on one cohesive eCommerce experience, designed to match the innovation and excellence of their physical products.</p>
49          </section>
50          <section class="proj-quote proj-quote--sn proj-reveal">
51            <p>SharkNinja makes some of the most talked-about products on the internet — the kind of kitchen and home gadgets that explode across TikTok feeds and rack up millions of views. The challenge was building a DTC experience worthy of that energy. We unified two iconic brands into a single global platform engineered for shareability, fandom, and buzz.</p>
52          </section>
53          <video class="proj-img proj-reveal" src="images/portfolio/sharkninja/beckham_home.mp4" autoplay loop muted playsinline></video>
54          <div class="sn-desktop-scroll proj-reveal">
55            <div class="sn-desktop-sticky">
56              <img class="sn-desktop-img" src="images/portfolio/sharkninja/desktop.webp" alt="SharkNinja homepage">
57            </div>
58          </div>
59
60          <div class="sn-phones-track proj-reveal">
61            <div class="sn-phones-scene">
62              ${colConfigs.map(c => `
63                <div class="sn-col" style="padding-top:${c.stagger}px" data-speed="${c.speed}">
64                  ${c.screens.map(s => `
65                    <div class="sn-phone">
66                      <img src="images/portfolio/sharkninja/${s}.webp" alt="SharkNinja mobile screen" loading="lazy">
67                    </div>
68                  `).join('')}
69                </div>
70              `).join('')}
71            </div>
72          </div>
73
74
75          <section class="proj-quote proj-quote--sn proj-reveal">
76            <p>We designed social, snackable, and watchable product experiences, DTC-exclusive moments like Build Your Own Bundles and a flexible design system that scales across countless products and markets. The result: a digital experience that doesn't just sell — it sells itself, turning customers into fans and advocates. Launched in under nine months, and engineered for what's next.</p>
77          </section>
78          <img class="proj-img proj-reveal" src="images/portfolio/sharkninja/slushi.webp"     width="1920" height="2011" alt="SharkNinja Slushi experience">
79          <img class="proj-img proj-reveal" src="images/portfolio/sharkninja/pdp.webp"        width="1920" height="1080" alt="SharkNinja product detail pages">
80
81          <img class="proj-img proj-reveal" src="images/portfolio/sharkninja/glam_01.webp" width="1920" height="1080" alt="SharkNinja GLAM before and after">
82
83          <img class="proj-img proj-reveal" src="images/portfolio/sharkninja/ds_grid.webp" width="3840" height="2160" alt="SharkNinja design system">
84
85          <nav class="proj-pager proj-reveal">
86            <a class="proj-pager-link" href="#android-xr"><svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg"><polyline points="7,1 1,7 7,13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Android XR</a>
87            <a class="proj-pager-link proj-pager-next" href="#kpmg-lakehouse">KPMG Lakehouse <svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg"><polyline points="1,1 7,7 1,13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></a>
88          </nav>
89        </div>
90
91      </div>
92    `;
93
94    const progress = document.createElement('div');
95    progress.id = 'proj-progress';
96    document.body.appendChild(progress);
97    this._progress = progress;
98
99    const scene = el.querySelector('.sn-phones-scene');
100    const cols  = el.querySelectorAll('.sn-col');
101
102    const tickParallax = () => {
103      if (!scene) return;
104      const rect    = scene.getBoundingClientRect();
105      const vh      = el.clientHeight;
106      const centerY = rect.top + rect.height / 2;
107      const maxDist = vh / 2 + rect.height / 2;
108      const t       = (centerY - vh / 2) / maxDist;
109      const clamped = Math.max(-1.2, Math.min(1.2, t));
110      cols.forEach(col => {
111        col.style.transform = `translateY(${clamped * +col.dataset.speed}px)`;
112      });
113    };
114
115    // Desktop scrolljack
116    const desktopScroll  = el.querySelector('.sn-desktop-scroll');
117    const desktopSticky  = el.querySelector('.sn-desktop-sticky');
118    const desktopImg     = el.querySelector('.sn-desktop-img');
119
120    let desktopRange = 0;
121
122    const initDesktop = () => {
123      if (window.innerWidth <= 768) {
124        desktopRange = 0;
125        if (desktopScroll) desktopScroll.style.height = '';
126        if (desktopImg)    desktopImg.style.transform  = '';
127        return;
128      }
129      if (!desktopScroll || !desktopImg || !desktopImg.naturalWidth) return;
130      // Use window dimensions rather than offsetHeight/offsetWidth — those can
131      // return 0 when called synchronously right after innerHTML is set.
132      const stickyH = window.innerHeight;                    // sticky is 100vh
133      const imgW    = window.innerWidth * 0.75;              // image is width:75%
134      const imgH    = imgW * (desktopImg.naturalHeight / desktopImg.naturalWidth);
135      desktopRange  = Math.max(0, imgH - stickyH);
136      desktopScroll.style.height = `${stickyH + desktopRange}px`;
137    };
138
139    const tickDesktop = () => {
140      if (!desktopScroll || !desktopImg || !desktopRange) return;
141      const rect   = desktopScroll.getBoundingClientRect();
142      const offset = Math.max(0, Math.min(desktopRange, -rect.top));
143      desktopImg.style.transform = `translateY(${-offset}px)`;
144    };
145
146    if (desktopImg?.complete && desktopImg.naturalHeight > 0) {
147      initDesktop();
148    } else {
149      desktopImg?.addEventListener('load', initDesktop);
150    }
151    this._onResize = initDesktop;
152    window.addEventListener('resize', this._onResize);
153
154    const isMobile = () => window.innerWidth <= 768;
155
156    let _scrollRafId = null;
157    this._onScroll = () => {
158      const pct = el.scrollTop / (el.scrollHeight - el.clientHeight);
159      progress.style.height = (Math.min(pct, 1) * 100) + '%';
160      if (!isMobile() && !_scrollRafId) {
161        _scrollRafId = requestAnimationFrame(() => {
162          _scrollRafId = null;
163          tickParallax(); tickDesktop();
164        });
165      }
166    };
167    this._cancelScrollRaf = () => { if (_scrollRafId) { cancelAnimationFrame(_scrollRafId); _scrollRafId = null; } };
168    el.addEventListener('scroll', this._onScroll);
169    if (!isMobile()) { tickParallax(); }
170
171    const observer = new IntersectionObserver(
172      entries => entries.forEach(entry => {
173        if (entry.isIntersecting) {
174          entry.target.classList.add('is-revealed');
175          observer.unobserve(entry.target);
176        }
177      }),
178      { threshold: 0, rootMargin: '0px 0px -80px 0px' }
179    );
180    el.querySelectorAll('.proj-reveal').forEach(node => observer.observe(node));
181    this._observer = observer;
182  },
183
184  unmount() {
185    this._observer?.disconnect();
186    this._observer = null;
187    if (this._progress) { this._progress.remove(); this._progress = null; }
188    this._cancelScrollRaf?.();
189    this._cancelScrollRaf = null;
190    if (this._onScroll) {
191      this._el?.removeEventListener('scroll', this._onScroll);
192      this._onScroll = null;
193    }
194    if (this._onResize) {
195      window.removeEventListener('resize', this._onResize);
196      this._onResize = null;
197    }
198    this._el = null;
199    document.getElementById('panel')?.classList.remove('is-project');
200    window.clearTerminalBack?.();
201    window.clearProjectAccent?.();
202  }
203});

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.