有什么方法可以模仿着实现QQ登录窗口动画呢?于是手柄开始了摸索
最终效果
https://www.bysb.net/study/180303/login.html
思路
- 仔细查看原版动画,找出包含组件
- 仔细查看原版动画,感受运动效果
- 使用canvas实现单个组件绘制
- 使用canvas绘制出所有组件(不运动)
- 设置运动函数,为组件套用运动函数
- 查找缺失部分
- 模拟缺失部分效果
拆分动画
包含组件
- 大量的三角形拼接成的动画主体
- QQ LOGO
- 右上角的按钮
运动效果
- 每个三角形颜色会有类似线性渐变效果
- 三角形的三个点有缓入缓出运动轨迹
组件绘制
因只有三角形拼接的动态背景打算使用canvas,其余部分可使用css和html实现效果,故本文略过不做说明,欢迎下载源代码查看。
单个组件
既然前文打算使用canvas模拟实现动画,那么,已知单个组件为三角形,则可以设置一个绘制三角形的函数。
三角形在canvas2d中没有提供现成函数,但我们可以通过首先绘制一个三角形路径,然后填充来绘制。
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | /** * @type {HTMLCanvasElement} canvas 绘图板 */ let canvas = document.getElementById("loginAnime"); let ctx = canvas.getContext("2d", { alpha: false }); /** * @description 绘制一个三角形 * @param {Array<number>} a 点A坐标[x,y] * @param {Array<number>} b 点B坐标[x,y] * @param {Array<number>} c 点C坐标[x,y] * @param {String} color 要绘制的颜色 */ function drawTriangle(a, b, c, color) { ctx.fillStyle = color; ctx.beginPath(); .moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.lineTo(c[0], c[1]); ctx.fill(); } |
对函数进行测试,随意地绘制三个三角形,设置为三个颜色。
61 62 63 | drawTriangle([10, 10], [50, 50], [0, 30], "#FF0000"); drawTriangle([60, 45], [170, 130], [110, 20], "#00FF00"); drawTriangle([300, 300], [300, 400], [200, 350], "#0000FF"); |
所有三角形组件
既然已经绘制出了一个三角形,那么根据原动画,可以作为每2*2个点绘制1*1*2个三角形,每4*3个点绘制3*2*2个三角形,再考虑到由于存在三角形顶点移动的可能性,故设置 宽度12,高度4的二维数组,数组每个项记录有一个高度2的一维数组,包含每个点的x和y坐标,为了点看上去不那么规则,设置一个随机数加上原始坐标得到点的坐标。
305 306 | points[i][j][0] = j * 140 - 600 + parseInt(Math.random() * 100); points[i][j][1] = i * 188 - 120 + parseInt(Math.random() * 20); |
以上可以得到每个三角形的三个顶点坐标,但是由于前面提到的,12*4个点,实际能够绘制的三角形数量是11*3*2个三角形,因为点是确定的,我们只需要为每个三角形记录颜色即可。为了方便,我将同一个四边形内的两个三角形的颜色放置在了两个不同数组的相同下标下分别存储,且存储时使用一个高度3的数组,分别对应r,g,b的数值。
204 205 206 207 208 209 | /** * @description 随机得到一个rgb颜色数组 */ function getRandomRGB() { return [parseInt(Math.random() * 255), parseInt(Math.random() * 255), parseInt(Math.random() * 255)]; } |
353 354 | shapeColorUPFrom[i][j] = getRandomRGB(); shapeColorDOWNFrom[i][j] = getRandomRGB(); |
运动效果
点的缓动效果
很明显的,每一个点都带有一个缓动效果,前一半效果类似于
f(x) = x² 在 x ∈ [0,1] 的表现
后一半效果则类似于
f(x) = 1 - (1-x)² 在 x ∈ [0,1] 的表现
根据数学计算,可以得到一个输入 x ∈ [0,1], 并按照 x ∈ [0,0.5) 或 x ∈ [0.5,1] 返回对应 f(x) 的值,组成一个连贯图像,实现代码如下
72 73 74 75 76 77 78 79 80 81 82 83 84 | /** * @param {number} percentComplete 移动完成百分比,最大1 */ function makeEaseInOut2(percentComplete) { if (percentComplete < 0.5) { percentComplete *= 2; return Math.pow(percentComplete, 2) / 2; } else { percentComplete = 1 - percentComplete; percentComplete *= 2; return 1 - Math.pow(percentComplete, 2) / 2; } } |
接下来,我们为每个点设置一个移动到的目标点,并设置一个单程移动需要的时间,通过随机数模拟相对自然的效果。因为写这个动画时候才疏学浅所以三维二维数组构造方式略蠢求轻喷
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | /** * @description 得到一个2维数组 * @type {Array<Array<>>} * @param {number} x 第一层大小 * @param {number} y 第二层大小 */ function creave2xArray(x, y) { let re = new Array(x); for (let i = 0; i < x; i++) { re[i] = new Array(y); } return re; } /** * @description 得到一个3维数组 * @type {Array<Array<Array<>>>} * @param {number} x 第一层大小 * @param {number} y 第二层大小 * @param {number} z 第三层大小 */ function creave3xArray(x, y, z) { let re = new Array(x); for (let i = 0; i < x; i++) { re[i] = new Array(y); for (let j = 0; j < y; j++) { re[i][j] = new Array(z); } } return re; } |
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | /** * 点用二维数组 包含 一维数组(实际三维,厚度2) * @type {Array<Array<Array<number>>>} 坐标点记录[行][列] [0]=>x [1]=>y */ let points = creave3xArray(4, 12, 2); /** * 目标点位置用二维数组 包含 一维数组(实际三维,厚度2) * @type {Array<Array<Array<number>>>} 坐标点记录[行][列] [0]=>x [1]=>y */ let pointsTarget = creave3xArray(4, 12, 2); /** * 点单程移动需要时间用二维数组 * @type {Array<Array<number>>} 坐标点记录[行][列] [0]=>x [1]=>y */ let pointsMoveTime = creave2xArray(4, 12); for (let i = 0; i < 4; i++) { for (let j = 0; j < 12; j++) { points[i][j][0] = j * 140 - 600 + parseInt(Math.random() * 100); points[i][j][1] = i * 188 - 120 + parseInt(Math.random() * 20);; pointsTarget[i][j][0] = points[i][j][0] + parseInt(Math.random() * 260); pointsTarget[i][j][1] = points[i][j][1] + parseInt(Math.random() * 20); pointsMoveTime[i][j] = 5000 + parseInt(Math.random() * 3000); //pointsMoveTime[i][j] = 500 + parseInt(Math.random() * 1000);//某人恶趣味的速度 } } |
通过window.requestAnimationFrame
实现每秒60次的函数调用,回调函数带有一个入参,为一个高精度时间戳,同performance.now()
313 314 315 316 317 | /** * 点在特定时间的位置用二维数组 包含 一维数组(实际三维,厚度2) * @type {Array<Array<Array<number>>>} 坐标点记录[行][列] [0]=>x [1]=>y */ let pointsNow = creave3xArray(4, 12, 2); |
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | //每秒60次重绘 function reDraw(timestamp) { let time = extraTime + timestamp //重算坐标位置 for (let i = 0; i < 4; i++) { for (let j = 0; j < 12; j++) { pointsNow[i][j] = getMovePoint(points[i][j], pointsMoveTime[i][j], time, pointsTarget[i][j][0], pointsTarget[i][j][1]); } } //绘制前最后的准备 for (let j = 0; j < 11; j++) { for (let i = 0; i < 3; i++) { //绘制图像 drawTriangle(pointsNow[i][j], pointsNow[i][j + 1], pointsNow[i + 1][j], shapeColorUP[i][j]) drawTriangle(pointsNow[i][j + 1], pointsNow[i + 1][j], pointsNow[i + 1][j + 1], shapeColorDOWN[i][j]) } } window.requestAnimationFrame(reDraw); } window.requestAnimationFrame(reDraw); |
实现效果如图
颜色的线性变化
颜色的变化感觉上像是线性变化,正由于前面提到的,颜色使用数组存储,那么我们可以方便的写出一个函数,带有三个参数,两个数组对应起始颜色和目标颜色,另一个参数代表百分比,得到如下代码
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | /** * @description 按照等速渐变,得到某点在t毫秒时的颜色,返回一个字符串 * @param {Array<number>} from 来源颜色 * @param {number} t 单程移动所需时间(ms) * @param {number} g 已经经过的时间 * @param {Array<number>} to 目标颜色 */ function getMoveColor(from, to, t, gone) { let persent = gone / t; persent %= 2; if (persent > 1) persent = 2 - persent; let r = from[0] + (to[0] - from[0]) * persent; let g = from[1] + (to[1] - from[1]) * persent; let b = from[2] + (to[2] - from[2]) * persent; r = parseInt(r); g = parseInt(g); b = parseInt(b); return "rgb(" + r + "," + g + "," + b + ")"; } |
然后修改颜色,让起始颜色和目标颜色更加美观
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | /** * @description 随机得到一个rgb颜色数组 */ function getRandomRGB() { let r = 0; let randomNum = Math.random(); let g = 130 + parseInt(randomNum * 30); let b = 160 + parseInt(randomNum * 50); return [r, g, b]; } /** * @description 随机得到一个亮一些的rgb颜色数组 */ function getRandomRGBLight() { let r = 0; let randomNum = Math.random(); randomNum = randomNum / 2 + 0.5 let g = 140 + parseInt(randomNum * 45); let b = 180 + parseInt(randomNum * 55); return [r, g, b]; } |
至此,带有运动函数的基本动态背景图绘制完成。
查漏补缺
重新对比原版QQ,感觉似乎少了点什么?
原版QQ有一种流光的感觉,然而目前我们实现的效果中对比发现并没有,接下来尝试实现流光效果。
首先观察原版,流光效果的光源更类似于点光源,但是对于单独每个三角形,每个位置的亮度是相等的,可整理出以下特点
- 同一个三角形同时每个像素亮度一致
- 光源类似于点光源
- 光源位置在不断变化
- 三角形亮度只和三角形与光源的距离有关
那么我们可能想要实现的有
- 记录一个光源坐标,并且移动它
- 计算每个不同三角形的距离和亮度
- 在计算某个时间三角形颜色的同时,计算亮度变化后的颜色
记录光源坐标并移动
这里使用一个数组记录光源的坐标,使用随机数设置一个随机的初始位置,将整个图形抽象为三角形组合矩形组合的更大的矩形,按照每一个小矩形长宽为1来计算
343 344 345 346 347 | /** * 当前高亮位置,x,y * @type {Array<number>} 高亮位置x,y */ let nowHiLight = [1 + Math.random(), 5 + Math.random()]; |
自然的移动需要速度,而速度需要加速度,加速度随时都在随机而变而速度不会突然大幅度变化,在这里,加速度没有用单独的变量记录,而是直接使用随机数来代替
347 | let nowHiLightSpeed = [Math.random() * 0.06 - 0.03, Math.random() * 0.1 - 0.05]; |
379 380 381 382 383 384 385 386 387 | //计算最新的高亮移动速度 nowHiLightSpeed[0] += (Math.random() - 0.5) * 0.028; nowHiLightSpeed[1] += (Math.random() - 0.5) * 0.04; //限制最大速度 if (nowHiLightSpeed[0] > 0.3) nowHiLightSpeed[0] = 0.3; if (nowHiLightSpeed[0] < -0.3) nowHiLightSpeed[0] = -0.3; if (nowHiLightSpeed[1] > 0.6) nowHiLightSpeed[1] = 0.6; if (nowHiLightSpeed[1] < -0.6) nowHiLightSpeed[1] = -0.6; |
计算三角形距离和亮度
在这里,因为三角形移动幅度不大,故假设三角形不移动,因为不需要高精度所以这里将同一个矩形内的两个三角形都抽象为一个点,坐标为矩形左上角的点,由直角三角形勾股定理可以得到两个点距离等于x轴差值平方和y轴差值平方的和的平方根,得到以下函数
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | /** * @description 设置图形亮度 * @type {void} * @param {Array<Array<number>>} brightArr 要用于存放亮度的已经被清空的数组 * @param {number} pointX 目标坐标X * @param {number} pointY 目标坐标Y * @param {number} decaySpeed 每一格光照强度等量衰减 * @param {number} bright 目标所在的光照强度 */ function setBright(brightArr, pointX, pointY, decaySpeed, bright) { let brightSet; for (let i = 0; i < brightArr.length; i++) { for (let j = 0; j < brightArr[0].length; j++) { brightArr[i][j] = bright - decaySpeed * Math.sqrt((pointX - i) * (pointX - i) + (pointY - j) * (pointY - j)) * 0.7; if (brightArr[i][j] < 0) brightArr[i][j] = 0; } } } |
在计算颜色同时考虑亮度
对之前的计算某时刻颜色的函数进行改造,加入亮度参数
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | /** * @description 按照等速渐变,得到某点在t毫秒时的颜色,返回一个字符串 * @param {Array<number>} from 来源颜色 * @param {number} t 单程移动所需时间(ms) * @param {number} g 已经经过的时间 * @param {number} l 额外亮度增益,最大1,默认0 * @param {Array<number>} to 目标颜色 */ function getMoveColor(from, to, t, gone, l) { let persent = gone / t; persent %= 2; if (persent > 1) persent = 2 - persent; let r = from[0] + (to[0] - from[0]) * persent; let g = from[1] + (to[1] - from[1]) * persent; let b = from[2] + (to[2] - from[2]) * persent; //如果存在亮度 if (l > 0) { let bAdded; if (b != 0) { bAdded = (255 - b) * l; b = (255 - b) * l + b } if (g != 0) { //g = (255 - g) * l + g g += bAdded; if (g > 255) g = 255 } } r = parseInt(r); g = parseInt(g); b = parseInt(b); return "rgb(" + r + "," + g + "," + b + ")"; } |
大功告成
在以上步骤全部完成后,重新绘图,查看最终效果
以上,绘制完成!
源码下载:本文最终效果源码
最近要做一个vue的界面参考一下,请问移动速度是哪个函数在控制
好久没弄vue了,帮不上忙抱歉了
前来学习
惊,韩大妈难道要开始学JS了么
其实已经学习了大半年啦=。=
好强!
效果很好
喵