2020年11月24日 星期二

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線

 WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線

前言

  在先前的 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 繪製了曲線,但最近需要得到 tangent (切線) ,所以就研究了一下如何計算切線,在此把學習的過程做個紀錄。


內容

  要計算切線的話我在 [ stackoverflow.com ] Find the tangent of a point on a cubic bezier curve 裡找到了公式如下


這個公式也可以在 [ en.wikipedia.org ] Bézier curve 裡面找到,限定在英文的 Wiki 才找的到請注意。


  接著把公式套到 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 的範例,程式碼如下

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>
<br>
<input id="btnDrawCtrlPoints"type="button" value="DrawCtrlPoints"/>
<br>
<input id="btnDrawCurve"type="button" value="DrawCurve"/>
<input type="range" min="2" max="100" value="30" class="slider" id="sliderLerp">
<label id="labelLerpValue">30</label>
<br>
<input type="checkbox" id="isShowTangent" checked><label>Show tangent </label>
<input type="range" min="0" max="1" value="0.5" step="0.001" class="slider" id="sliderTangent">
<label id="labelTangentValue">0.5</label>
</body>
</html>



Javascript 的部分

let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let vboPrimitiveCon = 0;
let vbo=createDynamicBuffer(glCTX1);
let shaderProg = createShader(glCTX1);
//
function createShader(glContext){
  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);  
  
  return prog;
}
function createDynamicBuffer(glContext){
  let vertexBuf = glContext.createBuffer();
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
  let dataArray=new Float32Array([ 
       0.0, 0.5, 0.0,  
      -0.5,-0.5, 0.0,  
      -0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.0, 0.5, 0.0
    ]);
  glContext.bufferData(
    glContext.ARRAY_BUFFER, 
    3000, 
    glContext.DYNAMIC_DRAW
  );
  //write deafult data...
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
  return vertexBuf;
}
function simpleDraw(glContext){
  glContext.useProgram(shaderProg);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  let posLoc = glContext.getAttribLocation(shaderProg, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.LINES, 0, vboPrimitiveCon);
}
function generateLineListData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return new Float32Array(tagAr);
}
function generateLineListArrayData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return tagAr;
}
function generateBezierCurve(p0,p1,p2,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT;
    let part1Value = 2 * t * invT;
    let part2Value = t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0]);
    tagAr.push(part0[1] + part1[1] + part2[1]);
    tagAr.push(part0[2] + part1[2] + part2[2]);
  }
  return tagAr;
}
function calCubicBezierCurvePosition(p0,p1,p2,p3,t){
  let invT = 1.0 - t;
  let part0Value = invT * invT * invT;
  let part1Value = 3 * t * invT * invT;
  let part2Value = 3 * t * t * invT;
  let part3Value = t * t * t;
  let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
  let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
  let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
  let part3 = [part3Value*p3[0], part3Value*p3[1], part3Value*p3[2] ];
  let position = [
    part0[0] + part1[0] + part2[0] + part3[0],
    part0[1] + part1[1] + part2[1] + part3[1],
    part0[2] + part1[2] + part2[2] + part3[2]
  ];
  return position;
}
function generateCubicBezierCurve(p0,p1,p2,p3,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let pos = calCubicBezierCurvePosition(p0,p1,p2,p3,t);
    tagAr.push( pos[0] );
    tagAr.push( pos[1] );
    tagAr.push( pos[2] );
  }
  return tagAr;
}
function calCubicBezierCurveTangent(p0,p1,p2,p3,t){
  let del10 = [ p1[0] - p0[0] , p1[1] - p0[1] , p1[2] - p0[2] ];
  let del21 = [ p2[0] - p1[0] , p2[1] - p1[1] , p2[2] - p1[2] ];
  let del32 = [ p3[0] - p2[0] , p3[1] - p2[1] , p3[2] - p2[2] ];
  let invT = 1.0-t;
  let part0Value = 3 * invT * invT ;
  let part1Value = 6 * t * invT;
  let part2Value = 3 * t * t;
  let tangent = [
    part0Value * del10[0] + part1Value * del21[0] + part2Value * del32[0],
    part0Value * del10[1] + part1Value * del21[1] + part2Value * del32[1],
    part0Value * del10[2] + part1Value * del21[2] + part2Value * del32[2]
  ];
  return tangent;
}
function myRender(){
  simpleDraw(glCTX1);
  //
  window.requestAnimationFrame(myRender);
}
//
let ctrlPointList=[
  [-0.9,0.0,0.0],
  [-0.45,0.9,0.0],
  [0.45,0.9,0.0],
  [0.9,0.0,0.0],
];
let tagLerpValue=document.getElementById("sliderLerp").value;
let tagTangentValue=document.getElementById("sliderTangent").value;
function UpdateCurveData(){
  let data=generateCubicBezierCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[2],
    ctrlPointList[3],
    tagLerpValue);
  //
  let dataArray=generateLineListArrayData(data);
  let isShowTangent = document.getElementById("isShowTangent").checked;
  if(isShowTangent){    
    let tagT = tagTangentValue;
    let pos = calCubicBezierCurvePosition(
      ctrlPointList[0],
      ctrlPointList[1],
      ctrlPointList[2],
      ctrlPointList[3],
      tagT
    );
    let tangent = calCubicBezierCurveTangent(
      ctrlPointList[0],
      ctrlPointList[1],
      ctrlPointList[2],
      ctrlPointList[3],
      tagT
    );
    dataArray.push(pos[0]);
    dataArray.push(pos[1]);
    dataArray.push(pos[2]);
    dataArray.push(pos[0]+tangent[0]);
    dataArray.push(pos[1]+tangent[1]);
    dataArray.push(pos[2]+tangent[2]);
  }

  
  let tagDataArray = new Float32Array( dataArray );
  //
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,tagDataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCtrlPoints").onclick=function(evt){
  let data = [];
  for(let i=0;i<ctrlPointList.length;i++)
    data.push(...ctrlPointList[i]);
  //
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCurve").onclick=function(evt){
  UpdateCurveData();
}
document.getElementById("sliderLerp").oninput=function(evt){
  tagLerpValue=this.value;
  UpdateCurveData();
  document.getElementById("labelLerpValue").innerHTML = this.value;
}
document.getElementById("isShowTangent").onclick=function(){
  UpdateCurveData();
}
document.getElementById("sliderTangent").oninput=function(evt){
  tagTangentValue=this.value;
  UpdateCurveData();
  document.getElementById("labelTangentValue").innerHTML = this.value;
}
window.onload = function(){
  window.requestAnimationFrame(myRender);
  
}


執行結果如下

範例的執行結果

這次的程式碼改得有點亂,跟之前的範例不一樣的部分會用紅字來表示,這次提供了 generateLineListArrayData() ,跟之前 generateLineListData() 的差異是回傳的資料型態不一樣,

generateLineListArrayData()  回傳的型態就是單純的 Array ,這是方便這次需要多畫切線要插入資料。 calCubicBezierCurvePosition() 是提供利用百分比來取得曲線的位置, generateCubicBezierCurve() 跟上次不太一樣,取得位置的部分會用 calCubicBezierCurvePosition() 來替代,計算的結果是一樣的,單純的程式碼優化。 calCubicBezierCurveTangent() 這次的主角,套用公式就可以取得該點的切線。接著看到 UpdateCurveData() ,這次由於需要多畫切線,所以用 generateLineListArrayData() 取得資料後再來插入切線資料,最後才轉成 Float32Array ,切線算出來的是變化量,所以要加上該點後才能形成切線的資料。


  切線的公式似乎是將原本的曲線公式作微分的動作,不過由於我數學不好所以不太確定,以後有機會再來研究看看。


參考資料

[ en.wikipedia.org ] Bézier curve

[ stackoverflow.com ] Find the tangent of a point on a cubic bezier curve


相關文章與資料

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )

2020年11月17日 星期二

在特製 Widget 的使用右鍵選單

 在特製 Widget 的使用右鍵選單

前言

  在先前的 在 TreeView 使用右鍵選單 使用過右鍵選單,但 TreeView 是由外部控制,而特製 Widget 時是採用"繼承"的方法來控制,這次把學習的過程做個紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Widget/CustomMenu,執行結果如下

範例的執行結果

在特製的 Widget 上按下右鍵會跳出右鍵選單。


  看到程式碼的部分,看到特製 Widget 的宣告,程式碼如下

#include <QWidget>

#include <QPainter>
#include <QMenu>
#include <QAction>
class MyWidget : public QWidget
{
  Q_OBJECT
public:
  explicit MyWidget(QWidget *parent = nullptr);

protected:
  void paintEvent(QPaintEvent *event) override;
  void onTreeViewCustomMenu(const QPoint &pos);
  void onTreeViewAction1Trigger();
  void onTreeViewAction2Trigger();
  void onTreeViewAction3Trigger();
signals:
  //
private:
  QMenu* m_pTreeViewCustomMenu;
};


右鍵選單在宣告的部分和之前的 TreeView 用馬差不多,只是這次是把事件直接宣告在 Widget ,而非 MainWindow 。接著看到實現的部分,程式碼如下

#include "mywidget.h"

MyWidget::MyWidget(QWidget *parent) : QWidget(parent)
{
  this->setContextMenuPolicy(Qt::CustomContextMenu);
  connect( this , &QWidget::customContextMenuRequested , this , &MyWidget::onTreeViewCustomMenu );
  //Set up custom menu
  m_pTreeViewCustomMenu = new QMenu( this );
  QAction* pAct1 = new QAction( "Action 1" , this );
  connect( pAct1 , &QAction::triggered , this , &MyWidget::onTreeViewAction1Trigger );
  m_pTreeViewCustomMenu->addAction( pAct1 );
  QAction* pAct2 = new QAction( "Action 2" , this );
  connect( pAct2 , &QAction::triggered , this , &MyWidget::onTreeViewAction2Trigger );
  m_pTreeViewCustomMenu->addAction( pAct2 );
  QAction* pAct3 = new QAction( "Action 3" , this );
  connect( pAct3 , &QAction::triggered , this , &MyWidget::onTreeViewAction3Trigger );
  m_pTreeViewCustomMenu->addAction( pAct3 );
}

void MyWidget::paintEvent(QPaintEvent *event)
{
  QPainter painter( this );
  QPen pen( Qt::black );
  pen.setWidth( 4 );
  painter.setPen( pen );
  QRect rc( 4 , 4 , this->width() - 8, this->height() - 8 );
  painter.drawRect( rc );

}

void MyWidget::onTreeViewCustomMenu(const QPoint &pos)
{
  //
  m_pTreeViewCustomMenu->popup( this->mapToGlobal( pos ) );
}

void MyWidget::onTreeViewAction1Trigger()
{
  qDebug( "Action1 triggered" );
}
void MyWidget::onTreeViewAction2Trigger()
{
  qDebug( "Action2 triggered" );
}
void MyWidget::onTreeViewAction3Trigger()
{
  qDebug( "Action3 triggered" );
}


看到 MyWidget::MyWidget() ,開頭需要執行 setContextMenuPolicy() ,這很重要,接著綁定 customContextMenuRequested() 事件,最後是 Menu 的初始化的部分,這部分和之前的 TiewView 範例一樣。觸發事件的部分就和之前 TiewView 的範例一樣,就只是剪貼過來。 右鍵選單的用法不管是外部控制或繼承幾乎是走同一個流程,本來還擔心繼承的用法會有所不同,看來是我多慮了。


參考資料

[ doc.qt.io ] QWidget Class


相關文章與資料

[ GitLab ] HelloQt

在 TreeView 使用右鍵選單

2020年11月10日 星期二

WebGL的繪製三次方赫密特曲線( Cubic hermite curve )

 WebGL的繪製三次方赫密特曲線( Cubic hermite curve )

前言

  在之前的 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 繪製了貝茲曲線,這次來繪製赫密特曲線( Cubic hermite curve ) ,後者常常用在動畫的內插,所以有必要了解與實現,在此把學習的過程做個紀錄。


內容

  赫密特曲線( Cubic hermite curve ) 跟貝茲曲線(  Cubic bezier curve ) 有所不同,並不採用控制點,而採用 Slope ,從 2 個控制點換成 2 個 Slope ,由於我不太會形容差異,但我找到可以線上繪製的網站在 [ www.desmos.com ] 線上繪製赫密特曲線 ,先到該網站體驗一下差異再執行範例會比較好。

  接著就來看範例,如下
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>
<br>
<input id="btnDrawCtrlPoints"type="button" value="DrawCtrlPoints"/>
<br>
<input id="btnDrawCubicHermiteCurve"type="button" value="DrawCubicHermiteCurve"/>
<br>
<input id="btnDrawCubicBezierCurve"type="button" value="DrawCubicBezierCurve"/>
<input type="range" min="2" max="100" value="30" class="slider" id="sliderLerp">
<label id="labelLerpValue">30</label>
</body>
</html>


Javascript 的部分

let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let vboPrimitiveCon = 0;
let vbo=createDynamicBuffer(glCTX1);
let shaderProg = createShader(glCTX1);
let curveType = 0;
//
function createShader(glContext){
  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);  
  
  return prog;
}
function createDynamicBuffer(glContext){
  let vertexBuf = glContext.createBuffer();
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
  let dataArray=new Float32Array([ 
       0.0, 0.5, 0.0,  
      -0.5,-0.5, 0.0,  
      -0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.0, 0.5, 0.0
    ]);
  glContext.bufferData(
    glContext.ARRAY_BUFFER, 
    3000, 
    glContext.DYNAMIC_DRAW
  );
  //write deafult data...
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
  return vertexBuf;
}
function simpleDraw(glContext){
  glContext.useProgram(shaderProg);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  let posLoc = glContext.getAttribLocation(shaderProg, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.LINES, 0, vboPrimitiveCon);
}
function generateLineListData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return new Float32Array(tagAr);
}
function generateBezierCurve(p0,p1,p2,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT;
    let part1Value = 2 * t * invT;
    let part2Value = t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0]);
    tagAr.push(part0[1] + part1[1] + part2[1]);
    tagAr.push(part0[2] + part1[2] + part2[2]);
  }
  return tagAr;
}
function generateCubicBezierCurve(p0,p1,p2,p3,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT * invT;
    let part1Value = 3 * t * invT * invT;
    let part2Value = 3 * t * t * invT;
    let part3Value = t * t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    let part3 = [part3Value*p3[0], part3Value*p3[1], part3Value*p3[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0] + part3[0]);
    tagAr.push(part0[1] + part1[1] + part2[1] + part3[1]);
    tagAr.push(part0[2] + part1[2] + part2[2] + part3[2]);
  }
  return tagAr;
}
function generateCubicHermiteCurve(p0,s0,p1,s1,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = (1 + ( 2 * t ) ) * ( invT * invT );
    let part1Value = t * ( ( invT * invT ) );
    let part2Value = ( t * t ) * ( 3 - ( 2 * t) );
    let part3Value = ( t * t ) * ( t - 1.0 );
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*s0[0], part1Value*s0[1], part1Value*s0[2] ];
    let part2 = [part2Value*p1[0], part2Value*p1[1], part2Value*p1[2] ];
    let part3 = [part3Value*s1[0], part3Value*s1[1], part3Value*s1[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0] + part3[0]);
    tagAr.push(part0[1] + part1[1] + part2[1] + part3[1]);
    tagAr.push(part0[2] + part1[2] + part2[2] + part3[2]);
  }
  return tagAr;
}
function myRender(){
  simpleDraw(glCTX1);
  //
  window.requestAnimationFrame(myRender);
}
//
let ctrlPointList=[
  [-0.9,0.0,0.0],
  [-0.45,0.9,0.0],
  [0.45,0.9,0.0],
  [0.9,0.0,0.0],
];
let tagLerpValue=document.getElementById("sliderLerp").value;
function UpdateCubicBezierCurveData(){
  let data=generateCubicBezierCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[2],
    ctrlPointList[3],
    tagLerpValue);
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
function UpdateCubicHermiteCurveData(){
  let data=generateCubicHermiteCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[3],
    ctrlPointList[2],
    tagLerpValue);
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCtrlPoints").onclick=function(evt){
  let data = [];
  for(let i=0;i<ctrlPointList.length;i++)
    data.push(...ctrlPointList[i]);
  //
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCubicBezierCurve").onclick=function(evt){
  UpdateCubicBezierCurveData();
  curveType = 1;
}
document.getElementById("btnDrawCubicHermiteCurve").onclick=function(evt){
  UpdateCubicHermiteCurveData();
  curveType = 2;
}
document.getElementById("sliderLerp").oninput=function(evt){
  tagLerpValue=this.value;
  //UpdateCurveData();
  switch( curveType ){
    case 1:
      UpdateCubicBezierCurveData();
      break;
    case 2:
      UpdateCubicHermiteCurveData();
      break;
      
  }
  document.getElementById("labelLerpValue").innerHTML = this.value;
}
window.onload = function(){
  window.requestAnimationFrame(myRender);
  
}


執行結果如下

範例的執行結果

這次的範例事由 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 的範例更改而來,重複的部分就不解說了,主要要注意的地方在 updateCubicHermiteCurveData() ,注意控制點的輸入順序和 updateCubicBeizerCurveData() 的順序不一致,這是因為曲線的節點定義不同所造成的,並不是範例有錯。


  赫密特曲線的 Slope 控制起來不是很能預測它會如何彎曲,這會造成很難調成自己想要的曲線,這個部分可能還要再研究一下要如何找出可以預測怎麼預測彎曲的方法。

參考資料

[ wiki ] Cubic Hermite spline


相關文章與資料

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )

[ www.desmos.com ] 線上繪製赫密特曲線

2020年11月3日 星期二

用 JavaScript 裡綁定類別

用 JavaScript 裡綁定類別

前言

  在之前的 用 JavaScript 控制 Widget  裡使用了 Qt 的 Widget 在 JavaScript 中,如果要在 JavaScript 裡使用自己設計的類別有可能嗎?答案是可以的,要如何做呢?在此把學習的過程作紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/JSEngine/WrapClass,執行結果如下

範例的執行結果



範例的上方會有預設的程式碼,按下右下的"Run"可以執行,執行預設的程式碼看列印的偵錯訊息。


  要在 JavaScript 綁定自己的類別,該類別必須繼承 QObject ,這很重要!利用 Qt Creator 的精靈產生類別的話,如下圖

使用精靈產生繼承 QObject 的類別

在精靈的"Class name"填上類別名稱,在"Base class"選擇 QObject 後即可,範例已產生"MyClass",所以看到"MyClass"的宣告,如下

#include <QObject>

class MyClass : public QObject
{
  Q_OBJECT
  Q_PROPERTY(int dataInt READ myInt WRITE setMyInt)
  Q_PROPERTY(double dataDouble READ myDouble WRITE setMyDouble)
  Q_PROPERTY(QString dataString READ myString WRITE setMyString)
public:
  explicit MyClass(QObject *parent = nullptr);
//
  void setMyInt( int value );
  int myInt() const;
  void setMyDouble( double value );
  double myDouble() const;
  void setMyString( const QString& value );
  QString myString() const;
  Q_INVOKABLE void callMethod();
signals:
private:
  int m_MyInt;
  double m_MyDouble;
  QString m_MyString;
};


MyClass 會綁定三個 Property ,"m_MyInt"、"m_MyDouble"與"m_MyString",並綁定一個 Function 名為"callMethod",綁定 Function 到 JavaScript 的部分比較簡單,只需再開頭加"Q_INVOKABLE"即可,但要注意輸入的引數與回傳值得資料型態必須是 JavaScript 可以接受的型態,至於那些型態可以接受以後來研究,這次都先使用基本的型態。在綁定 Property 的部份就比較麻煩一些,綁定些要用到"Q_PROPERTY"來綁定,後方的內容要照一定的格式寫,就拿 m_MyInt 來說,"int"是綁定到 JavaScript 的型態,這個型態最後會被自動轉成 JavaScript 的 "Number" ,"dataInt" 是 Property 的名稱,可以自己命名,不一定要和 C++ 這邊的變數同名,"READ myInt"指的是讀取時所喚起的 Function ,在這裡會喚起 myInt() ,接著是"WRITE setMyInt"指的是寫的時候所喚起的 Function ,這裡會喚起 setMyInt() 。在讀與寫的 Function 也要注意資料型態的問題,字串的部份要用 QString 來綁定而非 std::string, MyClass 的實作部分沒什麼好說明就直接略過,接著看到 MainWindow::MainWindow() ,如下

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->runButton , &QPushButton::clicked , this , &MainWindow::onRunButtonClicked );
  //
  m_cJSEngine.installExtensions( QJSEngine::ConsoleExtension );
  //
  MyClass* pMyClass = new MyClass();
  m_cJSEngine.globalObject().setProperty( "myClass" ,m_cJSEngine.newQObject( pMyClass ) );
  //
  ui->textEdit->setText( tr(
    "console.log( myClass.dataInt );\n"\
    "console.log( myClass.dataDouble );\n"\
    "console.log( myClass.dataString );\n"\
    "myClass.dataInt = 5678;\n"\
    "myClass.dataDouble = 1.414;\n"\
    "myClass.dataString = 'My string';\n"\
    "console.log( myClass.dataInt );\n"\
    "console.log( myClass.dataDouble );\n"\
    "console.log( myClass.dataString );\n"\
    "myClass.callMethod();\n"\
  ) );
}


程式開頭依舊初始化與綁定事件,這次就不說明了,接著直接 New 一個 MyClass ,透過  QJSEngine::newQObject() 來新增變數,就像上次在 用 JavaScript 控制 Widget  裡做的一樣,就著就是寫下預設的 JavaScript 程式碼,單純列印變數與喚起 Method ,這就不說明了。


參考資料

[ doc.qt.io ] QJSEngine Class


相關文章與資料

[ GitLab ] HelloQt

用 JavaScript 控制 Widget