CSS技巧:滚动视差与图片点击水纹效果的结合

Aditya2018/12/15前端CSS

引子

前几天浏览博客的时候,发现了一个自己未曾了解过的 CSS 属性: background-attachment, 从此打开了新世界的大门……

background-attachment

background-attachment 算是一个比较生僻的属性,基本上平时写业务样式都用不到这个属性。但是它本身很有意思。

属性值

==background-attachment==:如果指定了 background-image ,那么 background-attachment 决定背景是在视口中固定的还是随着包含它的区块滚动的。

  • background-attachment: scroll: scroll 此关键字表示背景相对于元素本身固定, 而不是随着它的内容滚动。
  • background-attachment: local: local 此关键字表示背景相对于元素的内容固定。如果一个元素拥有滚动机制,背景将会随着元素的内容滚动, 并且背景的绘制区域和定位区域是相对于可滚动的区域而不是包含他们的边框。
  • background-attachment: fixed: fixed 此关键字表示背景相对于视口固定。即使一个元素拥有滚动机制,背景也不会随着元素的内容滚动。

注意一下 scrollfixed,一个是相对元素本身固定,一个是相对视口固定,有点类似 position 定位的 absolutefixed

使用 background-attachment: fixed 实现滚动视差

// HTML
<section class="g-word">Header</section>
<section class="g-img">IMG1</section>
<section class="g-word">Content1</section>
<section class="g-img">IMG2</section>
<section class="g-word">Content2</section>
<section class="g-img">IMG3</section>
<section class="g-word">Footer</section>

// CSS

section {
    height: 100vh;
}
 
.g-img {
    background-image: url(...);
    background-attachment: fixed;
    background-size: cover;
    background-position: center center;
}

image

综上,就是 CSS 使用 background-attachment: fixed 实现滚动视差的一种方式,也是相对而言比较容易的一种。* background-attachment: fixed* 除了实现滚动视差效果,合理运用下,能完成一些其他的有趣的效果,比如图片点击水纹效果。

background-attachment: fixed 实现图片点击水纹效果

利用图片相对视口固定,可以有很多有趣的效果,譬如下面这个。

image

// HTML
<div class="g-container">

</div>

// SCSS
$img: 'https://images.unsplash.com/photo-1440688807730-73e4e2169fb8?dpr=1&auto=format&fit=crop&w=1500&h=1001&q=80&cs=tinysrgb&crop=';
$aniTime: 1s;

body,
html {
    width: 100%;
    height: 100%;
}
.g-container{
    position: absolute;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background-image: url($img);
    background-attachment: fixed;
    background-position: center center; 
    // background-size: auto;
    background-size: auto 100%;
    overflow: hidden;
    cursor: pointer;
    
}

.g-position {
    position:absolute;
    width: 80vmin;
    height: 80vmin;
}

.g-center {
    position: relative;
    width: 100%;
    height: 100%;
}

.wave {
    position: absolute;
    top: calc((100% - 20vmin)/2);
    left: calc((100% - 20vmin)/2);
    width: 20vmin;
    height: 20vmin;
    border-radius: 50%;
    background-image: url($img);
    background-attachment: fixed;
    background-position: center center;
    transform: translate3d(0, 0, 0);
    opacity: 0;
    transition: all .2s;
}

.g-wave1 {
    background-size: auto 106%;
    animation: wave $aniTime ease-out .1s;
    animation-fill-mode: forwards;
    z-index: 10;
}

.g-wave2 {
    background-size: auto 102%;
    animation: wave $aniTime ease-out .15s;
    animation-fill-mode: forwards;
    z-index: 20;
}

.g-wave3 {
    background-size: auto 104%;
    animation: wave $aniTime ease-out .25s;
    animation-fill-mode: forwards;
    z-index: 30;
}

.g-wave4 {
    background-size: auto 100%;
    animation: wave $aniTime ease-out .4s;
    animation-fill-mode: forwards;
    z-index: 40;
}

@keyframes wave {
    0% {
        top: calc((100% - 20vmin)/2);
        left: calc((100% - 20vmin)/2);
        width: 20vmin;
        height: 20vmin;
        opacity: 1;
    }
    10% {
        // opacity: 1;
    }
    99% {
        opacity: 1;
    }
    100% {
        top: calc((100% - 80vmin)/2);
        left: calc((100% - 80vmin)/2);
        width: 80vmin;
        height: 80vmin;
        opacity: 0;
    }

// JS
(function() {
    let x, y;
    let index = 0;
    let screenSizeWidth = $('body').width();
    let screenSizeHeight = $('body').height();
    let halfvmin = (screenSizeWidth > screenSizeHeight ? screenSizeHeight / 2 : screenSizeWidth / 2) * 0.8;
    
    console.log('halfvmin', halfvmin);
    
    $(document).on("click", function(e) {
        x = e.pageX;
        y = e.pageY;
        waveMove(x, y, index++);
    });

    function waveMove(x, y, z) {
        $(".g-container").append(`
            <div class="g-position g-position${z}" style="top:${y - halfvmin}px; left:${x - halfvmin}px; z-index:${z}">
                <div class="g-center">
                    <div class="wave g-wave1"></div>
                    <div class="wave g-wave2"></div>
                    <div class="wave g-wave3"></div>
                    <div class="wave g-wave4"></div>
                </div>
            </div>
        `);
        
        setTimeout(function() {
            $(`.g-position${z}`).remove();
        }, 3000);
    }
})();

如上便是 background-attachment 的两个应用,如果仅仅是这样,我便将我从哪里学的的帖子转出来即可,何必再写一篇呢,接下来便是重头戏。

滚动视差与图片点击水纹效果的结合

接下来就是我尝试将两种效果合二为一的一些测试

制作过程中遇到的一些问题

  • 该滚动视差完成的效果使得可视区域的背景的结合只有两种情况,一种是一张背景图完完整整占据整个可视区域,另一种便是上一张的头部,和下一张尾部拼凑在可视区域,对于背景图的处理就无法做到上述的结合 imageimage

  • 设置了 background-attachment:fixed 仍然可以通过 background-position 来调整背景图的位置

  • 如果背景图由多张拼凑的,background-attachment 也可以设置多属性,但 fixed 仅对第一张生效,若果后面的设置了 fixed 属性值会直接消失。

        background: url(http://pic7.photophoto.cn/20080407/0034034859692813_b.jpg) no-repeat, url(http://up.enterdesk.com/edpic_source/21/00/00/210000f….jpg) no-repeat, url(https://images.unsplash.com/photo-1440688807730-73e4e2169fb8?dpr=1&auto=format&fit=crop&w=1500&h=1001&q=80&cs=tinysrgb&crop=) no-repeat;
        height: 300vh;
        background-size: 100% 100vh;
        background-attachment: local, fixed;
        background-position: 0 50vh, 0 100vh, 0 200vh;
    
  • 了解一下定时器的传参和清除定时的方法

    • 根据 MDN 文档记录, setTimeout(func, delay, param1, param2, ...) 第三个参数及以后的参数都可以作为func函数的参数
    • 清除未知定时器的方法:利用数组存储定时器id,然后遍历数组,关闭定时器
    var pageTimer = {} ; //定义计算器全局变量
    //赋值模拟
    pageTimer["timer1"] = setInterval(function(){},2000);
    pageTimer["timer2"] = setInterval(function(){},2000);
    //全部清除方法
    for(var each in pageTimer){
        clearInterval(pageTimer[each]);
    }
     
    回目录
    2.暴力清除方式
    设置一个比较大的数值,循环清除,模拟代码如下:
    
    for(var i = 1; i < 1000; i++) {
    clearInterval(i);
    }
    分析:实际上暴力清除的方式是不可取的,在不得已情况下才使用,在IE下,定时器返回值在IE下面是8位数字如:248147094,并且起始值不能确定,而Chrome和firefox下是从1开始的个位数字,一般项目还是建议第一种,并且第一种的扩展性也好,比如可以做个方法,清除除了指定定时器之外的所有定时器。
    

在自己的博客添加该效果

vuelayout 布局中根元素添加点击事件,

    #app(v-on:mousedown="waveFn($event)")
      .sectionContainer(v-html="bgHTML")
        //section.g-img1
        //section.g-img2
        //section.g-img3
        //section.g-img4
        //section.g-img5

需要注意的是,由于页面的长短不同所以动态添加背景图元素的方法,也就是每次 路由变化(视图更新) 的时候来重新获取全文高度来计算。

通过 event 属性获取到点击的位置,然后切换水纹的背景图,放入到不同背景图所在的盒子。

export default {
    beforeMount () {
        // 首页刷新创建背景图
        const screenHeight = document.documentElement.clientHeight || document.body.clientHeight, // 可见区域高度
          documentHeight = document.body.scrollHeight // 全文高度
        this.createParallexBg(screenHeight, documentHeight)
      },
      methods: {
        // 水波 Fn
        waveFn(e) {
          this.waveIndx++
          let pageX = e.pageX,
            pageY = e.pageY,
            zIndex = 3,
            screenHeight = document.body.clientHeight || document.documentElement.clientHeight,
            img,
            className
    
          // 跟随背景图更换水波的背景图, 如果大于5张,循环一下
          className = 'bgIndex' + Math.ceil(pageY / screenHeight);
          if (Math.ceil(pageY / screenHeight) > 5) {
            // 处理取余结果为0情况
            img = 'bg' + (Math.ceil(pageY / screenHeight) % 5 == 0 ? 5 : Math.ceil(pageY / screenHeight) % 5)
          } else {
            img = 'bg' + Math.ceil(pageY / screenHeight);
          }
          pageY = pageY - screenHeight * parseInt(pageY / screenHeight);
    
          // 动态创建水波node
          let app = document.getElementsByClassName(className)[0],
            water = document.createElement('div'),
            waveBody = document.createElement('div');
    
          water.setAttribute('class', 'wave-position water' + zIndex);
          water.setAttribute('style', 'z-index:' + zIndex + ';top:' + (pageY - 150) + 'px;left:' + (pageX - 150) + 'px;');
          waveBody.setAttribute('class', 'wave-body');
    
          ['wave0', 'wave1', 'wave2', 'wave3', 'wave4', 'wave5'].forEach(item => {
              let node = document.createElement('div');
              node.setAttribute('class', 'wave ' + item + ' ' + img);
              waveBody.appendChild(node);
            });
          water.appendChild(waveBody);
          app.appendChild(water);
    
          // 3s后删除创建的元素
          // let waveIndx = this.waveIndx
          this.$store.state.pageTimer['timer' + this.waveIndx] = setTimeout((waveIndx) => {
            document.getElementsByClassName(className)[0].removeChild(water)
            delete  this.$store.state.pageTimer['timer' + waveIndx]
          }, 3000, this.waveIndx)
        },
        // 动态创建滚动视差背景图
        createParallexBg (screenHeight, documentHeight) {
          for (let i = 0; i < Math.ceil(documentHeight / screenHeight); i++) {
            const bgIndex = i + 1
            if (Math.ceil(documentHeight / screenHeight) > 5) {
              const index = i % 5
              this.bgHTML += `<section class='g-img${index + 1} bgIndex${bgIndex}'></section>`
            } else {
              this.bgHTML += `<section class='g-img${i + 1} bgIndex${bgIndex}'></section>`
            }
          }
        }
      },
      beforeUpdate () {
        // 每页新增的组件路由守护代码可以写在这里,由于点击时期页面的时候并不会触发,所以搁置修改
        console.log(this.$parent.$el)
      },
      watch: {
        documentHeight: function (val, oldVal) {
          // 路由变化后,更换背景图数量
          this.bgHTML = ''
          this.waveIndx = 0
          this.createParallexBg(this.$store.state.screenHeight, this.$store.state.documentHeight)
          // 清楚可能残余的定时器
          for (let each in this.$store.state.pageTimer) {
            clearInterval(this.$store.state.pageTimer[each])
            delete this.$store.state.pageTimer[each]
          }
        }
      },
}

还有个问题便是每个水波的定时器存在时间是 3s ,如果在这个定时器未能执行完毕,便进行了路由跳转,会造成一个报错——删除的该元素不存在,所以在每次跳转的时候(销毁页面 destroyed 也可以),将参与的定时器清理掉

上述代码是本博客页面效果的主要代码,目前也是在精简优化中。

参考

Last Updated 2024/12/27 11:36:49