Hightopo 2D 入门

Stella981
• 阅读 741

这是一片 HT 的入门级文章,如果您能读懂
http://www.hightopo.com/guide/guide/core/beginners/examples/example_overview.html
http://www.hightopo.com/guide/guide/core/beginners/examples/example_node.html
两个例子,那么可以跳过这篇文章,如果你对 ht.graph.GraphView,ht.DataModel 和 ht.Node 三者之间的关系还不是很了解,不知道如何工作的,那么不妨看下去,相信这篇文章能够帮到你。

之前在 cnblog 搜索到关于入门的例子,比如 http://www.cnblogs.com/xhload3d/p/5911978.htmlhttps://www.cnblogs.com/xhload3d/p/8249304.html 有讲解上面三者的关系,但是以前并没有看得很明白,我也是通过和 HT 的技术支持接触才慢慢理解 HT 是如何工作。下面通过一篇小文章像大家讲解下这三者总体上的关系,希望能帮助到刚接触这个框架的人。

既然你是在入门框架的时候遇到困难然后找到这篇博客,那么不妨先抛弃 HT ,通过一个小例子模拟下 HT 上三者的关系。
该例子使用了一些 es6 的语法,比如箭头函数和 class,如果你对es6不熟悉,可以移步 http://exploringjs.com/es6/ 了解。如果你有一定 JavaScript 功底,可以直接跳过看最终 demo。当然也可以跟随 demo,或者边看过做,这样或者能更好理解。

划 demo 核心点:

  1. View 作为展示层,会绑定一个 Model,然后根据Model里面的内容展示出内容
  2. Model 里面会储存要显示的图元信息和绑定他的组件,并在图元变化的时候更新组件
  3. Node 引用一个 DIV 来模拟一个图元

核心关系:View 绑定 Model,Model 管理很多 Node,Node 发生变化时通知 Model,然后 Model 更新绑定他的 View 组件。

demo 开始(下面有些地方说的 node,有些地方说的 data,暂时可以理解为一个概念,但其实不是,在学习 HT 的过程中你会了解到),新建一个 index.html,并插入如下内容

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body onload=init()>
  <script>
    function init(){
      
    }
  </script>
</body>
</html>

下面开始建 View组件,View组件 主要用于展示作用,展示层元素挂载到组件的 _view 上面,script标签里插入如下代码:

class View{
  constructor(){
    this._view = document.createElement('div');
    const style = this._view.style;
    style.position = 'absolute';
    style.top = 0;
    style.right = 0;
    style.bottom = 0;
    style.left = 0;
  }
  getView(){
    return this._view;
  }
  addToDom(parentNode){
    if(!!parentNode) {
      parentNode.appendChild(this.getView());
    } else {
      document.body.appendChild(this.getView());
    }
  }
}

并在 init 函数里面新建 view实例 并加入到 DOM 中,init 函数如下:

function init(){
  view = new View();
  view.addToDom();
}

此时在浏览器中打开 index.html,暂时的确什么都没有,但如果你在控制台 Elements 里面看到有个 div 插入到 script 标签下面,那么代表到这里你是成功的。

下面开始创建 Model 组件,首先分析一下 Model 的作用

  • 被不同的 view 组件绑定,然后在他管理的 data 元素发生改变时,通知绑定的 view 进行更新
  • 增加 data 元素并附加遍历 data 功能。

所以 Model 组件需要几个接口

  1. addListener: 用于给view层注册更新函数
  2. handleDataChange: 当管理的data元素更新时,调用view层注册的更新函数
  3. add,each,getDatas 分别是增加 data 元素,遍历 data 和获取 data 数组

创建 Model 组件代码如下:

class Model{
  constructor() {
    this._datas = [];
    this.listeners = [];
  }
  addListener(fn){
    this.listeners.push(fn);
  }
  handleDataChange(){
    this.listeners.forEach(fn => fn());
  }
  add(node){
    node.setModel(this);
    if(this._datas.includes(node)){
      return;
    }
    this._datas.push(node);
    this.handleDataChange();
  }
  each(fn){
    this._datas.forEach((data, index, list) => {
      fn(data, index, list)
    })
  }
  getDatas(){
    return this._datas;
  }
}

当然现在界面上依然什么都没有,因为还没有为 Model 加入任何展示的 Node,创建Node代码如下:

class Node{
  constructor() {
    this._node = document.createElement('div');
    this._name = '';
    const style = this._node.style;
    style.position = 'absolute';
    style.top = 0;
    style.left = 0;
    style.height = '100px';
    style.width = '100px';
    style.overflow = 'hidden';
    style.background = '#D8D8D8';
  }
  getElString(){
    return this._node.outerHTML;
  }
  fireChange(){
    !!this._model && this._model.handleDataChange();
  }
  setPosition(x, y){
    const style = this._node.style;
    style.left = x + 'px';
    style.top = y + 'px';
    this.fireChange();
  }
  setX(x){
    this._node.style.left = x + 'px';
    this.fireChange()
  }
  setY(y){
    this._node.style.top = y + 'px';
    this.fireChange();
  }
  setImage(url){
    const style = this._node.style;
    if(!!url){
      this._node.innerHTML = '';
      style.background = `url(${url}) no-repeat center`;
      this.fireChange();
    }
  }
  setSize(width, height){
    const style = this._node.style;
    style.width = width + 'px';
    style.height = height + 'px';
    this.fireChange();
  }
  setWidth(width){
    this._node.style.width = width + 'px';
    this.fireChange()
  }
  setHeigth(height){
    this._node.style.height = height + 'px';
    this.fireChange();
  }
  setName(name){
    this._name = name;
    this._node.innerHTML = name;
    this.fireChange();
  }
  setModel(model){
    this._model = model;
  }
}

这里暂时使用 _node 来挂载一个 div,然后操作 div 的一些属性显示出来,就像 canvas 上绘制一个矩形,如果你有基本的 JavaScript 功底,这里的 setXXX 函数功能应该都不会陌生,而 setModel 功能是让该 node 知道它是被哪一个 Model 管理,fireChange 功能则是通知 Model 有更新

当 Model 被通知更新调用 handleDataChange 的时候,功能则是执行注册的所有更新函数,来达到更新所有绑定该 Model 组件的目的。
此时 init 函数可以稍微修改一下来显示出一点内容,修改后 init 函数如下:

function init(){
  model = new Model()
  view = new View(model);
  view.addToDom();

  node1 = new Node();
  node1.setPosition(30, 30);
  node1.setName('我是node1');
  model.add(node1);
}

此时刷新页面还是什么都没有,因为 View 组件暂时缺少绑定 Model 和更新的方法,View 组件更新后代码如下:

class View{
  constructor(model){
    this._view = document.createElement('div');
    const style = this._view.style;
    style.position = 'absolute';
    style.top = 0;
    style.right = 0;
    style.bottom = 0;
    style.left = 0;
    !!model && this.setModel(model);
  }
  getView(){
    return this._view;
  }
  setModel(model){
    this._model = model;
    model.addListener(this.invalidate.bind(this));
  }
  invalidate(){
    const view = this.getView();
    let innerHTML = '';
    view.innerHTML = '';
    this._model.each((data) => {
      innerHTML += data.getElString();
    })
    view.innerHTML = innerHTML;
  }
  addToDom(parentNode){
    if(!!parentNode) {
      parentNode.appendChild(this.getView());
    } else {
      document.body.appendChild(this.getView());
    }
    this.invalidate();
  }
}

在 View 组件的构造函数中支持了可选的 model,setModel 函数可以供组件在后期更换 Model,在该函数中会让 model 注册该 view 组件的 invalidate 函数,invalidate 会在 Model 发生更新的时候被调用,此时再刷新一下浏览器,会发现一个 div 处于屏幕上,他的位置由 node.setPosition 决定。

第一版的 demo 到此完成,此时你应该理解 view<-->model<-->node 他们的关系,但是此时你可能会有一个疑问,node 的管理为什么不直接在它要显示的 view 组件上,而是要一个专门的 Model 管理,然后 view 去使用 model,HT 的设计是强大的,他可以让你在不同的 view 上显示相同的 model 类容,而且当 node 改变时,所有的 view 会同步更新。

现在先用两个不同的 view 来演示一下,在 body 下面加入两个 div 分别命名 view1 和 view2,这部分代码参考如下:

<body onload=init()>
  <div id="view1"></div>
  <div id="view2"></div>
  <script>
    class View{
    ...

然后为这两个 div 加一点样式,在 title 下面加入 style 标签并加入如下样式:

<style>
  div {
    box-sizing: border-box;
    overflow: hidden;
  }
  #view1 {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    width: 50%;
    height: 400px;
    border: 2px solid #4080BF;
  }
  #view2 {
    position: absolute;
    top: 0;
    right: 0;
    width: 50%;
    height: 400px;
    border: 2px solid #4080BF;
  }
</style>

最后在 init 函数里面建立两个 view 对象并分别挂载到 view1 和 view2 下面,修改后的init函数如下:

function init(){
  model = new Model()
  view = new View(model);
  view.addToDom(document.getElementById('view1'));

  node1 = new Node();
  node1.setPosition(30, 30);
  node1.setName('我是node1');
  model.add(node1);

  view2 = new View(model);
  view2.addToDom(document.getElementById('view2'))
}

现在刷新浏览器,会看到左右两个蓝框的div左上角分别有两个灰色的方块,里面显示的内容通过 node.setName() 设定

到这里你应该更加理解 view 和 model 的关系,但是可能你还有一个疑惑,干嘛需要两个相同的 view 来显示相同的内容。在一些场合,可能你不只是需要展示图形,还需要一个表格来展示 model 里面 data 元素的一些具体属性,比如 http://www.hightopo.com/guide/guide/core/beginners/examples/example_overview.html 左下方 TableView 组件 所示,这儿用 demo 模拟一下他们的工作。要创建一个 TableView,会发现它和已有的 View 有些类似,比如 setModel 和 addToDom,当然两者的内容肯定是不一样的,所以依靠 es6 class 和 extends,对 view 做一些修改以满足它可以被扩展,View 代码修改如下:

class View{
  constructor(model){
    this._view = document.createElement('div');
    const style = this._view.style;
    style.position = 'absolute';
    style.top = 0;
    style.right = 0;
    style.bottom = 0;
    style.left = 0;
    !!model && this.setModel(model);
  }
  getView(){
    return this._view;
  }
  setModel(model){
    this._model = model;
    model.addListener(this.invalidate.bind(this));
  }
  addToDOM(parentNode){
    if(!!parentNode) {
      parentNode.appendChild(this.getView());
    } else {
      document.body.appendChild(this.getView());
    }
    this.invalidate();
  }
}

主要修改是去掉 invalidate 方法,然后让扩张的组件来实现这个方法,建立第一个扩张组件:

class SimulateGraphView extends View{
  invalidate(){
    const view = this.getView();
    let innerHTML = '';
    view.innerHTML = '';
    this._model.each((data) => {
      innerHTML += data.getElString();
    })
    view.innerHTML = innerHTML;
  }
}

此时的 demo 肯定是无法工作,因为 init 函数里面还在使用View来实例化组件,所以需要将 new View 修改为 new SimulateGraphView,init 函数此时如下:

function init(){
  model = new Model()
  view = new SimulateGraphView(model);
  view.addToDOM(document.getElementById('view1'));

  node1 = new Node();
  node1.setPosition(30, 30);
  node1.setName('我是node1');
  model.add(node1);

  view2 = new SimulateGraphView(model);
  view2.addToDOM(document.getElementById('view2'))
}

刷新浏览器代码工作正常。然后要开始建立第二个扩展组件 TableView,同样继承自 View,所以也拥有 setModel 等方法,与 SimulateGraphView 的主要不同在于 invalidate 函数,TableView 代码如下:

class TableView extends View{
  constructor(model){
    super(model);
    this.content = `
      <table>
        <tr>
          <th>name</th>
          <th>x</th>
          <th>y</th>
          <th>width</th>
          <th>height</th>
        </tr>
        __content__
      <table>
    `;
  }
  invalidate(){
    const view = this.getView();
    let content = '';
    view.innerHTML = '';
    this._model.each((data) => {
      content += `
        <tr>
          <td>${data.getName()}</td>
          <td>${data.getX()}</td>
          <td>${data.getY()}</td>
          <td>${data.getWidth()}</td>
          <td>${data.getHeight()}</td>
        </tr>
      `
    })
    view.innerHTML = this.content.replace(/__content__/, content);
  }
}

可以看到此表格主要作用显示绑定的 Model 里面 node 的一些属性,比如 name,坐标 x 和 y 和宽度高度,此时 node 对象上还缺少这些方法,先给 Node 加上这些方法,修改后 Node 代码如下:

class Node{
  constructor() {
    this._node = document.createElement('div');
    this._name = '';
    const style = this._node.style;
    style.position = 'absolute';
    style.top = 0;
    style.left = 0;
    style.height = '100px';
    style.width = '100px';
    style.overflow = 'hidden';
    style.background = '#D8D8D8';
  }
  getElString(){
    return this._node.outerHTML;
  }
  fireChange(){
    !!this._model && this._model.handleDataChange();
  }
  setPosition(x, y){
    const style = this._node.style;
    style.left = x + 'px';
    style.top = y + 'px';
    this.fireChange();
  }
  setX(x){
    this._node.style.left = x + 'px';
    this.fireChange()
  }
  setY(y){
    this._node.style.top = y + 'px';
    this.fireChange();
  }
  getPosition(){
    return {x: this._node.style.left, y: this._node.style.top}
  }
  getX(){
    return this._node.style.left;
  }
  getY(){
    return this._node.style.top;
  }
  setImage(url){
    const style = this._node.style;
    if(!!url){
      this._node.innerHTML = '';
      style.background = `url(${url}) no-repeat center`;
      this.fireChange();
    }
  }
  setSize(width, height){
    const style = this._node.style;
    style.width = width + 'px';
    style.height = height + 'px';
    this.fireChange();
  }
  setWidth(width){
    this._node.style.width = width + 'px';
    this.fireChange()
  }
  getWidth(){
    return this._node.style.width;
  }
  setHeigth(height){
    this._node.style.height = height + 'px';
    this.fireChange();
  }
  getHeight(height){
    return this._node.style.height;
  }
  setName(name){
    this._name = name;
    this._node.innerHTML = name;
    this.fireChange();
  }
  getName(){
    return this._name;
  }
  setModel(model){
    this._model = model;
  }
}

此时 table 组件基本可以正常工作,但是还缺少一个挂载的 div,修改下 body 下里面内容如下:

<body onload = init()>
  <div id="view1"></div>
  <div id="view2"></div>
  <div id='view3'></div>
  <script>
    class View{
    ...

然后再修改一下 CSS,修改后 style 如下:

<style>
  div {
    box-sizing: border-box;
    overflow: hidden;
  }
  #view1 {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    width: 50%;
    height: 400px;
    border: 2px solid #4080BF;
  }
  #view2 {
    position: absolute;
    top: 0;
    right: 0;
    width: 50%;
    height: 400px;
    border: 2px solid #4080BF;
  }
  table {
    border-collapse: collapse;
    border-spacing: 0px;
  }
  table, th, td {
    padding: 5px;
    border: 1px solid black;
  }
  #view3 {
    position: absolute;
    top: 410px;
    right: 0;
    width: 100%;
    height: 300px;
    border: 2px solid #4080BF;
  }
</style>

接下来 new 一个 table 实例出来挂载到 view3 下面,此时 Model 只有一个图元,再加入一个演示,修改后 init 函数如下:

function init(){
  model = new Model();
  view = new SimulateGraphView(model);
  view.addToDOM(document.getElementById('view1'));

  node1 = new Node();
  node1.setPosition(30, 30);
  node1.setName('我是node1');
  model.add(node1);

  node2 = new Node();
  node2.setPosition(30, 150);
  node2.setName('我是node2');
  node2.setSize(200, 80)
  node2.setImage('http://www.hightopo.com/images/logo.png');
  model.add(node2);

  view2 = new SimulateGraphView(model);
  view2.addToDOM(document.getElementById('view2'));

  table = new TableView(model);
  table.addToDOM(document.getElementById('view3'));
}

刷新浏览器,可以在下方看到一个 table 显示 Model 里面 node 的一些属性,当然需要一些改变才能感受到效果,所以这时候可以打开控制台,然后在 Console 面板下面输入: node2.setPosition(200, 100) 并执行,这时候你会发现 graphView 和 table 都同步更新了,此时你可以在控制台里对 node1 和 node2 执行下其他的操作比如 node1.setSize(200, 60), graphView 和 table 同样都会更新。

Hightopo 2D 入门

这么长的 dmeo 到此就结束了,其实并不麻烦,主要目的是为了给大家介绍下 View,Model 和 Node 之间的关系,那么再回到 HT 
划 HT 重点:

  1. ht.graph.GraphView 是作为展示层的组件,也就是我们看到的东西都由他来呈现,每个组件上有个 _view 属性挂载着展示层的 div,可以通过 graphView.getView() 来获取,所以只要把这个组件插入到你的 DOM 里面, 就可以显示出图形。而显示的图形则是根据该组件绑定的 DataModel 决定。其他的功能性组件,如 TablePane 都需要一个 DataModel 来显示内容。
  2. ht.DataModel 是一个数据集,他管理着很多 ht.Data,可以通过 dotaModel.getDatas() 得到一个 ht.List,里面包含数据容器所管理的数据,每一个元素都是 ht.Data 或它的子类实例,而如果你需要在ht.graph.GraphView 上面显示出类容,那么每一个数据必须是 ht.Node 或它的子类实例( ht.Node 继承于 ht.Data )。
  3. ht.Node 抽象要显示的每一个数据元,比如一个图形名字,宽高,和位置,图片等所有其他信息,处了 ht.Node 之外,HT 还提供了很多其他类型的图元如线段和组,详见 http://www.hightopo.com/guide/guide/core/beginners/ht-beginners-guide.html#ref_node 及下面的内容。

现在结合 demo 的例子再来看这几条重点,应该好理解多了吧!

如果读到这里感觉没有问题,可以移步 http://www.hightopo.com/guide/guide/core/datamodel/ht-datamodel-guide.html#ref_designpattern 阅读下官方关于 DataModel 及其他几个核心概念的说明。然后基本所有 HT 关于 2d 的demo应该都能看明白。

关于 demo 划重点:

  1. demo 里面每一个 node 都是由 div 模拟,这是 html 里面实实在在存在的一个基本元素,但是 ht.Data 不是一个实实在在的 HTMLElement,每一个 data 的呈现都是 canvas 上的一部分类容。
  2. demo 主要内容只是为了介绍  ht.graph.GraphView 等展示层组件和 ht.DataModel 和 ht.Data 之间的关系,为了介绍总体关系和大体工作流程,所以请忽略 demo 里面 Node 会挂载一个 div,这条更是强调上一条重点。
  3. HT 的工作流程复杂到大概是这个 demo 的...额10个手指头算不过来还是不算了,所以不要以为 HT 就是这么简单!不要因为我的 demo 降低你的兴趣,请你深究并感受 HT 的美。

HT 中文网地址:

http://www.hightopo.com/cn-index.html

最后 demo 下载地址:

https://github.com/MuyNooB/ht-start

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
P2P技术揭秘.P2P网络技术原理与典型系统开发
Modular.Java(2009.06)\.Craig.Walls.文字版.pdf:http://www.t00y.com/file/59501950(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.t00y.com%2Ffile%2F59501950)\More.E
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Easter79 Easter79
3年前
Swift项目兼容Objective
!456.jpg(http://static.oschina.net/uploads/img/201509/13172704_1KcG.jpg"1433497731426906.jpg")本文是投稿文章,作者:一叶(博客(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2F00red
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这