2019年7月29日 星期一

多個 WebGLRenderContext 的問題

多個 WebGLRenderContext 的問題

前言

  在 Canvas2D 時沒有所謂的 Canvas 製造的資源,但 WebGLRenderContext 卻會有,如 VBO、  IBO 與 Texture ...等,這些資源能給同一個網頁的其它  WebGLRenderContext 使用嗎?會有這樣的疑問主要是因為要用 Electron 做一個遊戲編輯器,初步的版面配置如下
遊戲編輯器的版面配置
其中 GameView 與 ResourceView 都是 WebGLRenderContext ,GameView 用來顯示場景, ResourceView 用來顯示 Texture 或 Mesh 之類的資源,這樣的狀況就會發生需要多個 WebGLRenderContext 的狀況,在此把學習的過程做個紀錄。

內容

  在google後沒找到明確的答案,所以就直接寫一個範例直接實驗,這次的範例主要參考
Drawing a triangle with WebGL ,範例如下
HTML 的部分
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<canvas id="myCanvas1" width=400 height=300></canvas>
<canvas id="myCanvas2" width=400 height=300></canvas>
</body>
</html>

JavaScript 的部分
let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let canvas2 = document.getElementById('myCanvas2');
let glCTX2 = canvas2.getContext('webgl');
//Create vertex buffer from glCTX1
let globalVertexBuf = glCTX1.createBuffer();
glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, globalVertexBuf);
glCTX1.bufferData(
  glCTX1.ARRAY_BUFFER, 
  new Float32Array([ 0.0,1.0,0.0,  -1.0,-1.0,0.0,  1.0,-1.0,0.0 ]), 
  glCTX1.STATIC_DRAW
);

function simpleDraw(glContext,vbo){
  let vertShader = glContext.createShader(glContext.VERTEX_SHADER);
  glContext.shaderSource(
    vertShader , 
    'attribute vec3 pos;void main(void){gl_Position=vec4(pos, 1.0);}'
  );
  glContext.compileShader(vertShader);
  let fragShader = glContext.createShader(glContext.FRAGMENT_SHADER);
  glContext.shaderSource(
    fragShader, 
    'void main(void){gl_FragColor=vec4(1,1,1,1);}'
  );
  glContext.compileShader(fragShader);
  let prog = glContext.createProgram();
  glContext.attachShader(prog, vertShader);
  glContext.attachShader(prog, fragShader);
  glContext.linkProgram(prog);
  glContext.useProgram(prog);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  if(vbo){
    glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  }
  else{
    let vertexBuf = glContext.createBuffer();
    glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
    glContext.bufferData(
      glContext.ARRAY_BUFFER, 
      new Float32Array([ 0.0,0.5,0.0,  -0.5,-0.5,0.0,  0.5,-0.5,0.0 ]), 
      glContext.STATIC_DRAW
    );
  }
  //
  let posLoc = glContext.getAttribLocation(prog, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.TRIANGLES, 0, 3);
}

simpleDraw(glCTX1);
simpleDraw(glCTX2,globalVertexBuf);

程式會透過 simpleDraw() 來繪製一個三角形,如果 vbo 沒輸入的話會自己建一個, globalVertexBuf 從 glCTX1 製作,用來實驗  glCTX1  的資源是否可以給  glCTX2 使用,最後看到 simpleDraw() 的喚起,第二個喚起會將 globalVertexBuf 傳入來繪製,也是本次實驗的重點,執行的結果如下
範例的執行結果

不幸的是"不行", WebGLRenderContext 各自的資源只能給自己用,看來遊戲編輯器的版面配置必須邀遷就這個特性改變了。

參考資料

Drawing a triangle with WebGL

2019年7月22日 星期一

初探 Chart.js

初探 Chart.js

前言

  在前端直接畫圖表相當麻煩,透過 Chart.js 來畫圖表就簡單不少,可以大量簡化畫圖表的過程,在此把學習的過程做個紀錄。

內容

  這次的範例主要參考 Web 視覺化(一):使用 Chart.js 輕鬆建立圖表 ,接著直接看範例
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/Chart.js/2.7.2/Chart.min.js"></script>
<canvas id="mycanvas" width="800" height="600"></canvas>
</body>
</html>

Javascript 的部分
let ctx = document.getElementById('mycanvas').getContext('2d');
let chart = new Chart(ctx, {
    type: 'bar',
    data: {
        labels: ['A', 'B', 'C'],
        datasets: [{
            label: 'My bar chart',
            data: [10, 20, -15]
        }]
    }
});

執行結果
執行結果

範例使用 HTMLCanvasElement 來顯示圖表, Chart.js 的部分用起來很直覺, type 可以決定是哪一種圖表,像條狀圖、圓餅圖與雷達圖...等, data 的部分會依據 type 的不同給不同的資料,可以參考 Chart.js documentation 來填寫。

  範例的執行結果的 HTMLCanvasElement 會依據視窗大小自動擴展,這是預設行為,如果要拒絕自動擴展可以把範例改成以下
let ctx = document.getElementById('mycanvas').getContext('2d');
let chart = new Chart(ctx, {
    type: 'bar',
    options:{
      responsive:false
    },
    data: {
        labels: ['A', 'B', 'C'],
        datasets: [{
            label: 'My bar chart',
            data: [10, 20, -15]
        }]
    }
});

修改後就不自動擴展,但要注意 HTMLCanvasElement 的大小縮得太嚴重時畫出來的結果不太正常,最好可以限制最低的大小。

參考資料

[GitHub] Chart.js
Chart.js documentation
Web 視覺化(一):使用 Chart.js 輕鬆建立圖表

2019年7月15日 星期一

如何直接把檔案的 Binary data 給 HTMLImage​Element

如何直接把檔案的 Binary data 給 HTMLImage​Element

前言

  最近需要把Texture的資料存成JSON,JSON裡存的是檔案的 Binary data (經過 Base64 編碼),但WebGL並不能直接透過檔案的 Binary data 來初始化,WebGL提供從 HTMLImage​Element 來初始化 Texture ,所以需要用檔案的 Binary data 初始化 HTMLImage​Element ,在此把學習的過程做個紀錄。

內容

  在之前的 將canvas的結果輸出到image 裡有使用"HTMLCanvasElement.toDataURL()"來初始化 HTMLImage​Element ,如果打資料列印出來的話會得到以下
data:image/png;base64,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

這其實是一種資料格式稱為 MIME 類型,格式的類型如下
data:(MIME type);base64,(Base64 data)

會有變動只有 MIME type 與 Base64 data 這兩個地方, MIME type 表明的是資料格式,如PNG與BMP...等,詳細支援的格式可以參考 [MDN]MIME 类型 。Base64 data 就是被 Base64 編碼過的 Binary data ,為什麼 Binary data 要經過 Base64 編碼?因為單靠 String 沒辦法表達 Binary data , String 會有結束字元的問題,經過 Base64 編碼可以迴避結束字元的問題。

  接下來就可以 MIME 類型的字串直接給 "HTMLImage​Element.src",就像 將canvas的結果輸出到image 的範例一樣,這樣就完成 HTMLImage​Element 的初始化,整體的流程如下圖
直接把檔案的 Binary data 給HTMLImageElement的流程

參考資料

[Wiki]網際網路媒體型式
[MDN]MIME 类型
[Wiki]Base64

相關文章

將canvas的結果輸出到image

2019年7月8日 星期一

用 emit 取代 Vue.js 的 treeview 事件

用emit取代Vue.js的treeview事件

前言

  在之前的在Vue.js的treeview增加drag功能裡的事件是透過 props 來傳達,但之後又發現有 emit 可以用,最後決定用 emit 取代目前的 props 傳事件,在此做個紀錄。

內容

  這次的程式碼會整合在Vue.js的treeview增加drag功能初探Vue.js的emit,整合的程式碼如下
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' @evt-drag-node="onDragNode" @evt-drop-node="onDropNode" @evt-drop-node-parent="onDropNodeParent"></root_treenodeview>
</div>
</body>
</html>

JavaScript 的部分
Vue.component('treenodeview',{
  props : ['node'],
  template : [
    '<ul>\n',
    '  <li>\n',
    '    <div @click="onClick" draggable="true" @dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">\n',
    '      {{node.name}}\n',
    '      <span v-show="isFolder">[{{isExpand?"-":"+"}}]</span>\n',
    '    </div>\n',
    '  </li>\n',
    '  <div style="height:10px;" @dragenter="onDragEnterParent" @dragover="onDragOverParent" @dragleave="onDragLeaveParent" @drop="onDropParent">\n',
    '    <hr v-show="isShowDragParent" style="margin:0px;">\n',
    '  </div>\n',
    '  <treenodeview v-show="isExpand" v-for="childNode in node.child" :node="childNode" @evt-drag-node="onDragNode" @evt-drop-node="onDropNode" @evt-drop-node-parent="onDropNodeParent"></treenodeview>\n',
    '</ul>\n'
  ].join(''),
  data : function(){
    return {
      isExpand : false,
      isShowDragParent : false,
    };
  },
  computed:{
    isFolder:function(){
      return this.node.child &&
              this.node.child.length;
    }
  },
  methods:{
    onClick : function(){
      if(this.isFolder){
        this.isExpand=!this.isExpand;
      }
    },
    onDragStart : function(evt){
      this.$emit('evt-drag-node', this.node);
    },
    onDragOver : function(evt){
      evt.preventDefault();
    },
    onDrop : function(evt){
      evt.preventDefault();
      this.$emit('evt-drop-node', this.node);
    },
    onDragEnterParent : function(evt){
      this.isShowDragParent = true;
    },
    onDragOverParent : function(evt){
      evt.preventDefault();
      this.isShowDragParent = true;
    },
    onDragLeaveParent : function(evt){
      this.isShowDragParent = false;
    },
    onDropParent : function(evt){
      evt.preventDefault();
      this.isShowDragParent = false;
      this.$emit('evt-drop-node-parent', this.node);
    },
    onDragNode : function(node){
      this.$emit('evt-drag-node', node);
    },
    onDropNode : function(node){
      this.$emit('evt-drop-node', node);
    },
    onDropNodeParent : function(node){
      this.$emit('evt-drop-node-parent', node);
    }
  }
});
Vue.component('root_treenodeview',{
  props : ['rootnode_list'],
  template:[
    '<div>\n',
    '  <treenodeview v-for="rootNode in rootnode_list" :node="rootNode" @evt-drag-node="onDragNode" @evt-drop-node="onDropNode" @evt-drop-node-parent="onDropNodeParent"></treenodeview>\n',
    '</div>\n',
  ].join(''),
  methods:{
    onDragNode : function(node){
      this.$emit('evt-drag-node', node);
    },
    onDropNode : function(node){
      this.$emit('evt-drop-node', node);
    },
    onDropNodeParent : function(node){
      this.$emit('evt-drop-node-parent', node);
    }
  }
});
//
function Node(opt){
  this.name = (opt.name) ? opt.name : '';
  this.parent = undefined;
  this.child = [];
}
Node.prototype.setParent = function(parentNode){
  let isFindInParent = false;
  let findNode = parentNode;
  while(findNode){
    if(findNode === this){
      isFindInParent = true;
      break;
    }
    findNode = findNode.parent;
  }
  if(isFindInParent)
    return;
  
  if(this.parent){
    //
    if(isFindInParent)
      return;
    let index=-1;
    for(let i=0;i<this.parent.child.length;i++){
      if(this.parent.child[i] === this){
        index = i;
        break;
      }
    }
    //
    if(index >= 0){
      this.parent.child.splice(index,1);
    }
  }
  //
  this.parent = parentNode;
  if(parentNode)
    parentNode.child.push(this);
};
let nodeRoot=new Node({name:"root"});
let node1=new Node({name:"node1"});
let node2=new Node({name:"node2"});
node1.setParent(nodeRoot);
node2.setParent(node1);

let app = new Vue({
  el : '#app',
  data:function(){
    return {
      nodelist:[nodeRoot],
      dragNode : null
    }
  },
  methods:{
    onDragNode:function(node){
      this.dragNode = node;
    },
    onDropNode:function(node){
      if(this.dragNode){
        if(this.dragNode.parent === undefined){
          let index=-1;
          for(let i=0;i<this.nodeList.length;i++){
            if(this.nodeList[i] === this.dragNode){
              index = i;
              break;
            }
          }
          //
          if(index>=0){
            this.nodeList.splice(index,1);
          }
        }
        this.dragNode.setParent(node);
      }
    },
    onDropNodeParent:function(node){
      if(this.dragNode && this.dragNode!==node){
        if(node.parent)
          this.dragNode.setParent(node.parent);
        else{
          this.dragNode.setParent(undefined);
          this.nodelist.push(this.dragNode);
        }
      }
    }
  }
});

這次會在 treenodeview 與 root_treenodeview都新增 onDragNode 、 onDropNode 與 onDropNodeParent,實作的部分都只是喚起 emit,主要的原因在 初探Vue.js的emit 裡說過只能傳一層,所以這些 emit 其實是往上再傳。

  這次的做法有比較好嗎?只是比較接近"官方"用法而已,如果有看過官方寫的 treeview(Tree View Example),裡面的事件傳遞都是用 emit 的方式,而不是用 props 傳事件,所以改成"官方"的用法。

參考資料

Tree View Example

相關文章

在Vue.js的treeview增加drag功能
初探Vue.js的emit

2019年7月1日 星期一

在Vue.js的treeview增加drag功能

在Vue.js的treeview增加drag功能

前言

  還記得在使用vue.js建構treeview時說過treeview是為了給遊戲引擎用的,但前一篇雖做出了treeview但還是少了一個功能,就是drag and drop,這功能是必備的功能,這次要把這個功能實現,在此做個紀錄。

內容

  在實現drag and drop時由於之前都沒在網頁上實現過,所以到HTML5 Drag and Drop裡學習,大致上沒什麼問題,大致的流程如下:
1.在要Drag的DOMElement裡設定"draggable"為"true"。
2.在Drag的DOMElement裡新增"ondraagstart"事件,該事件需要記錄哪個物件被drag。
3.在要Drop的DOMElement裡新增"ondragover"事件,該事件會決定能不能被drop。
4.在要Drop的DOMElement裡新增"ondrop"事件,該事件會取得步驟2紀錄的drag物件,並執行  Drop。

正個過程看起來沒什麼問題,但實際用起來卻發現有個問題,在步驟2的事件紀錄drag物件,然後在步驟4取出,這個流程會利用事件的"dataTransfer"來記錄物件,而且會發現範例是用ID的方式紀錄資料,然後步驟4時取出ID後,透過"document.getElementById()"取得DOMElement。查了一下"dataTransfer",無法紀錄物件的reference,如果要用"dataTransfer"來記錄treeview的節點一定要支援可以用一個字串代表的ID來找到對應的點,這裡卡了一下子,最後想到在步驟2與步驟4的啟動事件,啟動事件會喚起控制物件的事件,並把drag的物件記錄在控制物件,說起來抽象了點,看以下範例
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' :evtdragnode="onDragNode" :evtdropnode="onDropNode" :evtdropnodeparent="onDropNodeParent"></root_treenodeview>
</div>
</body>
</html>

javascript的部分
Vue.component('treenodeview',{
  props : ['node','evtdragnode','evtdropnode','evtdropnodeparent'],
  template : [
    '<ul>\n',
    '  <li>\n',
    '    <div @click="onClick" draggable="true" @dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">\n',
    '      {{node.name}}\n',
    '      <span v-show="isFolder">[{{isExpand?"-":"+"}}]</span>\n',
    '    </div>\n',
    '  </li>\n',
    '  <div style="height:10px;" @dragenter="onDragEnterParent" @dragover="onDragOverParent" @dragleave="onDragLeaveParent" @drop="onDropParent">\n',
    '    <hr v-show="isShowDragParent" style="margin:0px;">\n',
    '  </div>\n',
    '  <treenodeview v-show="isExpand" v-for="childNode in node.child" :node="childNode" :evtdragnode="evtdragnode" :evtdropnode="evtdropnode" :evtdropnodeparent="evtdropnodeparent"></treenodeview>\n',
    '</ul>\n'
  ].join(''),
  data : function(){
    return {
      isExpand : false,
      isShowDragParent : false,
    };
  },
  computed:{
    isFolder:function(){
      return this.node.child &&
              this.node.child.length;
    }
  },
  methods:{
    onClick : function(){
      if(this.isFolder){
        this.isExpand=!this.isExpand;
      }
    },
    onDragStart : function(evt){
      if(this.evtdragnode)
        this.evtdragnode(this.node);
    },
    onDragOver : function(evt){
      evt.preventDefault();
    },
    onDrop : function(evt){
      evt.preventDefault();
      if(this.evtdropnode){
        this.evtdropnode(this.node);
      }
    },
    onDragEnterParent : function(evt){
      this.isShowDragParent = true;
    },
    onDragOverParent : function(evt){
      evt.preventDefault();
      this.isShowDragParent = true;
    },
    onDragLeaveParent : function(evt){
      this.isShowDragParent = false;
    },
    onDropParent : function(evt){
      evt.preventDefault();
      this.isShowDragParent = false;
      if(this.evtdropnodeparent){
        this.evtdropnodeparent(this.node);
      }
    }
  }
});
Vue.component('root_treenodeview',{
  props : ['rootnode_list','evtdragnode','evtdropnode','evtdropnodeparent'],
  template:[
    '<div>\n',
    '  <treenodeview v-for="rootNode in rootnode_list" :node="rootNode" :evtdragnode="evtdragnode" :evtdropnode="evtdropnode" :evtdropnodeparent="evtdropnodeparent"></treenodeview>\n',
    '</div>\n',
  ].join(''),
});
//
function Node(opt){
  this.name = (opt.name) ? opt.name : '';
  this.parent = undefined;
  this.child = [];
}
Node.prototype.setParent = function(parentNode){
  let isFindInParent = false;
  let findNode = parentNode;
  while(findNode){
    if(findNode === this){
      isFindInParent = true;
      break;
    }
    findNode = findNode.parent;
  }
  if(isFindInParent)
    return;
  
  if(this.parent){
    //
    if(isFindInParent)
      return;
    let index=-1;
    for(let i=0;i<this.parent.child.length;i++){
      if(this.parent.child[i] === this){
        index = i;
        break;
      }
    }
    //
    if(index >= 0){
      this.parent.child.splice(index,1);
    }
  }
  //
  this.parent = parentNode;
  if(parentNode)
    parentNode.child.push(this);
};
let nodeRoot=new Node({name:"root"});
let node1=new Node({name:"node1"});
let node2=new Node({name:"node2"});
node1.setParent(nodeRoot);
node2.setParent(node1);

let app = new Vue({
  el : '#app',
  data:function(){
    return {
      nodelist:[nodeRoot],
      dragNode : null
    }
  },
  methods:{
    onDragNode:function(node){
      this.dragNode = node;
    },
    onDropNode:function(node){
      if(this.dragNode){
        if(this.dragNode.parent === undefined){
          let index=-1;
          for(let i=0;i<this.nodeList.length;i++){
            if(this.nodeList[i] === this.dragNode){
              index = i;
              break;
            }
          }
          //
          if(index>=0){
            this.nodeList.splice(index,1);
          }
        }
        this.dragNode.setParent(node);
      }
    },
    onDropNodeParent:function(node){
      if(this.dragNode && this.dragNode!==node){
        if(node.parent)
          this.dragNode.setParent(node.parent);
        else{
          this.dragNode.setParent(undefined);
          this.nodeList.push(this.dragNode);
        }
      }
    }
  }
});


這次的範例是從改進Vue.js的treeview裡改的,node的資料之前是用JSON直接給,這次寫成class,並且新增"parent"屬性,這屬性是必要的,注意在"Node.setParent()"實現時,每次都會檢查是否可以加在該點的parent,要注意Node與Node之間不能形成循環關係!控制物件指的是範例的"app"這個變數,,可以看到新增的"onDragNode()"與"onDropNode()",在html的部分要記得綁定事件,"treenodeview"與"root_treenodeview"這兩個component也要記得綁定事件,"treenodeview"的部分就和一般的drag and drop一樣,實現那4個步驟即可,並在綁定事件時喚起事件讓控制物件的事件被喚起。接著說明Drag to parent的機制,先看下圖
Drag to parent的操作
drag到結點下方的位置時會出現一條分隔線表示drag到該點的parent,這個部分的實作需要在每個節點增加"isShowDragParent",由於需要在離開drag區域時關閉顯示,所以需要用到"ondragenter"與"ondragleave"兩個事件,具體的說明可以參考HTML 拖放 API,要注意該數值在"ondragover"與"ondrop"都要設定,不然容易發生閃爍顯示分隔線的狀況,接著在控制物件的"onDropNodeParent"與"onDropNode"裡都要考慮"nodeList"會不會改變的問題,因為"root_treenodeview"是利用"nodeList"的數量來決定哪些物件是root node。

參考資料

HTML5 Drag and Drop
Data​Transfer
HTML 拖放 API

相關文章

改進Vue.js的treeview
使用vue.js建構treeview