雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

我正在参加「初夏创意投稿大赛」详情请看:初夏创意投稿大赛

阅读本文,你将:

  1. 隔岸观火一场盛夏的暴雨。
  2. 学会用 canvas 开发动画特效的思路和技巧。

一、青春暴雨

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

每个人的青春,都有几场盛夏里难以忘怀的暴雨。

有的人在暴雨中失意;有的人在暴雨中相前端学什么拥;有的人在躲雨的屋檐下相遇;有的人只是坐在窗边听雨声、看雨落,一眨眼便是很多年;

我也还记得许多场雨。

18岁,高考最后一门英语结束,走出考场大雨倾盆,同窗们不及告别,匆匆避雨归家,踏上各自的人生;

21岁,十一假期,和大学室友去打工兼职,暴雨倾城,没有伞的我们举着纸壳子走了几里路搭乘公交;

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

23岁,下班路上电动httpclient车没电了,推着电动车在乌泱乌泱的雨幕里源码中的图片艰难跋涉归家。

喜怒前端面试题悲欢,皆是青春。http://www.baidu.com
……

暴雨常有,青春的经历却不会再有,那么我们有源码交易平台没有办法在屏幕上完成一次 “暴雨的演动画大放映绎” ,让你和盛夏的暴雨再度重逢呢?

当然,没问题。

我甚至为此专门写了一个 “开源” 作品,让我们一大学生创新创业大赛一品味雨天的感觉:

(感谢这张照片的提供者源码精灵永久兑换码:翊君 大佬,他是一位有才情、源码交易平台有技术的后端大佬)

源码精灵永久兑换码用害怕,这个效果的实现原理非常容易,你可以轻易 get动画片汪汪队 到,我会手把手教你理解其中的关隘,也可以来和我一起维护这个小小的开源作品;

二、代码组织、结构

首先,我大学专业们的目标不是写一个 “小 demo”,毕竟我已前端是什么工作经写了太多 demo,需要对自己有一些更高的要求了。

所以,我们需要是一个 可发布可调试 的组件库,其代码组织如下:

│  package.json
│  rollup.config.js
├─dist
├─examples
│  └─base
│          index.html
└─src
    │  Drop.ts
    │  DropKeeper.ts
    │  index.ts
    │  RainyCanvas.ts
    │  TimeKeeper.ts
    │  types.ts
    └─helpers
            css.ts
            drop.ts
            element.ts
            math.ts

代码结构比较常规:

  • src 是实现代码的目录;
  • examples 是存放各种示例的目录;
  • dist 目录是构建物目录;
  • rohttp 500llup.config动画片少儿小猪佩奇.js 是本工程构建工具 rol大学生创新创业大赛lup 的配置文件;

当你想进行代码调试时,只需要执行命令 yarn dhttp代理ev 或者 npm run dev,根前端开发需要掌握什么技术据提示动画访问调试页面了:

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

关于工前端开发需要学什么程的搭建、构建、发布,内容太多,不在本文细数,后续再专门发文解释;

Ok,现在工程搭建好了,让我们来看看 “雨打西窗” 的效果究竟是怎么实现的。

三、定位、图层、API 设计

为了让开发出来的工具简单好用,一眼就懂,我初步如此设计本库的 api:

import rainy from 'rainy'
// 参数一:div元素的ID 或者 元素本身;
// 参数二:图片的url
rainy('ElementId', 'https://pic.xxx.xxx/a.jpg')
// 可选参数三:各类配置项
rainy('ElementId', 'https://pic.xxx.xxx/a.jpg', { ... })

按照 API 的设计,我们现在大学英语四级考试有了一个 div 元素,然后我们就需要考虑需要几层http://192.168.1.1登录画布(canvas),以及如何定位它们了;动画片小猪佩奇

我的设计是在 div 内放置两层画布:

  • div 需要具备宽、高;如果不是 position:relativeposition:fixed 就会被赋予 positi源码精灵永久兑换码on:relative属性;

  • 背景画布:专门绘制模糊的背景图;通过 absolute 占满 div

  • 玻璃画布:专门绘制玻璃上滚动的水珠;通过 abs源码olute 占满 div;通过 z-index 比背景高出一层;

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

四、绘制背景、添加模糊

暴雨天,窗户上凝起了一层薄雾;

通过如下代码,我们可以将一张图片清晰地画在画布上:

  const ctx = getCtx(this.backgroundCanvas);
  ctx.drawImage(this.img, 0, 0, width, height)

实现如下效果:

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

但这动画片汪汪队显然不符合 “暴雨中窗户起雾” 的效果,因此,我们还需要给它加上一层淡淡的 “高斯模糊”:

ctx.filter = `blur(10px)`

加完之后,效果立竿见影:

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

五、水滴?视觉把戏罢了!

水滴的绘制,可能是本文最让人一开始摸不着头脑的地方了。前端面试题

绘制一个看起来像那么回事的水滴,其实只需要一点简单的 初中物理知识 : 凸透镜成像!

窗户上的水滴,就是一个天然的凸透镜。

因此,当大学生入党申请书我们想大学生自我鉴定画水滴时,其实只需要以下三个步骤:

5.1 首先需要画一个倒影

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

初中物理说:“只要你身处凸透大学生入党申请书镜的焦距以外,它的成像就是倒影。”

而水滴通常很饱满,焦距就很短,所以你看到的水滴上的图案,通常就是窗外风景的倒影;

代码如下:

// 因为水滴很多,所以需要压缩一下图片的尺寸
canvasEl.width = width * this.options.minification
canvasEl.height = height * this.options.minification
// 调整中心
ctx.translate(canvasEl.width/2, canvasEl.height/2)
// 旋转画布
ctx.rotate(Math.PI)
// 绘制图片
ctx.drawImage(this.img, -canvasEl.width/2, -canvasEl.height/2, canvasEl.width, canvasEl.height)

5.2 将倒影剪裁成一个圆形

水滴是圆的,至少看起来是动画电影

因此,我们大学不能直接放一个倒影给用户看,至少我们得将它剪裁成水滴的形状;

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

因此,我将水滴对象 Drop 赋予三个基础属性,即:

type Drop {
  x = number; // 圆心横坐标
  y = number; // 圆心纵坐标
  r = number; // 水滴半径
}

代码如下:

draw() {
  // 记录当前状态
  ctx.save()
  // 开始路径
  ctx.beginPath()
  // 画个圆
  ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, true)
  // 结束路径
  ctx.closePath()
  // 剪裁
  ctx.clip()
  // 绘制倒影图片
  ctx.drawImage(reflection, 0, 0, width, height)
  // 还原画布状态
  ctx.restore()
}

这样就完成水滴绘制了吗?

很可惜,并不是如此,并不是每个水滴中呈现的 “影像” 都完全一致的,在水滴滚落的过程中,我们可以清晰地观察到其中的影响再随着滚动发生着变换;身处玻璃不同位置的水滴,也总是动画片熊出没倒映着不同的图案;

5.3 光影变换,方显世界

让我们大学入党积极分子一定能入党吗5.2 节的思路画三颗水珠:

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

虽然它们已经很像是 “水珠” 了,但每颗水珠都显示一模一样的风景并不太符合我们平时观察风景时的现象;

“真实的表现” 应该是如右图所示,每颗水滴http代理都只倒影风景的一部分,这样才更显动画片熊出没真实。

那么,这要怎么实现呢?

分析一下,屏幕上水大学专业滴的移动范围,和 “水滴视野” 的httpwatch移动范围是存在差异的:

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

于是,可以写出如下代码:

  // 当前水滴到边沿的最小距离
  const minDistanceToEdge = Math.min(this.x, canvasWidth - this.x, this.y, canvasHeigh - this.y)
  // 画布短边长度
  const shorterEdge = Math.min(canvasHeigh, canvasWidth)
  // 视野半径 (最小半径和最大半径可以通过传参比例调整)
  const visionRadian = Math.min(Math.max(minDistanceToEdge, shorterEdge * this.minReflectionRatio / 2), shorterEdge * this.maxReflectionRatio / 2)
  // 视野开始位置 x 坐标
  const sx = canvasWidth - 2 * visionRadian - (canvasWidth - 2 * visionRadian) * this.x / canvasWidth
  // 视野开始位置 y 坐标
  const sy = canvasHeigh - 2 * visionRadian - (canvasHeigh - 2 * visionRadian) * this.y / canvasHeigh
  // 乘以倒影的缩放系数
  return [sx, sy, visionRadian, visionRadian].map(t => t * this.environment.minification)

这样,就能实现如下效果了:

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

六、让水滴动起来!

静止的画面是没有灵魂的,否则不如去拍照;

水不会一直凝固在玻璃上,重力会拽着它们奔向源码时代地面,因此大部分水滴都会选择合适的时候向下滚落,沿途将碰到的水滴据为己有,一路向下。

这种情况下,我们就必须补一个动画常识了:

Canvas 动画绘制按帧更新,绝大多数时候,都是通过以下函数:

window.requestAnimationFrame(fn)

一般而言,此函数的使用方法如下httpwatch

const render = () => {
  // 在此处做一些动画渲染
  renderSomeThing()
  // 递归调用
  requestAnimationFrame(render)
}
render()

对于我们的动画也是如此思路:

const render = () => {
  // 绘制此水珠。
  drop.draw()
  // 递归调用
  requestAnimationFrame(render)
}
render()

然后,我们使用 “补间动画库” 来完成其 x前端是什么工作y 轴数值的变换,就能完成水滴的绘制了。

import gsap from "gsap";
/**
 * @param speedX 水平速度
 * @param speedY 竖直速度
 * @param duration 运动时间(秒)
 * 通过此方法给水滴赋予速度、运动时间
 * */
setSpeed(speedX: number, speedY: number, duration: number) {
    this.speedX = speedX;
    this.speedY = speedY;
    this.moveTarget = [this.x + speedX * duration, this.y + speedY * duration]
    // 动画库,gsap 可太优秀了
    gsap.to(this, {
      duration: duration,
      x: this.moveTarget[0],
      y: this.moveTarget[1],
      onComplete: () => {
        this.speedX = 0
        this.speedY = 0
      }
    })
  }

通过以上思路,就可以源码成功让水滴运动起来了:

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

除了http 302主动随机地给水滴赋予速度外,我们还需要考虑到,“过大的水滴” 是不存在的,当水滴大到一定程度,就需要被赋予源码中的图片初始速度进行滚落;

七、残http 500留小水滴

上面图片里,你能够看到水滴在滚动时前端是什么工作留下了 “一滴滴” 微小的水滴。

在实际生活中,这是因为玻璃并不光滑和干净,导致水滴残留,而前端和后端哪个工资高在代码里,我更愿意将其称为 “生崽”。

原理也很简单,当一滴水珠在 “运动时”,每隔一个随机时间段,便在其末梢生成一个小半径的水滴加入页面水滴池即可;

为了体源码编辑器下载现随机性和合理性,让 “源码中的图片水滴崽” 在 [x – r/2, x + r/2] 的范围内产生;

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

此代码比较容易,就不赘述;

八、融合!沛然无形、海纳百川

没有两颗水滴能重叠在一起。

当两颗水滴重叠时,它们表面的张力会被破坏,从两颗圆润的水滴变成一颗更加圆润的水滴。

8.1 融合形式

但是,“水滴融合”究竟要怎么实现呢?

假设有水滴A和水滴B相交了:

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

就有以上三种情况:

  • A吃掉B
  • B大学英语四六级吃掉A
  • A和B消失,生成新的水滴C

其实三种思路都没毛病,但我们在实现时往往要选择和业务最贴合的方式,在我们的业务中有一个隐藏条件:“水滴可能正在运动。”

因此,我们可以前端工程师按这个思路来思考实现:

  1. 如果所有水滴都没运动,那完全可以生成一个源码编程器新水滴;
  2. 如果一前端静一动,那就是运动的水滴吃掉静止的水滴,并继续它的运动;
  3. 如果一快一慢都在运动,那应该继承快水滴。(如果做的更细致一点,则应该计算其动量保证动量守恒…那咱们整个运动模型可能就要重新设大学生入党申请书计了)

8.2 体积变化http 500、圆心变化

先算新水滴的半径:

设:水滴Ahttp代理的半径为r1,水滴http协议B的半径为r2,那么融合大学生自我鉴定后水滴C的半径大学英语四级报名官网r3是多少?

r1 * r1 * Math.PI + r2 * r2 * Math.PI = r3 * r3 * Math.PI
// 可得
r3 = Math.sqrt(r1 * r1 + r2 * r2)

再算新圆心:

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

const gravityRatio = r1 * r1 / (r1 * r1 + r2 * r2)
// 按动量守恒计算,新圆心:
x3 = x1 + ( x2 - x1 ) * ( 1 -  gravityRatio)
y3 = y1 + ( y2 - y1 ) * ( 1 -  gravityRatio)

8.3 效果成型

按照上面简单的计算,就可以完成 “水滴融合” 的炫酷效果啦,看起来是不是动画专业还真是像那么一回事呢?

雨打西窗,再见那年夏天的暴雨⛈️!用js实现(源码+npm)

九、在 “码上” 里运行

代码片段

十、源码

源码地址:github.com/zhangshichu…

npm地址:www.npmjs.com/package/rai…

unpkg:unpkg.com/rainy-windo…

安装:

yarn add rainy-window

使用:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="/node_modules/rainy-window/dist/umd/index.js"></script>
  <style>
    .outside-the-window {
      height: 500px;
      width: 600px;
    }
  </style>
</head>
<body>
  <div class="outside-the-window" id="OutsideTheWindow">
  </div>
  <script>
    window.rain('OutsideTheWindow', 'https://www.6hu.cc/wp-content/uploads/2022/06/38804-5v5hzY.jpg')
  </script>
</body>
</html>

十一、还需要实现的内容

为了参加活动,比较赶,很多细节还没处理好,包括但不限于:

  • 超出画布的水滴需要被移除;
  • 移动中的水滴应该变形
  • 水滴随机变形
  • 产生小水滴的逻辑需要优化
  • 更多

十二、结束语

我是大学生入党申请书
大龄前端打工仔,依然在努力学习。
我的目标是给大家分享最实用、最有用的知识点,动画专业希望大家都可以早早下班,并可以飞速完成工作,淡定摸鱼。

你可以在公众号里找到我:前端要摸鱼

发表评论

提供最优质的资源集合

立即查看 了解详情