国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Canvas + WebGL中文藝術(shù)字渲染

baihe / 3001人閱讀

摘要:將所有關(guān)聯(lián)對(duì)合并即并查集的過程,得到每個(gè)連通域的唯一標(biāo)記。此時(shí)每個(gè)連通域輪廓可以看做是一個(gè)多邊形,此時(shí)可以用經(jīng)典算法將其剖分成若干個(gè)三角形。若值為則說明該像素處于當(dāng)前連通域中。二維數(shù)組,表示每個(gè)像素是否是圖像邊緣。

筆者另一篇文章 https://segmentfault.com/a/11... 講了基于Canvas的文本編輯器“簡(jiǎn)詩(shī)”的實(shí)現(xiàn),其中文字由WebGL渲染藝術(shù)效果,這篇文章主要講述由Canvas獲取字體數(shù)據(jù)、筆畫分割解析、以及由WebGL進(jìn)行效果渲染的過程。

導(dǎo)言

用canvas原生api可以很容易地繪制文字,但是原生api提供的文字效果美化功能十分有限。如果想要繪制除描邊、漸變這些常用效果以外的藝術(shù)字,又不用耗時(shí)耗力專門制作字體庫(kù)的話,利用WebGL進(jìn)行渲染是一種不錯(cuò)的選擇。

這篇文章主要講述如何利用canvas原生api獲取文字像素?cái)?shù)據(jù),并對(duì)其進(jìn)行筆畫分割、邊緣查找、法線計(jì)算等處理,最后將這些信息傳入著色器,實(shí)現(xiàn)基本的光照立體文字。

利用canvas原生api獲取文字像素信息的好處是,可以繪制任何瀏覽器支持的字體,而無(wú)需制作額外的字體文件;而缺陷是對(duì)一些高級(jí)需求(如筆畫分割)的數(shù)據(jù)處理,時(shí)間復(fù)雜度較高。但對(duì)于個(gè)人項(xiàng)目而言,這是做出自定義藝術(shù)字效果比較快捷的方法。

最后實(shí)現(xiàn)的效果:

本文的重點(diǎn)在于文字?jǐn)?shù)據(jù)的處理,所以只用了比較簡(jiǎn)單的渲染效果,但有了這些數(shù)據(jù),很容易設(shè)計(jì)出更為酷炫的文字藝術(shù)效果。

“簡(jiǎn)詩(shī)”編輯器源碼:https://github.com/moyuer1992...
預(yù)覽地址:https://moyuer1992.github.io/...

其中文字處理的核心代碼:https://github.com/moyuer1992...
WebGL渲染核心代碼:https://github.com/moyuer1992...

canvas 獲取字體像素

獲取文字像素信息是首要的步驟。

我們利用一個(gè)離屏canvas繪制基本文字。設(shè)字號(hào)為size,項(xiàng)目中設(shè)size=200,并設(shè)置canvas邊長(zhǎng)和字號(hào)相同。這里size設(shè)置越大,獲得的像素信息就更為精確,當(dāng)然代價(jià)就是耗時(shí)更長(zhǎng),如果追求速度的話,可以將size減小。

ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.font = size + "px " + (options.font || "隸書");
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, width / 2, height / 2);

獲取像素信息:

var imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
var data = imageData.data;

好了,data變量就是我們最終得到的像素?cái)?shù)據(jù)。現(xiàn)在我們來看一下data的數(shù)據(jù)結(jié)構(gòu):

可以看到,結(jié)果是一個(gè)長(zhǎng)度為200x200x4的數(shù)組。200x200的canvas總共40000像素,每個(gè)像素上的顏色由四個(gè)值來表示。由于使用黑色著色,前三位必然是0。第四位表示透明度,對(duì)于無(wú)顏色的像素,其值為0,對(duì)于有顏色的點(diǎn),其值為大于零。所以,我們?nèi)粢袛嘣撐淖衷诘趈行,i列上是否有值,只需判斷data[(j ctx.canvas.width + i) 4 + 3]是否大于零即可。

于是,我們可以寫出判斷某位置是否有顏色的函數(shù):

var hasPixel = function (j, i) {
  //第j行,第i列
  if (i < 0 || j < 0) {
    return false;
  }
  return !!data[(j * ctx.canvas.width + i) * 4 + 3];
};
筆畫分割

接下來,我們需要對(duì)文字筆畫進(jìn)行分割。這實(shí)際上是一個(gè)尋找連通域的過程:把該文字看成一個(gè)圖像,找到該圖像上所有連通的部分,每一個(gè)部分就是一個(gè)筆畫。

尋找連通域的思路參考這篇文章:

http://www.cnblogs.com/ronny/...

算法大致分為幾個(gè)步驟:

逐行掃描圖像,記錄每一行的連通段。

對(duì)每個(gè)連通段進(jìn)行標(biāo)號(hào)。對(duì)第一行,從1開始依次為連通段進(jìn)行標(biāo)號(hào)。若非首行,則判斷是否與上一行某個(gè)連通段連通,若是,則賦予該連通段的標(biāo)號(hào)。

若某連通段同時(shí)與上一行兩個(gè)連通段連通,則記錄該關(guān)聯(lián)對(duì)。

將所有關(guān)聯(lián)對(duì)合并(即并查集的過程),得到每個(gè)連通域的唯一標(biāo)記。

下面是核心代碼,關(guān)鍵變量定義如下:

g: width * height二維數(shù)組,表示每個(gè)像素屬于哪個(gè)連通域。值為0代表該像素不在文字上,為透明值。

e: width * height二維數(shù)組,表示每個(gè)像素是否是圖像邊緣。

markMap: 記錄關(guān)聯(lián)對(duì)。

cnt: 關(guān)聯(lián)對(duì)合并前的總標(biāo)記數(shù)量。

逐行掃描:

for (var j = 0; j < ctx.canvas.height; j += grid) {
  g.push([]);
  e.push([]);

  for (var i = 0; i < ctx.canvas.width; i += grid) {
    var value = 0;
    var isEdge = false;


    if (hasPixel(j, i)) {
      value = markPoint(j, i);
    }
    e[j][i] = isEdge;
    g[j][i] = value;
  }
}

進(jìn)行標(biāo)記:

var markPoint = function (j, i) {
  var value = 0;

  if (i > 0 && hasPixel(j, i - 1)) {
    //與左邊連通
    value = g[j][i - 1];
  } else {
    value = ++cnt;
  }

  if ( j > 0 && hasPixel(j - 1, i) && ( i === 0 || !hasPixel(j - 1, i - 1) ) ) {
    //與上連通 且 與左上不連通 (即首次和上一行連接)
    if (g[j - 1][i] !== value) {
      markMap.push([g[j - 1][i], value]);
    }
  }

  if ( !hasPixel(j, i - 1) ) {
    //行首
    if ( hasPixel(j - 1, i - 1) && g[j - 1][i - 1] !== value) {
      //與左上連通
      markMap.push([g[j - 1][i - 1], value]);
    }
  }

  if ( !hasPixel(j, i + 1) ) {
    //行尾
    if ( hasPixel(j - 1, i + 1) && g[j - 1][i + 1] !== value) {
      //與右上連通
      markMap.push([g[j - 1][i + 1], value]);
    }
  }

  return value;
};

至此,將整個(gè)圖像遍歷一遍,已經(jīng)完成了算法中1-3的步驟。接下來需要根據(jù)markMap中的關(guān)聯(lián)信息,將標(biāo)記歸類,最終形成的圖像,帶有相同標(biāo)記的像素在同一連通域中(即同一筆畫)。

將標(biāo)記關(guān)聯(lián)對(duì)分類,是一個(gè)并查集問題,核心代碼如下:

for (var i = 0; i < cnt; i++) {
  markArr[i] = i;
}

var findFather = function (n) {
  if (markArr[n] === n) {
    return n;
  } else {
    markArr[n] = findFather(markArr[n]);
    return markArr[n];
  }
}

for (i = 0; i < markMap.length; i++) {
  var a = markMap[i][0];
  var b = markMap[i][3];

  var f1 = findFather(a);
  var f2 = findFather(b);

  if (f1 !== f2) {
    markArr[f2] = f1;
  }
}

最終得到markArr數(shù)組,即記錄了每一個(gè)原標(biāo)記號(hào)對(duì)應(yīng)的最終類別標(biāo)記。
打個(gè)比方:設(shè)上一步中標(biāo)記完成的圖像數(shù)組為g;假如markArr[3] = 1,mark[5] = 1, 則表示g中所有值為3、以及值為5的像素,最終都屬于一個(gè)連通域,這個(gè)連通域標(biāo)記為1。
根據(jù)markArr數(shù)組對(duì)g進(jìn)行處理,我們可以得到最終的連通域分割數(shù)據(jù)。

文字輪廓查找

得到分割后的圖像數(shù)據(jù)后,我們可以gl.POINTS的形式利用WebGL進(jìn)行渲染,且可以對(duì)不同筆畫設(shè)定不同的顏色。但這并不滿足我們的需要。我們希望將文字渲染成一個(gè)三維立體的模型,這就意味著我們要將二維的點(diǎn)陣轉(zhuǎn)化成三維圖形。

假設(shè)該文字有n個(gè)筆畫,那么現(xiàn)在我們擁有的數(shù)據(jù)可以看成n塊連通的點(diǎn)陣。首先,我們要將這n塊文字點(diǎn)陣轉(zhuǎn)換成n個(gè)二維平面圖形。在WebGL中,所有的面都必須由三角形組成。這就意味著我們要將一塊點(diǎn)陣轉(zhuǎn)換成一組毗鄰的三角形。

可能大家想到的第一個(gè)思路就是將每三個(gè)相鄰像素連接構(gòu)成三角形,這確實(shí)是一種辦法,但由于像素過多,這種方式耗時(shí)很長(zhǎng),并不推薦。

我們解決這個(gè)問題的思路是:

找到每個(gè)筆畫(即每塊連通域)的輪廓,并按順時(shí)針順序存儲(chǔ)在數(shù)組中。

此時(shí)每個(gè)連通域輪廓可以看做是一個(gè)多邊形,此時(shí)可以用經(jīng)典triangulation算法將其剖分成若干個(gè)三角形。

輪廓查找的算法同樣可以參考這篇文章:

http://www.cnblogs.com/ronny/...

大致思路是首先找到第一個(gè)上方為空像素的點(diǎn)作為外輪廓起始點(diǎn),記錄入口方向?yàn)?(正上方),沿著順時(shí)針方向?qū)ふ蚁乱粋€(gè)連接像素,并記錄入口方向,以此類推,直到終點(diǎn)與起始點(diǎn)重合。

接下來需要判斷是否存在鏤空,所以需要尋找內(nèi)輪廓點(diǎn),尋找第一個(gè)下方為空像素且不在任何輪廓上的點(diǎn),作為該內(nèi)輪廓起始點(diǎn),記錄入口為2(正下方),接下來步驟與尋找外輪廓相同。
注意圖像可能不只有一個(gè)內(nèi)輪廓,所以這里需要循環(huán)判斷。若不存在這樣的像素,則無(wú)內(nèi)輪廓。

通過前面的數(shù)據(jù)處理,我們可以很容易判斷某個(gè)像素是否處于輪廓之上:只要判斷是否四周都存在非空像素即可。但關(guān)鍵問題在于,三角化算法需要“多邊形”的頂點(diǎn)按順序排列。這樣一來,實(shí)際上核心邏輯在于如何按順時(shí)針為輪廓像素排序。

對(duì)單個(gè)連通域進(jìn)行輪廓順序查找的方法如下:

變量定義:

v: 當(dāng)前連通域標(biāo)記號(hào)

g: width * height二維數(shù)組,表示每個(gè)像素屬于哪個(gè)連通域。值為0代表該像素不在文字上,為透明值。若值為v則說明該像素處于當(dāng)前連通域中。

e: width * height二維數(shù)組,表示每個(gè)像素是否是圖像邊緣。

entryRecord: 入口方向標(biāo)記數(shù)組

rs: 最終輪廓結(jié)果

holes: 若有內(nèi)輪廓,則為內(nèi)輪廓起始點(diǎn)(內(nèi)輪廓點(diǎn)在數(shù)組最后面,若有多個(gè)內(nèi)輪廓,則只需記錄內(nèi)輪廓起始位置即可,這樣做是為了適應(yīng)triangulation庫(kù)earcut的參數(shù)設(shè)置,稍后會(huì)講到)

代碼:

function orderEdge (g, e, v, gap) {
  v++;
  var rs = [];
  var entryRecord = [];
  var start = findOuterContourEntry(g, v);
  var next = start;
  var end = false;
  rs.push(start);
  entryRecord.push(6);
  var holes = [];
  var mark;
  var holeMark = 2;
  e[start[1]][start[0]] = holeMark;

  var process = function (i, j) {
    if (i < 0 || i >= g[0].length || j < 0 || j >= g.length) {
      return false;
    }

    if (g[j][i] !== v || tmp) {
      return false;
    }

    e[j][i] = holeMark;
    tmp = [i, j]
    rs.push(tmp);
    mark = true;

    return true;
  }

  var map = [
    (i,j) => {return {"i": i + 1, "j": j}},
    (i,j) => {return {"i": i + 1, "j": j + 1}},
    (i,j) => {return {"i": i, "j": j +1}},
    (i,j) => {return {"i": i - 1, "j": j + 1}},
    (i,j) => {return {"i": i - 1, "j": j}},
    (i,j) => {return {"i": i - 1, "j": j - 1}},
    (i,j) => {return {"i": i, "j": j - 1}},
    (i,j) => {return {"i": i + 1, "j": j - 1}},
  ];

  var convertEntry = function (index) {
    var arr = [4, 5, 6, 7, 0, 1, 2, 3];
    return arr[index];
  }

  while (!end) {
    var i = next[0];
    var j = next[1];
    var tmp = null;
    var entryIndex = entryRecord[entryRecord.length - 1];

    for (var c = 0; c < 8; c++) {
      var index = ((entryIndex + 1) + c) % 8;
      var hasNext = process(map[index](i, j).i, map[index](i, j).j);
      if (hasNext) {
        entryIndex = convertEntry(index);
        break;
      }
    }

    if (tmp) {
      next = tmp;

      if ((next[0] === start[0]) && (next[1] === start[1])) {
        var innerEntry = findInnerContourEntry(g, v, e);
        if (innerEntry) {
          next = start = innerEntry;
          e[start[1]][start[0]] = holeMark;
          rs.push(next);
          entryRecord.push(entryIndex);
          entryIndex = 2;
          holes.push(rs.length - 1);
          holeMark++;
        } else {
          end = true;
        }
      }
    } else {
      rs.splice(rs.length - 1, 1);
      entryIndex = convertEntry(entryRecord.splice(entryRecord.length - 1, 1)[0]);
      next = rs[rs.length - 1];
    }

    entryRecord.push(entryIndex);
  }
  return [rs, holes];
}
function findOuterContourEntry (g, v) {
  var start = [-1, -1];
  for (var j = 0; j < g.length; j++) {
    for (var i = 0; i < g[0].length; i++) {
      if (g[j][i] === v) {
        start = [i, j];
        return start;
      }
    }
  }
  return start;
}
function findInnerContourEntry (g, v, e) {
  var start = false;
  for (var j = 0; j < g.length; j++) {
    for (var i = 0; i < g[0].length; i++) {
      if (g[j][i] === v && (g[j + 1] && g[j + 1][i] === 0)) {
        var isInContours = false;
        if (typeof(e[j][i]) === "number") {
          isInContours = true;
        }
        if (!isInContours) {
          start = [i, j];
          return start;
        }
      }
    }
  }
  return start;
}

為了特別檢查內(nèi)輪廓的查找,我們找一個(gè)擁有環(huán)狀連通域的文字測(cè)試一下:

看到一切ok,那么這一步就大功告成了。

triangulation構(gòu)造平面

對(duì)于triangulation的過程,我們用開源庫(kù)earcut進(jìn)行處理。earcut項(xiàng)目地址:

https://github.com/mapbox/earcut

利用earcut計(jì)算出三角形數(shù)組:

var triangles = earcut(flatten(points), holes);

對(duì)于每一個(gè)三角形,進(jìn)入著色器時(shí)需要設(shè)置三個(gè)頂點(diǎn)的坐標(biāo),同時(shí)計(jì)算該三角形平面的法向量。對(duì)于由a,b,c三個(gè)頂點(diǎn)構(gòu)成的三角形,法向量計(jì)算如下:

var normal = cross(subtract(b, a), subtract(c, a));
文字立體模型的建立

我們現(xiàn)在只得到了文字的一個(gè)面。既然想制作立體文字,我們需要同時(shí)計(jì)算出文字的正面、背面、以及側(cè)面。

正面和背面很容易得到:

for (var n = 0; n < triangles.length; n += 3) {
  var a = points[triangles[n]];
  var b = points[triangles[n + 1]];
  var c = points[triangles[n + 2]];

  //=====字體正面數(shù)據(jù)=====
  triangle(vec3(a[0], a[1], z), vec3(b[0], b[1], z), vec3(c[0], c[1], z), index);

  //=====字體背面數(shù)據(jù)=====
  triangle(vec3(a[0], a[1], z2), vec3(b[0], b[1], z2), vec3(c[0], c[1], z2), index);
}

重點(diǎn)在于側(cè)面的構(gòu)造,這里需要同時(shí)考慮內(nèi)外輪廓。輪廓上每組相鄰點(diǎn)的正、背面可構(gòu)成一個(gè)矩形,將矩形剖分成兩個(gè)三角形,即可得到側(cè)面的構(gòu)造。代碼如下:

var holesMap = [];
var last = 0;

if (holes.length) {
  for (var holeIndex = 0; holeIndex < holes.length; holeIndex++) {
    holesMap.push([last, holes[holeIndex] - 1]);
    last = holes[holeIndex];
  }
}

holesMap.push([last, points.length - 1]);

for (var i = 0; i < holesMap.length; i++) {
  var startAt = holesMap[i][0];
  var endAt = holesMap[i][1];

  for (var j = startAt; j < endAt; j++) {
    triangle(vec3(points[j][0], points[j][1], z), vec3(points[j][0], points[j][1], z2), vec3(points[j+1][0], points[j+1][1], z), index);
    triangle(vec3(points[j][0], points[j][1], z2), vec3(points[j+1][0], points[j+1][1], z2), vec3(points[j+1][0], points[j+1][1], z), index);
  }
  triangle(vec3(points[startAt][0], points[startAt][1], z), vec3(points[startAt][0], points[startAt][1], z2), vec3(points[endAt][0], points[endAt][1], z), index);
  triangle(vec3(points[startAt][0], points[startAt][1], z2), vec3(points[endAt][0], points[endAt][1], z2), vec3(points[endAt][0], points[endAt][1], z), index);
}
WebGL渲染

至此為止,我們已經(jīng)將所有需要的數(shù)據(jù)處理完畢,接下來,我們需要把有用的參數(shù)傳給頂點(diǎn)著色器。

傳入到頂點(diǎn)著色器中的參數(shù)定義如下:

attribute vec3 vPosition;
attribute vec4 vNormal;

uniform vec4 ambientProduct, diffuseProduct, specularProduct;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform vec4 lightPosition;
uniform float shininess;
uniform mat3 normalMatrix;

從頂點(diǎn)著色器輸出到片元著色器的變量定義如下:

varying vec4 fColor;

頂點(diǎn)著色器關(guān)鍵代碼:

vec4 aPosition = vec4(vPosition, 1.0);

……

gl_Position = projectionMatrix * modelViewMatrix * aPosition;
fColor = ambient + diffuse +specular;

片元著色器關(guān)鍵代碼:

gl_FragColor = fColor;
后續(xù)

一個(gè)立體漢字的渲染已經(jīng)完成了。你一定覺得這種效果不夠酷炫,或許還想為它加一些動(dòng)畫,不要著急,下一篇文章會(huì)拋磚引玉講一個(gè)文字效果及動(dòng)畫的設(shè)計(jì)。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/82276.html

相關(guān)文章

  • Canvas實(shí)現(xiàn)文本編輯器(支持藝術(shù)渲染與動(dòng)畫)

    摘要:項(xiàng)目中文字由進(jìn)行渲染。待觸發(fā)時(shí),取消中文輸入標(biāo)記,將文字渲染到上。而其中一些有趣的細(xì)節(jié)實(shí)現(xiàn)如文本渲染,對(duì)中文筆畫分割實(shí)現(xiàn)有趣的動(dòng)畫等并沒有描寫。 導(dǎo)言 目前富文本編輯器的實(shí)現(xiàn)主要有兩種技術(shù)方案:一個(gè)是利用contenteditable屬性直接對(duì)html元素進(jìn)行編輯,如draft.js;另一種是代理textarea + 自定義div + 模擬光標(biāo)實(shí)現(xiàn)。對(duì)于類似word的經(jīng)典富文本編輯器,...

    OldPanda 評(píng)論0 收藏0
  • SegmentFault 技術(shù)周刊 Vol.35 - WebGL:打開網(wǎng)頁(yè)看大片

    摘要:在文末,我會(huì)附上一個(gè)可加載的模型方便學(xué)習(xí)中文藝術(shù)字渲染用原生可以很容易地繪制文字,但是原生提供的文字效果美化功能十分有限。 showImg(https://segmentfault.com/img/bVWYnb?w=900&h=385); WebGL 可以說是 HTML5 技術(shù)生態(tài)鏈中最為令人振奮的標(biāo)準(zhǔn)之一,它把 Web 帶入了 3D 的時(shí)代。 初識(shí) WebGL 先通過幾個(gè)使用 Web...

    objc94 評(píng)論0 收藏0
  • WebGL入門demo

    摘要:而是一款框架,由于其易用性被廣泛應(yīng)用。如果要學(xué)習(xí),拋棄那些復(fù)雜的原生接口從這款框架入手是一個(gè)不錯(cuò)的選擇。需要相機(jī),演員這里是地球,場(chǎng)景,導(dǎo)演。最后拍好了戲交給渲染器來制片,發(fā)布。狀態(tài)也在不停的更新。 WebGL入門demo three.js入門 開場(chǎng)白 哇哦,繪制氣球耶,在網(wǎng)頁(yè)上?對(duì)啊!厲害了!3D效果圖也能在網(wǎng)頁(yè)上繪制出來啊,這么好玩的事情,趕緊來看看! 這里是屬于WebGL的應(yīng)用,...

    lijinke666 評(píng)論0 收藏0
  • 前端動(dòng)畫調(diào)研-V1

    摘要:支持動(dòng)畫狀態(tài)的,在動(dòng)畫開始,執(zhí)行中,結(jié)束時(shí)提供回調(diào)函數(shù)支持動(dòng)畫可以自定義貝塞爾曲線任何包含數(shù)值的屬性都可以設(shè)置動(dòng)畫倉(cāng)庫(kù)文檔演示功能介紹一定程度上,也是一個(gè)動(dòng)畫庫(kù),適用所有的屬性,并且實(shí)現(xiàn)的能更方便的實(shí)現(xiàn)幀動(dòng)畫,替代復(fù)雜的定義方式。 動(dòng)畫調(diào)研-V1 前言:動(dòng)畫從用途上可以分為兩種,一種是展示型的動(dòng)畫,類似于一張GIF圖,或者一段視頻,另一種就是交互性的動(dòng)畫。這兩種都有具體的應(yīng)用場(chǎng)景,比如...

    ddongjian0000 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<