2019年4月29日 星期一

用canvas2d繪製font texture的注意事項

用canvas2d繪製font texture的注意事項

前言

  用canvas2d繪製font可以輕鬆地找到範例,但繪製時有些需要注意的事,在此把過程做個紀錄。

內容

  由於WebGL本身不提供字型,所以需要產生font texture,產生的流程如下圖
產生font texture的流程

繪製字型是用canvas2d繪製後輸出到image,再來將imgae的資料輸出到WebGL的texture,之後WebGL再利用該texture來繪製font,整個流程不是很簡潔,但是我目前找到的方法中最好的。

  基本的繪製範例如下
let canvas=document.getElementById('myCanvas');
let ctx=canvas.getContext('2d');
canvas.width=300;
canvas.height=300;
let fontSize=36;
let str='|_jx/|';
ctx.fillStyle='#ff0000';
ctx.font=fontSize+'px sans-serif';
ctx.textBaseline='top';
for(let i=0,curOffset=0;i<str.length;i++){
  let width=ctx.measureText(str[i]).width;
  ctx.fillStyle='#ff0000ff';
  ctx.fillText(str[i],curOffset,0);

  curOffset+=width;
}

繪製的結果如下
範例繪製的結果

繪製的結果看起來沒問題,但事實上有問題,請看"_"與"j"連接的部分會連在一起,這對font texture而言可不是好事,如果WebGL在繪製"_"時會看到與"j"的部分,再想想如果將"_"改成"A",在繪製"A"時下方會多出"j"的一部分,這樣是無法讓人接受的。為什麼會發生這個問題呢?看看範例是如何知道該字的寬,"ctx.measureText()"就這個method,這是唯一可以得到字寬的方法,字高呢?font size有多高該字就多高,像"_"與"A"的高明顯不同,但只能把字型高的空間整個拿去畫。

   前面提到透過"ctx.measureText()"來取得字寬,但"j"這個字很特別,它的左側會會超出繪製的字寬,如下圖
"j"的字寬很特別

第一個"j"的左側會被截掉,是因為超出canvas的左側,第二個"j"就可以看到該字的左側會超出邊框,這就是問題了。這問題怎麼解決呢?目前我想到的是針對"j"來做特例處理,讓"j"的左側會多出一些空間來包住"j",具體的範例如下
let canvas=document.getElementById('myCanvas');
let ctx=canvas.getContext('2d');
canvas.width=300;
canvas.height=300;
let fontSize=36;
let str='jj|_jx/|';
let modWidth=10;
ctx.fillStyle='#ff0000';
ctx.font=fontSize+'px sans-serif';
ctx.textBaseline='top';
for(let i=0,curOffset=0;i<str.length;i++){
  let width=ctx.measureText(str[i]).width;
  if(str[i] === 'j')
    modWidth=5/72 *fontSize;
  else
    modWidth=0;
  ctx.fillStyle='#ff0000ff';
  ctx.fillText(str[i],curOffset+modWidth,0);
  ctx.strokeRect(curOffset+modWidth,0,width,fontSize);

  curOffset+=width+modWidth;
}

這個範例會對"j"這個字的左側增加"5/72"的寬度,這個寬度是我實驗的寬度,可以依據需要自己改,並且會考慮新增的寬度繪製新的邊框(邊框的寬度還是原本的寬度,但字跟字不會連在一起了),繪製的結果如下
修正"j"的寬度的繪製結果

參考資料

Drawing text

2019年4月22日 星期一

改進Vue.js的treeview

改進Vue.js的treeview

前言

  最近要為遊戲引擎製作物件的treeview,就想到之前的使用vue.js建構treeview,但沒想到實際使用後發現不太符合需求,之前的範例是把某個Node作root後將其繪製,但實際的需求是root的Node不只一個,所以要做一個改進,在此做個紀錄。

內容

  先來看改進後的html部分
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id='app'>
  <root_treenodeview :rootnode_list='nodelist'></root_treenodeview>
</div>
</body>
</html>

再來是javascript的部分
Vue.component('treenodeview',{
  props : ['node'],
  template : [
    '<ul>\n',
    '  <li>\n',
    '    <div @click="onClick">\n',
    '      {{node.name}}\n',
    '      <span v-show="isFolder">[{{isExpand?"-":"+"}}]</span>\n',
    '    </div>\n',
    '  </li>\n',
    '  <treenodeview v-show="isExpand" v-for="childNode in node.child" :node="childNode"></treenodeview>\n',
    '</ul>\n'
  ].join(''),
  data : function(){
    return {
      isExpand : false
    };
  },
  computed:{
    isFolder:function(){
      return this.node.child &&
              this.node.child.length;
    }
  },
  methods:{
    onClick:function(){
      if(this.isFolder){
        this.isExpand=!this.isExpand;
      }
    }
  }
});
Vue.component('root_treenodeview',{
  props : ['rootnode_list'],
  template:[
    '<div>\n',
    '  <treenodeview v-for="rootNode in rootnode_list" :node="rootNode"></treenodeview>\n',
    '</div>\n',
  ].join(''),
});
//
let node1={
  name:'node1',
  child:[
    {
      name : 'subNode1',
      child : []
    },
    {
      name : 'subNode1',
      child : []
    }
  ]
}
let node2={
  name:'node2',
  child:[
    {
      name : 'subNode1',
      child : []
    }
  ]
}
let app = new Vue({
  el : '#app',
  data:function(){
    return {
      nodelist:[node1,node2],
    }
  }
});

這次的程式碼風格跟上次有些不一樣,像之前的template是用html的方式來取值,這次是直接用字串陣列來輸入, 兩者的效果是一樣的,請根據喜好使用。在"treenodeview"的部分跟上次是一樣的,這次多了個"root_treenodeview",作用是將多個root顯示來符合本次的需求,程式碼的部分比"treenodeview"簡單許多,就只是依據"rootnode_list"的內容來產生對應的"treenodeview"。

  這次製作時卡了一陣子,一方面因為太久沒用Vue.js,所以還複習了一下,還有一個我以前沒發現的問題,那就是Vue.js的變數命名對"大寫"不友善,像是"props"的命名規則可以接受"_"與"-"但不接受大寫,而在Component的命名規則可以接受"_",但大寫與"-"都不被接受,在"data"的命名規則可以接受"_"與大寫,但不接受"-"。綜合3種狀況來看,"_"在Vue.js的接受度是最高的,3種狀況都能用,在Vue.js總是使用"_"來隔開字詞,這樣就可以不用管會不會違反命名規則了。

相關文章

使用vue.js建構treeview

2019年4月15日 星期一

關於Canvas的大小

關於Canvas的大小

前言

  Canvas的大小可以在MDN的Canvas 基本用途裡找到說明,但說明有點簡短,所以我就研究了一下,在此做個紀錄。

內容

  Canvas有兩種大小?是的,一個大小是繪製空間(Draw space)的大小另一個是顯示空間(Present space)的大小。繪製空間所代表的是畫布的空間,如CanvasRenderingContext2D.fillRect()、CanvasRenderingContext2D.fillText()與CanvasRenderingContext2D.drawImage()都是使用這個空間定址,顯示空間所表的是最後呈現在螢幕的大小,這兩個空間的關係是瀏覽器會將繪製空間縮放到顯示空間呈現在螢幕上,如下圖
繪製空間與顯示空間

圖中可以看到繪製空間的大小是用"canvas.width"與"canvas.height"來表達,具體的控制如下
let canvas=document.getElementById('myCanvas');
let ctx=canvas.getContext('2d');
ctx.canvas.width=300;
ctx.canvas.height=300;

這樣就可以控制繪製空間的大小。圖中的顯示空間的大小用"canvas.style.width"與"canvas.style.height"來表達,具體的控制如下
let canvas=document.getElementById('myCanvas');
let ctx=canvas.getContext('2d');
ctx.canvas.style.width='300px';
ctx.canvas.style.height='300px';

控制的方法其實就是控制CSS的屬性。如果要取得顯示空間的大小可以透過CSS取得,但畢竟該變數是字串不能直接變成"Number",所以要取得顯示空間大小可以透過"canvas.clientWidth"與"canvas.clientHeight"來取得,取得的範例如下
let canvas=document.getElementById('myCanvas');
let ctx=canvas.getContext('2d');
ctx.canvas.style.width='300px';
ctx.canvas.style.height='300px';
console.log('present space width:'+ctx.canvas.clientWidth);
console.log('present space height:'+ctx.canvas.clientHeight);

要注意的是這兩個數值只能"get"不能"set"。

參考資料

Canvas 基本用途

2019年4月8日 星期一

關於Function declaration和Function expression的差異

關於Function declaration和Function expression的差異

前言

  最近在youtube看到Top 10 JavaScript Interview Questions ( Part 2 ),才發現原來這有差別,在此做個紀錄。

內容

  Function expression的行為和我認知的行為一致,範例如下
funExpression();//ReferenceError: funExpression is not defined
let funExpression=function(){
  console.log('I am a function expression');
}
funExpression();//I am a function expression

再來看看Function declaration,範例如下
funDeclaration();//I am a function declaration
function funDeclaration(){
  console.log('I am a function declaration');
}
funDeclaration();//I am a function declaration

可以看到第一次喚起"funDeclaration"時遽然可以正常喚起!這表示Javascript其實會先把整段程式碼先解讀過才執行,而不是一行一行執行,如果是一行一行執行,在第一次喚起"funDeclaration"應該要和Function expression一樣認定該變數不存在。

  如果上網查詢Javascript會被歸類在"直譯",查了一下直譯器wiki,裡面解釋直譯為"把高階程式語言一行一行直接轉譯執行",而編譯是"編譯器已一次將所有原程式碼翻譯成另一種語言",而從Function declaration的行為來看,是將片段程式碼解讀後執行,這樣也算是"直譯"!?看來我必須對"直譯"的觀念該更新了。

參考資料

2019年4月1日 星期一

矩陣的排列順序(2)

矩陣的排列順序(2)

前言

  在前一篇矩陣的排列順序(1)中,提到了矩陣的排列順序,不過後來發現不是那麼正確,所以在寫一篇來補充。

內容

  在前一篇矩陣的排列順序(1)中有一張比較圖如下
矩陣排列順序比較
在上一篇得到的結論是用"右邊"的排列,但近日實驗了Direct3D、OpenGL、OpenGLES與WebGL後,當使用"右邊"的排列傳入後,乘法的順序如下
 WVPMatrix = WorldMatrix * ViewMatrix * ProjectionMatrix

這個順序會與關於Transform的矩陣乘法的順序一致,個人比較喜歡這個順序!

  在上一篇說過矩陣在ConstantBuffer與UBO的預設順序的規則,但事實上可以在Shader上來改變順序,在HLSL的改法如下
cbuffer cbTest
{
  float4x4 mat;//default is row_major 
  row_major float4x4 mat_row;
  column_major float4x4 mat_col;
};

在GLSL3以後改法如下
//Set all ubo default  
layout(column_major) uniform;

//
uniform uboTest
{
  mat4 mat;//default is column_major 
  layout(row_major) mat_row;
  layout(column_major) mat_col;
} 

如果在沒有UBO時透過glUniformMatrix4fv()的參數可以決定順序。

  雖說順序可以透過Shader來更改,但查了一下WebGL1.0時的uniformMatrix4fv()並不支援Transpose!但卻有該參數的欄位,這規格的設計非常奇怪,如果需要可以Transpose的話必須自己寫。

  最後來說說結論,從繪圖API送矩陣的順序會影響在寫Shader時的乘法順序,如果還要考慮到是否要Transpose的話,右邊的排序不需要做任何轉換,乘法順序與我在數學上的書籍看到的是一樣的,所以我個人較推薦右邊的順序。

參考資料

Interface Block (GLSL)

相關文章

矩陣的排列順序(1)
關於Transform的矩陣乘法


2019年3月25日 星期一

FreeType的字型排版資訊

FreeType的字型排版資訊

前言

  在之前的初探FreeType中讀取出字型的pixel資料,但只是這樣的資料並不夠用,只是畫出單一個字還可以,如果要畫一個字串的話,就需要知道"排版資訊",在此做個紀錄。

內容

  在之前的"初探FreeType"中,每個"glyph"的寬跟高都是不同的,要畫出一個左排到右的字串的話,會發現無法對齊,因為只知道該字的Pixel寬與高並沒有辦法對齊。

  排版的資訊在"FT_load_Glyph()"時就會存在"face"裡,個人整理了排版資訊如下圖
字型的排版資訊
排版需要的變數有"glyph width"、"glyph height"、"code width"、"code height"、"code offset x"與"code offset y",接著來看怎麼從Freetype取出這些數值。

  "glyph width"的部分可以在"face->glyph->matrics.horiAdvance"取得,取得後記得除以64,就可以取得pixel的寬。

  "glyph height"比較簡單,還記得"FT_Set_Pixel_sizes()"嗎?最後一個參數就是這個數值。

  "code width"可以在"face->glyph->matrics.width"取得,取得後記得除以64。

  "code width"可以在"face->glyph->matrics.height"取得,取得後記得除以64。

  "code offset x"可以在"face->glyph->matrics.horiBearingX"取得,取得後記得除以64。

  "code offset y"這個比較麻煩,"glyph height-(face->glyph->bitmap_top+glyph height/4)",這計算式看起很奇怪是因為FreeType是左下座標為基準,但我定義的這個數值卻是左上座標,所以要做個換算。

  接著來說一些注意事項,有些字是無法讀出字型資訊的,比如說"\n",也就是換行,在畫字串時要記得處理特殊字元。"code offset x"與"code offset y"是有"負值"的,如"j"這個字元的"code offset x"就是負的。如果"code offset x"是負值,該值就不會被算在 "glyph width"裡面,如下圖
"code offset x"是負值的狀況

參考資料

Glyph Metrics

相關文章

初探FreeType

2019年3月18日 星期一

初探FreeType

初探FreeType

前言

  繪圖API並不支援畫字型的功能,以前使用Direct2D搭配DirectWrite來畫字型,但不幸地現在繪圖引擎要跨平台,Direct2D的方法最多只能在Windows平台使用,FreeType就是我找到的可以跨平台的畫字型方法,這裡把學習心得做個紀錄。

內容

  首先先來說說"建置",FreeType自帶的建置個人覺得實在是不怎麼好用,Windows版的建置是由官方給個Visual studio專案直接開啟後建置即可,但Linux的建置就完全不一樣,是類似透過Shell script的方式來建置,Android則是透過NDK的專案來建置,最後我並沒選擇官方的建置是因為FreeType在下載時是在Windows平台,所以裡面的文字檔的換行是"\r\n",結果當我在Ubuntu建置時都會報錯,也就是說跟建置相關的檔案絕不能在Windows的環境編輯,不然上傳後就沒辦法用了。解決的方案很簡單,就是自己建置,還記得官方有自帶一個Visual studio專案嗎?我依據該專案的建置方案來製作Linux與Android的建置,這方法有風險,但我還是做了,幸運的是Linux與Android的建置與Windows是差不多,差別的地方是Linux與Android版並沒有所謂的"Debug"版,編譯時不用加"FT_DEBUG_LEVEL_ERROR"與"FT_DEBUG_LEVEL_TRACE"編譯,其它的部分和Windows版是一樣的。

  接著來看看如何使用,先看以下範例
#include "ft2build.h"
#include FT_FREETYPE_H

int main()
{
  //...
  //
  FT_Library library;
  FT_Error error=FT_Init_FreeType(&library);
  if(error!=0)
    return 1;
  //
  //...
  //Free
  FT_Done_FreeType(library);

  return 0;
}

這個範例會去製造FT_Library,整個程式應該只要製造一個,在程式要結束時,透過FT_Done_FreeType()來釋放。FT_Library製造完後就可以來讀取font檔,範例如下
#include "ft2build.h"
#include FT_FREETYPE_H

int main()
{
  //...
  //
  FT_Library library;
  FT_Error error=FT_Init_FreeType(&library);
  if(error!=0)
    return 1;
  //Load font file
  FT_Face face;
  error=FT_New_Face(library,"arial.ttf",0,&face);
  //...
  //Free
  FT_Done_Face(face);
  FT_Done_FreeType(library);

  return 0;
}

讀取的方法很簡單,透過FT_New_Face()來讀取,透過FT_Done_Face()來釋放。在Android時,由於Android的Asset不提供Handle,如果要將Font檔放在Asset的話,就要透過FT_New_Memory_Face()來讀取。接下來就來讀取字型的資料,範例如下
#include "ft2build.h"
#include FT_FREETYPE_H

int main()
{
  //...
  //
  FT_Library library;
  FT_Error error=FT_Init_FreeType(&library);
  if(error!=0)
    return 1;
  //Load font file
  FT_Face face;
  error=FT_New_Face(library,"arial.ttf",0,&face);
  //Set font size
  error=FT_Set_Pixel_Sizes(face,0,60);
  //Load glyph
  FT_UInt glyphIndex=FT_Get_Char_Index(face,'a');
  if(glyphIndex==0)
    return 1;
  error=FT_Load_Glyph(face,glyphIndex,FT_LOAD_DEFAULT);
  if(face->glyph->format!=FT_GLYPH_FORMAT_BITMAP)
  {
    error=FT_Render_Glyph(face->glyph,FT_RENDER_MODE_MONO);
    if(error!=0)
      return 1;
  }
  //Get glyph bitmap data from face->glyph->bitmap.buffer
  //...
  //Free
  FT_Done_Face(face);
  FT_Done_FreeType(library);

  return 0;
}

 透過FT_Set_Pixel_Sizes()來設定字型的大小,接著透過Glyph index來寫資料到"buffer"裡,"buffer"在哪呢?透過face->glyph->bitmap.buffer來取得。

  Buffer的資料就是一張圖,整個Buffer的資料的大小為face->glyph->bitmap.pitch*face->glyph->bitmap.rows,"face->glyph->bitmap.rows"就是高,但"face->glyph->bitmap.pitch"並不是寬,每個Pitch指的是把寬度以每8個Pixel存成一個Byte來計算的話會有幾個Byte,如果寬度是60Pixel,Pitch值為8,如果寬度是40Pixel,Pitch值為5,依此類推。

參考資料

FreeType官網
Text Rendering with Direct2D and DirectWrite