粒子效果:光斑发散 - 贝塞尔曲线缓动实践

510 阅读1分钟

光斑属性

class Facula {
    //初始状态的光斑小球
    constructor(x, y, r) {
        this.x = x;
        this.y = y;
        this.r = r || random(10, 15); // 光斑小球随机半径
        this.birth = Date.now(); // 光斑生成时间
        this.process = 0; // 光斑运动时长,相对于最大生命值的时长
        this.duration = 100000; // 光斑的最大生命时长
        this.distance = 5; // 单位位移,光斑的最大生命时长和单位位移动,决定了光斑的运动距离
        this.opacity = 1;
        this.color = randomColor();
        this.angle = degreeToRadian(random(0, 360)); // 光斑与坐标轴形成的夹角,用于三角函数换算x,y坐标
        this.animation = Easing.easeOut; // 光斑缓动函数
        this.direction = false;
    }
    
    // 实现光斑闪烁效果
    flash() {}
    // 实现光斑的运动轨迹
    move() {}
    // 在canvas上绘制光斑,ctx为canvas.getContent()
    draw(ctx) {}
}

/**
 * 角度值转弧度值
 * @param {*} deg 角度值
 */
function degreeToRadian(deg) {
  return deg * Math.PI / 180;
}

/**
  * 生成随机数 [min, max)
  * @param {*} min 随机数最小值
  * @param {*} max 随机数最大值
  * @param {*} times 随机数是times的倍数
  */
function random(min, max, times = 1) {
  const num = Math.floor(Math.random() * (max - min) + min);
  return (num % times) + num;
}

/**
 * 生成随机颜色
 */
function randomColor() {
  return `rgba(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)}, 1)`
}

// 贝塞尔曲线函数实现 Easing:https://github.com/gre/bezier-easing

光斑运动

光斑的运动由初始位置向 this.angle 角度随着时间变化而产生位移。

  • 匀速运动

匀速.png

class Facula { 
    //初始状态的光斑小球 
    constructor(x, y, r) {
      // ...其他属性
      this.distance = 5; // 单位位移,光斑的最大生命时长和单位位移动,决定了光斑的运动距离
      this.angle = degreeToRadian(random(0, 360)); // 光斑与坐标轴形成的夹角,用于三角函数换算x,y坐标
    }
    
    // 光斑运动
    move() {
        // 计算单位时间 requestAnimationFrame 后向角度 this.angle 运动 this.distance 距离所对应的(x,y)值
        // ---------- 匀速运动 ----------
        this.x += this.distance * Math.cos(this.angle);
        this.y += this.distance * Math.sin(this.angle);
        // ---------- 匀速运动 ----------
      
        // 光斑闪烁效果的属性值计算
        this.flash();
    }

    // 实现光斑闪烁效果
    flash() {}
    
    // 在canvas上绘制光斑,ctx为canvas.getContent()
    draw(ctx) {}
}
  • 变速运动

实现方案参考Army老师的DEMO

减速.png

(1)如上图,要求解光斑小球减速运动 dt 时间的距离:s(OB)

(2)我们将减速运动换算到等量的匀速运动上得到:s(OB) = s(OC)

根据匀速运动得知:t(OA) / t(OC) = s(OA) / s(OC)

所以 s(OC) = s(OA) * t(OC) / t(OA)

至此我们知道 s(OB) = s(OC) = s(OA) * t(OC) / t(OA)

将 t(OC) / t(OA) 看做一个时间倍率 percent

上文可知单位时间的匀速运动距离:s(OA) = distance * sin(angle)

我们就可以得到 s(OB) = distance * sin(angle) * percent

(3)贝塞尔曲线函数的作用就是关于时间的倍率换算:

所以 percent = easeOut(process/duration) 其中

process 是光斑生成后已经运动的时间,duration 是光斑整个生命周期的总时间

贝塞尔曲线函数的原理可以参考文章:深入浅出贝塞尔曲线及应用示例

class Facula { 
    //初始状态的光斑小球 
    constructor(x, y, r) {
      // ...其他属性
      this.birth = Date.now(); // 光斑生成时间
      this.process = 0; // 光斑运动时长,相对于最大生命值的时长
      this.duration = 100000; // 光斑的最大生命时长
      this.animation = Easing.easeOut; // 光斑缓动函数
    }
    
    // 光斑运动
    move() {
        // 计算单位时间 requestAnimationFrame 后向角度 this.angle 运动 this.distance 距离的(x,y)值
        // ---------- 缓动运动 ----------
        let diff = Date.now() - this.birth; // 计算运动时间
        let percent = this.process / this.duration; // 计算运动时间占总时间的比例
        percent = this.animation(percent); // 利用贝塞尔曲线渐出函数,通过运动时间比例换算出单位长度的变换倍率
        this.x += this.distance * Math.cos(this.angle) * percent;
        this.y += this.distance * Math.sin(this.angle) * percent;
        // ---------- 缓动运动 ----------
        this.process += diff; // 记录光斑生命进行时长
    }

    // 实现光斑闪烁效果
    flash() {}
    
    // 在canvas上绘制光斑,ctx为canvas.getContent()
    draw(ctx) {}
}

光斑闪烁

flash() {
    // 在一定范围内改变透明度和半径,实现闪烁效果
    // opacity 的区间为 [0.1, 0.8] 也可以放在光斑属性里动态设置
    if (this.opacity <= 0.1) {
      this.direction = true; // 光斑变亮 半径变大
    } 

    if (this.opacity >= 0.8) {
      this.direction = false; // 光斑变暗 半径变小
    }

    if (this.direction) {
      this.opacity += 0.1;
      this.r += 0.5;
    } else {
      this.opacity -= 0.1;
      if (this.r > 0.5) { // 这里要保证半径不能小于0,否则绘制过程会报错
          this.r -= 0.5;
      }
    } 
    this.color = getColorWithOpacity(this.color, this.opacity);
}
/**
 * 给颜色加透明度
 * @param {*} color 初始颜色值
 * @param {*} opacity 透明度值
 * @returns 计算后的颜色值
 */
function getColorWithOpacity(color, opacity) {
  if (/rgba/.test(color) || /rgb/.test(color)) {
    let cMatch = color.match(/((?<=(rgba\())|(?<=(rgb\())).*(?=(\)))/)[0]
    let cArr = cMatch.split(",");
    return `rgba(${+cArr[0] || random(0, 255)}, ${+cArr[1] || random(0, 255)}, ${+cArr[2] || random(0, 255)}, ${opacity})`
  }
  return `rgba(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)}, 1)`
}

绘制光斑

draw(ctx) {
    this.move(); // 光斑变速运动 计算位置
    this.flash(); // 光斑闪烁效果 计算透明度和半径
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI*2, true);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath();
}

DEMO预览

页面代码

Facula类代码