这主要用到贝赛尔曲线,简单的来说就是 PS 中钢笔画出来的路径,通常会有个控制杆。
二次贝塞尔需要三个点,开始点,控制点(控制杆顶部所在的点),结束点。
了解更多 点这里
我们通过改变结束点的位置来实现摇摆。
修改 heady 就可以摆动了,摆动的规律加个 sin 函数.
加入 alpha 添加透明度,根据近实远虚原理构建空间感,加入 amp,摆动振幅。
import { ctx_two, cvs_height } from "./init";
import { deltaTime } from "./game-loop";
class Anemones{
rootx: number[] = []; // x 轴的坐标
headx: number[] = []; // 海葵头部的 x
heady: number[] = []; // 海葵头部的 y
num = 50; // 绘制数量
alpha = 0 // 角度
amp: number[] = [] // 振幅
opacity: number[] = [] // 透明度
/**
* 其实就跟在 PS 里面画一样,只不过都是通过代码进行操作,不能通过鼠标进行操作。
*
* save() - restore() 做作用就是只对他们之间的代码应用这些画笔样式
*
* save() 就相当于暂存一下画笔的状态。开启一个快照,可以对这个快照进行任何操作
*
* restore() 恢复之前画笔的状态
*/
draw(){
this.alpha += deltaTime * 0.001; // 角度随时间变化
let l = Math.sin(this.alpha)
ctx_two.save() // 暂存画笔状态
// 设置画笔样式
ctx_two.strokeStyle = '#3b154e' // 设置画笔颜色
ctx_two.lineWidth = 20; // 画笔的宽度
ctx_two.lineCap = "round" // 圆角的线
for (let i = 0; i < this.num; ++i) {
this.headx[i] = this.rootx[i] + l * this.amp[i]
ctx_two.beginPath() // 开始绘画
ctx_two.globalAlpha = this.opacity[i]
ctx_two.moveTo(this.rootx[i], cvs_height) // 把画笔移动到 x 点,画布的最下方出,从下往上画海葵
ctx_two.quadraticCurveTo(
this.rootx[i], cvs_height - 100, // 控制点
this.headx[i], this.heady[i] // 结束点
)
ctx_two.stroke() // 确认,开始渲染
}
ctx_two.restore() // 恢复之前暂存的画笔状态
}
/**
* 初始化海葵的 x 坐标和高度
*/
constructor(){
for (let i = 0; i < this.num; ++i) {
this.rootx[i] = i * 16 + Math.random() * 20;
this.headx[i] = this.rootx[i];
this.heady[i] = cvs_height - 240 + Math.random() * 80;
this.amp[i] = 20 + Math.random() * 30 // 设置振幅
this.opacity[i] = (Math.random() * .6) + 0.6
}
}
}
export default Anemones;
此时果实的代码还要修改一下,当没有生长好的时候,也有一部分逻辑要完成。果实要跟着海葵摆动的逻辑。
import { cvs_height, ctx_two, anemones } from "./init";
import { deltaTime } from "./game-loop";
enum FruitType{
Blue = 1,
Orange
}
class Fruits{
num: number = 30; // 绘画果实的数量
alive: boolean[] =[]; // 判断果实是否存活
x : number[]= []; // 果实的 x 坐标数组
y : number[] = []; // 果实的 y 坐标数组
diameter : number[] = []; // 果实的直径数组
speed: number[] = []; // 控制果实成长速度、上浮速度的数组
fruitType: FruitType[] = []; // 控制果实类型的枚举数组
orange = new Image(); // 黄色果实
blue = new Image(); // 蓝色果实
aneNum: number[] = []; // 记录出生时候所在的海葵
constructor(){
for (let i = 0; i < this.num; ++i) {
this.aneNum[i] = 0; // 初始化
this.born(i);
}
this.orange.src = 'assets/img/fruit.png';
this.blue.src = 'assets/img/blue.png';
}
// 绘制果实
draw(){
for (let i = 0; i < this.num; ++i) {
// 只有属性为存活的时候才绘制
if(this.alive[i]){
let img = this.fruitType[i] === FruitType.Orange ? this.orange : this.blue; // 根据类型,拿到相应的图片
if(this.diameter[i] <= 17) {
this.diameter[i] += this.speed[i] * deltaTime; // 随着时间半径不断变大 也就是果实长大
this.x[i] = anemones.headx[this.aneNum[i]]
this.y[i] = anemones.heady[this.aneNum[i]] // 得到海葵顶点的 x 和 y
}else{
this.y[i] -= this.speed[i] * deltaTime; // 果实成熟之后, y 坐标减小,果实开始上升
}
// 把果实绘制出来,为了让果实居中,所以要减去图片高度一半,宽度一半
// 就像实现水平居中一样 left: 50px; margin-left: -(图片宽度 / 2);
// 第一个参数 图片, 第二三个参数,坐标轴的 x 和 y,第四五个参数,图片的宽高
ctx_two.drawImage(img, this.x[i] - this.diameter[i] / 2, this.y[i] - this.diameter[i], this.diameter[i], this.diameter[i]);
}
if(this.y[i] <= -10) {
this.alive[i] = false; // 果实出去了之后 存活状态为 flase
}
}
}
// 初始化果实
born(i){
let aneId = Math.floor( Math.random() * anemones.num ) // 随机拿到一个果实的 ID
this.aneNum[i] = aneId
this.speed[i] = Math.random() * 0.04 + 0.007; // 设置速度在区间 0.003 - 0.03 里
this.alive[i] = true; // 先设置它的存活为 true
this.diameter[i] = 0; // 未生长出来的果实半径为0
this.fruitType[i] = (Math.random() >= 0.7) ? FruitType.Blue : FruitType.Orange; // 设置30%的几率产生蓝色果实
}
// 监视果实
monitor() : void {
let num = 0;
for (let i = 0; i < this.num ; ++i) {
if(this.alive[i]) num++; // 计数存活果实的数量
if(num < 15) {
// 产生一个果实
this.reset()
return ;
}
}
}
//重置果实的状态
reset() {
for (let i = 0; i < this.num; ++i) {
if(!this.alive[i]) {
this.born(i); // 假如存活为 false , 让它重新出生。
return ; // 每次只重置一个果实
}
}
}
// 果实死亡
dead(i){
this.alive[i] = false;
}
}
export default Fruits;
export { FruitType };
当前内容版权归 nodelover.me 或其关联方所有,如需对内容或内容相关联开源项目进行关注与资助,请访问 nodelover.me .