酷尼实验室

-- kunnisser


  • 首页

  • 归档

React在IE下的性能优化

发表于 2019-07-22

项目初始加载优化

从白屏到页面显示的一些耗时因素:

  1. TTFB - 客户端拿到资源的耗时,一般受并发请求数,服务程序处理和网速影响

  2. contentDownload 受文件大小和网速影响

  3. stalled/block 失速和阻塞是在发送请求前等待的时间,受进入队列前的一些原因导致耗时。例如代理协商

  4. initial connection 建立连接所需时间。建立TCP连接协商SSL。受网络环境影响

  5. queueing 排队事件,受请求优先级影响,例如图片请求低于脚本样式,或者浏览器的tcp链接上限导致被搁置。

image.png-126.1kB

一些优化措施

  • 由于项目是用webpack4.x进行搭建,这一版本的脚手架优化了tree-shaking以及spliteChunks等内置处理。使得我们可以尽可能的减少单个包体积,同时可以合理的分离公共代码模块。控制并发的脚本数, 甚至根据按需引入或路由分割成多个模块进行异步加载。

  • 服务端Gzip压缩,这一点很重要。直接影响contentDownload时间

  • 域名DNS预解析
    会在浏览器空闲时把域名解析为IP地址而非请求时进行。例如

1
2
<meta http-equiv='x-dns-prefetch-control' content='on'>
<link rel='dns-prefetch' href='http://www.junruizx.com'>
  • TTFB 优化服务器,异地机房,CDN等

  • 缓存策略

  • 在IE中做预加载骨架。

  • js加载阻塞html渲染
    defer - 会将js先下载下来,但是会等HTML解析完成后,才执行
    async - 同时进行HTML解析与js下载,但js下载完成后立刻停止HTML解析并执行js代码

React在IE下的渲染优化

  1. IE10下面antd form组件中的input事件在placeholder为中文的情况下初始化会执行自动执行,导致二次渲染。

  2. 子组件执行redux action可能会导致父组件重绘。 父组件如没有业务需求,直接设置shouldComponentupdate return false

  3. IE下的table 数据渲染性能开销大,尽量减少重绘

  4. rowSelect尽量使用radio 和 checkbox ,使用onRow来处理选中会导致多次渲染

  5. 子组件尽量去分析业务,简单的组件直接用PureComponent,阻止无意义的render.复杂组件自定义shouldComponentUpdate来设置具体的逻辑比对。缩小业务数据比对的范围。

  6. form组件受控 - 改变单个控件,整个form会重绘。为了方便开发和监听校验,暂时忽略。

  7. input和textarea等控件输入导致多次render, 添加防抖机制。

  8. react渲染在IE下的速度明显没有chrome来的快,甚至IE10,IE11的渲染耗时居然比IE9高。(应该是IE9在渲染动画上做了一定的降级)


目前项目改版还未开始,暂时的优化思路为以上几点,后续继续探索和完善。

React+Antd 项目兼容ie8的几座大山

发表于 2019-06-15

新公司的业务是面向政府项目,然而政府项目的需求现状是会需要去兼容ie9以下版本。大量未升级的XP版本即使强制使用浏览器最高版本也只有到ie8。那么基于客户要求至上的原则,花费精力在低版本ie兼容上还是存在合理性的。

一、 技术栈选型

前后端分离对于IE兼容项目尤其是要兼容到IE8以下版本就是奇葩的存在,在整个babel编译过程从ES6 -> ES5 -> ES3之外,还要修复兼容一系列IE低版本本身不支持的api. 以及一系列怪异的样式问题。

然而工作还是要继续,恰当的技术选型就比较关键了,现在比较主流的处理方式就是做版本库降级,例如: react v0.16.4 + webpack v1.15.0 + react-router v2.3.0 + babel-polyfill + es5-shim等一系列降级库和垫片来处理。

在这里我们采用司徒正妹的anujs库,作为IE兼容狂魔的代表作我们都知道avalon。其实他还设计了一个基于React16版本的库,也就是anujs. 我们可以通过它来实现 react16 + webpack4.x + reach-router 来使用最新的前后端分离的工程技术栈进行开发。突然感觉到了一丝强大。

二、 webpack 环境搭建

这里我们基于react-dev-utils搭建一个类似于create-react-app的脚手架。这里为什么不直接用create-react-app呢? 这里主要有三个原因:

  1. 我们希望webpack工程能具有一定的灵活性
  2. 防止部分脚手架功能导致IE报错。
  3. 同时进行多项目的统一脚手架配置。
    (我们最近的改版项目涉及到30多个省市的项目,每个项目近乎独立,所以在jenkins自动化部署上,最好能配置单一脚手架实现多项目托管。)

三、IE兼容条目

compat.js

主要基于es-shim库做一些函数API兼容并加入Map定义的覆盖,UA的IE判断等。

分割打包initial模式的模块

  1. object-create-ie8(替代es-sham)
  2. object-defineproperty-ie8 (兼容object.defineproperty)
  3. console-polyfill (兼容console)
  4. bluebird (兼容Promise)
  5. fetch-polyfill2 (兼容fetch请求)

anujs的React兼容体系

  1. ReactIE - 对React16添加了一些特殊事件的兼容补丁与innerHTML的修复处理
  2. createClass - 对React的createClass进行兼容
  3. Reach-router - 对react路由进行兼容,值得注意的是IE9以下只能使用hash模式 (也可以使用低版本的react-router)

webpack的plugin

es3ify-plugin - 修复关键字在IE8中不能作为属性名和方法名,例如modue.default而default作为关键字会在IE8中报错。

四、Antd1.11.x的兼容

react的兼容只是顺利进行IE版本开发的一半,下面针对一些UI库还需要进行一些处理。

  1. rc-calendar - Picker.js - forcus报错 空对象 更换rc-calendar v7.5.3

  2. 点击下拉select报错 - dom-align - v.1.6.7 - rc-align - v.2.3.6

  3. select下拉样式错误 - dom-align fixTop修复

  4. pagination 报错 - 使用 rc-pagination 1.8.10版本

  5. Map 在IE11有自己的实现定义且区别于ES6的Map, 所以要重新定义Map

  6. window.matchMedia 未定义, 安装media-match 兼容

  7. IE10下面antd form组件中的input事件在placeholder为中文的情况下初始化会执行自动执行,导致二次渲染。

js原型世界

发表于 2019-06-08

先从instanceof说起

在日常开发中,我们通常使用typeof去判断一个数据类型或者是否为undefined。

但是其结果只能判断出基本的数据类型,即为:number, string, object, boolean, function, undefined等。所以在进行例如数组和对象判断时,我们需要使用instanceof来检测某个数据是否为Array或Object的实例。
例如:

1
2
3
4
5
const a = new Array();
const b = new Object();

console.log(a instanceof Array);
console.log(a instanceof Object);

我们会发现 输出的结果都为true。这个时候可能会疑惑,这样一来无法判断出a到底是Array还是Object的实例了。
我们接下来继续:

1
console.log(Array instanceof Object);

从这里我们看出来Array本身也是继承于Object类。

那么我们怎么判断ab的数据类型呢?只需要多做一层判断

1
2
console.log(b instanceof Array);
console.log(b instanceof Object);

前者返回false,后者返回true。这说明了b是一个对象而非数组。而a在第一次判断就可以确定是一个数组对象。a instanceof Object的判断是多余的。

instanceof 的实现

那么我们现在可以说instanceof 是用来判断生成实例对象所对应的构造函数么?那Array instanceof Object和extends继承有什么关系?

下面我们来实现一个instanceof:

1
2
3
4
5
6
7
8
9
10
11
12
13
function instanceOf (A, B) {
let proto = A.__proto__;
let prototype = B.prototype;
while (true) {
if (proto === null) {
return false;
}
if (proto === prototype) {
return true;
}
proto = proto.__proto__;
}
}

代码很简单,参数A就是instance左边的数据,首先取A的隐式原型(可以理解为对象中的原型,它本质上等于构造函数的原型,这也是我们用来做instanceof判断的基准)。
接下来,就是取B的原型。这里做了一个while循环,终止条件就是一直向上查找到Object.prototype的隐式原型,这里要注意,不管是实例对象还是函数原型,都包含了一个隐式原型对象。
在查询匹配到Object.prototype.proto的时候已经到达了终点就是null。这里可以理解为并没有匹配到对应的B的原型。
方式就是通过获取隐式原型向上查找,如匹配到A的隐式原型和B的原型相同则返回true。

这里我们再通过代码理解一下Array instanceof Object 为 true:

孔子说过:在js当中一切皆为对象。Array 可以理解为一个函数,也可以理解为是一个对象。

那么有如下代码:

1
2
Array.__proto__ === Function.prototype; (true)
Function.prototype.__proto__ === Object.prototype; (true)

九拐十八弯,我们终于发现其实 Array.proto.proto 往上2层寻找到了对应的Object原型。

这里值得注意的是,Function的隐式原型等于Function的原型。这个可能有点怪异,后面会另外做讲解。

准备上我呕心沥血制作的美图了

到这里,可能不太熟悉原型,原型链,构造函数以及相互之间的关系的同学已经要懵逼了。下面我通过原创的关系图来做一个形象的说明。

alt text

如果这张图还不能满足你对于原型的理解,我可以尽量说的再详细一点:

js饼干工厂

首先让我们脑洞一下,js世界里有一个饼干工厂。我就是厂长啦,既然是饼干工厂那肯定是要生产饼干的。
下面我要给饼干做一个市场定位,是专门生产8-14岁的青少年儿童饼干,所有的饼干都富含多种促发育的营养物质,这个就是Object.prototype。
他是一切饼干产品及制作的根源(一切皆为对象)

接下来建立产品饼干的研发中心Function.prototype, 它设立了好几条产线例如:
String, Number, Function, Object, Array, Boolean等函数,当然他们也有自己的prototype 独立的生产配方。但是他们都是基于这个研发中心来生产的。
所以上述函数的隐式原型就是来源于Function.prototype。
另外Function可以理解为处理产品的实验室而非产线,它既可以实例出新的函数,其本身又可以作为对象可以查询到自身的隐式原型指向。

最后通过这些函数可以new 出各种不同种类的饼干例如{}, [], 1, ‘23’, true等等。而这些实例的对象的隐式原型又指向生产他们的配方。

换句话说Object.prototype是一切对象的原型定义, Function.prototype是一切函数的原型定义。但是函数本身也是对象,所以就有了Function.prototype.__prototype =
Object.prototype. 这样一来Object.prototype站在了js原型的最顶端。

通过原型链的层次可以这样来排列:

  1. Object.prototype

  2. Function.prototype

  3. Object, Function, Symbol, Boolean, String, Number

  4. {}, function, symbol, true, ‘123’, 123

  5. 自定义的function本身还可以自定义原型并实例化新的对象{…}

pomelo 双人对战h5游戏

发表于 2019-05-19

前言

这几天赋闲在家,难得的几天空窗期,我想要静下心来,填填放置了一年的老项目。主要是做一个双人或多人的房间频道,并实现一套即时io的游戏玩法。

优秀但趋于沉寂的node游戏服务器框架pomelo

在今天,游戏服务器开发语言的首选仍是c++或者golang等。基于node的单线程架构以及内存管理问题,node做即时数据交互还是存在短板,但是它里面rpc接口异步调用的设计思路等还是给我们学习node亦或是游戏服务开发提供了一些借鉴。
这里我们想通过pomelo去记录了解一个对战游戏的本地逻辑、前端服务以及后台服务等是如何实现的。

Phaser 游戏开发实例(一)

发表于 2019-04-29

phaser是一个基于pixi渲染引擎的开发框架,用其api文档里的一句话来形容就是:你所见的世界是一幅油画。Phaser里最基本的对象或者说是元素就是World,如果有兴趣,你也可以把你的canvas当做艺术品,做出你想要的东西。Phaser的API和教程介绍已经很多了,不做重复,本文重点分享一下如何使用 phaser,webpack, es6 系统的开发一款小游戏。

环境搭建

  • 通常开发一款H5的phaser游戏的时候,如果通过es5语法来实现一些函数操作或者继承框架的类函数比较复杂,下文中我会详细介绍。所以我们需要引入babel模块来使用es6进行代码开发。
  • phaser虽然提供了强大的API,减少了很多的代码量,但是做一款不复杂但是完整的游戏在一个JS文件中无论是阅读还是维护相当的不方便,所以按照场景或者功能分发成多个模块进行开发是有必要的
  • 相关package.json可以参考https://github.com/lean/phaser-es6-webpack。
  • 核心需求主要是
  1. babel全家桶,如果你需要代码里进行对象结构等es7新特性可以引入babel-preset-stage-2
  2. webpack以及对应的本地服务器
  3. phaser v2.6.2(如果你不需要新增特性的话,不推荐使用phaser-ce);

webpack.config配置

1
2
3
4
5
6
var path = require('path');
var webpack = require('webpack');
var HtmlWebpackPlugin = require("html-webpack-plugin");
var phaserModule = path.join(__dirname, '/node_modules/phaser/');
var phaser = path.join(phaserModule, 'build/custom/phaser-arcade-physics.js');
var pixi = path.join(phaserModule, 'build/custom/pixi.js');

配置相应的库文件路径,这里我们使用的是带有arcade物理引擎的phaser版本,这里如果你使用的是phaser-ce版本的话,默认的物理引擎是p2, 所以如果你不打算使用P2的话,代码运行会报p2对象未定义的错误。

1
2
3
4
5
entry: {
app: [
path.resolve(__dirname, "./Parkour/main.js")
]
}

在module 的export里,我们首先要定义入口文件路径.

loaders配置

接下来是配置webpack中重要的功能 – Loaders

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module: {
loaders: [
{
test: /\.json$/,
loader: 'json-loader'
},
{
test: /\.js$/,
exclude: /node-modules/,
loader: 'babel-loader'
},
{
test: /\.(png|jpg)$/,
loader: 'url-loader?limit=1024&name=[name].[ext]'
},
{
test: /pixi\.js$/,
loader: 'expose-loader?PIXI'
},
{
test: /phaser-arcade-physics\.js$/,
loader: 'expose-loader?Phaser'
}
]
}

上面是通过正则在配置文件中绑定loaders
test部分是一个匹配被处理文件后缀名的正则表达式,上面是针对几种不同格式的文件所用到相对应的loader:

  1. json-loader用来处理对应的json文件转为js文件,你可以直接在js代码中调用它
  2. babel-loader用来将es6, es7的js文件转为可供现代浏览器识别的es5格式的js文件。
  3. url-loader类似于file-loader,但是它添加了limit属性,如果文件大小<limit,会自动将其转为base64编码减少资源的请求数。
  4. 最后针对pixi和phaser的第三方js源码进行模块化,这里通过expose将模块添加到全局对象Phaser和PIXI, 使得phaser能够正常调用pixi文件中的PIXI对象 (webpack2 需要写全称expose-loader).
1
2
3
4
5
6
7
8
9
10
11
12
entry: {
app: [
path.resolve(__dirname, "./Parkour/main.js")
],
vendor: ['pixi', 'phaser']
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.bundle.js'
})
]

针对CommonsChunkPlugin的使用,首先在入口配置中添加vendor,写入要打包的第三方库,然后我们实例化webpack的公共代码提取插件,将配置名为vendor里的第三方代码库进行合并。这样就可以将phaser, pixi以及其他业务需要引入的库文件合并成vendor.bundle.js

  • webpack.config的详细配置见:点我
  • 生产环境下的webpack.config: 点我

WebGl实现音频可视化

发表于 2019-04-28

WebAudio

在查阅MDN的Api的时候,看到是这么介绍:

Web Audio API使用户可以在音频上下文(AudioContext)中进行音频操作,具有模块化路由的特点。在音频节点上操作进行基础的音频, 它们连接在一起构成音频路由图。

所以接下来我试着去test, 尝试获取一首歌曲的音频数据。

获取步骤

  1. 新建一个audio上下文实例 audioCtx。
  2. 通过audio上下文创建音频源,这里有两种采样实现方式:
    2.1 createMediaElementSource 通过audio对象获取音频文件引入 - source
    2.2 createMediaStreamSource 通过stream流文件引入
  3. 再通过audio上下文创建一个分析器节点( analyserNode )它提供了实时时间频率和时间域的切点,这些数据构成了数据可视化的重要参数
  4. 接下来把分析器连接到音频源 source.connect(analyserNode)
  5. 最后把音频源连接至设备声卡 source.connect(destination)

简易流程如图:

alt text

捕获音频数据

首先设置分析器快速傅里叶变换来捕获音频数据 其决定了频谱密度。
而frequencyBinCount决定了数据链的长度,这个属性为只读属性,值默认为fftSize的一半。
这样我们可以构建出一个长度为fftSize * 0.5的32位浮点型的高速类型数组。

1
2
analyserNode.fftSize = 256;
var dataArray = new Float32Array(analyserNode.frequencyBinCount);

定义好了数组,就可以通过 getFloatFrequencyData 这个api来获取即时的音频数据。(当然还有其他获取不同类型数据的api)
此方法的调用通常定义在帧刷新方法中,做实时数据的变化监控和可视化渲染。
例如:

1
2
3
4
5
function draw() {
requestAnimationFrame(draw);
// 获取实时音频数据
analyser.getFloatFrequencyData(dataArray);
}

WebGL

为啥要用WebGL?

跳脱大数组图像渲染的内存消耗瓶颈

在1209年的今天,基本上主流浏览器都可以支持Webgl, 我们赶紧把吃力的遍历渲染的工作交给GPU的专业图像处理器来干。救救可怜的浏览器占用吧!
简单来说就是,使用glsl语言构建片元和顶点着色器这些GPU可以直接运行的图形命令,并通过webgl处理可在浏览器中运行的代码。

pixi.js中的Filter性能

在pixi v4.8.3版本的Filter中定义了fragShader(片元着色器)程序,却在stats插件监控发现帧率巨低。平均只有20多fps。
这个问题同时也存在基于pixi渲染的phaser游戏框架中,官方提供的一些filter示例也有不同程度的卡顿,希望在pixi v5的版本里的shader能得到优化。

初始化WebGL

在这里就不详细介绍概念了,首先先构建webGl的上下文并传递着色器程序。

  1. 定义一个canvas对象,尺寸设置为全屏。
1
2
3
const scene = document.getElementById('scene');
scene.width = document.documentElement.clientWidth
scene.height = document.documentElement.clientHeight;
  1. 获取webgl上下文
1
gl = scene.getContext('webgl');
  1. 初始化着色器

3.1 定义创建shader对象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function loadShader (gl, type, sourceStr) {
// 通过gl 根据类型建立一个着色器对象
// type : gl.VERTEX_SHADER 顶点着色器 gl.FRAGMENT_SHADER
var shader = gl.createShader(type);

// 设置着色器代码
gl.shaderSource(shader, sourceStr);

// 编译着色器代码
gl.compileShader(shader);

// 处理编译结果
var result = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!result) {
var error = gl.getShaderInfoLog(shader);
console.error('Failed to compile shader: ' + error);
// 清除生成的shader
gl.deleteShader(shader);
return null;
}
// 编译成功返回对象
return shader;
}

3.2 定义创建program对象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function createProgram (gl, vshader, fshader) {
//创建顶点和片元着色器对象
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}

// 开始构建program
var program = gl.createProgram();
if (!program) {
return null;
}

// 将shader对象装载至program
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

// 将webgl上下文连接程序对象
gl.linkProgram(program);

// 检查连接结果
// 获取连接状态的参数信息
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
// 获取错误信息
var error = gl.getProgramInfoLog(program);
console.log(error);
// 删除之前装配至上下文的程序和着色器
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
// 连接正常 返回program
return program;
}

3.3 定义初始化着色器方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function initShaders (gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('create program failed!');
return false;
}

// 程序创建正常
gl.useProgram(program);
// 定义在上下文属性
gl.program = program;

return true;
}

initShaders(gl, shaderPointer, shaderFrag);

// 指定清空画布的颜色
gl.clearColor(0.0, 0.0, 0.4, 1);
// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT);
  1. 顶点着色器的定义

在这里,demo想要展示一个平面的画布并在上面进行webgl像素渲染。这时候需要通过顶点着色器去构造一个画布,即为一个矩形。

这里设置顶点着色器的代码非常简单,至于代码详细大家可以参考glsl的语法。

1
2
3
4
5
6
const shaderPointer = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}`;

这里设置了顶点着色器的顶点坐标和尺寸,然后这里的a_Position只是一个定义了一个vec4类型的参数。下面需要通过js代码将对应的数据传递进去。

因为在这之前,我们执行了initShader方法,从而让我们可以轻松的获取到glsl里相关的顶点属性参数。

const aPosition = gl.getAttribLocation(gl.program, ‘a_Position’);

但是我们需要的是一个矩形图像,而不是一个点,这样一来我们需要传递4个坐标参数来实现。
所以我们需要引入一个缓冲区对象的概念来进行参数传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建缓冲区对象
let vertexBuff = gl.createBuffer();
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuff);
// 定义浮点类型数组存放四个坐标的数据
let vertices = new Float32Array([1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0]);
// 将数据写入缓冲区 (STATIC_DRAW 数据一次写入多次绘制)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 将缓冲区对象分配到attribute变量
gl.vertexAttribPointer(
aPosition,
2, // 2 代表着x, y, z三个参数只有x, y 起作用。只选取前两个分量
gl.FLOAT,
false,
0,
0
);

// 开启attribute变量让顶点着色器能够访问到缓冲区内的数据
gl.enableVertexAttribArray(aPosition);

// 从0号顶点开始绘制,绘制4个坐标形成矩形。
// TRIANGLE_FAN 是由v0, v1, v2 三角和v0, v2, v3三角组成的图形。在当前坐标下为一个矩形
// TRIANGLE_STRIP 则是由v0, v1, v2 与 v1, v2, v3组成的图形,在当前坐标系为一个旗帜飘带的形状

gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

如图:

alt text

alt text

在实践了如上的一些基本代码后,我们就可以创建出矩形图形。

  1. 片元着色器

在之前我们刚通过顶点shader定义了一个矩形。那么片元着色器解释起来可能比较复杂,大家可以理解为其在光栅化后很类似于像素,但又不是像素。
我们可以在顶点着色器所定义的多个三角形组合的图形中,进行特定的渲染效果。

这是一段片元着色器代码,可能比较复杂,主要是实现多种正弦,余弦的几何图像变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const shaderFrag = `
precision highp float;
uniform vec2 u_resolution;
uniform float time;
uniform float point[16];
const float PI = acos(-1.0);
const float TAU = PI * 2.0;
const float phi = sqrt(5.0) * 0.5 + 0.5;

const float goldenAngle = TAU / phi / phi;

vec2 rotateAroundPoint(float x){
return vec2(sin(x), cos(x));
}

vec3 hsv2rgb(vec3 c)
{
const vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);

vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

vec3 calculateGoldenShape(vec2 p){
const int steps = 128;
const float rSteps = 1.0 / float(steps);

vec3 result = vec3(0.0);

for (int i = 0; i < steps; ++i)
{
float n = float(i);

float inc = n * rSteps;
vec2 offset = rotateAroundPoint(fract(-100.0*0.055)*6.28+n * goldenAngle*sin(abs(point[i * 4]*0.0025))) * inc * abs(point[i * 4]/340.0);

vec3 dist = vec3(distance(p, offset));
dist = exp2(-dist * 128.0) * hsv2rgb(vec3(fract(time*0.2)+inc*0.75, 1.0, 1.0));

result = max(result, dist);
}

return result;
}

void main( void ) {

vec2 position = (gl_FragCoord.xy / u_resolution.xy - 0.5) * vec2(u_resolution.x/ u_resolution.y, 1.0);

vec3 color = vec3(0.0);
color += sin(calculateGoldenShape(position) * 3.0);
color = pow(color, vec3(2.2));
color /= sin(color + 1.0);
color = pow(color, vec3(1.0 / 2.2));


gl_FragColor = vec4(color, 1.0 );

}
`;

同时我们通过js获取相关参数并传递数据:

1
2
3
4
5
6
7
// 设置片元渲染的范围,数值可以使用像素点数量。
const resolutionUniformLocation = gl.getUniformLocation(gl.program, "u_resolution");
gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
// 设置时间间隔线性变化
const time = gl.getUniformLocation(gl.program, 'time');
// 设置多个渲染点的数据变化(接入音频数据)
let point = gl.getUniformLocation(gl.program, 'point');
  1. 获取音频数据并刷新帧时更新渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function draw () {
if (t > 100) {
t = 0.0;
} else {
t += 0.02;
}
if (tt > 3) {
analyserNode.getFloatFrequencyData(dataArray);
gl.uniform1fv(point, dataArray);
gl.uniform1f(time, t);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
tt = 0;
} else {
tt += 1;
}
requestAnimationFrame(draw);
}

最后我们一起看看效果吧!!!

最好在chrome上看效果

因为不同机型的webgl支持不同,支持最大uniform的数目也不相同。可能会出现too many uniform的报错。
实测pc和ios7p以上设备都可以正常运行
uniform的相关报错问题会在之后的研究中去解决。

钱酷尼

动效实验记录

6 日志
1 标签
© 2019 钱酷尼
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4