这主要用到贝赛尔曲线,简单的来说就是 PS 中钢笔画出来的路径,通常会有个控制杆。

    二次贝塞尔需要三个点,开始点,控制点(控制杆顶部所在的点),结束点。

    了解更多 点这里

    我们通过改变结束点的位置来实现摇摆。

    修改 heady 就可以摆动了,摆动的规律加个 sin 函数.

    加入 alpha 添加透明度,根据近实远虚原理构建空间感,加入 amp,摆动振幅。

    1. import { ctx_two, cvs_height } from "./init";
    2. import { deltaTime } from "./game-loop";
    3. class Anemones{
    4. rootx: number[] = []; // x 轴的坐标
    5. headx: number[] = []; // 海葵头部的 x
    6. heady: number[] = []; // 海葵头部的 y
    7. num = 50; // 绘制数量
    8. alpha = 0 // 角度
    9. amp: number[] = [] // 振幅
    10. opacity: number[] = [] // 透明度
    11. /**
    12. * 其实就跟在 PS 里面画一样,只不过都是通过代码进行操作,不能通过鼠标进行操作。
    13. *
    14. * save() - restore() 做作用就是只对他们之间的代码应用这些画笔样式
    15. *
    16. * save() 就相当于暂存一下画笔的状态。开启一个快照,可以对这个快照进行任何操作
    17. *
    18. * restore() 恢复之前画笔的状态
    19. */
    20. draw(){
    21. this.alpha += deltaTime * 0.001; // 角度随时间变化
    22. let l = Math.sin(this.alpha)
    23. ctx_two.save() // 暂存画笔状态
    24. // 设置画笔样式
    25. ctx_two.strokeStyle = '#3b154e' // 设置画笔颜色
    26. ctx_two.lineWidth = 20; // 画笔的宽度
    27. ctx_two.lineCap = "round" // 圆角的线
    28. for (let i = 0; i < this.num; ++i) {
    29. this.headx[i] = this.rootx[i] + l * this.amp[i]
    30. ctx_two.beginPath() // 开始绘画
    31. ctx_two.globalAlpha = this.opacity[i]
    32. ctx_two.moveTo(this.rootx[i], cvs_height) // 把画笔移动到 x 点,画布的最下方出,从下往上画海葵
    33. ctx_two.quadraticCurveTo(
    34. this.rootx[i], cvs_height - 100, // 控制点
    35. this.headx[i], this.heady[i] // 结束点
    36. )
    37. ctx_two.stroke() // 确认,开始渲染
    38. }
    39. ctx_two.restore() // 恢复之前暂存的画笔状态
    40. }
    41. /**
    42. * 初始化海葵的 x 坐标和高度
    43. */
    44. constructor(){
    45. for (let i = 0; i < this.num; ++i) {
    46. this.rootx[i] = i * 16 + Math.random() * 20;
    47. this.headx[i] = this.rootx[i];
    48. this.heady[i] = cvs_height - 240 + Math.random() * 80;
    49. this.amp[i] = 20 + Math.random() * 30 // 设置振幅
    50. this.opacity[i] = (Math.random() * .6) + 0.6
    51. }
    52. }
    53. }
    54. export default Anemones;

    此时果实的代码还要修改一下,当没有生长好的时候,也有一部分逻辑要完成。果实要跟着海葵摆动的逻辑。

    1. import { cvs_height, ctx_two, anemones } from "./init";
    2. import { deltaTime } from "./game-loop";
    3. enum FruitType{
    4. Blue = 1,
    5. Orange
    6. }
    7. class Fruits{
    8. num: number = 30; // 绘画果实的数量
    9. alive: boolean[] =[]; // 判断果实是否存活
    10. x : number[]= []; // 果实的 x 坐标数组
    11. y : number[] = []; // 果实的 y 坐标数组
    12. diameter : number[] = []; // 果实的直径数组
    13. speed: number[] = []; // 控制果实成长速度、上浮速度的数组
    14. fruitType: FruitType[] = []; // 控制果实类型的枚举数组
    15. orange = new Image(); // 黄色果实
    16. blue = new Image(); // 蓝色果实
    17. aneNum: number[] = []; // 记录出生时候所在的海葵
    18. constructor(){
    19. for (let i = 0; i < this.num; ++i) {
    20. this.aneNum[i] = 0; // 初始化
    21. this.born(i);
    22. }
    23. this.orange.src = 'assets/img/fruit.png';
    24. this.blue.src = 'assets/img/blue.png';
    25. }
    26. // 绘制果实
    27. draw(){
    28. for (let i = 0; i < this.num; ++i) {
    29. // 只有属性为存活的时候才绘制
    30. if(this.alive[i]){
    31. let img = this.fruitType[i] === FruitType.Orange ? this.orange : this.blue; // 根据类型,拿到相应的图片
    32. if(this.diameter[i] <= 17) {
    33. this.diameter[i] += this.speed[i] * deltaTime; // 随着时间半径不断变大 也就是果实长大
    34. this.x[i] = anemones.headx[this.aneNum[i]]
    35. this.y[i] = anemones.heady[this.aneNum[i]] // 得到海葵顶点的 x 和 y
    36. }else{
    37. this.y[i] -= this.speed[i] * deltaTime; // 果实成熟之后, y 坐标减小,果实开始上升
    38. }
    39. // 把果实绘制出来,为了让果实居中,所以要减去图片高度一半,宽度一半
    40. // 就像实现水平居中一样 left: 50px; margin-left: -(图片宽度 / 2);
    41. // 第一个参数 图片, 第二三个参数,坐标轴的 x 和 y,第四五个参数,图片的宽高
    42. ctx_two.drawImage(img, this.x[i] - this.diameter[i] / 2, this.y[i] - this.diameter[i], this.diameter[i], this.diameter[i]);
    43. }
    44. if(this.y[i] <= -10) {
    45. this.alive[i] = false; // 果实出去了之后 存活状态为 flase
    46. }
    47. }
    48. }
    49. // 初始化果实
    50. born(i){
    51. let aneId = Math.floor( Math.random() * anemones.num ) // 随机拿到一个果实的 ID
    52. this.aneNum[i] = aneId
    53. this.speed[i] = Math.random() * 0.04 + 0.007; // 设置速度在区间 0.003 - 0.03 里
    54. this.alive[i] = true; // 先设置它的存活为 true
    55. this.diameter[i] = 0; // 未生长出来的果实半径为0
    56. this.fruitType[i] = (Math.random() >= 0.7) ? FruitType.Blue : FruitType.Orange; // 设置30%的几率产生蓝色果实
    57. }
    58. // 监视果实
    59. monitor() : void {
    60. let num = 0;
    61. for (let i = 0; i < this.num ; ++i) {
    62. if(this.alive[i]) num++; // 计数存活果实的数量
    63. if(num < 15) {
    64. // 产生一个果实
    65. this.reset()
    66. return ;
    67. }
    68. }
    69. }
    70. //重置果实的状态
    71. reset() {
    72. for (let i = 0; i < this.num; ++i) {
    73. if(!this.alive[i]) {
    74. this.born(i); // 假如存活为 false , 让它重新出生。
    75. return ; // 每次只重置一个果实
    76. }
    77. }
    78. }
    79. // 果实死亡
    80. dead(i){
    81. this.alive[i] = false;
    82. }
    83. }
    84. export default Fruits;
    85. export { FruitType };