AudioContext技术和音乐可视化(2)

Stella981
• 阅读 665

Intro

转载请注明来源,可以在测试博客查看完成效果。

本篇讲述如何绘制动态的星空,其实关联到频域数据已经没什么悬念了。

一、使用Canvas绘图

1.1 位置和大小

绘制背景的第一要务便是把canvas元素放置在背景这一层次上,避免遮盖其他元素。

对我而言,个人习惯用css来设置大小和位置,用html来确定渲染顺序而不是z-index。

下面是html代码。

<html>

<body>
  <canvas id="background-canvas"></canvas>
  <!-- other elements -->
</body>

</html>

下面是css代码。

#background-canvas {
  position: fixed;
  left: 0;
  top: 0;
  width: 100vw;
  height: 100vh;
  background-color: black;
}

fixed确保拖动页面不会令背景也跟随移动。

其余部分我想应该没什么有疑问的地方。

1.2 CanvasContext2D

对于canvas元素的绘图操作我想很多人应该接触过。

以绘制圆形为例,使用如下代码。

const canvas = document.getElementById("background-canvas");
const ctx = canvas.getContext("2d");

ctx.fillStyle='#fff';
ctx.beginPath();
ctx.arc(100,100,50,0,Math.PI*2); // 参数分别为坐标x,y,半径,起始弧度,结束弧度
ctx.fill();

这样就画完了一个实心圆。

需要注意,canvas的大小通过css设置可能导致画面被拉伸变形模糊,所以最好的办法是绘制前确定一下canvas的大小。

此外需要注意的是,重置大小会导致画面清空,用这种方式可以替代fillRect或者clearRect,有的浏览器平台更快但也有浏览器更慢。可以查阅这篇博文来参考如何提升canvas绘图性能。

fillStyle可以使用css的颜色代码,也就是说我们可以写下诸如rgbahsla之类的颜色,这给我们编写代码提供了很多方便。

1.3 绘制星星

星空是由星星组成的这显然不用多说了,先来看如何绘制单个星星。

星星的绘制方法很多,贴图虽然便利但显然不够灵活,我们的星星是要随节奏改变亮度和大小的,利用贴图的话就只能在alpha值和drawImage缩放来处理了。虽然是一种不错的办法,不过这里我使用了RadialGradient来控制绘图。

PS:RadialGradient 的性能比较差,大量使用会导致明显的性能下降,这是一个显著降低绘制效率的地方。

那么,我们先画一个圆(加点细节预警)。

const canvas = document.getElementById("background-canvas");
const ctx = canvas.getContext("2d");

// 确保不会变形
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;

// 参数分别为起始坐标x,y,半径,结束坐标x,y,半径
const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 50); 
gradient.addColorStop(0.025, "#fff"); // 中心的亮白色
gradient.addColorStop(0.1, "rgba(255, 255, 255, 0.9)"); // 核心光点和四周的分界线
gradient.addColorStop(0.25, "hsla(198, 66%, 75%, 0.9)"); // 核心亮点往四周发散的蓝光
gradient.addColorStop(0.75, "hsla(198, 64%, 33%, 0.4)"); // 蓝光边缘
gradient.addColorStop(1, "hsla(198, 64%, 33%, 0)"); // 淡化直至透明
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fill();

可以在codepen查看效果或直接编辑你的星(圈)星(圈)。

看上去还不错?

让我们用代码控制它的亮度和大小。

const canvas = document.getElementById("background-canvas");
const ctx = canvas.getContext("2d");

// 确保不会变形
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;

// 通过energy控制亮度和大小
let energy = 255;
let radius = 50;
let energyChangeRate = -1;

function draw() {
  requestAnimationFrame(draw); // 定时绘制,requestAnimationFrame比setTimeout更好。
  energy += energyChangeRate; // 见过呼吸灯吧?我们让它变亮~再变暗~反复循环~
  if (energy <= 0 || energy >= 255) energyChangeRate = -energyChangeRate;
    
  // 计算出当前的大小
  const r = radius + energy * 0.1;

  // 清空屏幕
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // 参数分别为起始坐标x,y,半径,结束坐标x,y,半径
  const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, r);
  gradient.addColorStop(0.025, "#fff"); // 中心的亮白色
  gradient.addColorStop(0.1, "rgba(255, 255, 255, 0.9)"); // 核心光点和四周的分界线
  gradient.addColorStop(0.25, `hsla(198, 66%, ${Math.min(75+energy*0.01,100)}%, 0.9)`); // 核心亮点往四周发散的蓝光
  gradient.addColorStop(0.75, `hsla(198, 64%, ${Math.min(33+energy*0.01,100)}%, 0.4)`); // 蓝光边缘
  gradient.addColorStop(1, "hsla(198, 64%, 33%, 0)"); // 淡化直至透明
  ctx.fillStyle = gradient;
  ctx.beginPath();
  ctx.arc(100, 100, r, 0, Math.PI * 2);
  ctx.fill();
}
draw();

可以在codepen查看并编辑效果。

1.4 封装星星

通常来说粒子系统不大会把单个粒子封装成类,因为函数调用的开销还是蛮大的。。。

不过在这里我们这里就先这样了,方便理解和阅读。渲染的瓶颈解决之前,粒子函数调用这点开销根本不是回事儿。

const canvas = document.getElementById("background-canvas");
const ctx = canvas.getContext("2d");

// 确保不会变形
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;

// 用javascript原生的class而不是prototype
class Star {
  constructor(x, y, radius, lightness) {
    this.radius = radius;
    this.x = x;
    this.y = y;
    this.lightness;
  }

  draw(ctx, energy) {
    // 计算出当前的大小
    const r = this.radius + energy * 0.1;

    // 参数分别为起始坐标x,y,半径,结束坐标x,y,半径
    const gradient = ctx.createRadialGradient(
      this.x,
      this.y,
      0,
      this.x,
      this.y,
      r
    );
    gradient.addColorStop(0.025, "#fff"); // 中心的亮白色
    gradient.addColorStop(0.1, "rgba(255, 255, 255, 0.9)"); // 核心光点和四周的分界线
    gradient.addColorStop(
      0.25,
      `hsla(198, 66%, ${Math.min(75 + energy * 0.01, 100)}%, 0.9)`
    ); // 核心亮点往四周发散的蓝光
    gradient.addColorStop(
      0.75,
      `hsla(198, 64%, ${Math.min(33 + energy * 0.01, 100)}%, 0.4)`
    ); // 蓝光边缘
    gradient.addColorStop(1, "hsla(198, 64%, 33%, 0)"); // 淡化直至透明
    ctx.fillStyle = gradient;
    ctx.beginPath();
    ctx.arc(this.x, this.y, r, 0, Math.PI * 2);
    ctx.fill();
  }
}

const star = new Star(100, 100, 50);

let energy = 255;
let energyChangeRate = -1;

// 渲染函数来循环渲染!
function render() {
  requestAnimationFrame(render);
  energy += energyChangeRate;
  if (energy <= 0 || energy >= 255) energyChangeRate = -energyChangeRate;
  // 清空屏幕
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  star.draw(ctx, energy);
}

// 开始渲染动画!
render();

可以在codepen查看代码效果。

完成!

1.5 银河

绘制银河的核心在于随机分布的星星绕着同一中心点旋转,分为两步来讲,第一步是随机分布,这很简单,用Math.random就好了。

// star 部分略

class Galaxy {
  constructor(canvas) {
    this.stars = [];
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");

    this.energy = 255;
    this.energyChangeRate = -2;
  }

  init(num) {
    for (let i = 0; i < num; i++) {
      this.stars.push(
        // 随机生成一定数量的星星,初始化星星位置和大小。
        new Star(
          Math.random() * this.canvas.width,
          Math.random() * this.canvas.height,
          Math.random() * 10 + 1,
          Math.random() * 30 + 33
        )
      );
    }
  }

  render() {
    this.energy += this.energyChangeRate;
    if (this.energy <= 0 || this.energy >= 255)
      this.energyChangeRate = -this.energyChangeRate;

    // 清空屏幕
    this.ctx.fillStyle = "black";
    this.ctx.fillRect(0, 0, canvas.width, canvas.height);

    for (const star of this.stars) {
      star.draw(this.ctx, this.energy);
    }
  }
}

const canvas = document.getElementById("background-canvas");
// 确保不会变形
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
const galaxy = new Galaxy(canvas);
galaxy.init(50);

function render() {
  requestAnimationFrame(render);
  galaxy.render();
}

render();

可以在codepen查看效果和完整代码。

1.6 旋转起来!

【加点细节预警】

接下来我们为星星准备轨道参数,让它们动起来!

首先修改Star类,加入几个字段。

class Star {
  constructor(x, y, radius, lightness, orbit, speed, t) {
    this.radius = radius;
    this.x = x;
    this.y = y;
    this.lightness;
    this.orbit = orbit; // 轨道
    this.speed = speed; // 运动速度
    this.t = t; // 三角函数x轴参数,用 sin/cos 组合计算位置
  }
    // 下略
}

修改初始化代码。

// 前略  
  init(num) {
    const longerAxis = Math.max(this.canvas.width, this.canvas.height);
    const diameter = Math.round(
      Math.sqrt(longerAxis * longerAxis + longerAxis * longerAxis)
    );
    const maxOrbit = diameter / 2;

    for (let i = 0; i < num; i++) {
      this.stars.push(
        // 随机生成一定数量的星星,初始化星星位置和大小。
        new Star(
          Math.random() * this.canvas.width,
          Math.random() * this.canvas.height,
          Math.random() * 10 + 1,
          Math.random() * 30 + 33,
          Math.random() * maxOrbit, // 随机轨道
          Math.random() / 1000, // 随机速度
          Math.random() * 100 // 随机位置
        )
      );
    }
  // 后略

然后在Galaxy里加入控制移动的代码。

move() {
    for (const star of this.stars) {
      console.log(star.orbit)
      star.x = this.canvas.width/2+ Math.cos(star.t) * star.orbit;
      star.y = this.canvas.height/2+ Math.sin(star.t) * star.orbit/2;
      star.t += star.speed;
    }

然后每一帧进行移动!

function render() {
  requestAnimationFrame(render);
  galaxy.render();
  galaxy.move(); // 动起来!
}

大功告成!

codepen查看完整源码!

1.7 待续

PS:不保证粘贴的代码都能跑,反正codepen上是都能的。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这