给纸片人女友的虚拟礼物——二次元的七夕回顾

418 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情

年中总结的文章里,我提到过今年沉迷虚拟主播,并在虚拟主播的生日会上为主播制作了一个网页作为礼物,今天正好是(咕呱的)七夕,把它拿出来聊聊吧。

可以直接去看下效果:xiaoxideai.ren/

整个页面是这样的:

左右侧的装饰和鸣谢列表之类的不说,重点就是中间的千图成像部分,中间的图像实际上是由很多幅她的小图按照颜色组合而成的大图,当鼠标移上去的时候小图会呈现出来:

这个千图成像的算法我是拿nodejs来写的,具体怎么实现的因为篇幅有限所以回头专门说(才不是因为我自己写的代码自己都看不懂了呢),今天只讲讲前端页面的交互是怎么做的。

每个小图都是可以点击的,点击之后会出现这幅小图的全貌以及收集来的很多粉丝的生日祝福:

每张小图点击过一次之后,同样的图片区域会翻过来,出现另一幅图的一部分,最终需要通过翻开所有小图来展现一副完整图(她自己画的):

完整图呈现之后,出现PPT中打字机效果那样的,我给她写的专属祝福:

每个小图的点击事件

按照刚才的交互逻辑,每个小图都要绑定各自的点击事件,一开始的设想是把小图排列在各个位置,但是这样一来一打开就要加载25*25=625张图片,页面渲染和请求一定会出问题,因此我选择了将小图们拼接好的整幅图当做一张图片显示出来,然后在上面覆盖透明DOM,使用flex布局让其铺满整个中心区域:

<div class="center">
    <!-- 完整图 -->
    <img class="main" src="./main625.png"/>
    <!-- 625个透明DOM,部分属性跟这个交互无关暂时省略了,后面第三个交互处是全部的 -->
    <div v-for="(item,index) of grid" @click.stop="clickGrid(index)" v-bind:key="item.id" class="grid"></div>
</div>
.center {
  width: 70vh;
  height: 70vh;
  margin: auto;
  position: relative;
  top: 0;
  left: 0;
  display: flex;
  flex-wrap: wrap;
  z-index: 4;
}
.grid {
  width: 4%;
  height: 4%;
  background: white;
  background-size: cover;
  opacity: 0.3;
  position: relative;
  z-index: 2;
}

此外,我在生成千图成像的时候,同时生成了每个小图对应索引位置的顺序,类似于这样的数据:

{
  "dduser": "超能诺诺",
  "content": "每每看着你便不自觉地打起精神,愿带给大家元气与活力的小希在今后的日子里能永远与快乐相伴,相信闪闪发亮的你能获得越来越多的认可和期待,祝最可爱的小希——生日快乐!",
  "used": true,
  "index": 86,
  "author": "超能诺诺",
  "uid": "88663304",
  "url": "./images/超能诺诺_88663304/17812b99d4a889fec5d5b05a35047615e66fe6d3.webp"
}

所以就可以根据每个空白DOM的索引位置进行图片和祝福语的呈现:

<div v-if="currentData" @click="hideImg()" class="blackCover">
  <div @click="next()" class="next"></div> -->
  <div class="postcard">
    <div class="image">
      <img @click.stop="showImg(currentData.url)" style="cursor:pointer" :src="`/static/${currentData.url}`" alt="">
      <div class="author" @click.stop="toSpace(currentData.uid)">图:<span :class="currentData.uid ? 'toUid' : ''">{{currentData.author}}</span></div>
    </div>
    <div class="bless" @click.stop="()=>{}">
      <div class="content">{{currentData.content}}</div>
      <div class="dduser">{{currentData.dduser}}</div>
    </div>
  </div>
</div>
clickGrid(index) {
      if (this.showSurpirse || this.grid[index]) return
      
      this.currentData = database.find(item=>item.index == index)
      let url = this.database.find(item=>index === item.index).url
      // database就是上面说的数据
      this.database.filter(item=>item.url === url).forEach(item => {
        this.grid[item.index] = true
      })
      // 保存已经点击过的小图
      localStorage.setItem('grid',JSON.stringify(this.grid))
    },

点开小图后的图片翻转

点卡小图后,小图的位置会翻开成另一个图的一部分,以达到最后翻开全部图片的目的。这时候也面临着与前一个交互类似的问题:加载625张图片从浏览器性能和服务器开销上来说都不是最优选择,而这个时候,625个位置是为了呈现最后那张图的625个区域的内容,透明DOM显然不可行了,这时候我用到了CSS背景的background-positon属性:

<div
    :style="{'background-position':`${index % 25 * 4}% ${Math.floor(index / 25) * 4}%`}"
    :class="{'gridRead':item,'showBackground':item}"
    @mouseleave="showMoasic = false"
    v-for="(item,index) of grid"
    @click.stop="clickGrid(index)"
    v-bind:key="item.id"
    class="grid"
 ></div>

实际上就是625个小块同样将整幅图设定成了背景图片,通过更改每个小块背景图片的位置(图大块小),使得每个小块呈现大图不同部分的内容,原理如下:

具体内容可以参考Mozilla的background-position属性文档

类似PPT的打字机效果

实际上就是将字符串使用split方法分割成数组,之后调用setinterval按照固定时间间隔将文字追加进去(当然还得益于MVVM的机制省的频繁操纵DOM了):

happyBirthDay() {
  let bless = `选择了一幅你自己的画作为礼物,这幅画让我想到那个因为整晚没有击败盖侬而哭鼻子的小希,她却不知道出了台地就单挑盖侬已经击败了多少玩家,看着这个小希的背影,我知道她有时会很脆弱,但往往最后都会打起精神,甚至眼泪还没擦干,就会像这幅画一样,坚定的勇往直前。小希,四岁生日快乐!愿你一如既往,愿你心有暖阳~`
  let splitBless = bless.split("")
  let index = 0
  let interval = setInterval(() => {
    if (!splitBless[index]) {
      clearInterval(interval)
      this.showResetPage = true
      return
    }
    this.supriseBless += splitBless[index]
    index++
  },300)
},

这时候音乐也随之切换成了她自己唱的一首歌的副歌部分,很励志的一段歌词:

总有人开拓着未来
总有美好不必倒带
总有圆满值得期待
世界陆离光怪
转眼桑田沧海
何必只惦念 互相伤害
不论在 深山或山外
都有人 传递着色彩
隔离也 分割不了爱
别沉沦 在失败
别辜负 了等待
惨淡过后是 苦尽甘来

后来就有人跟我说:

你要把这心思用在谈恋爱上,还能一直单身这么多年??

我:虽然很不爽,但是找不到什么反驳的理由……

image.png