<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>IntersectionObserver &#8211; 小豬日常</title>
	<atom:link href="https://piglife.tw/tag/intersectionobserver/feed/" rel="self" type="application/rss+xml" />
	<link>https://piglife.tw</link>
	<description>Hello World，一個紀錄生活與學習的地方</description>
	<lastBuildDate>Wed, 10 Dec 2025 09:45:07 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://piglife.tw/wp-content/uploads/2017/10/cropped-logo-1-32x32.png</url>
	<title>IntersectionObserver &#8211; 小豬日常</title>
	<link>https://piglife.tw</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>使用 IntersectionObserver 實作數字滾動動畫效果</title>
		<link>https://piglife.tw/technical-notes/%e4%bd%bf%e7%94%a8-intersectionobserver-%e5%af%a6%e4%bd%9c%e6%95%b8%e5%ad%97%e6%bb%be%e5%8b%95%e5%8b%95%e7%95%ab%e6%95%88%e6%9e%9c/</link>
					<comments>https://piglife.tw/technical-notes/%e4%bd%bf%e7%94%a8-intersectionobserver-%e5%af%a6%e4%bd%9c%e6%95%b8%e5%ad%97%e6%bb%be%e5%8b%95%e5%8b%95%e7%95%ab%e6%95%88%e6%9e%9c/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Tue, 09 Dec 2025 18:48:02 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[IntersectionObserver]]></category>
		<category><![CDATA[JavaScript]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/%e4%bd%bf%e7%94%a8-intersectionobserver-%e5%af%a6%e4%bd%9c%e6%95%b8%e5%ad%97%e6%bb%be%e5%8b%95%e5%8b%95%e7%95%ab%e6%95%88%e6%9e%9c/</guid>

					<description><![CDATA[在網頁中，常見的數字滾動動畫能有效提升視覺吸引力，尤其用於統計數據或關鍵指標的呈現。這段程式碼示範如...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在網頁中，常見的數字滾動動畫能有效提升視覺吸引力，尤其用於統計數據或關鍵指標的呈現。這段程式碼示範如何結合 IntersectionObserver 與計時器，實現當數字元素滾動進入視窗時，從初始值平滑遞增到目標數字的動畫效果。適合有基礎 JavaScript 知識，並希望理解如何優化滾動觸發動畫的工程師或自學者。</p>
<h2 class="wp-block-heading">使用 IntersectionObserver 監控元素可見性</h2>
<p>為避免動畫在頁面初始就執行，程式利用 IntersectionObserver 監控所有帶有 <code>.number</code> 類別的元素。當元素至少有 30% 進入視窗時，觸發動畫函式。</p>
<pre><code class="lang-javascript language-javascript javascript">const observer = new IntersectionObserver(
  (entries) =&gt; {
    entries.forEach(entry =&gt; {
      if (entry.isIntersecting) {
        animateCounter(entry.target);
      }
    });
  },
  { threshold: 0.3 }
);

counters.forEach(counter =&gt; {
  observer.observe(counter);
});</code></pre>
<p>這裡的 threshold 設為 0.3，代表元素可見度達 30% 即觸發動畫，這個數值可以根據需求調整，達到更精準的觸發時機。</p>
<h2 class="wp-block-heading">數字動畫的實作細節</h2>
<p>動畫函式 <code>animateCounter</code> 主要負責數字從初始值（此處為 1）平滑遞增到目標數字。實作重點如下：</p>
<h3 class="wp-block-heading">避免重複執行</h3>
<p>利用元素的 <code>dataset.animated</code> 屬性標記，確保同一元素不會重複觸發動畫，避免動畫重疊或閃爍。</p>
<pre><code class="lang-javascript language-javascript javascript">if (counter.dataset.animated) return;
counter.dataset.animated = &quot;true&quot;;</code></pre>
<h3 class="wp-block-heading">處理數字與附加的 <code>&lt;sup&gt;</code> 標籤</h3>
<p>有些數字後面可能會帶有 <code>&lt;sup&gt;</code> 標籤（例如加號「+」），程式先將其分離並保留，動畫過程只針對純數字進行計算，最後再將 <code>&lt;sup&gt;</code> 加回去。</p>
<pre><code class="lang-javascript language-javascript javascript">const sup = counter.querySelector(&quot;sup&quot;);
const supHTML = sup ? sup.outerHTML : &quot;&quot;;
const rawText = counter.childNodes[0].nodeValue.trim();
const target = parseFloat(rawText);</code></pre>
<h3 class="wp-block-heading">動畫邏輯</h3>
<ul>
<li>設定動畫總時長為 1500 毫秒，幀率為 30 fps。</li>
<li>使用 <code>setInterval</code> 逐幀更新數字。</li>
<li>根據進度比例計算當前數字，並判斷是否為小數，決定顯示格式。</li>
<li>動畫結束時，強制將數字顯示為原始目標值，避免浮點數誤差。</li>
</ul>
<pre><code class="lang-javascript language-javascript javascript">const duration = 1500;
const frameRate = 30;
const totalFrames = Math.round(duration / (1000 / frameRate));
let frame = 0;

const timer = setInterval(() =&gt; {
  frame++;
  const progress = frame / totalFrames;
  const current = start + (target - start) * progress;
  counter.innerHTML = (isDecimal ? current.toFixed(1) : Math.floor(current)) + supHTML;
  if (frame &gt;= totalFrames) {
    clearInterval(timer);
    counter.innerHTML = rawText + supHTML;
  }
}, 1000 / frameRate);</code></pre>
<h2 class="wp-block-heading">實際應用與優化方向</h2>
<ul>
<li>此動畫適合用於展示統計數字、銷售數據、會員人數等 KPI。</li>
<li>可依需求調整動畫時長與幀率，達到不同的視覺效果。</li>
<li>若數字範圍極大，建議調整起始值或使用指數型動畫增長，提升動畫流暢度。</li>
<li>可結合 CSS 動畫或其他視覺效果，增強整體互動體驗。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li><strong>重複觸發動畫</strong>：未使用 <code>dataset.animated</code> 標記，可能導致動畫重複執行。</li>
<li><strong>浮點數誤差</strong>：動畫過程中使用浮點數計算，最後強制顯示原始文字可避免誤差。</li>
<li><strong>IntersectionObserver 支援度</strong>：大部分現代瀏覽器支援，舊版瀏覽器需考慮 polyfill。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&quot;DOMContentLoaded&quot;, function () {

    const counters = document.querySelectorAll(&quot;.number&quot;);

    const animateCounter = (counter) =&gt; {

        // 避免重複執行
        if (counter.dataset.animated) return;
        counter.dataset.animated = &quot;true&quot;;

        // 取出 &lt;sup&gt;+&lt;/sup&gt;
        const sup = counter.querySelector(&quot;sup&quot;);
        const supHTML = sup ? sup.outerHTML : &quot;&quot;;

        // 取純數字（忽略 sup）
        const rawText = counter.childNodes[0].nodeValue.trim();
        const target = parseFloat(rawText);
        const isDecimal = rawText.includes(&quot;.&quot;);

        let start = 1;
        const duration = 1500;
        const frameRate = 30;
        const totalFrames = Math.round(duration / (1000 / frameRate));
        let frame = 0;

        const timer = setInterval(() =&gt; {
            frame++;

            const progress = frame / totalFrames;
            const current = start + (target - start) * progress;

            counter.innerHTML = (isDecimal ? current.toFixed(1) : Math.floor(current)) + supHTML;

            if (frame &gt;= totalFrames) {
                clearInterval(timer);
                counter.innerHTML = rawText + supHTML;
            }
        }, 1000 / frameRate);
    };

    // IntersectionObserver：滾動到可見才啟動
    const observer = new IntersectionObserver(
        (entries) =&gt; {
            entries.forEach(entry =&gt; {
                if (entry.isIntersecting) {
                    animateCounter(entry.target);
                }
            });
        },
        { threshold: 0.3 } // 可見 30% 即觸發，可調整
    );

    counters.forEach(counter =&gt; {
        observer.observe(counter);
    });

});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/%e4%bd%bf%e7%94%a8-intersectionobserver-%e5%af%a6%e4%bd%9c%e6%95%b8%e5%ad%97%e6%bb%be%e5%8b%95%e5%8b%95%e7%95%ab%e6%95%88%e6%9e%9c/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
