CSS秘密花园: 交互式图像对比

cross523 8年前
   <p> </p>    <p>《 <a href="/misc/goto?guid=4958976660757929944" rel="nofollow,noindex">CSS Secrets</a> 》是 <a href="/misc/goto?guid=4958976660849001117" rel="nofollow,noindex">@Lea Verou</a> 最新著作,这本书讲解了有关于CSS中一些小秘密。是一本CSSer值得一读的一本书,经过一段时间的阅读,我、@南北和@彦子一起将在W3cplus发布一系列相关的读后感,与大家一起分享。</p>    <p><img src="https://simg.open-open.com/show/b873adc9c0b700ab68ae0efe9cf398be.jpg"></p>    <p>有时候我们需要向别人展示两幅图像的视觉差异,通常一幅是修改前的图像、一幅是修改后的。例如,把两幅图像放在一起,展示照片处理的效果。比如一些美容师的网站要展示某些美容护理的效果,某个地理区域发生灾难的结果。</p>    <p>最常见的方案是将两张图片并排放置。但是,这样的话人的眼睛就只能注意到非常突出的差异、而察觉不到那些小的变化。如果对比不是那么重要或者两图之间的差异非常大的话,这种方法是没有问题的,但是对于其它情况呢,我们需要一些更好的方法。</p>    <p>从用户体验的角度看,这个问题有很多解决方案。一种常见的解决方案是通过一个GIF动图或CSS动画在同一个位置快速连续地显示两张图片。这比两张图片并排放要好很多。但是用户要发现这之间的差异也比较耗时,因为用户需要等待好几次迭代,好让他们的眼睛能够把整张图片观察完。</p>    <p><img src="https://simg.open-open.com/show/358b5d4950ed883eb0baec3a8047b17a.png"></p>    <p>一个交互式图像对比小部件的例子,可以使用户对比2011年伦敦暴动带来的灾难性结果,来自英国主要新闻媒体The Guardian。用户可以拖动两幅图像中间的白色竖条,但是并没有提示来告诉用户“这是可以拖动的”,这就是为什么需要帮助文本(“Move the slider…”)了。理想情况下,一个良好的、可学习的界面是不需要帮助文本的。</p>    <p>一个更实用的解决方案是“image comparison slider”。它把两个图像叠加在一起,让用户拖动divisor进行选择,是显示这一个还是另一个。当然,这样的控制在HTML中不是实际存在的。我们需要通过我们真实存在的元素来模拟它,多年来也一直有许多实现这个效果的示例,通常需要JavaScript框架和一大堆的JS代码。</p>    <p>在一些变型中,用户可以直接移动鼠标来代替拖动。这样做的好处是可以更容易让用户注意到并去使用,但是体验却是相当刺激的。</p>    <p>有没有什么简单的方法可以实现这样的一个控制?当然有,而且有俩!</p>    <h2>CSS resize方案</h2>    <p>如果我们仔细想想,image comparison slider通常包括一张图像,和一个水平方向大小可调的元素,用于显示另一张图像。这通常就是需要JavaScript框架的地方了:让顶部的图像水平方向大小可调。但是,我们并不是一定需要脚本来让元素大小可调。在CSS3中,我们有这样一个属性: resize 。</p>    <p>即使你没有听过这个属性,你很可能已经使用过它,因为它在 <textarea> 中的默认值是 both ,这使得它们在各个方向都是可调整的。但是,实际上它可以被设置在任何元素上,只要它的 overflow 属性值不是 visible 。几乎所有元素的 resize 属性的默认值都是 none ,这使得它们不能调整大小。除了 both ,它还接受 horizontal 和 vertical 值,用来限制哪个方向可以调整大小。</p>    <p>给 <textarea> 应用 resize: vertical 是一个不错的方法,既可以禁用水平调整,又保持大小可调整,因为水平调整通常会破坏页面布局。</p>    <p>这里可能有一个疑问:我们也许可以使用这个属性来实现我们的图像滑块?不试试怎么知道呢!</p>    <p>我们的第一个想法可能是使用两个 <img> 元素。但是,直接给 <img> 应用 resize 看起来非常丑,直接调整图像大小的话会导致扭曲。所以,还是把它应用到一个 <div> 容器中可能比较合理。因此,我们的HTML标签如下:</p>    <pre>  <code class="language-css"><div class="image-slider">  <div>      <img src="adamcatlace-before.jpg" alt="Before" />  </div>  <img src="adamcatlace-after.jpg" alt="After" />  </div>  </code></pre>    <p>一旦 object-fit 和 object-position 得到了更广泛的浏览器支持,这就不成问题了,因为我们可以用控制背景图像缩放的方式,同样的方法来控制图像的缩放。</p>    <p>然后我们需要应用一些基础的CSS用于定位和设置尺寸:</p>    <pre>  <code class="language-css">.image-slider {      position:relative;      display: inline-block;  }  .image-slider > div {      position: absolute;      top: 0; bottom: 0; left: 0;      width: 50%; /* Initial width */      overflow: hidden; /* Make it clip the image */  }  .image-slider img { display: block; }  </code></pre>    <p>现在结果如图所示,但是这仍然是静态的。</p>    <p><img src="https://simg.open-open.com/show/3d5bc7bdbb89eb5e48c8c4e74f980c65.png"></p>    <p>如果我们手动改变宽度,我们可以看到用户所有可能的调整结果。通过 resize 属性让宽度随着用户交互动态改变,我们还需要两个声明:</p>    <pre>  <code class="language-css">.image-slider > div {      position: absolute;      top: 0; bottom: 0; left: 0;      width: 50%;      overflow: hidden;      resize: horizontal;  }  </code></pre>    <p>唯一的视觉变化就是,现在大小调整句柄出现在了top图像的右下角,</p>    <p><img src="https://simg.open-open.com/show/46874428fb3e0d996d618c6c98843a9d.png"></p>    <p>我们现在可以拖动它,把它调整到我们的主要内容!但是,拖动我们的小工具时,也发现了一些缺陷:</p>    <ul>     <li>我们可以调整 <div> 的大小到超过图像的宽度</li>     <li>resize 句柄很难点中</li>    </ul>    <p>第一个问题很容易解决。我们只需要指定 max-width 的值为 100% 。但是,第二个问题稍微复杂一点。可惜,现在还是没有标准的方法来给resize句柄添加样式。一些渲染引擎支持私有伪元素(如, ::-webkitresizer ),但是它们的结果非常受限制,不仅是在浏览器支持方面,还有样式灵活性方面。但是,希望仍在:这说明在 resize 句柄上覆盖一个伪元素不会干扰它的功能,甚至不需要设置 pointer-events: none 。所以,给 resize 句柄添加样式的跨浏览器的解决方案就是在它的上边覆盖一个伪元素。如下:</p>    <pre>  <code class="language-css">.image-slider > div::before {      content: '';      position: absolute;      bottom: 0; right: 0;      width: 12px; height: 12px;      background: white;      cursor: ew-resize;  }  </code></pre>    <p>注意 cursor: ew-resize 这条声明:它增加了一个额外的支持,因为它提示用户说这块区域可以用作一个resize句柄。但是,我们不能依赖光标改变作为我们唯一的支持,因为它们是当用户和控件交互时才是可视的。</p>    <p>现在,我们的resize句柄将会变成一个白色方块。</p>    <p><img src="https://simg.open-open.com/show/8c22bbb81281ba1f6e8470dbedb1cc4d.png"></p>    <p>在这里,我们可以接着往下,把它变成我们喜欢的样式。例如,把它变成一个距图像边距为 5px 的白色三角形,</p>    <p><img src="https://simg.open-open.com/show/8eba08b1d1c85b1250d84df3878efbd3.png"></p>    <p>CSS如下:</p>    <pre>  <code class="language-css">padding: 5px;  background:linear-gradient(-45deg, white 50%, transparent 0);  background-clip: content-box;  </code></pre>    <p>为了进一步改进,我们可以给两个图像都应用 user-select: none 。这样,没有抓取到resize句柄的话也不会白白导致它们被selected。总结一下,完整的代码如下:</p>    <pre>  <code class="language-css">.image-slider {      position:relative;      display: inline-block;  }    .image-slider > div {      position: absolute;      top: 0; bottom: 0; left: 0;      width: 50%;      max-width: 100%;      overflow: hidden;      resize: horizontal;  }    .image-slider > div::before {      content: '';      position: absolute;      bottom: 0; right: 0;      width: 12px; height: 12px;      padding: 5px;      background:      linear-gradient(-45deg, white 50%, transparent 0);      background-clip: content-box;      cursor: ew-resize;  }    .image-slider img {      display: block;      user-select: none;  }  </code></pre>    <h2>Range input解决方案</h2>    <p>在上一节中提到的CSS resize方法运行是非常ok的,而且代码量也非常小。但是,它有一些缺点:</p>    <ul>     <li>它不是keyboard accessible(不能通过键盘调整resize)的</li>     <li>拖动是调整上边的图像的唯一方法,这对于非常大的图像和有运动障碍的用户而言是很麻烦的。如果能够在用户点击一个点的时候,图像就调整到那个点,可以提供一个更好的用户体验。</li>     <li>用户只能从右下角对顶部图像进行调整,这个右下角可能很那被注意到,即使我们把它变成我们之前描述的样式。如果我们愿意使用一点脚本,我们就可以使用一个滑块控件(HTML range input)覆盖在图像的顶部,来控制调整大小,就可以把这三个问题都解决了。因为我们无论如何都是要使用JS的,我们可以通过脚本添加额外的元素,我们从如下简单的HTML标签开始:</li>    </ul>    <pre>  <code class="language-css"><div class="image-slider">      <img src="adamcatlace-before.jpg" alt="Before" />      <img src="adamcatlace-after.jpg" alt="After" />  </div>  </code></pre>    <p>这样,我们的JS代码会把它转变如下,然后给滑块添加一个事件,并设置div的宽度:</p>    <pre>  <code class="language-css"><div class="image-slider">      <div>          <img src="adamcatlace-before.jpg" alt="Before" />      </div>      <img src="adamcatlace-after.jpg" alt="After" />      <input type="range" />  </div>  </code></pre>    <p>JavaScript代码相当简单:</p>    <pre>  <code class="language-css">$$('.image-slider').forEach(function(slider) {      // Create the extra div and      // wrap it around the first image      var div = document.createElement('div');      var img = slider.querySelector('img');      slider.insertBefore(img, div);      div.appendChild(img);        // Create the slider      var range = document.createElement('input');      range.type = 'range';      range.oninput = function() {      div.style.width = this.value + '%';      };      slider.appendChild(range);  });  </code></pre>    <p>我们初始的CSS基本和上一个方案的一样。我们只把不需要的部分删掉:</p>    <ul>     <li>我们不需要 resize 属性</li>     <li>我们不需要 .image-slider > div::before 规则,因为我们已经不需要resizer了。</li>     <li>我们不需要 max-width ,因为滑块可以进行控制</li>    </ul>    <p>经过这些调整之后我们的CSS代码如下:</p>    <pre>  <code class="language-css">.image-slider {      position:relative;      display: inline-block;  }  .image-slider > div {      position: absolute;      top: 0; bottom: 0; left: 0;      width: 50%;      overflow: hidden;  }  .image-slider img {      display: block;      user-select: none;  }  </code></pre>    <p>使用 input:in-range ,而不仅仅是 input ,这样可以在当且仅当range input被支持的时候才会给range input添加样式。然后你可以使用级联来隐藏它,或针对旧的浏览器给它应用不同的样式。</p>    <p>如果我们测试了这些代码,你会发现它是可运行的,但是它看起来有点糟糕:我们的图片下方随意地放置了一个range input的控件。</p>    <p><img src="https://simg.open-open.com/show/706c2b08b9943f6e1290138a8764735c.png"></p>    <p>我们需要给它应用一些CSS,让它定位在图片上边,并设置宽度和容器宽度一致。</p>    <pre>  <code class="language-css">.image-slider input {      position: absolute;      left: 0;      bottom: 10px;      width: 100%;      margin: 0;  }  </code></pre>    <p>如下图所示,这看起来终于比较像样了。</p>    <p><img src="https://simg.open-open.com/show/4fbf579a32d0488cf9b7d1042861abdb.png"></p>    <p>还有几个私有伪元素可以给range input添加样式,让它变成我们期望的样子。包括: ::-moz-range-track , ::-ms-track , ::-webkit-slider-thumb , ::-moz-range-thumb , 和 ::-ms-thumb 。像大多数私用特性,它们的结果往往是不一致的、脆弱的、并且无法预测的,所以我建议不要使用它们,除非你真的需要。我就提醒你们一下~</p>    <p>但是,如果我们只是想要在视觉上将range input和控件统一,我们可以使用一个混合模式或滤镜。混合模式 multiply 、 screen 、 luminosity 应该可以生成不错的效果。还有, filter: contrast(4) 可以让滑块变成黑白,小于1的对比值可以让它更偏灰色。可能性是无限的,没有什么通用的最佳选择。你甚至可以混合模式和滤镜一起用,如下:</p>    <pre>  <code class="language-css">filter: contrast(.5);  mix-blend-mode: luminosity;  </code></pre>    <p>我们还可以扩展那块用户使用来调整大小的区域,使之成为更棒的用户体验(根据费茨法则)。可以减小宽度,并结合CSS transform来完成:</p>    <pre>  <code class="language-css">width: 50%;  transform: scale(2);  transform-origin: left bottom;  </code></pre>    <p>你可以在图中看到两种处理结果。</p>    <p><img src="https://simg.open-open.com/show/e37e2195439dba6c301e658a6a0de585.png"></p>    <p>这种做法的另一个好处是——尽管只是暂时性的——目前range input有比 resize 属性更好的浏览器支持。</p>    <p> </p>    <p>来自: <a href="/misc/goto?guid=4959670143504282853" rel="nofollow">http://www.w3cplus.com/css3/css-secrets/interactive-image-comparison.html</a></p>