Kity Formula

HTML(SVG)数学公式呈现库

Getting started

Kity Formula(后文简称KF)是基于Kity Graphic图形库的一个数学公式呈现库, 所以在使用KF前需要引入Kity Graphic图形库资源文件。

Kity Graphic图形库项目地址:https://github.com/kitygraph/kity

初始工作

KF的dev版本基于CMD规范开发,在发布之后,通过使用build工具打包 成非CMD版本,所以有如下两种引用资源的方式,分别对应着开发和生产两个场景:

开发环境:

<!DOCTYPE html>
<html>
<head>
    <title>资源引入demo</title>
    <meta charset="utf-8">

    <!-- !假定所有资源文件都在根目录下的dev-lib目录下 -->

    <!-- 引入Kity graphic库 -->
    <script src="/dev-lib/kitygraph.all.min.js"></script>

    <!-- 引入seajs库, seajs是CMD的实现, 具体使用方式请参考http://seajs.org -->
    <script src="../dev-lib/sea-debug.js"></script>

    <!-- 在这里就可以根据需要require不同的KF资源 -->
    <script>

        // 设置seajs引入资源的的base path, 具体意义请参考seajs官网
        seajs.config( {
            base: "../src"
        } );

        // start模块
        define( "start", function ( require ) {

            // 引入公式载体Formula对象, 组成公式的表达式都必须放入一个Formula的实例中才能被呈现出来
            var Formula = require( "formula" ),
                // 引入加法表达式对象
                AdditionExpression = require( "expression/compound-exp/binary-exp/addition" ),
                // 引入文本表达式对象
                TextExpression = require( "expression/text" );

            // 创建载体对象的实例, 其参数代表一个你想要呈现公式的DOM容器
            // 可以根据自己的需要使用任何dom对象, 比如div等
            var formula = new Formula( document.body ),

                // 创建一个加法表达式
                // 对于所有需要文本表达式(TextExpression)对象的地方, 都可以用它的字符串表示形式来代替
                // 比如这里的加法表达式, 实际上相当于:
                // new AdditionExpression( new TextExpression("a"), new TextExpression("b"));
                addExp = new AdditionExpression( new TextExpression("a"), "b" );

            // 把表达式添加到公式中呈现出来
            formula.appendExpression( addExp );

        } );

        // 执行start模块
        seajs.use( "start" );

    </script>
</head>
<body>

</body>
</html>

生产环境:

当前未提供生产环境使用示例

字符集扩展

KF内置了一套基本字符集希腊字符集

要构建一个文本表达式(TextExpression)必须且只能使用已提供的字符集里的字符。对于一般的场景, KF提供的字符集已经能够满足需求,但如果需要使用已提供字符集之外的字符,则你可以扩展该字符集以满足需求。

下面以创建一个希腊字符“α”(alpha)为例, 讲解如何扩展KF的字符集。

  1. 创建字符数据文件

    在KF里面, 一切图形都是通过path绘制出来的,字符也不例外。所以,要创建一个字符,需要提供该字符的path数据。 每个字符都有一个字符边框,用以控制该字符实际占用的空间的大小,同时该字符边框也决定了和其他字符排列在一起时的相对位置。 有了path数据和字符边框之后,你还需要定义好字符在边框中的偏移,以使字符处于边框中的合适位置。总结来说, 提供的字符数据文件应该包含: path(字符数据)、box(字符边框)、offset(偏移量)这三个数据。

    "α"字符数据文件:

    /*!
     * File: src/char/greek/alpha.js
     */
    
    // CMD格式模块定义
    define( {
    
        // path数据,该数据描述了如何去绘制一个“α”字符
        // **这里的path仅仅是一个示例,实际的path比这里的长**
        path: "M31.395,19.297c3.384-4.248,5.76-9.721Z",
    
        // 字符在字符边框内的偏移
        // 当前的值表示, 字符在边框内水平方向上偏移0个单位,在竖直方向上偏移32个单位
        offset: {
            x: 0,
            y: 32
        },
    
        // 字符边框的大小, 字符边框的起始位置应该始终是从( 0, 0 )坐标开始
        box: "M 0 0 L 43 0 L 43 86 L 0 86 Z"
    
    } );
    
  2. 定义数据映射项

    创建了数据文件之后,需要把字符和该数据文件关联起来,KF通过使用映射文件来创建这种关联关系。

    映射文件实际上是一个JS的普通对象(PlainObject),其Key表示要映射的字符,Value是该字符所对应的数据文件。 需要注意的是,Key必须是单个字符。在这个示例里,不能通过键盘方便地输入字符“α”, 这就会造成在构建文本表达式的时候输入上会有障碍,KF为解决这种情况提供了一个字符别名的机制, 允许你通过有意义的别名来代表一个特殊的字符。

    字符别名是以一个“\”开始,并以一个“\”结尾,其间可以包含任意个非“\”字符字符序列

    以下是字符“α”的映射项:

    /*!
     * File: src/char/data.js
     */
    define( function ( require, exports, module ) {
    
        return {
    
            // 省略了其他字符的映射关系
            ...
    
            // u03B1 是“α”的unicode编码
            // 这里直接把字符映射到数据文件是必要的
            // 这能使用户直接输入“α”字符时也能找到正确的数据文件
            "\u03B1": require( "char/data/greek/alpha" ),
    
            // “α”的字符别名
            "\\alpha\\": require( "char/data/greek/alpha" ),
            ...
    
        };
    
    } );
    
  3. 使用新扩展的字符

    创建好字符之后就可以在TextExpression(文本表达式)中使用该字符了。

    /*!
     * 使用示例文件,
     */
    define( function ( require, exports, module ) {
    
        // 引入所需对象
        // 无须直接处理字符对象, 仅仅只需要有TextExpression对象就足够了
    
        var Formula = require( "formula" ),
            TextExpression = require( "expression/text" );
    
        // 以body为容器创建一个公式
        var formula = new Formula( document.body );
    
        // 向公式中添加一个文本表达式,该文本表达式使用了我们刚刚创建的“α”字符
        // 在创建文本表达式时, 直接传递了字符“α”
        // 也可以用另一种更简单更易输入的方式来创建
        //      new TextExpression( "\\alpha\\" );
    
        formula.appendExpression( new TextExpression( "α" ) );
    
    } );
    

以上就是创建一个字符的完整过程,在实际应用中,可以定义任何字符,需要注意的是: 字符只能用于文本表达式中,而不能出现在其类型的表达式中

表达式扩展

KF提供了大部分常用的表达式,如:加、减、乘、除、方根、幂、求和、积分、上下标等表达式, 但如果你所需的表达式KF并没有提供,你可以通过简单的步骤来扩展表达式集,帮助KF为你绘制出正确的表达式。

下面以加法表达式来说明创建一类表达式所需的步骤。

  1. 创建操作符

    在KF里,除了文本表达式(TextExpression),其他所有的表达式都对应着一个操作符, 比如加法表达式(AdditionExpression)就对应着一个加法操作符(AdditionOperator)。

    所以,我们需要先创建一个加法操作符。

    /*!
     * File: src/operator/binary-opr/addition.js
     */
    define( function ( require, exports, modules ) {
    
        var kity = require( "kity" );
    
        // kity.createClass 方法是Kity Graphic库里的方法, 用于创建一个类
        // 具体的使用方法请查看Kity Graphic官方文档
    
        return kity.createClass( 'AdditionOperator', {
    
            // 所有的操作符都继承于基类Operator
            base: require( "operator/operator" ),
    
            // 构造函数
            constructor: function () {
    
                this.callBase( "Addition" );
    
                // 绘制符号图形
                // 如果一个操作符没有显示的操作符号, 则可以什么也不画
                // addOperatorShape() 会把绘制出来的矩形添加到该表达式的画布中
                this.addOperatorShape( new kity.Rect( 0, 20, 43, 3, 3 ).fill( "black" ) );
                this.addOperatorShape( new kity.Rect( 20, 0, 3, 43, 3 ).fill( "black" ) );
    
                // 设置操作符的边框大小, 根据操作符的不同这里设置不同的值
                // 默认为 width: 0, height: 0
                this.setBoxSize( 43, 43 );
    
            },
    
            // 当该操作符所属的表达式被添加到Formula对象上时, 将会调用该方法
            // applyOperand接受的参数是来自于其对应的表达式的操作数
            // 该方法的详细说明在后文中会提到
            applyOperand: function ( operand... /*操作数列表*/ ) {
    
                /* 在这里处理操作数和操作符之间的偏移和大小等工作 */
                /* 对于加法操作符,当前只需要调整操作数和操作符的偏移位置即可 */
    
            }
    
        } );
    
    } );
    

    上面的文件示例创建了加法操作符,特别需要注意的是,如果你将要创建的表达式没有显示的操作符, 你也应该有一个操作符对象,并且该操作符对象至少要具有以上所示的结构, 那怕这些构造函数和方法什么都不做。

  2. 创建表达式

    有了操作符以后,我们需要创建一个表达式。表达式才是提供给使用者直接使用的接口。

    /*!
     * File: src/expression/compound-exp/binary-exp/addition.js
     * 加法表达式
     */
    define( function ( require, exports, modules ) {
    
        var kity = require( "kity" ),
            // 引入加法操作符对象, 加法表达式包含了加法操作符
            AdditionOperator = require( "operator/binary-opr/addition" );
    
        return kity.createClass( 'AdditionExpression', {
    
            // 所有的表达式都直接或间接继承于复合表达式(CompoundExpression)
            base: require( "expression/compound" ),
    
            constructor: function ( firstOperand, lastOperand ) {
    
                this.callBase();
    
                // 根据参数设置不同的操作数
                this.setFirstOperand( firstOperand );
                this.setLastOperand( lastOperand );
    
                // 由于是加法表达式, 所以要求在构造表达式的时候就要存在操作符对象
                // 如果不这么做, 那么用户在表达式被添加到Formula对象中前,
                // 需要手动调用setOperator()设置操作符
                this.setOperator( new AdditionOperator() );
    
            },
    
            /* 一系列自定义方法, 用于根据操作符的不同,设置不同的操作数 */
    
            setFirstOperand: function ( operand ) {
    
                return this.setOperand( operand, 0 );
    
            },
    
            getFirstOperand: function () {
    
                return this.getOperand( 0 );
    
            },
    
            setLastOperand: function ( operand ) {
    
                return this.setOperand( operand, 1 );
    
            },
    
            getLastOperand: function () {
    
                return this.getOperand( 1 );
    
            }
    
        } );
    
    } );
    
  3. applyOperand的参数

    在成功创建表达式和操作符之后,需要再说明一下applyOperand()的参数问题。

    以上面刚创建的加法表达式为例,以下代码:

    /* 省略了其他代码 */
    new AdditionExpression( new TextExpression("a"), new TextExpression("b") );
    

    创建了一个以“a”为前操作数,以“b”为后操作数的加法表达式,从表达式的构造函数中我们可以看出, 构造函数分别调用了setFirstOperand()和setLastOperand()方法存储这两个操作数。 而setFirstOperand()和setLastOperand()方法分别调用了其父类的setOperand()方法, 这些操作的结果就是把用户传递的两个文本表达式“a”和“b”按一定的顺序存储了起来。 当用户把一个表达式添加到Formula对象中时,就会递归地调用该表达式及其所包含的所有子表达式的applyOperand方法, 同时,在调用applyOperand方法时, 会把之前存储的操作数作为参数依次传递过去。 在这个示例里,即applyOperand()方法调用时,接受到的参数就是用户在构造函数中传递的两个文本表达式的引用。

    而applyOperand方法的职责就是在被调用时,根据操作符的要求去排列、调整传递进来的操作数, 甚至在此时才去绘制操作符,而不是在操作符构造函数中去绘制, 这种方法对于某些需要根据操作数的不同而具有不同大小和外观的操作符来说是唯一的方案。比如: 方根和分数操作符。

其他相关资源

更多的资源可以参考源代码目录中的examples目录下的示例。