# 模仿QQ登录窗口html5动画实现

### 思路

1. 仔细查看原版动画，找出包含组件
2. 仔细查看原版动画，感受运动效果
3. 使用canvas实现单个组件绘制
4. 使用canvas绘制出所有组件（不运动）
5. 设置运动函数，为组件套用运动函数
6. 查找缺失部分
7. 模拟缺失部分效果

### 拆分动画

#### 包含组件

1. 大量的三角形拼接成的动画主体
2. QQ LOGO
3. 右上角的按钮

#### 运动效果

1. 每个三角形颜色会有类似线性渐变效果
2. 三角形的三个点有缓入缓出运动轨迹

### 组件绘制

#### 单个组件

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, a); ctx.lineTo(b, b); ctx.lineTo(c, c); 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"); #### 所有三角形组件

305 306 points[i][j] = j * 140 - 600 + parseInt(Math.random() * 100); points[i][j] = i * 188 - 120 + parseInt(Math.random() * 20); 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] 的表现

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>>>} 坐标点记录[行][列] =>x =>y */ let points = creave3xArray(4, 12, 2); /** * 目标点位置用二维数组 包含 一维数组（实际三维，厚度2） * @type {Array<Array<Array<number>>>} 坐标点记录[行][列] =>x =>y */ let pointsTarget = creave3xArray(4, 12, 2); /** * 点单程移动需要时间用二维数组 * @type {Array<Array<number>>} 坐标点记录[行][列] =>x =>y */ let pointsMoveTime = creave2xArray(4, 12); for (let i = 0; i < 4; i++) { for (let j = 0; j < 12; j++) { points[i][j] = j * 140 - 600 + parseInt(Math.random() * 100); points[i][j] = i * 188 - 120 + parseInt(Math.random() * 20);; pointsTarget[i][j] = points[i][j] + parseInt(Math.random() * 260); pointsTarget[i][j] = points[i][j] + parseInt(Math.random() * 20); pointsMoveTime[i][j] = 5000 + parseInt(Math.random() * 3000); //pointsMoveTime[i][j] = 500 + parseInt(Math.random() * 1000);//某人恶趣味的速度 } }

313 314 315 316 317 /** * 点在特定时间的位置用二维数组 包含 一维数组（实际三维，厚度2） * @type {Array<Array<Array<number>>>} 坐标点记录[行][列] =>x =>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], pointsTarget[i][j]); } }   //绘制前最后的准备 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 + (to - from) * persent; let g = from + (to - from) * persent; let b = from + (to - from) * 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]; }

### 查漏补缺

1. 同一个三角形同时每个像素亮度一致
2. 光源类似于点光源
3. 光源位置在不断变化
4. 三角形亮度只和三角形与光源的距离有关

1. 记录一个光源坐标，并且移动它
2. 计算每个不同三角形的距离和亮度
3. 在计算某个时间三角形颜色的同时，计算亮度变化后的颜色

#### 记录光源坐标并移动

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 += (Math.random() - 0.5) * 0.028; nowHiLightSpeed += (Math.random() - 0.5) * 0.04;   //限制最大速度 if (nowHiLightSpeed > 0.3) nowHiLightSpeed = 0.3; if (nowHiLightSpeed < -0.3) nowHiLightSpeed = -0.3; if (nowHiLightSpeed > 0.6) nowHiLightSpeed = 0.6; if (nowHiLightSpeed < -0.6) nowHiLightSpeed = -0.6;

#### 计算三角形距离和亮度

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.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 + (to - from) * persent; let g = from + (to - from) * persent; let b = from + (to - from) * 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 + ")"; }

### 大功告成

1. 韩大妈说道：

前来学习

1. Handle说道：

惊，韩大妈难道要开始学JS了么

1. 韩大妈说道：

其实已经学习了大半年啦=。=

1. Handle说道：

好强！

2. 九宫格大魔王说道：

效果很好

3. Katyusha说道：
{"ft_lang_label":"__label__zh","ft_lang_prob":0.5141574,"math_prob":0.9974622,"size":9357,"snap":"2020-45-2020-50","text_gpt3_token_len":5098,"char_repetition_ratio":0.13995509,"word_repetition_ratio":0.28406632,"special_character_ratio":0.43913648,"punctuation_ratio":0.15287009,"nsfw_num_words":0,"has_unicode_error":false,"math_prob_llama3":0.99533486,"pos_list":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18],"im_url_duplicate_count":[null,6,null,6,null,6,null,null,null,null,null,null,null,null,null,null,null,null,null],"WARC_HEADER":"{\"WARC-Type\":\"response\",\"WARC-Date\":\"2020-11-27T23:27:37Z\",\"WARC-Record-ID\":\"<urn:uuid:21b5a796-2276-4585-b26f-636c75e9c872>\",\"Content-Length\":\"128633\",\"Content-Type\":\"application/http; msgtype=response\",\"WARC-Warcinfo-ID\":\"<urn:uuid:46e0560b-5dfa-4ba1-93df-2d209b9aaf89>\",\"WARC-Concurrent-To\":\"<urn:uuid:8ed7285a-0324-4705-80fa-4c3426db44a5>\",\"WARC-IP-Address\":\"49.234.235.151\",\"WARC-Target-URI\":\"https://www.bysb.net/3164.html\",\"WARC-Payload-Digest\":\"sha1:BW7ABKKYLTLOMSXDZDE6AXKPTGRNTCVU\",\"WARC-Block-Digest\":\"sha1:HZ2IWZL63GFIYZTVRNKH6M7XY2VBQCYU\",\"WARC-Identified-Payload-Type\":\"text/html\",\"warc_filename\":\"/cc_download/warc_2020/CC-MAIN-2020-50/CC-MAIN-2020-50_segments_1606141194634.29_warc_CC-MAIN-20201127221446-20201128011446-00442.warc.gz\"}"}