笔记 – ntzyz's blog https://archive.ntzyz.io Mon, 18 Sep 2017 12:11:28 +0000 zh-CN hourly 1 https://wordpress.org/?v=5.8 网易云音乐 Linux 版高分屏问题解决方案 https://archive.ntzyz.io/2017/09/07/fix-cloud-music-linux-client-hidpi-issue/ https://archive.ntzyz.io/2017/09/07/fix-cloud-music-linux-client-hidpi-issue/#respond Thu, 07 Sep 2017 05:54:42 +0000 https://ntzyz.io/?p=861 继续阅读网易云音乐 Linux 版高分屏问题解决方案]]> .emm > p { text-indent: 2em; }

网易云音乐 Linux 版推出很久了,肯定已经有很多人反馈过 HiDPI 下不能正常工作的问题了,然而似乎网易生娃不养娃,版本号 1.0.0 从一开始就没有变过(

在网上搜索 HiDPI 下字太小难以阅读就能找到最简单的解决方案:添加参数 `–force-device-scale-factor=2` 就能让他强制两倍渲染。不过这个方法在我这儿(ArchLinux,KDE)上并不是什么好的解决方案:启动程序时,程序按照环境 DPI 创建了窗体,但是内容部分则依然使用原分辨率渲染,效果就是只有坐上 1/4 能正常显示内容,剩下的都是白色的空白。只要我的全局 DPI 不是 100%,这些空白就一直存在。

既然网易支持强制缩放倍数,只要我让他认为自己运行的环境是 100% DPI 即可,解决方案如下:

QT_SCREEN_SCALE_FACTORS=1 /usr/bin/netease-cloud-music --force-device-scale-factor=2

为了方便使用,我们可以在 `/usr/local/bin/` 下创建同名脚本,执行后就可以直接 pin 到任务栏了。

]]>
https://archive.ntzyz.io/2017/09/07/fix-cloud-music-linux-client-hidpi-issue/feed/ 0
单页应用下实现动态的脚本加载 https://archive.ntzyz.io/2017/08/13/dynamically-load-scripts-on-sap-page/ https://archive.ntzyz.io/2017/08/13/dynamically-load-scripts-on-sap-page/#respond Sat, 12 Aug 2017 16:33:23 +0000 https://ntzyz.io/?p=836 继续阅读单页应用下实现动态的脚本加载]]> p.indent { text-indent: 2em; }

之前在写自己的博客框架的时候遇到了一个问题:文章中的 `script` 标签没有任何作用,以 Vue 为 MVVM 框架举个例子:


  


这段代码的意思很简单,创建一个 ID 为 app 的 DIV,其中包含一个 p 标签,vm 实例化之后,p 的内容就是 data 字段的 html,渲染结束后效果应该是这样:


  

好吧……看起来这代码很不规矩,但是浏览器应该是接受在任意位置出现的 script 标签的,也就是按照预期,我们会在控制台里看到 JavaScript is awesome 这句话,没毛病。

然而实际结果是……并没有。原因很简单,v-html 是通过 innerHTML 来实现的,而 HTML5 的标准规定了通过 innerHTML 得到的 script 标签不会被执行。此举的目的是为了防止利用这个特性来进行 XSS 攻击,但是依然有不少场景需要使用这些脚本,所以我们需要一些 trick 来让浏览器加载这些脚本。与此同时,如果 script 是通过 appendChild 添加到 DOM,这段脚本是能被正常执行的。

所以解决方案也就很简单了:找到需要执行的 script 标签,然后通过 createElement 创建空 script,并将待执行标签的数据复制到空 script 中(比如 src,或者 innerHTML),最后通过 appendChild 将它添加到 DOM 中去。

其中,找到 script 标签的方法比较多,比如可以通过正则对字符串进行匹配。不过考虑到通过 innerHTML 设置的 script 标签仅仅只是没有执行,他们仍然在 DOM 树中,所以 querySelector 在鲁棒性(划掉)上就比正则高出一截,代码如下:


  

这样,我们就能顺利地在控制台里看到输出的字符串啦(

当然这样做也有他的缺点:

  1. 使用了全局的 JavaScript 环境。如果代码中出现了一些声明(比如用 let 定义了变量),或者是通过 new Audio() 之类的方式播放了音频,即使你在适当的时候将他们从 DOM 树中移除,他们产生的影响仍然将保留在当前的上下文中。我个人的解决方案是,使用一个 flag 来记录是否添加过 script,若添加过,则后续的路由操作将在 beforeEach 的钩子处被劫持,并将 fullPath 直接应用到 window.location.href 上,强制刷新当前页面,来获得一个全新的 JavaScript 上下文。
  2. 没有充分模拟 script 标签的特性。按照正常的网页加载模式,浏览器在加载和执行 script 标签时是阻塞的,顺序执行的。通过这种方式添加的 script 执行顺序并没有任何明确的规定,可能导致出现依赖上的问题。
]]>
https://archive.ntzyz.io/2017/08/13/dynamically-load-scripts-on-sap-page/feed/ 0
[HTML] AudioContext 折腾笔记 02 https://archive.ntzyz.io/2017/01/11/html-audiocontext-01-2/ https://archive.ntzyz.io/2017/01/11/html-audiocontext-01-2/#respond Wed, 11 Jan 2017 15:53:00 +0000 https://ntzyz.io/?p=763 继续阅读[HTML] AudioContext 折腾笔记 02]]> p.indent { text-indent: 2em; }

之前我们已经看到了如何利用 HTML 的 WebAudio API 播放一段自定义的音频,其实难度并不大,主要步骤就是创建音频上下文,创建缓冲区和音频源,之后在缓存区内暴力写入波形数据,就可以播放了。

有了这些功能,理论上我们是可以在浏览器里使用 JavaScript 解码任意的音频文件并播放的,然而实际上 JavaScript 本身算是个半残废语言,而且既然能用 HTMLAudio 标签来播放音频,为什么要自己解码呢(

不过话说回来,不是所有代码都是写了拿去用的,有的时候折腾折腾一些比较奇怪的东西也非常有趣。想到世界上有一个叫 WAV(audio/x-wave) 的音频格式,其结构听说也是非常的简单粗暴,在解码上基本没有什么复杂度。于是为什么不尝试着自己用 JavaScript 把 wav 音频通过音频上下文接口播放出来呢?

音频文件的加载和分析

这一段其实没什么好说的,走 XMLHttpRequest 请求一个 ArrayBuffer 基本就可以了,值得注意的是需要解决跨域问题(如果媒体文件和当前 URL 不在同一域下)。不过既然 Google Chrome 已经支持 Promise 了,就试试走 Promise 来封装一个 HTTP 客户端吧(x

  function $get(url) {
    return new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.responseType = 'arraybuffer';
      xhr.addEventListener('progress', (e) => {
        status.innerText = `Loading media ${(100 * e.loaded / e.total).toFixed(3)}% ...`;
      });
      xhr.onreadystatechange = () => {
        switch(xhr.readyState) {
        case xhr.DONE:
          resolve(xhr.response)
          break;
        default:
          break;
        }
      };
      xhr.send();
    });
  }

代码里的 status 是一个 span,用来实时显示 xhr 的加载进度的,毕竟 wav 这么大……

之后的代码里只要使用 $get(url).then(callback) 这样的形式就可以加载 wav 文件了。

wav 文件格式

嗯搞到文件之后就得开始解码了,在解码之前,必须先获得一些元数据例如采样率啊文件大小声道数什么的。

wav 格式本身是很简单的,首先得介绍一下 wav 文件里的整体描述单位:Chunk,其结构如下

struct wavChunk {
  unsigned char chunkName[4];
  unsigned int  chunkSize;
  unsigned char chunkData[];  // sizeof is chunkSize
}

知道了 Chunk 之后,剩下的就很好解释了:wav 文件本身就是一个名为 RIFF 的 Chunk,随后包含若干 Chunk 来表示一些元数据,比如 FMT Chunk 中描述了音频格式、声道数、采样率、采样位数,而 data Chunk 中就是音频的 PCM 数据。刚才提到的三个 Chunk 的结构都是固定的,剩下一些比如 LIST 之类的 Chunk 我们可以暂时先不管。

于是,一些分析前的准备:

      let rawFile = file;
      file = new Uint8Array(rawFile);

      // Reference for RIFF chunk and FMT chunk
      let RIFF = {
        RIFF:           { begin:    0, length:    4, type: 'CHAR'},
        ChunkSize:      { begin:    4, length:    4, type: 'INTEGER'},
        WAVE:           { begin:    8, length:    4, type: 'CHAR'},
      }
      let FMT = {
        fmt:            { begin:   12, length:    4, type: 'CHAR'},
        Subchunk1Size:  { begin:   16, length:    2, type: 'INTEGER'},
        AudioFormat:    { begin:   20, length:    2, type: 'INTEGER'},
        NumOfChan:      { begin:   22, length:    2, type: 'INTEGER'},
        SamplesPerSec:  { begin:   24, length:    4, type: 'INTEGER'},
        bytesPerSec:    { begin:   28, length:    4, type: 'INTEGER'},
        blockAlign:     { begin:   32, length:    2, type: 'INTEGER'},
        bitsPerSample:  { begin:   34, length:    2, type: 'INTEGER'},
      }
      
      file.subArray = (begin, length) => {
        return file.slice(begin, begin + length);
      };

      let readChar = (begin, length) => Array
        .from(file
          .subArray(begin, length))
        .map(ch => String.fromCharCode(ch))
        .join('');

      let readInt = (begin, length) => Array
        .from(file
          .subArray(begin, length))
        .reverse()
        .reduce((a, b) => a * 256 + b);

      let readChunk = (ref, save) => {
        for (let item in ref) {
          switch(ref[item].type) {
            case 'CHAR':
              save[item] = readChar(ref[item].begin, ref[item].length);
              break;
            case 'INTEGER':
              save[item] = readInt(ref[item].begin, ref[item].length);
              break;
          }
        }
      }

简单的描述一下吧,由于 JavaScript 这个破烂语言数据类型残缺,对于 uint32_t 类的数字我们需要自己准备办法读取,还好这是一个无符号整数,只要看下小端的定义就能轻松的将其转换为具体的数据。上面代码中的 readInt 就能完成这项需求。

接下来继续,开始读取 RIFF Chunk:

      // Read the RIFF chunk
      readChunk(RIFF, this);

这句没什么好说的,按照之前定义的 RIFF 变量,按格式读取数据。然后依次读取接下来的 Chunk,直到读到 data Chunk。其中,如果遇到 FMT Chunk 就需要按格式读取,剩下的就暂时先不管了吧。

      for (let offset = 12;;) {
        let chunkName = readChar(offset, 4); offset += 4;
        let chunkSize = readInt(offset, 4); offset += 4;
        this[chunkName] = {};
        this[chunkName].size = chunkSize;
        if (chunkName === 'fmt ') {
          // Read the RIFF chunk
          readChunk(FMT, this[chunkName]);
        }
        else {
          if (chunkName == 'data') {
            let type = `Int${this['fmt '].bitsPerSample}Array`;
            this[chunkName] = new window[type](rawFile.slice(offset, chunkSize))
            break;
          }
          // Read as raw
          this[chunkName]._rawData = file.subArray(offset, chunkSize);
        }
        offset += chunkSize;
      }

将以上部分放到 Wav 类的构造函数中去,构造完毕后,我们能得到这样的一个对象:

Wav {
 ChunkSize: 20045138,
 LIST: { ... },
 RIFF: 'RIFF',
 WAVE: 'WAVE',
 data: Int16Array[...],
 "fmt ": Object,
}

关键数据就在 fmt 和 data 里了:fmt 定义了如何去播放,data 则是待播放的内容。

初始化 AudioContext 和其他相关变量

这里还是比较简单的,将 fmt 区块里的数据拿出来就可以了,内容可以参考上一篇笔记:

      let ctx = this.ctx = new AudioContext();
      let framesCount = this.framesCount = wav.ChunkSize / (wav['fmt '].bitsPerSample / 8);

      let audioBuffer = this.audioBuffer = ctx.createBuffer(
        wav['fmt '].NumOfChan, 
        framesCount / 2, 
        wav['fmt '].SamplesPerSec
      );

      this.source = ctx.createBufferSource();
      this.source.loop = false;
      this.source.connect(ctx.destination);
      this.source.buffer = this.audioBuffer;

这里面少了一步:PCM 数据的填入。我是想着让音频一边播放一边填入的,可惜这样下来只有 Chrome 系浏览器能正常播放了)

边播放边填入本身是可以用 setInterval 实现,顺便趁机长试一波 ES6 的 yield 关键字:

    *step() {
      let ctx = this.ctx;
      let channelBuffering = [];
      for (let i = 0; i < wav['fmt '].NumOfChan; ++i) {
        channelBuffering[i] = this.audioBuffer.getChannelData(i);
      }

      for (let currentFrame = 0; currentFrame < this.framesCount; ++currentFrame) {
        channelBuffering[currentFrame % 2][Math.ceil(currentFrame / 2)]
          = Number(wav.data[currentFrame]) / (1 << (wav['fmt '].bitsPerSample - 1));

        if (!this.started) {
          setTimeout(() => {this.source.start()}, 10);
          this.started = true;
        }

        yield currentFrame;
      }
    }

将以上函数都怼到 class Wav 中后,只要在准备一个主函数就可以完成 wav 的解码和播放了。具体代码的话就不贴了,没什么好讨论的了(逃

最后附上一个运行效果:playground/wav-decode完整源代码,其中还加了点可视化的魔法,wav 的体积真的是槽点(

参考链接

  1. WAVE PCM soundfile format
  2. WAV – Wikipedia
]]>
https://archive.ntzyz.io/2017/01/11/html-audiocontext-01-2/feed/ 0
[HTML] AudioContext 折腾笔记 01 https://archive.ntzyz.io/2016/12/17/html-audiocontext-01/ https://archive.ntzyz.io/2016/12/17/html-audiocontext-01/#respond Sat, 17 Dec 2016 11:14:24 +0000 https://ntzyz.io/?p=753 继续阅读[HTML] AudioContext 折腾笔记 01]]> p.indent { text-indent: 2em; }

前天尝试了一波那个什么 MediaSourceExtension,结果发现那套API目前限制蛮大的,而且对我来说没什么帮助(audio/x-wav 完全不正常支持,audio/mpeg 也只能在 Chrome 上使用)于是只能放弃折腾了 QAQ

昨天突然想起之前写 nanoPlayer 的时候,使用了一个叫 Audio Context 的接口,nanoPlayer 用了这个 API 里的 createAnalyser 方法,来获得音频的频率数据,进而实现了一个频谱可视化功能。之前就注意到了这个接口中有个自定义 AudioBufferSource 的方法,可以指定若干 Float32Array 并交给浏览器播放,应该是蛮有意思的。

这里就实现一个可制定频率的正弦波音频吧,如果这个实现起来没有什么难度的话,就准备试试浏览器端解码 WAV 音频。

首先是 HTML,我们要准备一个文本框来获得指定的频率,两个按钮分别控制播放的开始与停止:



  
    
    
    
    
  

然后初始化若干变量:

let ctx = new AudioContext();
let frames = ctx.sampleRate;
let start = document.querySelector('button[start]');
let stop = document.querySelector('button[stop]');
let last = null;

其中 ctx.sampleRate 是 AudioContext 的采样率,这里直接将一秒钟的采样数作为帧数,产生一段持续时间为一秒的 AudioSourceBuffer 就足够了,还有 last 是用来保存上一次的 AudioSource,可以随时调用 stop 方法来终止播放。

然后就是为 start 按钮增加一个点击事件:根据文本框里的数值产生一段指定频率的音频采样:

start.onclick = (e) => {
  if (last) last.stop();

  let freq = document.querySelector('#freq').value;
  // 初始化一个单声道,采样率和 AudioCotnext 一致,持续时间为 1 sec 的 AudioBuffer
  let audioBuffer = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
  // 获得其中第一个声道的数据源,类型是 Float32Array
  let nowBuffering = audioBuffer.getChannelData(0);
  // 填充数据,取值范围是 [-1, 1],直接用正弦函数就行了
  for (let i = 0; i < frames; ++i) {
    nowBuffering[i] = Math.sin((Math.PI * 2 * freq) * (i / ctx.sampleRate));
  }
  // 初始化一个 AudioBufferSource,并将上面的 AudioBuffer 与之绑定,并输出到 AudioContext
  let source = ctx.createBufferSource();
  source.loop = true;
  source.buffer = audioBuffer;
  source.connect(ctx.destination);
  // 开始回放
  source.start();
  // 保存以备后续使用
  last = source;
}

当然还需要对停止按钮绑定一个事件,来停止回放:

stop.onclick = (e) => {
  if (last) last.stop();
  last = null;
};

以上就是所有代码啦,效果的话,我在这里直接放个demo就行:


]]>
https://archive.ntzyz.io/2016/12/17/html-audiocontext-01/feed/ 0
[HTML] MediaSource 折腾笔记 01 https://archive.ntzyz.io/2016/12/16/html-fuck-mediasource-01/ https://archive.ntzyz.io/2016/12/16/html-fuck-mediasource-01/#respond Thu, 15 Dec 2016 17:26:26 +0000 https://ntzyz.io/?p=735 继续阅读[HTML] MediaSource 折腾笔记 01]]> p.indent { text-indent: 2em; }

MediaSource 是 HTML5 中的一个实验性特性,用于给 `HTMLMediaElement` 对象提供数据源。这里的数据源通常是使用 `XMLHttpRequest` 获得的数据。在 XHR 取得数据后,我们可以使用 JavaScript 对数据进行一些操作,比如提取 ID3、修改封装类型等等。 某 bilibili 开源的 Flv.js 就是使用这个特性实现的在 HTML5 环境下播放 H264 + AAC 格式的 FLV 视频。

最近在写 `Nano Player` 的时候遇到了两个比较尴尬的问题:1、ID3 信息必须手动指定;2、网易云音乐上下载的 MP3 由于某些原因必须缓冲 2MB 才能播放(详见MP3缓冲2MB才开始播放的解决方法)。于是想到是不是可以用这个 MediaSource 来暴改 MP3 呢?我也不知道

不管怎样,要用这个 MSE 来解决问题,首先就是要用它来播放文件(如果获得的数据直接喂给 MSE 都不能播放那还讨论个什么鬼)

创建若干变量并初始化:

let mediaSource = new MediaSource();
let audio = new Audio();
let media = 'audio.mp3';

audio.src = URL.createObjectURL(mediaSource);
audio.controls = 'controls';

然后是对 `sourceopen` 事件添加监听。当 `sourceopen` 事件触发后,我们需要创建一个 `sourceBuffer`,随后使用 `XMLHttpRequest` 获得媒体文件的内容,并附加到这个 `sourceBuffer` 内。当以上工作完成后,调用 `play` 方法播放音频:

mediaSource.addEventListener('sourceopen', () => {
  let sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

  let xhr = new XMLHttpRequest();
  xhr.open('GET', media);
  xhr.responseType = 'arraybuffer';
  xhr.onreadystatechange = () => {
    if (xhr.readyState === xhr.DONE && xhr.status === 200) {
      sourceBuffer.addEventListener('updateend', () => {
        mediaSource.endOfStream();
        audio.play();
      });
      sourceBuffer.appendBuffer(xhr.response);
    }
  };
  xhr.send();
});

最后,为了方便检查,将 `audio` 添加到 `body` 中:

document.querySelector('body').appendChild(audio);

嗯,以上的这些代码就能实现一个本来只要三行代码就完成的工作了。具体效果可以看这里:MSE Playground 01

下面需要解决的问题是,为什么刚开始尝试使用的 M4A 格式无法正常播放,这个坑死人了我哪知道只能 MP3 啊浪费了两个多小时……


由于目前来看,MediaSource 的表现在各个浏览器中并不一致,同时支持的格式就目前而言还是十分有限,这个系列估计就这一篇了吧 23333(逃

参考链接:

  1. Media Source Extensions
  2. MediaSource – Web APIs | MDN
  3. Media Source Extensions for Audio
  4. HTML5 Media – types and codecs examples + canPlayType(type) test
  5. html5 video tag codecs attribute
  6. nickdesaulniers/netfix: Let’s build a Netflix
]]>
https://archive.ntzyz.io/2016/12/16/html-fuck-mediasource-01/feed/ 0
[LeetCode] 按类型的刷题总结(长期更新) https://archive.ntzyz.io/2016/10/19/leetcode-%e6%8c%89%e7%b1%bb%e5%9e%8b%e7%9a%84%e5%88%b7%e9%a2%98%e6%80%bb%e7%bb%93%ef%bc%88%e9%95%bf%e6%9c%9f%e6%9b%b4%e6%96%b0%ef%bc%89/ https://archive.ntzyz.io/2016/10/19/leetcode-%e6%8c%89%e7%b1%bb%e5%9e%8b%e7%9a%84%e5%88%b7%e9%a2%98%e6%80%bb%e7%bb%93%ef%bc%88%e9%95%bf%e6%9c%9f%e6%9b%b4%e6%96%b0%ef%bc%89/#respond Wed, 19 Oct 2016 02:47:36 +0000 https://ntzyz.io/?p=719 继续阅读[LeetCode] 按类型的刷题总结(长期更新)]]> 最近几天无聊,又去上 LeetCode 探智商下限了(划掉

数组类 Array:

1. 二分查找:

此类题整体较简单,而且在编码过程中不一定需要使用二分查找,用一些 hash based 的数据结构也是不错的选择。在 C++ 下借助 set, map 或是 binary_search + vector 都可以较为轻松的解决。

参考题目:
Two Sum
Two Sum II – Input array is sorted
3 Sum

2. 高精度计算:

此类题整体较简单,基本就是使用数组来进行超大规模的加减计算。

参考题目:
Plus One

3. 递归计算:

递归计算在我们接触 C 语言的时候就已经开始接触了,所以应该是不困难的。借助一维或是多维数组来缓存计算结果可以有效提升递归的执行效率,防止出现爆栈、或是递归太久这种坑爹的情景。

参考题目:
Pascal’s Triangle
Pascal’s Triangle II

4. 数组元素查找与删改:

目前遇到的不多,目前看到的有查找查重、去重、移动和删除,去重和统计出现次数方面使用 set/map 就基本摆平了;移动的话参考插入排序,依次前移或者后移;删除的话,直接将元素 swap 到数组尾部,然后重新标记数组大小就完成了。都还是蛮简单的。

参考题目:
Remove Duplicates from Sorted Array
Contains Duplicate
Contains Duplicate II
Majority Element

5. 最大子列和以及类似问题:

难易程度看题目,简单的问题 O(n) 的遍历就能解决,稍复杂的从动态规划入手,如果遇到其他类型的到时候再补充。

参考题目:
Maximum Subarray
Best Time to Buy and Sell Stock
Best Time to Buy and Sell Stock II

6. 排序:

考虑到现在各类语言都已经有自己的排序方法,直接排序的问题都不难。当然有些题目里排序作为关键的步骤然而想不到时……

参考题目:
Merge Sorted Array

6. 前缀、后缀和与运算律:

感觉没啥好说的这个……到时候在补充吧

参考题目:
Product of Array Except Self

7. 概率与统计相关:

比如 A -> B 有多少中走法啦,如何从样本容量超级大的样本中随机抽样啦这种,递《概率论与数理统计》(划掉

参考题目:
Unique Paths
Unique Paths II

8. 搜索:

比如给一个数组,一个起点,一些限制规则,让你找出一个最怎么怎么样的路径。这类题目熟练的 DFS, BFS 可破,不算难。对于部分题目,逆向搜索会有奇效。当然必须的标记(flag)和缓存结果也是必要的。

参考题目:
Minimum Path Sum

inf. 无归类:

一个简单的 O(n) 遍历:Container With Most Water
上一题的拓展,当然重新动规也是可以的:Trapping Rain Water

]]>
https://archive.ntzyz.io/2016/10/19/leetcode-%e6%8c%89%e7%b1%bb%e5%9e%8b%e7%9a%84%e5%88%b7%e9%a2%98%e6%80%bb%e7%bb%93%ef%bc%88%e9%95%bf%e6%9c%9f%e6%9b%b4%e6%96%b0%ef%bc%89/feed/ 0
[WebGL] 更加灵活的描点实现 https://archive.ntzyz.io/2016/04/10/more-flexible-way-to-draw-a-point/ https://archive.ntzyz.io/2016/04/10/more-flexible-way-to-draw-a-point/#respond Sat, 09 Apr 2016 16:23:47 +0000 https://blog.dimension.moe/?p=412 继续阅读[WebGL] 更加灵活的描点实现]]> p.indent {text-indent: 2em;margin-top: 0.75em; margin-bottom: 0.75em;}

之前的代码实现了描绘一个巨大的、正方形的、红色的点,但是,稍有常识的人都能看出,那个点的颜色、位置、大小,都是无法在运行时改变的,这就比较尴尬了……

毫无疑问,Web 前端的编程语言是 ECMAScript,然而顶点着色器和片元着色器的控制用的却是 GLSL ES。为了能动态的修改着色器代码,我们就需要在着色器代码里添加需要动态编辑的变量,并且通过已有的方法来通过 JavaScript 获取并修改他们的值。

与之前相同,我们需要一个具有 canvas 的网页,其 ID 为 webgl。此处 HTML 代码可以直接参考之前的。同样我们也需要获取 WebGL 上下文,这些代码也是和之前一致的:

	var canvas = document.getElementById('webgl');
	var gl = getWebGLContext(canvas);

	if (!gl) {
		console.log('Error on getting context of WebGL');
		return;
	}

我们需要在着色器代码中添加相应的变量。其中,顶点着色器需要用 attribute 修饰符,是一个四维向量,而片元着色器则是使用的 uniform 修饰符,同样也是一个四维向量。片元着色器里的第一行 precision mediump float; 指明了数据精度:

	var VSHADER_SOURCE =
		'attribute vec4 a_Position;\n' +
		'void main() {\n' +
		'    gl_Position = a_Position;\n' +
		'    gl_PointSize = 10.0;\n' +
		'}\n';
	var FSHADER_SOURCE =
		'precision mediump float;\n' + 
		'uniform vec4 u_FragColor;\n' + 
		'void main() {\n' +
		'    gl_FragColor = u_FragColor;\n' +
		'}\n';

初始化这两个着色器。其实这里的工作不是调用一个函数这么简单。initShaders 函数是我使用的参考书中的库提供的,稍微看了下代码,似乎涉及了 shader 的编译与链接,还有程序对象的生成等内容。按照书本的意思,这些细节都会在后面详细说明,这里我也不去关注他们了。

	if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
		console.log('Error on initialize shaders.');
		return;
	}

嗯……接着就是使用 WebGL 提供的 getAttribLocation 函数和 getUniformLocation 函数,来获得那两个变量的位置。当获得位置出现错误时,函数将返回一个负值:

	var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
	if (a_Position < 0) {
		console.log('Error on getting the storage location of a_Position');
		return;
	}
	var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
	if (u_FragColor < 0) {
		console.log('Error on getting the storage location of u_FragColor');
		return;
	}

这样下来,我们就可以直接的调用 gl.vertexAttrib3f 方法和 gl.uniform4f 方法,来动态的修改两个着色器的内容。为了体现出动态,做一个点到哪儿画到哪儿的页面,无疑是最简明的了。

因为涉及到了多个点的存储,我们需要新增加一个变量来保存已经单击过的点的位置,在 app.js 最开头增加一行:

	var points = new Array();

然后再写一个函数,来描绘 points 数组内的所有的点。其中,右上、左上、左下、右下的点的颜色依次为红、绿、蓝、白。

	function drawPoints() {
		gl.clearColor(0, 0 , 0, 1);
		gl.clear(gl.COLOR_BUFFER_BIT);
		for (var i = 0; i != points.length; ++i) {
			gl.vertexAttrib3f(a_Position, points[i].x, points[i].y, 0.0);
			if (points[i].x > 0 && points[i].y > 0)
				gl.uniform4f(u_FragColor, 1, 0, 0, 1);
			else if (points[i].x < 0 && points[i].y < 0)
				gl.uniform4f(u_FragColor, 0, 0, 1, 1);
			else if (points[i].x < 0)
				gl.uniform4f(u_FragColor, 0, 1, 0, 1);
			else
				gl.uniform4f(u_FragColor, 1, 1, 1, 1);
			gl.drawArrays(gl.POINT, 0, 1);
		}
	};

点的位置来源可以通过增加一个对 canvas 的 click 事件的监听,将获得的信息转换后 push 到 points 中。这里需要注意的就是 HTML 坐标与 GL 坐标的转换。说起来我还没说 WebGL 的默认坐标系呢(躺

想了想感觉还是没啥好说的,随手糊两张图应该就都能理解了……吧

名稱未設定 1

请叫我灵魂画师

然后就是代码了,理解了坐标系后转换应该也没啥难度了吧 pup

	canvas.onmousedown = function (e) => {
		var mx = e.clientX;
		var my = e.clientY;
		var rect = e.target.getBoundingClientRect();
		
		points.push({
			x: (2 * (mx - rect.left) - canvas.height) / canvas.height,
			y: (canvas.width - 2 * (my - rect.top)) / canvas.width
		});
		drawPoints();
	}

这样我们就做到了一个稍复杂但是更加灵活的描点网页了(

webgl2

]]>
https://archive.ntzyz.io/2016/04/10/more-flexible-way-to-draw-a-point/feed/ 0
[WebGL] 简单的 WebGL 描点实现 https://archive.ntzyz.io/2016/04/07/simple-point-drawing-program-in-webgl/ https://archive.ntzyz.io/2016/04/07/simple-point-drawing-program-in-webgl/#respond Thu, 07 Apr 2016 15:32:10 +0000 https://blog.dimension.moe/?p=391 继续阅读[WebGL] 简单的 WebGL 描点实现]]> p.indent {text-indent: 2em;margin-top: 0.75em; margin-bottom: 0.75em;}

相关库的地址:WebGLBook/lib at master

效果预览见最后。

注意:这只是个人的理解,不保证正确。如有错误欢迎指正!

首先准备具有一个画布(canvas)的页面,所有的 WebGL 程序都会通过这个 canvas 展现出来:



    
        
        Hello Canvas
    
    
        
            你可能是正版IE的受害者。
        
        
        
        
        
        
    

然后就是 app.js,即实现描点的相关代码。通过 DOM 操作,可以很容易地获得 canvas 的 DOM 节点,进而获得 WebGL 的上下文:

    var canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if (!gl) {
        console.log('Error on getting context of WebGL');
        return;
    }

初始化完毕后,分别准备顶点着色器和片元着色器的GLSL ES代码。值得注意的是,JavaScript 是一个弱类型的语言,但 GLSL ES 不是,所以不能手滑将 0.0 写成 0,否则会出现类型错误。

在每一行 GLSL ES 代码后添加换行符 ‘\n’ 后可以在发生错误时输出行号,降低调试难度。当然,不写也是完全可以的。

    var VSHADER_SOURCE =
        'void main() {' +
        '    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);' +
        '    gl_PointSize = 10.0;' +
        '}';
    var FSHADER_SOURCE =
        'void main() {' +
        '    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);' +
        '}';

继续初始化 shader:

    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('Error on initialize shaders.');
        return;
    }

设置画布默认空颜色,并清空画布:

    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

然后,描点:

    gl.drawArrays(gl.POINT, 0, 1);

简单说说我的理解吧。这些代码里获得 GL 上下文没啥好说的,主要的问题就在着色器(Shader)。此处一共出现了两个着色器:顶点着色器、片元着色器。描绘一个点所必须提供的参数就是点的位置与颜色,可以认为这里的顶点着色器指明了点的位置,而片元着色器则指明了其颜色。

顶点着色器代码 VSHADER_SOURCE 中有一个 main 函数,其中存在两个变量。其中 gl_Position 是一个四维向量,由四个浮点分量组成,是一个齐次坐标。其次坐标[x, y, z, w]等价于通常理解的[x/w, y/w, z/w]。使用齐次坐标进行三维处理可以获得更高的效率(?)。此处将 w 分量置为 1.0,即可将齐次坐标直接当成三位笛卡尔坐标使用。第二个变量 gl_PointSize,指明了点的大小。

片元着色器中只存在一个变量:gl_FragColor。它也是一个四维的矢量,其意义分别为红、绿、蓝与透明度的值。与常规的表示方法不同,GL 使用[0, 1]来表示单个颜色的强度,例如[0., 0., 0., 0.]表明了完全透明黑色,而[1., 1., 1., 1.]则表明完全不透明白色。

清空画布后,我们使用了 gl.drawArrays 来描点,其本质就是执行着色器代码。三个参数的意义分别为 描绘类型,从哪个点开始描绘,以及描绘的次数。当此函数被调用时,顶点着色器将会被执行 n 次,顶点着色器代码执行完毕后立即执行片元着色器。全部完成后,就可以看到灰色屏幕中的黑色画布里,描绘了一个红色的点。

顺便这哪里是点啊 这么大明明就是个正方形了嘛好伐)

效果预览~

QQ截图20160408000243

]]>
https://archive.ntzyz.io/2016/04/07/simple-point-drawing-program-in-webgl/feed/ 0