週末長知識: Parallax Scrolling on Touch Devices

October 06, 2014

寫在前面,我個人不是這麼熱愛 parallax scrolling。

觸控裝置制作 parallax scrolling 面臨的問題

大部分的 parallax scrolling 效果都是透過 listen windowscroll event 來達成,然而在觸控裝置上的體驗會顯得非常詭異。

可以參考一下兩年以前的這篇 onscroll Event Issues on Mobile Browsers

Unlike desktop browsers, most all mobile browsers simply do not fire an onscroll event until the scrolling action comes to a complete stop.

The only mobile browser that handled this event elegantly in my testing was Android’s Jelly Bean browser.

譯:大部分的行動裝置在捲動完全停止前都不會觸發 onscroll 事件(Android 4.3+ 除外)

因此觸控裝置在瀏覽一般 parallax scrolling 網站的時候都是看到一段一段定格的效果,而不會像是使用一般桌面瀏覽器那樣流暢的感覺。

當然,我們也不用太絕望,畢竟那是兩年前的事兒了,現在的 Android 4.3+ 普及率還是比較高的(是這樣說的嗎)。

純 JavaScript 解法(吧)

可能需要視是否為行動裝置來載入 Scrollability,我沒有實際使用過,但理論上是可行的。

請見 Stellar.js 的 iOS demo

基於 HTML/CSS 結構的解法

就在一陣子以前我讀到了一個系列文章:

意外的發現裡面的 demo 在手機上是可以運作的,讓我非常好奇原因是什麼;從整篇教學文以及網頁原始碼裡看起來,沒有使用任何別的 JS library。經過一番仔細的研究,發現關鍵是在於 裡面的 parallax 舞台不是基於 windowonscroll

讓我們來看一下關鍵的 HTML/CSS 片段:

<body>
  <div class="parallax">
    <div class="parallax__group">
      <div class="parallax__layer parallax__layer--fore">...</div>
      <div class="parallax__layer parallax__layer--base">...</div>
      <div class="parallax__layer parallax__layer--back">...</div>
      <div class="parallax__layer parallax__layer--deep">...</div>
    </div>
  </div>
</body>
.parallax {
  height: 500px; /* fallback for older browsers */
  height: 100vh;
  overflow-x: hidden;
  overflow-y: scroll;
  -webkit-perspective: 300px;
  perspective: 300px;
}
.parallax__group {
  position: relative;
  height: 500px; /* fallback for older browsers */
  height: 100vh;
  -webkit-transform-style: preserve-3d;
  transform-style: preserve-3d;
}
.parallax__layer {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

可以發現基本上整個的 scroll 動作會被限制在 .parallax 這個 <div> 裡面,而不像一般的做法是在 <body> 上做 scroll。在這樣的設定之下,神秘的現象發生了!

onscroll 事件會持續觸發 (做為等價交換,momentum scrolling 無法作用)

而此時的 JS 程式碼 listen 的目標也確實就是 .parallax 那個 <div>

var body = document.querySelector('.parallax'), scrollTop = 0;
...
body.addEventListener('scroll', function (e){
  ...
});

所以

前陣子在弄 angular-scroll-watch 的範例的時候還特別針對同一個範例做了 支援/不支援 觸控裝置的版本,大家可以用觸控裝置親自試試看:

但其實

iOS 8 Safari no longer disables scroll events

不好意思讓你讀了這麼久(誤)。

目前 iOS 8 上似乎也只有 Safari 有支援持續觸發 onscroll,所以這篇長的知識應該還算是依然有用的啦!iOS 8 Safari 瀏覽 parallax 網站還真的是順順的欸…

最後關於 parallax scrolling

開頭有提到我不熱愛,其實並不是我不喜歡那個效果的感覺,而是大部分的網站往往會因為過度使用或沒有對效能做最佳化而導至實際上使用的體驗不好。在 parallax scrolling 風靡的這些日子裡,讓我真心覺得做得非常好的網站屈指可數(其實也沒有這麼少啦)。

如果你本身有在做 parallax scrolling 的話,我個人有幾個建議:

  • 加個 smooth mouse wheel 的效果 - 不是所有人都用 Mac/Firefox
  • 全程 60FPS - 偶爾低一點點沒關係,但 50FPS 是底限
  • 不要為了 parallax 而 parallax - 應該先從網頁想傳達的概念下手,不是真的所有網站都適合做 parallax
  • 承上,如果還是想,請適量 - 在不是這麼適合的情況下,如果只有加一點點,還是可以擁有小確幸

關於 60FPS 的部分,如果你不是很了解如何調校的話,可以看看下面這影片

A case study of using jQuery plugin with AngularJS: One Page Nav

September 20, 2014

坊間的 jQuery plugin + AngularJS 的做法都蠻基本又簡單的(e.g. An approach to use jQuery Plugins with AngularJS)。

實際上真正想要做一個對 jQuery plugin 的完善包裝是相當費工的,最近正好因緣際會之下有個使用 jQuery plugin 的需求,我們就來將這一次包裝成 directive 的過程走一遍吧!

jQuery One Page Nav

這次要包裝的 jQuery plugin 是 jQuery One Page Nav,效果基本上就是提供一個「知道你捲動到哪一個區塊了」的導覽列,同時也提供「捲動到特定區塊」的功能。

效果可以參考它的官方網頁: http://davist11.github.io/jQuery-One-Page-Nav/

Directive: jq-one-page-nav

基本的 directive 包裝就可以讓這個 jQuery plugin 動起來(在基本的情境下),經過一番深思熟慮之後,我決定將這個 directive 叫做 jq-one-page-nav

/* file: jq-one-page-nav.js */

angular.module('pymaster.jq-one-page-nav', [])

.directive('jqOnePageNav', function ($timeout, $rootScope) {
  return {
    restrict: 'EA',
    scope: {
      items: '=',
      options: '='
    },
    replace: true,
    template: '\
      <div class="jq-one-page-nav">\
        <ul>\
          <li ng-repeat="item in items">\
            <a ng-href="#{{ item.id }}" ng-bind="item.content" title="{{ item.content }}"></a>\
          </li>\
        </ul>\
      </div>\
    ',
    link: function (scope, iElm, iAttrs) {
      $timeout(function () {
        iElm.find('ul').onePageNav(scope.options || {});
      });
    }
  };
});

這邊提供了兩個 attribute 可以傳入,分別是 itemsoptions

參數 功能
items 想要擺在導覽列的連結陣列,單一 item 的內容為 {id: '{HTML ID}', name: '{nav button name}'}
options 對應 jQuery One Page Nav 原本的 options

Code 說明

  • template 寫死 ul>li>a ,因為這是 jQuery One Page Nav 官方期待的 markup 結構
  • 裡面使用 $timeout 的原因是為了確保在 items 傳入後已經正確 compile 出 HTML 才做 jQuery plugin 的 initialization
  • 另外一個使用 $timeout 的原因是 One Page Nav 在 .onePageNav(...) 的當下會去取得各個導覽項目對應的 HTML ID 元素的捲動高度,若是你的 jq-one-page-nav 在 HTML 結構裡擺的 位置/順序 不對,很可能就無法取得正確的捲動高度

改進 1:支援重覆使用

Read on 

Understanding Gutter Positions in Susy

September 07, 2014

http://www.zell-weekeat.com/susy-gutter-positions/

One question that was asked in the Susy survey I created a month ago really stood out to me. The question is “how to remove margins or paddings of the first and last column without using first-child and last-child in the grid system?”.

自從把某專案的 Susy 從 1 升到 2,我整個人像掉入無底洞… Susy 2 的 doc 真的不知道在寫什麼,感覺非常的沒有組織,跳來跳去的;偏偏 Susy 1 -> 2 又給人一種完全不認識它的感覺,我就這樣迷失在 Susy 2 的 doc 海裡了。

後來在網路上尋尋覓覓終於找到了幾篇好文(其實是尋覓了幾個月的期間,這些文章才被寫出來… Susy 2 太新了)。

這篇文章很詳盡的解釋了在 Susy 裡面各種 gutter 模式是怎麼一回事,當下讓我門戶洞開(?)。老實說我不知道 Susy 1 的年代有沒有分這些模式,但到了 Susy 2 它們就像天上掉下來一樣到處都是,而我卻一個都不認識。

結論

基本上可以整理成一張表。

名稱 種類 位置 方向 單位 注意搭配 nest 使用
after margin 外側 % no
before margin 外側 % no
split margin 外側 雙向 % yes
inside padding 內側 雙向 % yes
inside-static padding 內側 雙向 固定單位 yes

知道這些,除了在 Susy 2 裡面用爽爽之外,也可以讓你對目前各種實作 grid 的方法有更多的了解,進而使自己的 CSS 能力更上一層樓。

祝各位 Susy 2 使用愉快啦!

我想像中的走路被車撞

September 06, 2014

在我走路的思緒裡,一直以來都有一個項目是「會不會有車撞我」。

綠燈被右轉車撞

其實綠燈被右轉車撞的情況理應是不太會發生,畢竟車子在轉彎時基本都會 慢下來/停下來 等行人過。 如果此時行人也預測一下眼前那台車是想先讓你過還是想先轉,就會發生類似走路想閃路人結果兩人一起連續閃同一邊的情況,每每覺得有點心驚。

於是後來我決定採放任策略,明明看到前面有車要轉就也照樣走路,把我被撞與否交給車主。你可能覺得這樣一定不會被撞吧?我相信在人性本惡的前提之下,總有人會被撞的.. 也覺得非常有可能是我。 如果我犧牲一下可以淨化車主的心,那被撞一下似乎也是划算。

所以我隨時做好了綠燈右轉被車撞的準備。

註: 有些路口是轉彎點距離斑馬線有一小段距離,而且路又大,這時候的車速可能會比較快,如果再發生上述的閃人現象就格外容易被撞。

走路闖紅燈被車撞

是說沒事幹嘛走路闖紅燈?其實走路闖紅燈的人非常非常多。

越小的路口,人類在人性本惡的驅動之下就是想貪小便宜闖個紅燈。小路口的話,車子應該也會減速之類的吧?會的。 理想情況下,開車/騎車的人為了避免去撞在小路口闖紅燈的人,通常在小路口就算是綠燈也不會太快。但這是建立在銜接的路口不是接到大馬路的前提之下。

如果小路口銜接到大馬路,通常表示可以通往大馬路的紅燈時間會蠻長的,於是車主就算在小路的深處,看到出口處是綠燈也會盡快的開到路口;而這時候就是比較無暇顧及是否有闖紅燈的人的時候。

所以我個人提倡,並且實踐,再小的路口也不闖紅燈。

註: 其實往往看到有闖小路口紅燈的人竟然連闖紅燈的時候也不注意一下來車,我心裡會出現「被撞一次就知道了吧」的想法,屢試不爽。

走路被闖紅燈的車撞

這個相較於上一項,發生率比較低一些。

畢竟相對於在小路口愛闖紅燈的人,愛闖紅燈的車還是非常少的;而往往偶爾在小路口闖紅燈的車危及到的大都是其它的車而不是人。但基於人性本惡的原則,就算是過綠燈馬路還是要小心,畢竟會有 綠燈右轉車闖紅燈的車 來撞你。

被腳踏車撞

在 YouBike 橫行的台北人行道上,我想被腳踏車撞應該也不是什麼稀奇的事(至少親眼看到腳踏車擦撞腳踏車)。

由於腳踏車的速度相較於人走路的速度實在差非常多,有很多情況下的閃避是會來不及的(只好撞你)。

我理想中的腳踏車應該是會完全禮讓行人並且適時的減速(甚至停下來),避免在人行道高密度的時候 錯行人/錯車,但由於有為數不少的人在騎腳踏車的時候就是會貪快,理想終究只是理想。

What’s your point

所以本文介紹了幾種走路被車撞的情況:

  • 綠燈被右轉車撞
  • 走路闖紅燈被車撞
  • 走路被闖紅燈的車撞
  • 被腳踏車撞

關鍵在於實在非常多人都寧可冒著這個險,也要達成相較之下通常比較微不足道的目標(少數較重要的例外可能是老婆要生了或是家裡失火之類的)。

希望不管是上面所陳述被撞的也好、撞人的也好,在走路的時候多看看四周,多想想其他 行人/行車。

世界太黑暗了。從微小的地方做起,提昇一下你的層次吧!

最後附上核心概念影片

簡單粗暴,偵測網頁元素的建立

August 30, 2014

說到偵測網頁元素的建立,我想有在 follow web platform 的人們應該會立刻想到 MutationObserver ;不過這邊既然說了是 簡單粗暴 ,表示並不是要用 MutationObserver 來達成這個目標。

其實這招是一個行之有年的方法,不過礙於我本人最近才知道(長知識)所以才在這邊分享。

原理

利用 CSS selector 來對指定的元素套上一個無意義(不會影響原本元素顯示)的 CSS animation,於是在該元素建立的時候將會觸發 animationstart 事件,而我們就可以利用此事件找到觸發的來源而取得新建立的元素。

實作

原理簡單粗暴,實作也是簡單粗暴。

CSS

套用 CSS animation 的關鍵在於確實讓各主要瀏覽器都能夠確實的觸發 animationstart 事件(或它的 vender specific 版本),經過測試之後,以下的 CSS animation 可以確實觸發各主要瀏覽器(Chrome, Firefox, IE)上的事件。

@-webkit-keyframes pymaster-detect-dom-insertion { from {opacity: 0.99;} to {opacity: 1;} }
@keyframes         pymaster-detect-dom-insertion { from {opacity: 0.99;} to {opacity: 1;} }
{selector} {
  -webkit-animation-delay: 0s !important;
  -webkit-animation-name: pymaster-detect-dom-insertion !important;
  -webkit-animation-duration: 1ms !important;
  -webkit-animation-play-state: running !important;
  animation-delay: 0s !important;
  animation-name: pymaster-detect-dom-insertion !important;
  animation-duration: 1ms !important;
  animation-play-state: running !important;
}

{selector} 的部分可以自行代換成任何想要偵測的 selector。

另外,像 Chrome 這種可怕的瀏覽器在某些嚴苛條件下也還是可以觸發 animationstart 事件,於是套用的 CSS animation 可以顯得更無關緊要:

/* 切記,在你的目標只有 Chrome 時才可以這樣用 */

@-webkit-keyframes pymaster-detect-dom-insertion { from {} }
@keyframes         pymaster-detect-dom-insertion { from {} }
{selector} {
  -webkit-animation-delay: 0s !important;
  -webkit-animation-name: pymaster-detect-dom-insertion !important;
  -webkit-animation-play-state: running !important;
  animation-delay: 0s !important;
  animation-name: pymaster-detect-dom-insertion !important;
  animation-play-state: running !important;
}

JavaScript

要取得事件的方法也很單純,直接在 document 上 listen 就好啦! 至於要不要 use capture 就看自己的使用情況。

document.addEventListener('animationstart', onAnimationStart, true);
document.addEventListener('webkitAnimationStart', onAnimationStart, true);

function onAnimationStart(evt) {
	// Created element
  console.log(evt.target);
}
Read on