我如何构建SiriWaveJS库:看一下数学和代码

by Flavio De Stefano

由弗拉维奥·德·斯特凡诺(Flavio De Stefano)

我如何构建SiriWaveJS库:看一下数学和代码 (How I built the SiriWaveJS library: a look at the math and the code)

It was 4 years ago when I had the idea to replicate the Apple® Siri wave-form (introduced with the iPhone 4S) in the browser using pure Javascript.

4年前,我想到了使用纯Javascript在浏览器中复制Apple®Siri 波形 (随iPhone 4S引入)的想法。

During the last month, I updated this library by doing a lot of refactoring using ES6 features and reviewed the build process using RollupJS. Now I’ve decided to share what I've learned during this process and the math behind this library.

在上个月,我通过使用ES6功能进行了大量重构来更新了该库,并使用RollupJS回顾了构建过程 现在,我决定分享在此过程中所学到的知识以及该库背后的数学知识。

To get an idea what the output will be, visit the live example; the whole codebase is here.

要了解输出结果,请访问在线示例 整个代码库在这里

Additionally, you can download all plots drawn in this article in GCX (OSX Grapher format): default.gcx and ios9.gcx.

此外,您可以以GCX(OSX Grapher格式)下载本文中绘制的所有图: default.gcxios9.gcx

经典波浪风格 (The classic wave style)

Initially, this library only had the classic wave-form style that all of you remember using in iOS 7 and iOS 8.

最初,此库仅具有大家都记得在iOS 7和iOS 8中使用的经典波形样式。

It’s no hard task to replicate this simple wave-form, only a bit of math and basic concepts of the Canvas API.

复制这种简单的波形并不是一件容易的事,只需复制一点画布和Canvas API的基本概念即可。

You’re probably thinking that the wave-form is a modification of the Sine math equation, and you're right…well, almost right.

您可能会认为该波形是Sine数学方程式的一种修改,并且您是正确的…嗯,几乎是正确的。

Before starting to code, we’ve got to find our linear equation that will be simply applied afterwards. My favourite plot editor is Grapher; you can find it in any OSX installation under Applications > Utilities > Grapher.app.

在开始编码之前,我们必须找到我们的线性方程式,然后将其简单应用。 我最喜欢的绘图编辑器是Grapher; 您可以在任何OSX安装中的“ 应用程序”>“实用程序”>“ Graphe r.app”下找到它。

We start by drawing the well known:

我们首先绘制众所周知的:

Perfecto! Now, let’s add some parameters (Amplitude [A], Time coordinate[t] and Spatial frequency [k]) that will be useful later (Read more here: https://en.wikipedia.org/wiki/Wave).

完美! 现在,让我们添加一些参数(振幅[A] ,时间坐标[t]和空间频率[k] ),这些参数以后将有用(在此处了解更多信息: https : //en.wikipedia.org/wiki/Wave )。

Now we have to “attenuate” this function on plot boundaries, so that for |x| >; 2, the y values tends to 0. Let’s draw separately an equation g(x) that has these characteristics.

现在我们必须在图边界上“衰减”此函数,以便| x | > ; 2,T y值趋向于0。让我们画单独地对gequati(X),其具有这些特性。

This seems to be a good equation to start with. Let’s add some parameters here too to smooth the curve for our purposes:

首先,这似乎是一个很好的方程式。 让我们在这里也添加一些参数来平滑曲线以达到我们的目的:

Now, by multiplying our f(x, …) and g(x, …), and by setting precise parameters to the other static values, we obtain something like this.

现在,通过将f(x,…)g(x,…)相乘并为其他静态值设置精确的参数,我们得到了类似的结果。

  • A = 0.9 set the amplitude of the wave to max Y = A

    A = 0.9将波幅设置为最大Y = A

  • k = 8 set the spatial frequency and we obtain “more peaks” in the range [-2, 2]

    k = 8设置空间频率,我们在[-2,2]范围内获得“更多峰”

  • t = -π/2 set the phase translation so that f(0, …) = 1

    t =-π/ 2设置相位转换,以便f(0,…)= 1

  • K = 4 set the factor for the “attenuation equation” so that the final equation is y = 0 when |x| ≥ 2

    K = 4设置“衰减方程”的因数,使得当| x |时,最终方程为y = 0 ≥2

It looks good! ?

看上去不错! ?

Now, if you notice on the original wave we have other sub-waves that will give a lower value for the amplitude. Let’s draw them for A = {0.8, 0.6, 0.4, 0.2, -0.2, -0.4, -0.6, -0.8}

现在,如果您在原始波上注意到,我们还有其他子波将为振幅提供较低的值。 让我们为A = {0.8,0.6,0.4,0.2,-0.2,-0.4,-0.6,-0.8}绘制它们

In the final canvas composition the sub-waves will be drawn with a decreasing opacity tending to 0.

在最终的画布合成中,将以不透明性趋于0的方式绘制子波。

基本代码概念 (Basic code concepts)

What do we do now with this equation?

我们现在用这个方程式做什么?

We use the equation to obtain the Y value for an input X.

我们使用等式获得输入XY值

Basically, by using a simple for loop from -2 to 2, (the plot boundaries in this case), we have to draw point by point the equation on the canvas using the beginPath and lineTo API.

基本上,通过使用从-2到2的简单for循环 ( 在这种情况下绘图边界) ,我们必须使用beginPath在画布上逐点绘制方程式 lineTo API。

const ctx = canvas.getContext('2d');
ctx.beginPath();ctx.strokeStyle = 'white';
for (let i = -2; i <= 2; i += 0.01) {   const x = _xpos(i);   const y = _ypos(i);   ctx.lineTo(x, y);}
ctx.stroke();

Probably this pseudo-code will clear up these ideas. We still have to implement our _xpos and _ypos functions.

此伪代码可能会清除这些想法。 我们仍然必须实现_xpos_ypos函数。

But… hey, what is 0.01⁉️ That value represents how many pixels you move forward in each iteration before reaching the right plot boundary… but what is the correct value?

但是...嘿,什么是0.01⁉️这个值表示在到达正确的绘图边界之前,每次迭代中向前移动了多少个像素 ...但是正确的值是多少?

If you use a really small value (<0.01), you’ll get an insanely precise rendering of the graph but your performance will decrease because you’ll get too many iterations.

如果使用非常小的值( < 0。01),将获得非常精确的图形渲染,但是由于迭代次数过多,因此性能会下降。

Instead, if you use a really big value (> 0.1) your graph will lose precision and you’ll notice this instantly.

相反,如果您使用非常大的值( >0。1 ),则图形将失去精度,并且您会立即注意到这一点。

You can see that the final code is actually similar to the pseudo-code: https://github.com/kopiro/siriwave/blob/master/src/curve.js#L25

您可以看到最终代码实际上类似于伪代码: https : //github.com/kopiro/siriwave/blob/master/src/curve.js#L25

实现_xpos(i) (Implement _xpos(i))

You may argue that if we’re drawing the plot by incrementing the x, then _xpos may simply return the input argument.

您可能会争辩说,如果我们通过增加x来绘制图,则_xpos 可能只是返回输入参数。

This is almost correct, but our plot is always drawn from -B to B (B = Boundary = 2).

这几乎是正确的,但是我们的图总是从-B绘制到B (B =边界= 2)。

So, to draw on the canvas via pixel coordinates, we must translate -B to 0, and B to 1 (simple transposition of [-B, B] to [0,1]); then multiply [0,1] and the canvas width (w).

因此,要通过像素坐标在画布上绘制,我们必须将-B转换为0,将 B转换为1 ([-B,B]的简单换位为[0,1]); 然后将[0,1]乘以画布宽度(w)。

_xpos(i) = w * [ (i + B) / 2B ]
_xpos(i)= w * [(i + B)/ 2B]

https://github.com/kopiro/siriwave/blob/master/src/curve.js#L19

https://github.com/kopiro/siriwave/blob/master/src/curve.js#L19

实施_ypos (Implement _ypos)

To implement _ypos, we should simply write our equation obtained before (closely).

为了实现_ypos ,我们应该简单地编写之前(接近)获得的方程。

const K = 4;const FREQ = 6;
function _attFn(x) {   return Math.pow(K / (K + Math.pow(x, K)), K);}
function _ypos(i) {   return Math.sin(FREQ * i - phase) *       _attFn(i) *       canvasHeight *      globalAmplitude *       (1 / attenuation);}

Let’s clarify some parameters.

让我们澄清一些参数。

  • canvasHeight is Canvas height expressed in PX

    canvasHeight是以PX表示的画布高度

  • i is our input value (the x)

    是我们的输入值( x )

  • phase is the most important parameter, let’s discuss it later

    相位是最重要的参数,我们稍后再讨论

  • globalAmplitude is a static parameter that represents the amplitude of the total wave (composed by sub-waves)

    globalAmplitude是一个静态参数,代表总波(由子波组成)的幅度

  • attenuation is a static parameter that changes for each line and represents the amplitude of a wave

    衰减是每行变化的静态参数,代表波的幅度

https://github.com/kopiro/siriwave/blob/master/src/curve.js#L24

https://github.com/kopiro/siriwave/blob/master/src/curve.js#L24

(Phase)

Now let’s discuss about the phase variable: it is the first changing variable over time, because it simulates the wave movement.

现在让我们讨论相位变量:它是随时间变化第一个变量 ,因为它模拟了波的运动。

What does it mean? It means that for each animation frame, our base controller should increment this value. But to avoid this value throwing a buffer overflow, let’s modulo it with 2π (since Math.sin dominio is already modulo 2π).

这是什么意思? 这意味着对于每个动画帧,我们的基本控制器都应该增加该值。 但是,为避免此值引发缓冲区溢出,让我们使用2π对其取模(因为Math.sin dominio已经是2π模了)。

phase = (phase + (Math.PI / 2) * speed) % (2 * Math.PI);

We multiply speed and Math.PI so that with speed = 1 we have the maximum speed (why? because sin(0) = 0, sin(π/2) = 1, sin(π) = 0, … ?)

我们将速度Math.PI相乘,以使速度= 1时,我们得到最大速度(为什么?因为sin(0)= 0,sin(π/ 2)= 1,sin(π)= 0,…?)

定稿 (Finalizing)

Now that we have all code to draw a single line, we define a configuration array to draw all sub-waves, and then cycle over them.

现在我们有了所有的代码来绘制一条直线,我们定义了一个配置数组来绘制所有子波,然后循环遍历它们。

return [   { attenuation: -2, lineWidth: 1.0, opacity: 0.1 },   { attenuation: -6, lineWidth: 1.0, opacity: 0.2 },   { attenuation: 4, lineWidth: 1.0, opacity: 0.4 },   { attenuation: 2, lineWidth: 1.0, opacity: 0.6},
// basic line   { attenuation: 1, lineWidth: 1.5, opacity: 1.0},];

https://github.com/kopiro/siriwave/blob/master/src/siriwave.js#L190

https://github.com/kopiro/siriwave/blob/master/src/siriwave.js#L190

iOS 9+风格 (The iOS 9+ style)

Now things start to get complicated. The style introduced with iOS 9 is really complex and the reverse engineering to simulate it it’s not easy at all! I’m not fully satisfied of the final result, but I’ll continue to improve it until I get the desired result.

现在事情开始变得复杂。 iOS 9引入的样式真的很复杂,要模拟它的反向工程并不容易 ! 我对最终结果并不完全满意,但是我会继续改进它,直到获得期望的结果。

As previously done, let’s start to obtain the linear equations of the waves.

如上所述,让我们开始获得波浪的线性方程。

As you can notice:

如您所见:

  • we have three different specular equations with different colours (green, blue, red)

    我们有三个具有不同颜色( 绿色,蓝色,红色 )的镜面反射方程

  • a single wave seems to be a sum of sine equations with different parameters

    单个波似乎是具有不同参数 的正弦方程总和

  • all other colours are a composition of these three base colours

    所有其他颜色都是这三种基色的组合

  • there is a straight line at the plot boundaries

    地块边界处有一条直线

By picking again our previous equations, let’s define a more complex equation that involves translation. We start by defining again our attenuation equation:

通过再次选择以前的方程式,让我们定义一个涉及翻译的更复杂的方程式 我们从再次定义衰减方程开始:

Now, define h(x, A, k, t) function, that is the sine function multiplied for attenuation function, in its absolute value:

现在,定义h(x,A,k,t)函数,即正弦函数乘以衰减函数的绝对值:

We now have a powerful tool.

我们现在有了一个强大的工具。

With h(x), we can now create the final wave-form by summing different h(x) with different parameters involving different amplitudes, frequency and translations. For example, let’s define the red curve by putting random values.

使用h(x) ,我们现在可以通过将具有不同幅度,频率和平移的不同参数的不同h(x)相加来创建最终波形。 例如,让我们通过放置随机值来定义红色曲线

If we do the same with a green and blue curve, this is the result:

如果对绿色蓝色曲线执行相同操作,则结果如下:

This is not quite perfect, but it could work.

这不是很完美,但是可以工作。

To obtain the specular version, just multiply everything by -1.

要获得镜面反射版本,只需将所有内容乘以-1。

In the coding side, the approach is the same, we have only a more complex equation for _ypos.

在编码方面,方法是相同的,我们只有一个更复杂的_ypos方程

const K = 4;const NO_OF_CURVES = 3;
// This parameters should be generated randomlyconst widths = [ 0.4, 0.6, 0.3 ];const offsets = [ 1, 4, -3 ];const amplitudes = [ 0.5, 0.7, 0.2 ];const phases = [ 0, 0, 0 ];
function _globalAttFn(x) {   return Math.pow(K / (K + Math.pow(x, 2)), K);}
function _ypos(i) {   let y = 0;   for (let ci = 0; ci < NO_OF_CURVES; ci++) {      const t = offsets[ci];      const k = 1 / widths[ci];      const x = (i * k) - t;            y += Math.abs(         amplitudes[ci] *          Math.sin(x - phases[ci]) *          _globalAttFn(x)      );   }
y = y / NO_OF_CURVES;   return canvasHeightMax * globalAmplitude * y;}

There’s nothing complex here. The only thing that changed is that we cycle NO_OF_CURVES times over all pseudo-random parameters and we sum all y values.

这里没有什么复杂的。 唯一发生变化的是,我们对所有伪随机参数循环执行NO_OF_CURVES次,并对所有y值 求和

Before multiplying it for canvasHeightMax and globalAmplitude that give us the absolute PX coordinate of the canvas, we divide it for NO_OF_CURVES so that y is always ≤ 1.

在将其与给出画布绝对PX坐标的canvasHeightMaxglobalAmplitude相乘之前,我们将其除以NO_OF_CURVES,以使y始终≤1。

https://github.com/kopiro/siriwave/blob/master/src/ios9curve.js#L103

https://github.com/kopiro/siriwave/blob/master/src/ios9curve.js#L103

复合操作 (Composite operation)

One thing that actually matters here is the globalCompositeOperation mode to set in the Canvas. If you notice, in the original controller, when there’s a overlap of 2+ colors, they’re actually mixed in a standard way.

在这里真正重要的一件事是在Canvas中设置的globalCompositeOperation模式。 如果您注意到,在原始控制器中,当2种以上颜色重叠时,它们实际上是以标准方式混合的。

The default is set to source-over, but the result is poor, even with an opacity set.

默认设置为source-over ,但即使设置了不透明性,效果也很差。

You can see all examples of vary globalCompositeOperation here: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation

您可以在此处查看各种globalCompositeOperation的所有示例: https : //developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation

By setting globalCompositeOperation to “ligther”, you notice that the intersection of the colours is nearest to the original.

通过设置globalCompositeOperation“ligther”,您将发现颜色的交集是最接近原始的。

使用RollupJS进行构建 (Build with RollupJS)

Before refactoring everything, I wasn’t satisfied at all with the codebase: old prototype-like classes, a single Javascript file for everything, no uglify/minify and no build at all.

在重构所有内容之前,我对代码库一点都不满意:像原型的旧类,一个包含所有内容的Javascript文件,没有uglify / minify,也没有构建。

Using the new ES6 feature like native classes, spread operators and lambda functions, I was able to clean everything, split files, and decrease lines of unnecessary code.

使用本机类,扩展运算符lambda函数等新的ES6 功能 ,我能够清理所有内容,拆分文件并减少不必要的代码行。

Furthermore, I used RollupJS to create a transpiled and minified build in various formats.

此外,我使用RollupJS创建了各种格式的经过编译的最小化版本。

Since this is a browser-only library, I decided to create two builds: an UMD (Universal Module Definition) build that you can use directly by importing the script or by using CDN, and another one as an ESM module.

由于这是一个仅用于浏览器的库,因此我决定创建两个构建:一个UMD(通用模块定义)构建,您可以通过导入脚本或使用CDN直接使用它,另一个构建为ESM模块。

The UMD module is built with this configuration:

UMD模块是使用以下配置构建的:

{   input: 'src/siriwave.js',   output: {      file: pkg.unpkg,      name: pkg.amdName,      format: 'umd'    },    plugins: [       resolve(),       commonjs(),       babel({ exclude: 'node_modules/**' }),    ]}

An additional minified UMD module is built with this configuration:

使用此配置还可以构建另一个缩小的UMD模块

{   input: 'src/siriwave.js',   output: {      file: pkg.unpkg.replace('.js', '.min.js'),      name: pkg.amdName,      format: 'umd'    },    plugins: [       resolve(),       commonjs(),       babel({ exclude: 'node_modules/**' }),       uglify()]}

Benefiting of UnPKG service, you can find the final build on this URL served by a CDN: https://unpkg.com/siriwave/dist/siriwave.min.js

受益于UnPKG服务,您可以在CDN服务的该URL上找到最终版本: https ://unpkg.com/siriwave/dist/siriwave.min.js

This is the “old style Javascript way” — you can just import your script and then refer in your code by using SiriWave global object.

这是“旧样式Javascript方式”-您可以导入脚本,然后使用SiriWave全局对象在代码中进行引用

To provide a more elegant and modern way, I also built an ESM module with this configuration:

为了提供一种更优雅和现代的方式,我还使用以下配置构建了一个ESM模块:

{    input: ‘src/siriwave.js’,   output: {       file: pkg.module,       format: ‘esm’   },    plugins: [       babel({ exclude: ‘node_modules/**’ })   ]}

We clearly don’t want the resolve or commonjs RollupJS plugins because the developer transplier will resolve dependencies for us.

我们显然不希望使用resolvecommonjs RollupJS插件,因为开发者转换器将为我们解决依赖关系。

You can find the final RollupJS configuration here: https://github.com/kopiro/siriwave/blob/master/rollup.config.js

您可以在这里找到最终的RollupJS配置: https : //github.com/kopiro/siriwave/blob/master/rollup.config.js

观看并重新加载热代码 (Watch and Hot code reload)

Using RollupJS, you can also take advantage of rollup-plugin-livereload and rollup-plugin-serve plugins to provide a better way to work on scripts.

使用RollupJS,您还可以利用rollup-plugin-livereloadrollup-plugin-serve插件提供更好的脚本处理方式。

Basically, you just add these plugins when you’re in “developer” mode:

基本上,您只是在“开发人员”模式下添加以下插件:

import livereload from 'rollup-plugin-livereload';import serve from 'rollup-plugin-serve';
if (process.env.NODE_ENV !== 'production') { additional_plugins.push(  serve({   open: true,   contentBase: '.'  }) ); additional_plugins.push(  livereload({   watch: 'dist'  }) );}

We finish by adding these lines into the package.json:

我们将这些行添加到package.json中来完成:

"module": "dist/siriwave.m.js","jsnext:main": "dist/siriwave.m.js","unpkg": "dist/siriwave.js","amdName": "SiriWave","scripts": {   "build": "NODE_ENV=production rollup -c",   "dev": "rollup -c -w"},

Let’s clarify some parameters:

让我们澄清一些参数:

  • module / jsnext:main: path of dist ESM module

    module / jsnext:main: dist ESM模块的路径

  • unpkg: path of dist UMD module

    unpkg: dist UMD模块的路径

  • amdName: name of the global object in UMD module

    amdName: UMD模块中全局对象的名称

Thanks a lot RollupJS!

非常感谢RollupJS!

Hope that you find this article interesting, see you soon! ?

希望您觉得这篇文章有趣,很快再见! ?

翻译自: https://www.freecodecamp.org/news/how-i-built-siriwavejs-library-maths-and-code-behind-6971497ae5c1/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值