Переглянути джерело

添加上格云绘图引擎库.

庞利祥 6 роки тому
батько
коміт
e6787edb4d
37 змінених файлів з 4303 додано та 3 видалено
  1. 3 3
      saga-web-base/package.json
  2. 9 0
      saga-web-draw/.editorconfig
  3. 32 0
      saga-web-draw/.eslintrc.js
  4. 12 0
      saga-web-draw/.npmignore
  5. 12 0
      saga-web-draw/jest.config.js
  6. 43 0
      saga-web-draw/package.json
  7. 79 0
      saga-web-draw/src/SBrush.ts
  8. 418 0
      saga-web-draw/src/SCanvasView.ts
  9. 196 0
      saga-web-draw/src/SColor.ts
  10. 69 0
      saga-web-draw/src/SFont.ts
  11. 21 0
      saga-web-draw/src/SGradient.ts
  12. 36 0
      saga-web-draw/src/SGradientStop.ts
  13. 114 0
      saga-web-draw/src/SLinearGradient.ts
  14. 641 0
      saga-web-draw/src/SPainter.ts
  15. 238 0
      saga-web-draw/src/SPath2D.ts
  16. 78 0
      saga-web-draw/src/SPen.ts
  17. 136 0
      saga-web-draw/src/SRadialGradient.ts
  18. 490 0
      saga-web-draw/src/engines/SCanvasPaintEngine.ts
  19. 276 0
      saga-web-draw/src/engines/SPaintEngine.ts
  20. 67 0
      saga-web-draw/src/engines/SPaintState.ts
  21. 371 0
      saga-web-draw/src/engines/SSvgPaintEngine.ts
  22. 13 0
      saga-web-draw/src/enums/SAspectRatioMode.ts
  23. 11 0
      saga-web-draw/src/enums/SBrushType.ts
  24. 13 0
      saga-web-draw/src/enums/SLineCapStyle.ts
  25. 13 0
      saga-web-draw/src/enums/SLineJoinStyle.ts
  26. 15 0
      saga-web-draw/src/enums/SPaintEngineType.ts
  27. 13 0
      saga-web-draw/src/enums/STextAlign.ts
  28. 14 0
      saga-web-draw/src/enums/STextBaseLine.ts
  29. 11 0
      saga-web-draw/src/enums/STextDirection.ts
  30. 58 0
      saga-web-draw/src/index.ts
  31. 170 0
      saga-web-draw/src/types/SLine.ts
  32. 92 0
      saga-web-draw/src/types/SPoint.ts
  33. 287 0
      saga-web-draw/src/types/SRect.ts
  34. 160 0
      saga-web-draw/src/types/SSize.ts
  35. 70 0
      saga-web-draw/src/utils/SPolygonUtil.ts
  36. 16 0
      saga-web-draw/tsconfig.json
  37. 6 0
      saga-web-draw/typedoc.json

+ 3 - 3
saga-web-base/package.json

@@ -1,6 +1,6 @@
 {
-    "name": "@sagacloud-web/base",
-    "version": "2.1.7",
+    "name": "@saga-web/base",
+    "version": "2.1.8",
     "description": "上格云Web基础库。",
     "main": "lib/index.js",
     "types": "lib/index.d.js",
@@ -18,7 +18,7 @@
     "author": "庞利祥 (sybotan@126.com)",
     "license": "ISC",
     "publishConfig": {
-        "registry": "http://dev.dp.sagacloud.cn:8082/repository/npm-host/"
+        "registry": "http://dev.dp.sagacloud.cn:8082/repository/npm-hosted/"
     },
     "devDependencies": {
         "@typescript-eslint/eslint-plugin": "^1.12.0",

+ 9 - 0
saga-web-draw/.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+end_of_line =lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 32 - 0
saga-web-draw/.eslintrc.js

@@ -0,0 +1,32 @@
+module.exports = {
+    root: true,
+    parser: '@typescript-eslint/parser',
+    extends: [
+        'plugin:@typescript-eslint/recommended',
+        // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
+        'prettier/@typescript-eslint',
+        // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
+        // 此行必须在最后
+        'plugin:prettier/recommended'
+    ],
+    env: {
+        es6: true,
+        node: true
+    },
+    parserOptions: {
+        // 支持最新 JavaScript
+        ecmaVersion: 2018,
+        sourceType: 'module'
+    },
+    rules: {
+        // 缩进
+        'indent': ['error', 4],                         // 缩进控制4空格
+        'max-len': ['error', 120],                      // 每行字符不超过120
+        'no-mixed-spaces-and-tabs': 'error',            // 禁止使用空格和tab混合缩进
+        // 语句
+        'curly': ["error", "multi-line"],               // if、else if、else、for、while强制使用大括号,但允许在单行中省略大括号。
+        'semi': ['error', 'always'],                    //不得省略语句结束的分号
+        '@typescript-eslint/no-unused-vars': 'off',     // 取消未使用变量检查
+        '@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }]       // public访问不需加访问修饰符
+    }
+};

+ 12 - 0
saga-web-draw/.npmignore

@@ -0,0 +1,12 @@
+# 发布时排除
+__tests__/
+api/
+docs/
+src/
+.editorconfig
+.eslintrc.js
+*.iml
+coverage
+jest.config.js
+tsconfig.json
+typedoc.json

+ 12 - 0
saga-web-draw/jest.config.js

@@ -0,0 +1,12 @@
+module.exports = {
+    preset: "ts-jest",
+    moduleFileExtensions: ["js", "ts"],
+    transform: {
+        "^.+\\.tsx?$": "ts-jest"
+    },
+    transformIgnorePatterns: ["/node_modules/"],
+    moduleNameMapper: {
+        "^@/(.*)$": "<rootDir>/src/$1"
+    },
+    collectCoverage: true
+};

+ 43 - 0
saga-web-draw/package.json

@@ -0,0 +1,43 @@
+{
+    "name": "@saga-web/draw",
+    "version": "2.1.56",
+    "description": "上格云绘制引擎。",
+    "main": "lib/index.js",
+    "types": "lib/index.d.js",
+    "scripts": {
+        "build": "tsc",
+        "publish": "npm publish",
+        "lint": "eslint src/**/*.{js,ts,tsx}",
+        "test": "jest",
+        "typedoc": "typedoc --hideGenerator src ../saga-web-base/src"
+    },
+    "keywords": [
+        "draw"
+    ],
+    "author": "庞利祥 (sybotan@126.com)",
+    "license": "ISC",
+    "publishConfig": {
+        "registry": "http://dev.dp.sagacloud.cn:8082/repository/npm-hosted/"
+    },
+    "devDependencies": {
+        "@typescript-eslint/eslint-plugin": "^1.12.0",
+        "@typescript-eslint/parser": "^1.12.0",
+        "eslint": "^6.0.1",
+        "eslint-config-prettier": "^6.0.0",
+        "eslint-plugin-prettier": "^3.1.0",
+        "prettier": "^1.18.2",
+        "@types/jest": "^24.0.15",
+        "ts-jest": "^24.0.2",
+        "jest-canvas-mock": "^2.1.0",
+        "typescript": "^3.5.3"
+    },
+    "dependencies": {
+        "@saga-web/base": "^2.1.8"
+    },
+    "jest": {
+        "setupFiles": [
+            "./__setups__/canvas.js",
+            "jest-canvas-mock"
+        ]
+    }
+}

+ 79 - 0
saga-web-draw/src/SBrush.ts

@@ -0,0 +1,79 @@
+import { SBrushType } from "./enums/SBrushType";
+import { SColor, SGradient } from "./";
+
+/**
+ * 画刷
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SBrush {
+    /** 画笔类型 */
+    type: SBrushType = SBrushType.Color;
+
+    /** 画刷颜色 */
+    private _color = SColor.Black;
+    get color(): SColor {
+        return this._color;
+    } // Get color
+    set color(value: SColor) {
+        this.type = SBrushType.Color;
+        this._color = value;
+    } // Set color
+
+    /** 画刷渐变 */
+    private _gradient: SGradient | null = null;
+    get gradient(): SGradient | null {
+        return this._gradient;
+    } // Get gradient
+    set gradient(value: SGradient | null) {
+        this._gradient = value;
+        this.type = SBrushType.Gradient;
+    } // Set gradient
+
+    /**
+     * 构造函数
+     */
+    constructor();
+
+    /**
+     * 构造函数
+     *
+     * @param   brush       画刷
+     */
+    constructor(brush: SBrush);
+
+    /**
+     * 构造函数
+     *
+     * @param   color       颜色
+     */
+    constructor(color: SColor);
+
+    /**
+     * 构造函数
+     *
+     * @param   gradient    过渡
+     */
+    constructor(gradient: SGradient);
+
+    /**
+     * 构造函数(重载实现)
+     *
+     * @param   brush       画笔
+     */
+    constructor(brush?: SColor | SGradient | SBrush) {
+        if (brush == undefined) {
+            return;
+        }
+
+        // 如果是渐变类型
+        if (brush instanceof SGradient) {
+            this.gradient = brush;
+        } else if (brush instanceof SBrush) {
+            this.type = brush.type;
+            this.color = new SColor(brush.color);
+        } else {
+            this.color = new SColor(brush);
+        }
+    } // Constructor()
+} // Class SBrush

+ 418 - 0
saga-web-draw/src/SCanvasView.ts

@@ -0,0 +1,418 @@
+import {
+    SMouseButton,
+    SMouseEvent,
+    SNetUtil,
+    SObject,
+    STouchState
+} from "@saga-web/base/lib/";
+import { SSvgPaintEngine } from "./engines/SSvgPaintEngine";
+import { SPoint } from "./types/SPoint";
+import { SPainter } from "./SPainter";
+
+/**
+ * Canvas视图基类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SCanvasView extends SObject {
+    /** canvas视图 */
+    protected readonly canvasView: HTMLCanvasElement;
+    get canvas(): CanvasRenderingContext2D | null {
+        return this.canvasView.getContext("2d") as CanvasRenderingContext2D;
+    } // Get canvas
+
+    /** 宽度 */
+    get width(): number {
+        return this.canvasView.width;
+    } // Get width()
+
+    /** 高度 */
+    get height(): number {
+        return this.canvasView.height;
+    } // Get height()
+
+    /** 原点坐标 */
+    private _origin = new SPoint();
+    get origin(): SPoint {
+        return this._origin;
+    } // Get origin
+    set origin(v: SPoint) {
+        if (!this.moveable) {
+            return;
+        }
+        this._origin.x = v.x;
+        this._origin.y = v.y;
+    } // Set origin
+    /** 可否进行平移操作 */
+    moveable = true;
+    /** 缩放比例 */
+    private _scale: number = 1;
+    get scale(): number {
+        return this._scale;
+    } // Get scale
+    set scale(v: number) {
+        if (!this.scalable) {
+            return;
+        }
+        this._scale = v;
+    } // Set scale
+
+    /** 可否执行绽放操作 */
+    scalable = true;
+    /** 鼠标滚轮缩放速度 */
+    wheelZoom = 1.05;
+    /** 最小缩放比例 */
+    minScale = 0.004;
+    /** 最大缩放比例 */
+    maxScale = 500;
+
+    /** 最后一次更新时间 */
+    private _lastFrameTime = 0;
+
+    /** 鼠标中键按下时位置 */
+    private _midKeyPos = new SPoint();
+
+    /** 手执状态 */
+    private _touchState = STouchState.None;
+    /** 未缩放时二指间距离 */
+    private _beforeLength = 0;
+    /** 缩放后二指间距离 */
+    private _afterLength = 0;
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    /**
+     * 构造函数
+     *
+     * @param   id      画布对象ID
+     */
+    constructor(id: string) {
+        super();
+        this.canvasView = document.getElementById(id) as HTMLCanvasElement;
+        this.bindEvent(this.canvasView);
+
+        this.requestAnimationFrame(this.loop.bind(this));
+    } // Constructor
+
+    /**
+     * 更新视图
+     */
+    update(): void {
+        /** painter对象 */
+        let ctx = this.canvas;
+        if (null != ctx) {
+            let painter = new SPainter(this);
+            this.onDraw(painter);
+        }
+    } // Function update()
+
+    /**
+     * 保存图像
+     *
+     * @param   name    名称
+     * @param   type    图像类型
+     */
+    saveImage(name: string, type: string): void {
+        let url = this.canvasView.toDataURL(`image/${type}`);
+        SNetUtil.downLoad(name, url);
+    } // Function saveImage()
+
+    /**
+     * 图像的URL
+     *
+     * @param   type    图像类型
+     */
+    imageUrl(type: string): string {
+        return this.canvasView.toDataURL(`image/${type}`);
+    } // Function imageUrl()
+
+    /**
+     * 视图内容保存为SVG文件
+     *
+     * @param   name    文件名
+     */
+    saveSvg(name: string): void {
+        let url = URL.createObjectURL(
+            new Blob([this.svgData()], { type: "text/xml,charset=UTF-8" })
+        );
+        SNetUtil.downLoad(name, url);
+    } // Function saveSvg()
+
+    /**
+     * 视图SVG图形的数据
+     *
+     * @return  URL地址
+     */
+    svgData(): string {
+        let engine = new SSvgPaintEngine(this.width, this.height);
+        let painter = new SPainter(engine);
+        this.onDraw(painter);
+        return engine.toSvg();
+    } // Function saveSvg()
+
+    /**
+     * 缩放视图时计算视图的位置与缩放比例
+     *
+     * @param   zoom        缩放比例
+     * @param   x0          缩放计算的中心点X坐标
+     * @param   y0          缩放计算的中心点Y坐标
+     */
+    scaleByPoint(zoom: number, x0: number, y0: number): void {
+        if (!this.scalable) {
+            return;
+        }
+
+        let z = zoom;
+        /**
+         * 缩放比例在最小比例和最大比例范围内
+         */
+        if (this.scale * zoom >= this.maxScale) {
+            z = this.maxScale / this.scale;
+            this.scale = this.maxScale;
+        } else if (this.scale * zoom <= this.minScale) {
+            z = this.minScale / this.scale;
+            this.scale = this.minScale;
+        } else {
+            this.scale *= zoom;
+        }
+
+        this.origin.x = x0 - (x0 - this.origin.x) * z;
+        this.origin.y = y0 - (y0 - this.origin.y) * z;
+    } // Function scaleByPoint()
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    /**
+     * 循环
+     */
+    protected loop(): void {
+        this.update();
+        this.requestAnimationFrame(this.loop.bind(this));
+    } // Function loop()
+
+    /**
+     * 绘制视图
+     *
+     * @param   painter     painter对象
+     */
+    protected onDraw(painter: SPainter): void {} // Function onDraw()
+
+    /**
+     * 鼠标单击事件
+     *
+     * @param   event       事件参数
+     */
+    protected onClick(event: MouseEvent): void {} // Function onClick()
+
+    /**
+     * 鼠标双击事件
+     *
+     * @param   event       事件参数
+     */
+    protected onDoubleClick(event: MouseEvent): void {} // Function onDoubleClick()
+
+    /**
+     * 鼠标按下事件
+     *
+     * @param   event       事件参数
+     */
+    protected onMouseDown(event: MouseEvent): void {
+        let se = new SMouseEvent(event);
+        if (se.buttons & SMouseButton.MidButton) {
+            this._midKeyPos.x = se.x;
+            this._midKeyPos.y = se.y;
+        }
+    } // Function onMouseDown()
+
+    /**
+     * 鼠标移动事件
+     *
+     * @param   event       事件参数
+     */
+    protected onMouseMove(event: MouseEvent): void {
+        // 如果可以移动
+        if (this.moveable) {
+            // 按中键移动
+            let se = new SMouseEvent(event);
+            if (se.buttons & SMouseButton.MidButton) {
+                this.origin.x += se.x - this._midKeyPos.x;
+                this.origin.y += se.y - this._midKeyPos.y;
+                this._midKeyPos.x = se.x;
+                this._midKeyPos.y = se.y;
+                return;
+            }
+        }
+    } // Function onMouseMove()
+
+    /**
+     * 鼠标松开事件
+     *
+     * @param   event       事件参数
+     */
+    protected onMouseUp(event: MouseEvent): void {} // Function onMouseUp()
+
+    /**
+     * 上下文菜单事件
+     *
+     * @param   event       事件参数
+     */
+    protected onContextMenu(event: MouseEvent): void {} // Function onContextMenu()
+
+    /**
+     * 鼠标滚轮事件
+     *
+     * @param   event       事件参数
+     */
+    protected onMouseWheel(event: WheelEvent): void {
+        if (event.deltaY < 0) {
+            this.scaleByPoint(1 / this.wheelZoom, event.offsetX, event.offsetY);
+        } else {
+            this.scaleByPoint(this.wheelZoom, event.offsetX, event.offsetY);
+        }
+    } // Function onMouseWheel()
+
+    /**
+     * 按键按下事件
+     *
+     * @param   event       事件参数
+     */
+    protected onKeyDown(event: KeyboardEvent): void {} // Function onKeydown()
+
+    /**
+     * 按键press事件
+     *
+     * @param   event       事件参数
+     */
+    protected onKeyPress(event: KeyboardEvent): void {} // Function onKeypress()
+
+    /**
+     * 按键松开事件
+     *
+     * @param   event       事件参数
+     */
+    protected onKeyUp(event: KeyboardEvent): void {} // Function onKeyup()
+
+    /**
+     * 开始触摸事件
+     *
+     * @param   event       事件参数
+     */
+    protected onTouchStart(event: TouchEvent): void {} // Function onTouchStart()
+
+    /**
+     * 触摸移动事件
+     *
+     * @param   event       事件参数
+     */
+    protected onTouchMove(event: TouchEvent): void {} // Function onTouchMove()
+
+    /**
+     * 结束触摸事件
+     *
+     * @param   event       事件参数
+     */
+    protected onTouchEnd(event: TouchEvent): void {} // Function onTouchEnd()
+
+    /**
+     * View大小变更事件
+     *
+     * @param   event       事件参数
+     */
+    protected onResize(event: UIEvent): void {} // Function onClick()
+
+    /**
+     * 绑定事件
+     *
+     * @param   element     要绑定事件的元素
+     */
+    private bindEvent(element: HTMLElement): void {
+        // 绑定鼠标事件
+        element.onclick = this.onClick.bind(this);
+        element.ondblclick = this.onDoubleClick.bind(this);
+        element.onmousedown = this.onMouseDown.bind(this);
+        element.onmousemove = this.onMouseMove.bind(this);
+        element.onmouseup = this.onMouseUp.bind(this);
+        element.oncontextmenu = this.onContextMenu.bind(this);
+        element.onwheel = this.onMouseWheel.bind(this);
+
+        // 绑定按键事件
+        element.onkeydown = this.onKeyDown.bind(this);
+        element.onkeypress = this.onKeyPress.bind(this);
+        element.onkeyup = this.onKeyUp.bind(this);
+
+        // 触摸事件
+        element.ontouchstart = this.onTouchStart.bind(this);
+        element.ontouchmove = this.onTouchMove.bind(this);
+        element.ontouchend = this.onTouchEnd.bind(this);
+
+        // 绑定窗口事件
+        element.onresize = this.onResize.bind(this);
+    } // Function bindEvent()
+
+    /**
+     * 启动动画帧(即指定时间执行回调函数)
+     *
+     * @param   callback        回调函数
+     */
+    private requestAnimationFrame(callback: FrameRequestCallback): number {
+        let currTime = new Date().getTime();
+        let timeToCall = Math.max(0, 16 - (currTime - this._lastFrameTime));
+        let id = setTimeout(function(): void {
+            callback(currTime + timeToCall);
+        }, timeToCall);
+        this._lastFrameTime = currTime + timeToCall;
+        return id;
+    } // Function requestAnimationFrame()
+
+    /**
+     * 缩放视图
+     *
+     * @param   event       触摸事件
+     */
+    private viewZoom(event: TouchEvent): boolean {
+        if (this._touchState == STouchState.Zoom) {
+            // 获取两点的距离
+            this._afterLength = this.getDistance(event);
+            // 变化的长度;
+            let gapLenght = this._afterLength - this._beforeLength;
+            if (Math.abs(gapLenght) > 5 && this._beforeLength != 0.0) {
+                // 求的缩放的比例
+                let tempScale = this._afterLength / this._beforeLength;
+                let p = this.getMiddlePoint(event);
+                // 重设置
+                this.scaleByPoint(tempScale, p.x, p.y);
+                this._beforeLength = this._afterLength;
+            }
+        }
+        return true;
+    } // Function onTouchMove()
+
+    /**
+     * 获取两手指之间的距离
+     *
+     * @param   event       触摸事件
+     * @return 两手指之间的距离
+     */
+    private getDistance(event: TouchEvent): number {
+        if (event.touches.length < 2) {
+            return 0;
+        }
+
+        let dx = event.touches[0].clientX - event.touches[1].clientX;
+        let dy = event.touches[0].clientY - event.touches[1].clientY;
+        return Math.sqrt(dx * dx + dy * dy);
+    } // Function getDistance()
+
+    /**
+     * 获得两个手指触摸点的中点坐标
+     *
+     * @param   event
+     * @return  得到视图的x坐标中点
+     */
+    private getMiddlePoint(event: TouchEvent): SPoint {
+        return new SPoint(
+            (event.touches[0].clientX + event.touches[1].clientX) / 2,
+            (event.touches[0].clientY + event.touches[1].clientY) / 2
+        );
+    } // Function getMiddlePoint()
+} // Class SCanvasView

+ 196 - 0
saga-web-draw/src/SColor.ts

@@ -0,0 +1,196 @@
+import { SStringUtil } from "@saga-web/base/lib";
+
+/**
+ * 颜色类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SColor {
+    static Transparent = new SColor("#00000000");
+    static Black = new SColor("#000000");
+    static DarkBlue = new SColor("#000080");
+    static Blue = new SColor("#0000FF");
+    static DarkGreen = new SColor("#008000");
+    static Green = new SColor("#00FF00");
+    static DarkCyan = new SColor("#008080");
+    static Cyan = new SColor("#00FFFF");
+    static DarkRed = new SColor("#800000");
+    static Red = new SColor("#FF0000");
+    static DarkMagenta = new SColor("#800080");
+    static Magenta = new SColor("#FF00FF");
+    static DarkYellow = new SColor("#808000");
+    static Yellow = new SColor("#FFFF00");
+    static White = new SColor("#FFFFFF");
+    static DarkGray = new SColor("#808080");
+    static Gray = new SColor("#A0A0A0");
+    static LightGray = new SColor("#C0C0C0");
+
+    /**
+     * 根据rgb分量生成颜色
+     *
+     * @param   r       红色分量
+     * @param   g       绿色分量
+     * @param   b       蓝色分量
+     * @return  颜色
+     */
+    static rgb(r: number, g: number, b: number): SColor {
+        return new SColor(r, g, b);
+    } // Function rgb()
+
+    /**
+     * 根据rgba分量生成颜色
+     *
+     * @param   r       红色分量
+     * @param   g       绿色分量
+     * @param   b       蓝色分量
+     * @param   a       透明度分量
+     * @return  颜色
+     */
+    static rgba(r: number, g: number, b: number, a: number): SColor {
+        return new SColor(r, g, b, a);
+    } // Function rgb()
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** 红色分量 */
+    get red(): number {
+        return (this._value >> 24) & 0xff;
+    } // Get red
+
+    /** 绿色分量 */
+    get green(): number {
+        return (this._value >> 16) & 0xff;
+    } // Get green
+
+    /** 蓝色分量 */
+    get blue(): number {
+        return (this._value >> 8) & 0xff;
+    } // Get blue
+
+    /** 透明度 */
+    get alpha(): number {
+        return this._value & 0xff;
+    } // Get alpha
+
+    /** 颜色 */
+    private _value: number = 0xff;
+    get value(): string {
+        return "#" + SStringUtil.num2Hex(this._value, 8);
+    } // Get value
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     */
+    constructor();
+
+    /**
+     * 构造函数
+     *
+     * @param   r       红色分量
+     * @param   g       绿色分量
+     * @param   b       蓝色分量
+     */
+    constructor(r: number, g: number, b: number);
+
+    /**
+     * 构造函数
+     *
+     * @param   r       红色分量
+     * @param   g       绿色分量
+     * @param   b       蓝色分量
+     * @param   a       透明度分量
+     */
+    constructor(r: number, g: number, b: number, a: number);
+
+    /**
+     * 构造函数
+     *
+     * @param   color   颜色
+     */
+    constructor(color: string);
+
+    /**
+     * 构造函数
+     *
+     * @param   color   颜色
+     */
+    constructor(color: SColor);
+
+    /**
+     * 构造函数(重载实现)
+     *
+     * @param   r       红色分量 | 字符串描述颜色 | 颜色
+     * @param   g       绿色分量
+     * @param   b       蓝色分量
+     * @param   a       透明度分量
+     */
+    constructor(
+        r?: number | string | SColor,
+        g?: number,
+        b?: number,
+        a?: number
+    ) {
+        if (r == undefined) {
+            this._value = 0xff;
+        } else if (typeof r == "string") {
+            this.setColor(r);
+        } else if (r instanceof SColor) {
+            this._value = r._value;
+        } else if (a == undefined) {
+            this.setRgb(r as number, g as number, b as number);
+        } else {
+            this.setRgba(r as number, g as number, b as number, a as number);
+        }
+    } // Constructor
+
+    /**
+     * 设置颜色
+     *
+     * @param   r       红色分量
+     * @param   g       绿色分量
+     * @param   b       蓝色分量
+     */
+    private setRgb(r: number, g: number, b: number): void {
+        this._value =
+            ((r as number) << 24) +
+            ((g as number) << 16) +
+            ((b as number) << 8) +
+            0xff;
+    } // Function setRgb()
+
+    /**
+     * 设置颜色
+     *
+     * @param   r       红色分量
+     * @param   g       绿色分量
+     * @param   b       蓝色分量
+     * @param   a       透明度分量
+     */
+    private setRgba(r: number, g: number, b: number, a: number): void {
+        this._value =
+            ((r as number) << 24) +
+            ((g as number) << 16) +
+            ((b as number) << 8) +
+            (a as number);
+    } // Function setRgb()
+
+    /**
+     * 设置颜色
+     *
+     * @param   str     颜色字符串
+     */
+    private setColor(str: string): void {
+        if (str.substr(0, 1) != "#") {
+            return;
+        }
+
+        // 先去“#”,再转换16进制数
+        this._value = parseInt(str.substr(1), 16);
+        // 如果未写alpha值,则左移8位+0xff;
+        if (str.length == 7) {
+            this._value = (this._value << 8) + 0xff;
+        }
+    } // Function setColor()
+} // Class SColor

+ 69 - 0
saga-web-draw/src/SFont.ts

@@ -0,0 +1,69 @@
+import { STextAlign } from "./enums/STextAlign";
+import { STextBaseLine } from "./enums/STextBaseLine";
+import { STextDirection } from "./enums/STextDirection";
+
+/**
+ * 字体
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SFont {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** 字体 */
+    name = "sans-serif";
+    /** 字号 */
+    size = 16;
+
+    /** 文本对齐选项 */
+    textAlign = STextAlign.Start;
+
+    /** 文本基线对齐选项 */
+    textBaseLine = STextBaseLine.Alphabetic;
+
+    /** 文本方向 */
+    textDirection = STextDirection.Inherit;
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     */
+    constructor();
+
+    /**
+     * 构造函数
+     *
+     * @param   font        FONT
+     */
+    constructor(font: SFont);
+
+    /**
+     * 构造函数
+     *
+     * @param   name        字体
+     * @param   size        字号
+     */
+    constructor(name: string, size?: number)
+    /**
+     * 构造函数
+     *
+     * @param   name        字体 | SFont
+     * @param   size        字号
+     */
+    constructor(name?: string | SFont, size?: number) {
+        if (name == undefined) {
+            return;
+        }
+        if (name instanceof SFont) {
+            this.name = name.name;
+            this.size = name.size;
+            this.textAlign = name.textAlign;
+            this.textBaseLine = name.textBaseLine;
+            this.textDirection = name.textDirection;
+        } else {
+            this.name = name;
+            this.size = size != undefined ? size : 16;
+        }
+    } // Constructor
+} // Class SFont

+ 21 - 0
saga-web-draw/src/SGradient.ts

@@ -0,0 +1,21 @@
+import { SColor, SGradientStop } from "./";
+
+/**
+ * 渐变颜色节点
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export abstract class SGradient {
+    /** 渐变颜色节点列表 */
+    stopList: SGradientStop[] = [];
+
+    /**
+     * 添加渐变颜色节点
+     *
+     * @param   pos     位置[0-1]
+     * @param   color   颜色
+     */
+    addColorStop(pos: number, color: SColor): void {
+        this.stopList.push(new SGradientStop(pos, color));
+    } // Functin addColorStop()
+} // Class SGradientStop

+ 36 - 0
saga-web-draw/src/SGradientStop.ts

@@ -0,0 +1,36 @@
+import { SColor } from "./";
+
+/**
+ * 渐变颜色节点
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGradientStop {
+    /** 位置 */
+    private _pos: number = 0;
+    get pos(): number {
+        return this._pos;
+    } // Get pos
+    set pos(value: number) {
+        if (value < 0) {
+            value = 0;
+        }
+        if (value > 1) {
+            value = 1;
+        }
+        this._pos = value;
+    } // Set pos
+    /** 颜色 */
+    color: SColor;
+
+    /**
+     * 构造函数
+     *
+     * @param   pos         节点位置
+     * @param   color       节点颜色
+     */
+    constructor(pos: number, color: SColor) {
+        this.pos = pos;
+        this.color = color;
+    } // Constructor()
+} // Class SGradientStop

+ 114 - 0
saga-web-draw/src/SLinearGradient.ts

@@ -0,0 +1,114 @@
+import { SGradient, SPoint } from "./";
+
+/**
+ * 线性渐变
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SLinearGradient extends SGradient {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** 起点 */
+    start = new SPoint();
+
+    /** 结束点 */
+    end = new SPoint();
+
+    /** 起点X坐标 */
+    get x1(): number {
+        return this.start.x;
+    } // Get x1
+    set x1(value: number) {
+        this.start.x = value;
+    } // Set x1
+
+    /** 起点Y坐标 */
+    get y1(): number {
+        return this.start.y;
+    } // Get y1
+    set y1(value: number) {
+        this.start.y = value;
+    } // Set y1
+
+    /** 终点X坐标 */
+    get x2(): number {
+        return this.end.x;
+    } // Get x2
+    set x2(value: number) {
+        this.end.x = value;
+    } // Set x2
+
+    /** 终点Y坐标 */
+    get y2(): number {
+        return this.end.y;
+    } // Get y2
+    set y2(value: number) {
+        this.end.y = value;
+    } // Set y2
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     *
+     * @param   start   起点坐标
+     * @param   end     终点坐标
+     */
+    constructor(start: SPoint, end: SPoint);
+
+    /**
+     * 构造函数
+     *
+     * @param   x1      起点X坐标
+     * @param   y1      起点Y坐标
+     * @param   x2      终点X坐标
+     * @param   y2      终点Y坐标
+     */
+    constructor(x1: number, y1: number, x2: number, y2: number);
+
+    /**
+     * 构造函数(重载实现)
+     *
+     * @param   x1      起点X坐标
+     * @param   y1      起点Y坐标
+     * @param   x2      终点X坐标
+     * @param   y2      终点Y坐标
+     */
+    constructor(
+        x1: number | SPoint,
+        y1: number | SPoint,
+        x2?: number,
+        y2?: number
+    ) {
+        super();
+        if (x1 instanceof SPoint && y1 instanceof SPoint) {
+            this.start = new SPoint(x1);
+            this.end = new SPoint(y1);
+        } else {
+            this.start = new SPoint(x1 as number, y1 as number);
+            this.end = new SPoint(x2 as number, y2 as number);
+        }
+    } // Constructor()
+
+    /**
+     * 设置起点坐标
+     *
+     * @param   x       X坐标
+     * @param   y       Y坐标
+     */
+    setStart(x: number, y: number): void {
+        this.start.x = x;
+        this.start.y = y;
+    } // Function setStart()
+
+    /**
+     * 设置终点坐标
+     *
+     * @param   x       X坐标
+     * @param   y       Y坐标
+     */
+    setEnd(x: number, y: number): void {
+        this.end.x = x;
+        this.end.y = y;
+    } // Function setEnd()
+} // Class SGradientBrush

+ 641 - 0
saga-web-draw/src/SPainter.ts

@@ -0,0 +1,641 @@
+import { SObject } from "@saga-web/base/lib";
+import { SPaintEngine } from "./engines/SPaintEngine";
+import {
+    SBrush,
+    SCanvasPaintEngine,
+    SCanvasView,
+    SFont,
+    SLine,
+    SPath2D,
+    SPen,
+    SPoint,
+    SRect,
+    SSize
+} from "./";
+
+/**
+ * Painter
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SPainter extends SObject {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** 绘制引擎 */
+    private readonly engine: SPaintEngine;
+
+    /** 画笔 */
+    get pen(): SPen {
+        return this.engine.state.pen;
+    } // Get pen
+    set pen(value: SPen) {
+        this.engine.state.pen = value;
+    } // Set pen
+
+    /** 画刷 */
+    get brush(): SBrush {
+        return this.engine.state.brush;
+    } // Get brush
+    set brush(value: SBrush) {
+        this.engine.state.brush = value;
+    } // Set brush
+
+    /** 字体属性 */
+    get font(): SFont {
+        return this.engine.state.font;
+    } // Get font
+    set font(value: SFont) {
+        this.engine.state.font = value;
+    } // Set font
+
+    /** 变换矩阵 */
+    get worldTransform(): DOMMatrix {
+        return this.engine.state.matrix;
+    } // Get worldTransform
+
+    /**
+     * 构造函数
+     *
+     * @param   engine      绘制引擎
+     */
+    constructor(engine: SCanvasView | SPaintEngine) {
+        super();
+        if (engine instanceof SCanvasView) {
+            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+            this.engine = new SCanvasPaintEngine(engine.canvas!!);
+        } else {
+            this.engine = engine;
+        }
+
+        this.pen = new SPen();
+        this.brush = new SBrush();
+        this.font = new SFont();
+    } // Constructor()
+
+    /**
+     * 保存painter状态
+     */
+    save(): void {
+        this.engine.save();
+    } // Function save()
+
+    /**
+     * 恢复painter状态
+     */
+    restore(): void {
+        this.engine.restore();
+    } // Function restore()
+
+    // =================================================================================================================
+    // 变换相关
+    /**
+     * 平移变换
+     *
+     * @param   x       X轴方向平移
+     * @param   y       Y辆方向平移
+     */
+    translate(x: number, y: number): void {
+        this.engine.translate(x, y);
+    } // Function translate()
+
+    /**
+     * 缩放
+     *
+     * @param   x       X轴方向缩放
+     * @param   y       Y辆方向缩放
+     */
+    scale(x: number, y: number): void {
+        this.engine.scale(x, y);
+    } // Function scale()
+
+    /**
+     * 旋转
+     *
+     * @param   angle   旋转角度(单位弧度)
+     */
+    rotate(angle: number): void {
+        this.engine.rotate(angle);
+    } // Function rotate()
+
+    /**
+     * 将当前的变形矩阵乘上一个基于自身参数的矩阵
+     *
+     * @param   m11     水平方向的缩放
+     * @param   m12     水平方向的倾斜偏移
+     * @param   m21     竖直方向的倾斜偏移
+     * @param   m22     竖直方向的缩放
+     * @param   dx      水平方向的移动
+     * @param   dy      竖直方向的移动
+     */
+    transform(
+        m11: number,
+        m12: number,
+        m21: number,
+        m22: number,
+        dx: number,
+        dy: number
+    ): void {
+        this.engine.transform(m11, m12, m21, m22, dx, dy);
+    } // Function transform()
+
+    /**
+     * 将当前的变形矩阵重置为单位矩阵,再将当前的变形矩阵乘上一个基于自身参数的矩阵
+     *
+     * @param   m11     水平方向的缩放
+     * @param   m12     水平方向的倾斜偏移
+     * @param   m21     竖直方向的倾斜偏移
+     * @param   m22     竖直方向的缩放
+     * @param   dx      水平方向的移动
+     * @param   dy      竖直方向的移动
+     */
+    setTransform(
+        m11: number,
+        m12: number,
+        m21: number,
+        m22: number,
+        dx: number,
+        dy: number
+    ): void {
+        this.engine.setTransform(m11, m12, m21, m22, dx, dy);
+    } // Function transform()
+
+    /**
+     * 重置当前变形为单位矩阵。等价于调用setTransform(1, 0, 0, 1, 0, 0)
+     */
+    resetTransform(): void {
+        this.engine.resetTransform();
+    } // Function resetTransform()
+
+    // =================================================================================================================
+    // 绘制相关
+    /**
+     * 设置裁剪路径
+     *
+     * @param   path        裁剪路径
+     */
+    setClip(path: Path2D): void {
+        this.engine.setClip(path);
+    } // Function setClip()
+
+    /**
+     * 清空矩形区域
+     *
+     * @param   rect        矩形
+     */
+    clearRect(rect: SRect): void;
+
+    /**
+     * 清空矩形区域
+     *
+     * @param   leftTop         左上角坐标
+     * @param   rightBottom     右下角坐标
+     */
+    clearRect(leftTop: SPoint, rightBottom: SPoint): void;
+
+    /**
+     * 清空矩形区域
+     *
+     * @param   leftTop     左上角坐标
+     * @param   size        大小
+     */
+    clearRect(leftTop: SPoint, size: SSize): void;
+
+    /**
+     * 清空矩形区域
+     *
+     * @param   x       X坐标
+     * @param   y       Y坐标
+     * @param   w       宽度
+     * @param   h       高度
+     */
+    clearRect(x: number, y: number, w: number, h: number): void;
+
+    /**
+     * 清空矩形区域(重载实现)
+     *
+     * @param   x       X坐标 | 左上角坐标 | 矩形
+     * @param   y       Y坐标 | 右下角坐标 | 大小
+     * @param   w       宽度
+     * @param   h       高度
+     */
+    clearRect(
+        x: number | SPoint | SRect,
+        y?: number | SPoint | SSize,
+        w?: number,
+        h?: number
+    ): void {
+        if (x instanceof SRect) {
+            this.engine.clearRect(x);
+        } else if (x instanceof SPoint && y instanceof SPoint) {
+            this.engine.clearRect(new SRect(x, y));
+        } else if (x instanceof SPoint && y instanceof SSize) {
+            this.engine.clearRect(new SRect(x, y));
+        } else {
+            this.engine.clearRect(
+                new SRect(x as number, y as number, w as number, h as number)
+            );
+        }
+    } // Function clearRect()
+
+    /**
+     * 绘制矩形
+     *
+     * @param   rect        矩形
+     */
+    drawRect(rect: SRect): void;
+
+    /**
+     * 绘制矩形
+     *
+     * @param   leftTop         左上角坐标
+     * @param   rightBottom     右下角坐标
+     */
+    drawRect(leftTop: SPoint, rightBottom: SPoint): void;
+
+    /**
+     * 绘制矩形
+     *
+     * @param   leftTop     左上角坐标
+     * @param   size        大小
+     */
+    drawRect(leftTop: SPoint, size: SSize): void;
+
+    /**
+     * 绘制矩形
+     *
+     * @param   x       X坐标
+     * @param   y       Y坐标
+     * @param   w       宽度
+     * @param   h       高度
+     */
+    drawRect(x: number, y: number, w: number, h: number): void;
+
+    /**
+     * 绘制矩形
+     *
+     * @param   x       X坐标 | 左上角坐标 | 矩形
+     * @param   y       Y坐标 | 右下角坐标 | 大小
+     * @param   w       宽度
+     * @param   h       高度
+     */
+    drawRect(
+        x: number | SPoint | SRect,
+        y?: number | SPoint | SSize,
+        w?: number,
+        h?: number
+    ): void {
+        if (x instanceof SRect) {
+            this.engine.drawRect(x);
+        } else if (x instanceof SPoint && y instanceof SPoint) {
+            this.engine.drawRect(new SRect(x, y));
+        } else if (x instanceof SPoint && y instanceof SSize) {
+            this.engine.drawRect(new SRect(x, y));
+        } else {
+            this.engine.drawRect(
+                new SRect(x as number, y as number, w as number, h as number)
+            );
+        }
+    } // Function drawRect()
+
+    // /**
+    //  * 绘制带导角空心矩形
+    //  *
+    //  * @param   x           X坐标
+    //  * @param   y           Y坐标
+    //  * @param   w           宽度
+    //  * @param   h           高度
+    //  * @param   r           导角半径
+    //  */
+    // drawRoundedRect(
+    //     x: number,
+    //     y: number,
+    //     w: number,
+    //     h: number,
+    //     r: number
+    // ): void {} // Function drawRoundedRect()
+
+    /**
+     * 绘制圆形
+     *
+     * @param   cx          圆心X坐标
+     * @param   cy          圆心X坐标
+     * @param   r           圆半径
+     */
+    drawCircle(cx: number, cy: number, r: number): void {
+        this.engine.drawCircle(cx, cy, r);
+    } // Function drawCircle()
+
+    /**
+     * 绘制椭圆
+     *
+     * @param   cx          圆点X坐标
+     * @param   cy          圆点Y坐标
+     * @param   rx          水平半径
+     * @param   ry          垂直半径
+     */
+    drawEllipse(cx: number, cy: number, rx: number, ry: number): void {
+        this.engine.drawEllipse(cx, cy, rx, ry);
+    } // Function drawEllipse()
+
+    /**
+     * 绘制椭圆弧
+     *
+     * @param   rect            椭圆所在矩形
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawArc(rect: SRect, startAngle: number, endAngle: number): void;
+
+    /**
+     * 绘制椭圆弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawArc(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void;
+
+    /**
+     * 绘制椭圆弧(重载实现)
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawArc(
+        x: SRect | number,
+        y: number,
+        width: number,
+        height?: number,
+        startAngle?: number,
+        endAngle?: number
+    ): void {
+        if (x instanceof SRect) {
+            this.engine.drawArc(x.x, x.y, x.width, x.height, y, width);
+        } else {
+            this.engine.drawArc(
+                x,
+                y,
+                width,
+                height as number,
+                startAngle as number,
+                endAngle as number
+            );
+        }
+    } // Function drawArc()
+
+    /**
+     * 绘制椭圆弦弧
+     *
+     * @param   rect            椭圆所在矩形
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawChord(rect: SRect, startAngle: number, endAngle: number): void;
+
+    /**
+     * 绘制椭圆弦弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawChord(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void;
+
+    /**
+     * 绘制椭圆弦弧(重载实现)
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawChord(
+        x: SRect | number,
+        y: number,
+        width: number,
+        height?: number,
+        startAngle?: number,
+        endAngle?: number
+    ): void {
+        if (x instanceof SRect) {
+            this.engine.drawChord(x.x, x.y, x.width, x.height, y, width);
+        } else {
+            this.engine.drawChord(
+                x,
+                y,
+                width,
+                height as number,
+                startAngle as number,
+                endAngle as number
+            );
+        }
+    } // Function drawChord()
+
+    /**
+     * 绘制椭圆饼
+     *
+     * @param   rect            椭圆所在矩形
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawPie(rect: SRect, startAngle: number, endAngle: number): void;
+
+    /**
+     * 绘制椭圆饼
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawPie(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void;
+
+    /**
+     * 绘制椭圆饼
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawPie(
+        x: SRect | number,
+        y: number,
+        width: number,
+        height?: number,
+        startAngle?: number,
+        endAngle?: number
+    ): void {
+        if (x instanceof SRect) {
+            this.engine.drawPie(x.x, x.y, x.width, x.height, y, width);
+        } else {
+            this.engine.drawPie(
+                x,
+                y,
+                width,
+                height as number,
+                startAngle as number,
+                endAngle as number
+            );
+        }
+    } // Function drawPie()
+
+    /**
+     * 绘制一条线段
+     *
+     * @param   line    线段
+     */
+    drawLine(line: SLine): void;
+
+    /**
+     * 绘制一条线段
+     *
+     * @param   p1      启点坐标
+     * @param   p2      终点坐标
+     */
+    drawLine(p1: SPoint, p2: SPoint): void;
+
+    /**
+     * 绘制一条线段
+     *
+     * @param   x1      启点X坐标
+     * @param   y1      启点Y坐标
+     * @param   x2      终点X坐标
+     * @param   y2      终点Y坐标
+     */
+    drawLine(x1: number, y1: number, x2: number, y2: number): void;
+
+    /**
+     * 绘制一条线段(重载实现)
+     *
+     * @param   x1      启点X坐标 | 起点1坐标 | 线段 |
+     * @param   y1      启点Y坐标 | 终点坐标
+     * @param   x2      终点X坐标
+     * @param   y2      终点Y坐标
+     */
+    drawLine(
+        x1: number | SPoint | SLine,
+        y1?: number | SPoint,
+        x2?: number,
+        y2?: number
+    ): void {
+        if (x1 instanceof SLine) {
+            this.engine.drawLine(x1);
+        } else if (x1 instanceof SPoint && y1 instanceof SPoint) {
+            this.engine.drawLine(new SLine(x1, y1));
+        } else {
+            this.engine.drawLine(
+                new SLine(
+                    x1 as number,
+                    y1 as number,
+                    x2 as number,
+                    y2 as number
+                )
+            );
+        }
+    } // Function drawLine()
+
+    /**
+     * 绘制折线
+     *
+     * @param   points      折线折点
+     */
+    drawPolyline(points: SPoint[]): void {
+        this.engine.drawPolyline(points);
+    } // Function drawPolyline()
+
+    /**
+     * 绘制多边形
+     *
+     * @param   points      多边形顶点
+     */
+    drawPolygon(points: SPoint[]): void {
+        this.engine.drawPolygon(points);
+    } // Functin drawPolygon()
+
+    /**
+     * 绘制路径
+     *
+     * @param   path        路径
+     */
+    drawPath(path: SPath2D): void {
+        this.engine.drawPath(path);
+    } // Function drawPath()
+
+    /**
+     * 绘制文本
+     *
+     * @param   text        文本内容
+     * @param   x           X坐标
+     * @param   y           Y坐标
+     * @param   maxWidth    最大宽度
+     */
+    drawText(text: string, x: number, y: number, maxWidth?: number): void {
+        this.engine.drawText(text, x, y, maxWidth);
+    } // Function drawText()
+
+    /**
+     * 绘制图片
+     *
+     * @param img       图片
+     * @param x         X坐标
+     * @param y         Y坐标
+     * @param width     宽度
+     * @param height    高度
+     */
+    drawImage(
+        img: CanvasImageSource,
+        x: number,
+        y: number,
+        width?: number,
+        height?: number
+    ): void {
+        this.engine.drawImage(img, x, y, width, height);
+    } // Function drawImage()
+
+    /**
+     * painter转实现view象素
+     *
+     * @param   p       绘制坐标
+     */
+    toPx(p: number): number {
+        return p / this.engine.state.matrix.a;
+    } // Function painterToView()
+} // Class SPainter

+ 238 - 0
saga-web-draw/src/SPath2D.ts

@@ -0,0 +1,238 @@
+import { SPoint } from "./index";
+
+/**
+ * Path对象
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SPath2D {
+    /** 内部path对象 */
+    _path = new Path2D();
+
+    /**
+     * 添加路径
+     *
+     * @param   path    添加的路径
+     */
+    addPath(path: SPath2D): void {
+        this._path.addPath(path._path);
+    } // Function addPath()
+
+    /**
+     * 移动到指定位置
+     *
+     * @param   x       x坐标
+     * @param   y       y坐标
+     */
+    moveTo(x: number, y: number): void {
+        this._path.moveTo(x, y);
+    } // Function moveTo()
+
+    /**
+     * 从当前位置到指定位置画直线
+     *
+     * @param   x       x坐标
+     * @param   y       y坐标
+     */
+    lineTo(x: number, y: number): void {
+        this._path.lineTo(x, y);
+    } // Function moveTo()
+
+    /**
+     * 矩形
+     *
+     * @param   x           左上角x坐标
+     * @param   y           左上角y坐标
+     * @param   width       宽度
+     * @param   height      高度
+     */
+    rect(x: number, y: number, width: number, height: number): void {
+        let r = x + width;
+        let b = y + height;
+        this._path.moveTo(x, y);
+        this._path.lineTo(r, y);
+        this._path.lineTo(r, b);
+        this._path.lineTo(x, b);
+        this._path.lineTo(x, y);
+        // 逆时针
+        this._path.moveTo(x, y);
+        this._path.lineTo(r, y);
+        this._path.lineTo(r, b);
+        this._path.lineTo(x, b);
+        this._path.lineTo(x, y);
+    } // Function rect()
+
+    /**
+     * 绘制折线
+     *
+     * @param   points      折线折点
+     */
+    polyline(points: SPoint[]): void {
+        if (points.length < 2) {
+            return;
+        }
+        points.map((it: SPoint, index: number): void => {
+            if (index == 0) {
+                this._path.moveTo(it.x, it.y);
+            } else {
+                this._path.lineTo(it.x, it.y);
+            }
+        });
+    } // Function drawPolyline()
+
+    /**
+     * 绘制多边形
+     *
+     * @param   points      多边形顶点
+     */
+    polygon(points: SPoint[]): void {
+        if (points.length < 3) {
+            return;
+        }
+        points.map((it: SPoint, index: number): void => {
+            if (index == 0) {
+                this._path.moveTo(it.x, it.y);
+            } else {
+                this._path.lineTo(it.x, it.y);
+            }
+        });
+        this._path.lineTo(points[0].x, points[0].y);
+    } // Functin polygon()
+
+    /**
+     * 椭圆弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    static arc(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): string {
+        let a = width / 2;
+        let b = height / 2;
+        let p1 = SPath2D.arcPoint(a, b, startAngle);
+        let p2 = SPath2D.arcPoint(a, b, endAngle);
+        let large = SPath2D.largeArcFlag(startAngle, endAngle);
+        p1.x = x + a + p1.x;
+        p1.y = y + b - p1.y;
+        p2.x = x + a + p2.x;
+        p2.y = y + b - p2.y;
+        return `M ${p1.x} ${p1.y} A ${a} ${b} 0 ${large} 0 ${p2.x} ${p2.y}`;
+    } // Function arc()
+
+    /**
+     * 绘制椭圆弦弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    static chord(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): string {
+        let a = width / 2;
+        let b = height / 2;
+        let p1 = SPath2D.arcPoint(a, b, startAngle);
+        let p2 = SPath2D.arcPoint(a, b, endAngle);
+        let large = SPath2D.largeArcFlag(startAngle, endAngle);
+        p1.x = x + a + p1.x;
+        p1.y = y + b - p1.y;
+        p2.x = x + a + p2.x;
+        p2.y = y + b - p2.y;
+        return `M ${p1.x} ${p1.y} A ${a} ${b} 0 ${large} 0 ${p2.x} ${p2.y} Z`;
+    } // Function chord()
+
+    /**
+     * 绘制椭圆饼
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    static pie(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): string {
+        let a = width / 2;
+        let b = height / 2;
+        let p1 = SPath2D.arcPoint(a, b, startAngle);
+        let p2 = SPath2D.arcPoint(a, b, endAngle);
+        let large = SPath2D.largeArcFlag(startAngle, endAngle);
+        p1.x = x + a + p1.x;
+        p1.y = y + b - p1.y;
+        p2.x = x + a + p2.x;
+        p2.y = y + b - p2.y;
+        return `M ${x + a} ${y + b} L ${p1.x} ${
+            p1.y
+        } A ${a} ${b} 0 ${large} 0 ${p2.x} ${p2.y} Z`;
+    } // Function pie()
+
+    /**
+     * 计算大弧标志
+     *
+     * @param   startAngle      启动角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    static largeArcFlag(startAngle: number, endAngle: number): number {
+        return ((Math.abs(endAngle - startAngle) * 180) / Math.PI) % 360 >
+            179.99999
+            ? 1
+            : 0;
+    } // Function largeArcFlag()
+
+    // https://blog.csdn.net/xiamentingtao/article/details/85804823
+    static arcPoint(a: number, b: number, angle: number): SPoint {
+        let p = new SPoint();
+        if (angle >= 0) {
+            angle = angle - Math.floor(angle / Math.PI / 2) * 2 * Math.PI;
+        } else {
+            angle = angle + Math.ceil(-angle / Math.PI / 2) * 2 * Math.PI;
+        }
+        if (Math.abs(Math.PI / 2 - angle) <= 0.000001) {
+            p.x = 0;
+            p.y = b;
+        } else if (Math.abs((Math.PI * 3) / 2 - angle) <= 0.000001) {
+            p.x = 0;
+            p.y = -b;
+        } else if (Math.PI / 2 < angle && angle < (Math.PI * 3) / 2) {
+            p.x =
+                -(a * b) /
+                Math.sqrt(b * b + a * a * Math.tan(angle) * Math.tan(angle));
+            p.y =
+                -(a * b * Math.tan(angle)) /
+                Math.sqrt(b * b + a * a * Math.tan(angle) * Math.tan(angle));
+        } else {
+            p.x =
+                (a * b) /
+                Math.sqrt(b * b + a * a * Math.tan(angle) * Math.tan(angle));
+            p.y =
+                (a * b * Math.tan(angle)) /
+                Math.sqrt(b * b + a * a * Math.tan(angle) * Math.tan(angle));
+        }
+        return p;
+    } // Function arcPoint()
+} // Class SPath2D

+ 78 - 0
saga-web-draw/src/SPen.ts

@@ -0,0 +1,78 @@
+import { SColor, SLineCapStyle, SLineJoinStyle } from "./";
+
+/**
+ * 画笔
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SPen {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** 画笔颜色 */
+    color = SColor.Black;
+
+    /** 线宽 */
+    lineWidth = 1;
+
+    /** 线段端点样式 */
+    lineCapStyle = SLineCapStyle.Butt;
+
+    /** 线段与线段间接合处的样式 */
+    lineJoinStyle = SLineJoinStyle.Miter;
+
+    /** 限制当两条线相交时交接处最大长度 (默认值是10.0)*/
+    miterLimit = 10;
+
+    /** 虚线样式 */
+    lineDash: number[] | null = null;
+
+    /** 虚线样式的起始偏移量 */
+    dashOffset = 0;
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     */
+    constructor();
+
+    /**
+     * 构造函数
+     *
+     * @param   pen     画笔
+     */
+    constructor(pen: SPen);
+
+    /**
+     * 构造函数
+     *
+     * @param   color       颜色
+     * @param   lineWidth   线宽
+     */
+    constructor(color: SColor, lineWidth?: number);
+
+    /**
+     * 构造函数
+     *
+     * @param   color       颜色 | 画笔
+     * @param   lineWidth   线宽
+     */
+    constructor(color?: SColor | SPen, lineWidth?: number) {
+        if (color == undefined) {
+            return;
+        }
+        if (color instanceof SPen) {
+            this.color = new SColor(color.color);
+            this.lineWidth = color.lineWidth;
+            this.lineCapStyle = color.lineCapStyle;
+            this.lineJoinStyle = color.lineJoinStyle;
+            this.miterLimit = color.miterLimit;
+            this.dashOffset = color.dashOffset;
+            this.lineDash =
+                color.lineDash != null ? color.lineDash.slice() : null;
+        } else {
+            this.color = new SColor(color);
+            this.lineWidth = lineWidth != undefined ? lineWidth : 1;
+        }
+    } // Constructor()
+} // Class SPen

+ 136 - 0
saga-web-draw/src/SRadialGradient.ts

@@ -0,0 +1,136 @@
+import { SGradient, SPoint } from "./";
+
+/**
+ * 放射性渐变
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SRadialGradient extends SGradient {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** 开始圆形坐标 */
+    start = new SPoint();
+    /** 开始圆形半径 */
+    r1: number;
+    /** 结束圆形坐标 */
+    end = new SPoint();
+    /** 结束圆形半径 */
+    r2: number;
+
+    /** 开始圆形X坐标 */
+    get x1(): number {
+        return this.start.x;
+    } // Get x1
+    set x1(value: number) {
+        this.start.x = value;
+    } // Set x1
+
+    /** 开始圆形Y坐标 */
+    get y1(): number {
+        return this.start.y;
+    } // Get y1
+    set y1(value: number) {
+        this.start.y = value;
+    } // Set y1
+
+    /** 结束圆形X坐标 */
+    get x2(): number {
+        return this.end.x;
+    } // Get x2
+    set x2(value: number) {
+        this.end.x = value;
+    } // Set x2
+
+    /** 结束圆形Y坐标 */
+    get y2(): number {
+        return this.end.y;
+    } // Get y2
+    set y2(value: number) {
+        this.end.y = value;
+    } // Set y2
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     *
+     * @param   start   开始圆形坐标
+     * @param   r1      开始圆形半径
+     * @param   end     结束圆形坐标
+     * @param   r2      结束圆形半径
+     */
+    constructor(start: SPoint, r1: number, end: SPoint, r2: number);
+
+    /**
+     * 构造函数
+     *
+     * @param   x1      开始圆形X坐标
+     * @param   y1      开始圆形Y坐标
+     * @param   r1      开始圆形半径
+     * @param   x2      结束圆形X坐标
+     * @param   y2      结束圆形Y坐标
+     * @param   r2      结束圆形半径
+     */
+    constructor(
+        x1: number,
+        y1: number,
+        r1: number,
+        x2: number,
+        y2: number,
+        r2: number
+    );
+
+    /**
+     * 构造函数(重载实现)
+     *
+     * @param   x1      开始圆形X坐标 | 开始圆形坐标
+     * @param   y1      开始圆形Y坐标 | 开始圆形半径
+     * @param   r1      开始圆形半径 | 结束圆形坐标
+     * @param   x2      结束圆形X坐标 | 结束圆形半径
+     * @param   y2      结束圆形Y坐标
+     * @param   r2      结束圆形半径
+     */
+    constructor(
+        x1: number | SPoint,
+        y1: number,
+        r1: number | SPoint,
+        x2: number,
+        y2?: number,
+        r2?: number
+    ) {
+        super();
+        if (x1 instanceof SPoint && r1 instanceof SPoint) {
+            this.start = new SPoint(x1);
+            this.end = new SPoint(r1);
+            this.r1 = y1;
+            this.r2 = x2;
+        } else {
+            this.start = new SPoint(x1 as number, y1 as number);
+            this.end = new SPoint(x2 as number, y2 as number);
+            this.r1 = r1 as number;
+            this.r2 = r2 as number;
+        }
+    } // Constructor()
+
+    /**
+     * 设置开始圆形坐标
+     *
+     * @param   x       X坐标
+     * @param   y       Y坐标
+     */
+    setStart(x: number, y: number): void {
+        this.start.x = x;
+        this.start.y = y;
+    } // Function setStart()
+
+    /**
+     * 设置结束圆形坐标
+     *
+     * @param   x       X坐标
+     * @param   y       Y坐标
+     */
+    setEnd(x: number, y: number): void {
+        this.end.x = x;
+        this.end.y = y;
+    } // Function setEnd()
+} // Class SRadialGradient

+ 490 - 0
saga-web-draw/src/engines/SCanvasPaintEngine.ts

@@ -0,0 +1,490 @@
+import { SPaintEngine } from "./SPaintEngine";
+import {
+    SLine,
+    SLineCapStyle,
+    SPaintEngineType,
+    SPoint,
+    SRect,
+    STextAlign,
+    STextBaseLine,
+    STextDirection
+} from "..";
+import { SPath2D } from "../SPath2D";
+
+/**
+ * Canvas绘制引擎基类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SCanvasPaintEngine extends SPaintEngine {
+    /** 画布对象 */
+    private readonly _canvas: CanvasRenderingContext2D;
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /**
+     * 绘制引擎类型
+     *
+     * @return  返回Canvas绘制引擎类型
+     */
+    get type(): SPaintEngineType {
+        return SPaintEngineType.Canvas;
+    } // Get type
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     *
+     * @param   canvas      canvas对象
+     */
+    constructor(canvas: CanvasRenderingContext2D) {
+        super();
+        this._canvas = canvas;
+        this._canvas.imageSmoothingEnabled = true;
+        // this._canvas.imageSmoothingQuality;
+    } // Constructor()
+
+    // =================================================================================================================
+    // 绘制图形
+    /**
+     * 设置裁剪路径
+     *
+     * @param   path        裁剪路径
+     */
+    setClip(path: Path2D): void {
+        this.setMatrix();
+        // this._canvas.stroke(path);
+        this._canvas.fill(path);
+        this._canvas.clip();
+    } // Function setClip()
+
+    /**
+     * 清空矩形区域
+     *
+     * @param   rect        矩形
+     */
+    clearRect(rect: SRect): void {
+        this.setMatrix();
+        this._canvas.clearRect(rect.x, rect.y, rect.width, rect.height);
+    } // Function clearRect()
+
+    /**
+     * 绘制空心矩形
+     *
+     * @param   rect        矩形
+     */
+    drawRect(rect: SRect): void {
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+        this._canvas.fillRect(rect.x, rect.y, rect.width, rect.height);
+        this._canvas.strokeRect(rect.x, rect.y, rect.width, rect.height);
+    } // Function drawRect()
+
+    /**
+     * 绘制圆形
+     *
+     * @param   cx          圆心X坐标
+     * @param   cy          圆心X坐标
+     * @param   r           圆半径
+     */
+    drawCircle(cx: number, cy: number, r: number): void {
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+        this._canvas.beginPath();
+        this._canvas.arc(cx, cy, r, 0, 2 * Math.PI, true);
+        this._canvas.fill();
+        this._canvas.stroke();
+    } // Function drawCircle()
+
+    /**
+     * 绘制椭圆
+     *
+     * @param   cx          圆点X坐标
+     * @param   cy          圆点Y坐标
+     * @param   rx          水平半径
+     * @param   ry          垂直半径
+     */
+    drawEllipse(cx: number, cy: number, rx: number, ry: number): void {
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+        this._canvas.beginPath();
+        this._canvas.ellipse(cx, cy, rx, ry, 0.5, 0, 2 * Math.PI, true);
+    } // Function drawEllipse()
+
+    /**
+     * 绘制椭圆弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawArc(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void {
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+        let p = SPath2D.arc(x, y, width, height, startAngle, endAngle);
+        let path = new Path2D(
+            SPath2D.arc(x, y, width, height, startAngle, endAngle)
+        );
+        this._canvas.stroke(path);
+    } // Function drawArc()
+
+    /**
+     * 绘制椭圆弦弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawChord(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void {
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+        let path = new Path2D(
+            SPath2D.chord(x, y, width, height, startAngle, endAngle)
+        );
+
+        this._canvas.fill(path);
+        this._canvas.stroke(path);
+    } // Function drawChord()
+
+    /**
+     * 绘制椭圆饼
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawPie(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void {
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+        let path = new Path2D(
+            SPath2D.pie(x, y, width, height, startAngle, endAngle)
+        );
+        this._canvas.fill(path);
+        this._canvas.stroke(path);
+    } // Function drawPie()
+
+    /**
+     * 绘制线段
+     *
+     * @param   line        线段
+     */
+    drawLine(line: SLine): void {
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+        this._canvas.beginPath();
+        this._canvas.moveTo(line.x1, line.y1);
+        this._canvas.lineTo(line.x2, line.y2);
+        this._canvas.stroke();
+    } // Function drawLine()
+
+    /**
+     * 绘制折线
+     *
+     * @param   points      折线折点
+     */
+    drawPolyline(points: SPoint[]): void {
+        // 折线至少要有2个节点
+        if (points.length < 2) {
+            return;
+        }
+
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+
+        this._canvas.beginPath();
+        this._canvas.moveTo(points[0].x, points[0].y);
+        for (let p of points) {
+            this._canvas.lineTo(p.x, p.y);
+        }
+
+        this._canvas.stroke();
+    } // Function drawPolyline()
+
+    /**
+     * 绘制多边形
+     *
+     * @param   points      多边形顶点
+     */
+    drawPolygon(points: SPoint[]): void {
+        // 多边形至少要有3个节点
+        if (points.length < 3) {
+            return;
+        }
+
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+
+        this._canvas.beginPath();
+        this._canvas.moveTo(points[0].x, points[0].y);
+        for (let p of points) {
+            this._canvas.lineTo(p.x, p.y);
+        }
+        this._canvas.closePath();
+
+        this._canvas.fill();
+        this._canvas.stroke();
+    } // Function drawPolygon()
+
+    /**
+     * 绘制路径
+     *
+     * @param   path        路径
+     */
+    drawPath(path: SPath2D): void {
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+
+        this._canvas.fill(path._path);
+        this._canvas.stroke(path._path);
+    } // Function drawPath()
+
+    /**
+     * 绘制文本
+     *
+     * @param   text        文本内容
+     * @param   x           X坐标
+     * @param   y           Y坐标
+     * @param   maxWidth    最大宽度
+     */
+    drawText(text: string, x: number, y: number, maxWidth?: number): void {
+        this.setMatrix();
+        this.setPen();
+        this.setBrush();
+        this.setFont();
+
+        if (maxWidth == undefined) {
+            this._canvas.fillText(text, x, y);
+        } else {
+            this._canvas.fillText(text, x, y, maxWidth);
+        }
+    } // Function drawText()
+
+    /**
+     * 绘制图片
+     *
+     * @param img       图片
+     * @param x         X坐标
+     * @param y         Y坐标
+     * @param width     宽度
+     * @param height    高度
+     */
+    drawImage(
+        img: CanvasImageSource,
+        x: number,
+        y: number,
+        width?: number,
+        height?: number
+    ): void {
+        this.setMatrix();
+        if (width == undefined) {
+            this._canvas.drawImage(img, x, y);
+        } else {
+            this._canvas.drawImage(img, x, y, width, height as number);
+        }
+    } // Function drawImage()
+
+    // /**
+    //  * 绘制带导角空心矩形
+    //  *
+    //  * @param   x           X坐标
+    //  * @param   y           Y坐标
+    //  * @param   w           宽度
+    //  * @param   h           高度
+    //  * @param   r           导角半径
+    //  */
+    // drawRoundedRect(x: number, y: number, w: number, h: number, r: number): void {
+    //     this.canvas.beginPath();
+    //     this.canvas.moveTo(x, y + r);
+    //     this.canvas.lineTo(x, y + h - r);
+    //     this.canvas.quadraticCurveTo(x, y + h, x + r, y + h);
+    //     this.canvas.lineTo(x + w - r,y + h);
+    //     this.canvas.quadraticCurveTo(x + w, y + h, x + w, y + h - r);
+    //     this.canvas.lineTo(x + w, y + r);
+    //     this.canvas.quadraticCurveTo(x + w, y, x + w - r, y);
+    //     this.canvas.lineTo(x + r, y);
+    //     this.canvas.quadraticCurveTo(x, y, x,y + r);
+    //     this.canvas.fill();
+    //     this.canvas.stroke();
+    // } // Function drawRoundedRect()
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 私有函数
+    /**
+     * 设置画笔
+     */
+    private setPen(): void {
+        //this._canvas.strokeStyle = this.state.pen.color.value;
+        this._canvas.strokeStyle = `rgba(${this.state.pen.color.red}, ${
+            this.state.pen.color.green
+        }, ${this.state.pen.color.blue}, ${this.state.pen.color.alpha /
+            255.0})`;
+
+        this._canvas.lineWidth = this.state.pen.lineWidth;
+        this._canvas.miterLimit = this.state.pen.miterLimit;
+
+        if (this.state.pen.lineDash != null) {
+            this._canvas.setLineDash(this.state.pen.lineDash);
+            this._canvas.lineDashOffset = this.state.pen.dashOffset;
+        } else {
+            this._canvas.setLineDash([]);
+            this._canvas.lineDashOffset = 0;
+        }
+
+        this.setLineCapStyle(this.state.pen.lineCapStyle);
+    } // Function setPen()
+
+    /**
+     * 线段端点风格
+     *
+     * @param   canvas      canvas对象
+     * @param   style       风格
+     */
+    private setLineCapStyle(style: SLineCapStyle): void {
+        // TODO: 找原因,去下边一行
+        if (style == undefined) return;
+        if (style == SLineCapStyle.Round) {
+            this._canvas.lineCap = "round";
+        } else if (style == SLineCapStyle.Square) {
+            this._canvas.lineCap = "square";
+        } else {
+            this._canvas.lineCap = "butt";
+        }
+    } // Set lineCapStyle
+
+    /**
+     * 设置画刷
+     */
+    private setBrush(): void {
+        // this._canvas.fillStyle = this.state.brush.color.value;
+        this._canvas.fillStyle = `rgba(${this.state.brush.color.red}, ${
+            this.state.brush.color.green
+        }, ${this.state.brush.color.blue}, ${this.state.brush.color.alpha /
+            255.0})`;
+    } // Function setBrush()
+
+    /**
+     * 设置字体
+     */
+    private setFont(): void {
+        this._canvas.font = `${this.state.font.size}px ${this.state.font.name}`;
+        this.setTextAlign(this.state.font.textAlign);
+        this.setBaseLine(this.state.font.textBaseLine);
+        this.setTextDirection(this.state.font.textDirection);
+    } // Function setFont()
+
+    /**
+     * 文本对齐选项
+     *
+     * @param   style       对齐方式
+     */
+    private setTextAlign(value: STextAlign): void {
+        // TODO: 找原因,去下边一行
+        if (value == undefined) return;
+        if (value === STextAlign.Start) {
+            this._canvas.textAlign = "start";
+        } else if (value === STextAlign.End) {
+            this._canvas.textAlign = "end";
+        } else if (value === STextAlign.Left) {
+            this._canvas.textAlign = "left";
+        } else if (value === STextAlign.Center) {
+            this._canvas.textAlign = "center";
+        } else {
+            this._canvas.textAlign = "right";
+        }
+    } // Function setTextAlign()
+
+    /**
+     * 设置文本基线对齐选项
+     *
+     * @param   value       对齐方式
+     */
+    private setBaseLine(value: STextBaseLine): void {
+        // TODO: 找原因,去下边一行
+        if (value == undefined) return;
+        if (value == STextBaseLine.Alphabetic) {
+            this._canvas.textBaseline = "alphabetic";
+        } else if (value == STextBaseLine.Top) {
+            this._canvas.textBaseline = "top";
+        } else if (value == STextBaseLine.Hanging) {
+            this._canvas.textBaseline = "hanging";
+        } else if (value == STextBaseLine.Middle) {
+            this._canvas.textBaseline = "middle";
+        } else if (value == STextBaseLine.Ideographic) {
+            this._canvas.textBaseline = "ideographic";
+        } else {
+            this._canvas.textBaseline = "bottom";
+        }
+    } // Set textBaseLine()
+
+    /**
+     * 设置文本方向选项
+     *
+     * @param   value       文本方向
+     */
+    private setTextDirection(value: STextDirection): void {
+        // TODO: 找原因,去下边一行
+        if (value == undefined) return;
+        if (value == STextDirection.Inherit) {
+            this._canvas.direction = "inherit";
+        } else if (value == STextDirection.LTR) {
+            this._canvas.direction = "ltr";
+        } else {
+            this._canvas.direction = "rtl";
+        }
+    } // Set textDirection
+
+    /**
+     * 设置变型矩阵
+     */
+    private setMatrix(): void {
+        this._canvas.setTransform(
+            this.state.matrix.a,
+            this.state.matrix.b,
+            this.state.matrix.c,
+            this.state.matrix.d,
+            this.state.matrix.e,
+            this.state.matrix.f
+        );
+    } // Function setMatrix()
+} // class SPaintEngine

+ 276 - 0
saga-web-draw/src/engines/SPaintEngine.ts

@@ -0,0 +1,276 @@
+import { SLine, SPaintEngineType, SPath2D, SPoint, SRect } from "..";
+import { SPaintState } from "./SPaintState";
+
+/**
+ * 绘制引擎基类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export abstract class SPaintEngine {
+    /** 当关状态 */
+    private _state = new SPaintState();
+    get state(): SPaintState {
+        return this._state;
+    } // Function state()
+    set state(value: SPaintState) {
+        this._state = value;
+    } // Function state()
+
+    /** 状态栈 */
+    private stateStack = new Array<SPaintState>();
+
+    /** 绘制引擎类型 */
+    abstract get type(): SPaintEngineType;
+
+    /**
+     * 保存painter状态
+     */
+    save(): void {
+        let state = new SPaintState(this.state);
+        this.stateStack.push(state);
+    } // Function save()
+
+    /**
+     * 恢复painter状态
+     */
+    restore(): void {
+        if (this.stateStack.length > 0) {
+            this.state = this.stateStack.pop() as SPaintState;
+        }
+    } // Function restore()
+
+    // =================================================================================================================
+    // 变换相关
+    /**
+     * 平移变换
+     *
+     * @param   x       X轴方向平移
+     * @param   y       Y辆方向平移
+     */
+    translate(x: number, y: number): void {
+        this.state.matrix.translateSelf(x, y);
+    } // Function translate()
+
+    /**
+     * 缩放
+     *
+     * @param   x       X轴方向缩放
+     * @param   y       Y辆方向缩放
+     */
+    scale(x: number, y: number): void {
+        this.state.matrix.scaleSelf(x, y);
+    } // Function scale()
+
+    /**
+     * 旋转
+     *
+     * @param   angle   旋转角度(单位弧度)
+     */
+    rotate(angle: number): void {
+        this.state.matrix.rotateSelf(0, 0, (angle * 180) / Math.PI);
+    } // Function rotate()
+
+    /**
+     * 将当前的变形矩阵乘上一个基于自身参数的矩阵
+     *
+     * @param   m11     水平方向的缩放
+     * @param   m12     水平方向的倾斜偏移
+     * @param   m21     竖直方向的倾斜偏移
+     * @param   m22     竖直方向的缩放
+     * @param   dx      水平方向的移动
+     * @param   dy      竖直方向的移动
+     */
+    transform(
+        m11: number,
+        m12: number,
+        m21: number,
+        m22: number,
+        dx: number,
+        dy: number
+    ): void {} // Function transform()
+
+    /**
+     * 将当前的变形矩阵重置为单位矩阵,再将当前的变形矩阵乘上一个基于自身参数的矩阵
+     *
+     * @param   m11     水平方向的缩放
+     * @param   m12     水平方向的倾斜偏移
+     * @param   m21     竖直方向的倾斜偏移
+     * @param   m22     竖直方向的缩放
+     * @param   dx      水平方向的移动
+     * @param   dy      竖直方向的移动
+     */
+    setTransform(
+        m11: number,
+        m12: number,
+        m21: number,
+        m22: number,
+        dx: number,
+        dy: number
+    ): void {} // Function transform()
+
+    /**
+     * 重置当前变形为单位矩阵。等价于调用setTransform(1, 0, 0, 1, 0, 0)
+     */
+    resetTransform(): void {
+        this.state.matrix = new DOMMatrix();
+    } // Function resetTransform()
+
+    // =================================================================================================================
+    // 绘制图形
+    /**
+     * 设置裁剪路径
+     *
+     * @param   path        裁剪路径
+     */
+    abstract setClip(path: Path2D): void;
+
+    /**
+     * 清空矩形区域
+     *
+     * @param   rect        矩形
+     */
+    abstract clearRect(rect: SRect): void;
+
+    /**
+     * 绘制空心矩形
+     *
+     * @param   rect        矩形
+     */
+    abstract drawRect(rect: SRect): void;
+
+    /**
+     * 绘制圆形
+     *
+     * @param   cx          圆心X坐标
+     * @param   cy          圆心X坐标
+     * @param   r           圆半径
+     */
+    abstract drawCircle(cx: number, cy: number, r: number): void;
+
+    /**
+     * 绘制椭圆
+     *
+     * @param   cx          圆点X坐标
+     * @param   cy          圆点Y坐标
+     * @param   rx          水平半径
+     * @param   ry          垂直半径
+     */
+    abstract drawEllipse(cx: number, cy: number, rx: number, ry: number): void;
+
+    /**
+     * 绘制椭圆弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startRadian     开始角度(单位弧度)
+     * @param   endRadian       结束角度(单位弧度)
+     */
+    abstract drawArc(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startRadian: number,
+        endRadian: number
+    ): void;
+
+    /**
+     * 绘制椭圆弦弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startRadian     开始角度(单位弧度)
+     * @param   endRadian       结束角度(单位弧度)
+     */
+    abstract drawChord(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startRadian: number,
+        endRadian: number
+    ): void;
+
+    /**
+     * 绘制椭圆饼
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    abstract drawPie(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void;
+
+    /**
+     * 绘制线段
+     *
+     * @param   line        线段
+     */
+    abstract drawLine(line: SLine): void;
+
+    /**
+     * 绘制折线
+     *
+     * @param   points      折线折点
+     */
+    abstract drawPolyline(points: SPoint[]): void;
+
+    /**
+     * 绘制多边形
+     *
+     * @param   points      多边形顶点
+     */
+    abstract drawPolygon(points: SPoint[]): void;
+
+    /**
+     * 绘制路径
+     *
+     * @param   path        路径
+     */
+    abstract drawPath(path: SPath2D): void;
+
+    /**
+     * 绘制文本
+     *
+     * @param   text        文本内容
+     * @param   x           X坐标
+     * @param   y           Y坐标
+     * @param   maxWidth    最大宽度
+     */
+    abstract drawText(
+        text: string,
+        x: number,
+        y: number,
+        maxWidth?: number
+    ): void;
+
+    /**
+     * 绘制图片
+     *
+     * @param img       图片
+     * @param x         X坐标
+     * @param y         Y坐标
+     * @param width     宽度
+     * @param height    高度
+     */
+    abstract drawImage(
+        img: CanvasImageSource,
+        x: number,
+        y: number,
+        width?: number,
+        height?: number
+    ): void;
+} // class SPaintEngine

+ 67 - 0
saga-web-draw/src/engines/SPaintState.ts

@@ -0,0 +1,67 @@
+import { SBrush, SFont, SPen } from "..";
+
+/**
+ * 绘制状态
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SPaintState {
+    /** 变换矩阵 */
+    matrix = new DOMMatrix();
+
+    /** 画笔 */
+    private _pen = new SPen();
+    get pen(): SPen {
+        return this._pen;
+    } // Get pen
+    set pen(value: SPen) {
+        this._pen = value;
+    } // Set pen
+
+    /** 画刷 */
+    private _brush = new SBrush();
+    get brush(): SBrush {
+        return this._brush;
+    } // Get brush
+    set brush(value: SBrush) {
+        this._brush = value;
+    } // Set brush
+
+    /** 字体 */
+    private _font = new SFont();
+    get font(): SFont {
+        return this._font;
+    } // Get font
+    set font(value: SFont) {
+        this._font = value;
+    } // Set font
+
+    /**
+     * 构造函数
+     */
+    constructor(state?: SPaintState) {
+        if (state != undefined) {
+            this.pen = new SPen(state.pen);
+            this.brush = new SBrush(state.brush);
+            this.font = new SFont(state.font);
+            let m = new DOMMatrix();
+            m.m11 = state.matrix.m11;
+            m.m12 = state.matrix.m12;
+            m.m13 = state.matrix.m13;
+            m.m14 = state.matrix.m14;
+            m.m21 = state.matrix.m21;
+            m.m22 = state.matrix.m22;
+            m.m23 = state.matrix.m23;
+            m.m24 = state.matrix.m24;
+            m.m31 = state.matrix.m31;
+            m.m32 = state.matrix.m32;
+            m.m33 = state.matrix.m33;
+            m.m34 = state.matrix.m34;
+            m.m41 = state.matrix.m41;
+            m.m42 = state.matrix.m42;
+            m.m43 = state.matrix.m43;
+            m.m44 = state.matrix.m44;
+            this.matrix = m;
+        }
+    } // Constructor
+} // class SPaintState

+ 371 - 0
saga-web-draw/src/engines/SSvgPaintEngine.ts

@@ -0,0 +1,371 @@
+import { SStringBuilder } from "@saga-web/base/lib";
+import { SPath2D } from "../SPath2D";
+import { SLine, SPaintEngine, SPaintEngineType, SPoint, SRect } from "..";
+
+/**
+ * Canvas绘制引擎基类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SSvgPaintEngine extends SPaintEngine {
+    private _builder = new SStringBuilder();
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** svg版本号(1.1) */
+    version = "1.1";
+    /** 宽度 */
+    width: number = 0;
+    /** 高度 */
+    height: number = 0;
+
+    /**
+     * 绘制引擎类型
+     *
+     * @return  返回SVG绘制引擎类型
+     */
+    get type(): SPaintEngineType {
+        return SPaintEngineType.SVG;
+    } // Get type
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     *
+     * @param   w       宽度
+     * @param   h       高度
+     */
+    constructor(w: number, h: number) {
+        super();
+        this.width = w;
+        this.height = h;
+
+        this._builder.append(`<?xml version='1.0' standalone='no'?>`);
+        this._builder.append(`<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"`);
+        this._builder.append(
+            `    "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">`
+        );
+        this._builder.append(
+            `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"`
+        );
+        this._builder.append(
+            `version="${this.version}" width="${w}" height="${h}">`
+        );
+    } // Constructor
+
+    /**
+     * 转svg图像
+     *
+     * @return  svg图像
+     */
+    toSvg(): string {
+        return this._builder.toString("\n") + "</svg>\n";
+    } // Function toSvg()
+
+    // =================================================================================================================
+    // 绘制图形
+    /**
+     * 设置裁剪路径
+     *
+     * @param   path        裁剪路径
+     */
+    setClip(path: Path2D): void {
+        // TODO:  PLX添加实现代码
+    } // Function setClip()
+    /**
+     * 清空矩形区域
+     *
+     * @param   rect        矩形
+     */
+    clearRect(rect: SRect): void {} // Function clearRect()
+
+    /**
+     * 绘制空心矩形
+     *
+     * @param   rect       矩形
+     */
+    drawRect(rect: SRect): void {
+        this._builder.append(
+            // eslint-disable-next-line max-len
+            `<rect x="${rect.x}" y="${rect.y}" width="${rect.width}" height="${
+                rect.height
+            }" ${this.getStyle(true, true, false)} ${this.getSvgMatrix()}/>`
+        );
+    } // Functin drawRects()
+
+    /**
+     * 绘制圆形
+     *
+     * @param   cx          圆心X坐标
+     * @param   cy          圆心X坐标
+     * @param   r           圆半径
+     */
+    drawCircle(cx: number, cy: number, r: number): void {
+        this._builder.append(
+            `<circle cx="${cx}" cy="${cy}" r="${r}" ${this.getStyle(
+                true,
+                true,
+                false
+            )} ${this.getSvgMatrix()}/>`
+        );
+    } // Function drawCircle()
+
+    /**
+     * 绘制椭圆
+     *
+     * @param   cx          圆点X坐标
+     * @param   cy          圆点Y坐标
+     * @param   rx          水平半径
+     * @param   ry          垂直半径
+     */
+    drawEllipse(cx: number, cy: number, rx: number, ry: number): void {
+        this._builder.append(
+            `<ellipse  cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" ${this.getStyle(
+                true,
+                true,
+                false
+            )} ${this.getSvgMatrix()}/>`
+        );
+    } // Function drawEllipse()
+
+    /**
+     * 绘制椭圆弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawArc(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void {
+        let path = SPath2D.arc(x, y, width, height, startAngle, endAngle);
+
+        this._builder.append(
+            `<path d="${path}" ${this.getStyle(
+                false,
+                true,
+                false
+            )} ${this.getSvgMatrix()}/>`
+        );
+    } // Function drawArc()
+
+    /**
+     * 绘制椭圆弦弧
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawChord(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void {
+        let path = SPath2D.chord(x, y, width, height, startAngle, endAngle);
+
+        this._builder.append(
+            `<path d="${path}" ${this.getStyle(
+                true,
+                true,
+                false
+            )} ${this.getSvgMatrix()}/>`
+        );
+    } // Function drawChord()
+
+    /**
+     * 绘制椭圆饼
+     *
+     * @param   x               椭圆所在矩形X坐标
+     * @param   y               椭圆所在矩形Y坐标
+     * @param   width           椭圆所在矩形宽度
+     * @param   height          椭圆所在矩形高度
+     * @param   startAngle      开始角度(单位弧度)
+     * @param   endAngle        结束角度(单位弧度)
+     */
+    drawPie(
+        x: number,
+        y: number,
+        width: number,
+        height: number,
+        startAngle: number,
+        endAngle: number
+    ): void {
+        let path = SPath2D.pie(x, y, width, height, startAngle, endAngle);
+
+        this._builder.append(
+            `<path d="${path}" ${this.getStyle(
+                true,
+                true,
+                false
+            )} ${this.getSvgMatrix()}/>`
+        );
+    } // Function drawPie()
+
+    /**
+     * 绘制线段
+     *
+     * @param   line        线段
+     */
+    drawLine(line: SLine): void {
+        this._builder.append(
+            `<line x1="${line.x1}" y1="${line.y1}" x2="${line.x2}" y2="${
+                line.y2
+            }" ${this.getStyle(false, true, false)} ${this.getSvgMatrix()}/>`
+        );
+    } // Function drawLine()
+
+    /**
+     * 绘制折线
+     *
+     * @param   points      折线折点
+     */
+    drawPolyline(points: SPoint[]): void {
+        this._builder.append(
+            `<polyline points="${SSvgPaintEngine.pointsToStr(
+                points
+            )}" ${this.getStyle(false, true, false)} ${this.getSvgMatrix()}/>`
+        );
+    } // Function drawPolyline()
+
+    /**
+     * 绘制多边形
+     *
+     * @param   points      多边形顶点
+     */
+    drawPolygon(points: SPoint[]): void {
+        this._builder.append(
+            `<polygon points="${SSvgPaintEngine.pointsToStr(
+                points
+            )}" ${this.getStyle(true, true, false)} ${this.getSvgMatrix()}/>`
+        );
+    } // Function drawPolyline()
+
+    /**
+     * 绘制路径
+     *
+     * @param   path        路径
+     */
+    drawPath(path: SPath2D): void {
+        // TODO: 添加实现代码
+    } // Function drawPath()
+
+    /**
+     * 绘制文本
+     *
+     * @param   text        文本内容
+     * @param   x           X坐标
+     * @param   y           Y坐标
+     * @param   maxWidth    最大宽度
+     */
+    drawText(text: string, x: number, y: number, maxWidth?: number): void {
+        this._builder.append(
+            `<text x="${x}" y="${y}" ${this.getStyle(
+                true,
+                false,
+                true
+            )} ${this.getSvgMatrix()}>${text}</text>>`
+        );
+    } // Function drawText()
+
+    /**
+     * 绘制图片
+     *
+     * @param img       图片
+     * @param x         X坐标
+     * @param y         Y坐标
+     * @param width     宽度
+     * @param height    高度
+     */
+    drawImage(
+        img: CanvasImageSource,
+        x: number,
+        y: number,
+        width?: number,
+        height?: number
+    ): void {
+        // TODO: 未完成
+    } // Function drawImage()
+
+    /**
+     * 获得svg需要的变形信息
+     *
+     * @return  变换信息
+     */
+    private getSvgMatrix(): string {
+        // eslint-disable-next-line max-len
+        return `transform="matrix(${this.state.matrix.a}, ${this.state.matrix.b}, ${this.state.matrix.c}, ${this.state.matrix.d}, ${this.state.matrix.e}, ${this.state.matrix.f})"`;
+    } // Function getMatrix()
+
+    /**
+     * 将点列表转换为字符串表达形式
+     *
+     * @param   points      被转换的点
+     * @return  点列表字符串
+     */
+    private static pointsToStr(points: SPoint[]): string {
+        let strBuilder = new SStringBuilder();
+        for (let p of points) {
+            strBuilder.append(`${p.x},${p.y}`);
+        }
+        return strBuilder.toString(" ");
+    } // Function pointsToStr()
+
+    /** 获得风格 */
+    private getStyle(
+        brush: boolean = false,
+        pen: boolean = false,
+        font: boolean = false
+    ): string {
+        let builder = new SStringBuilder();
+        builder.append('style="');
+        if (pen) {
+            // 画笔
+            // eslint-disable-next-line max-len
+            builder.append(
+                `stroke:rgba(${this.state.pen.color.red}, ${
+                    this.state.pen.color.green
+                }, ${this.state.pen.color.blue}, ${this.state.pen.color.alpha /
+                    255.0});`
+            );
+            builder.append(`stroke-width:${this.state.pen.lineWidth};`);
+        } else {
+            builder.append(`stroke-width:0;`);
+        }
+
+        if (brush) {
+            // 画刷
+            builder.append(
+                `fill:rgba(${this.state.brush.color.red}, ${
+                    this.state.brush.color.green
+                }, ${this.state.brush.color.blue}, ${this.state.brush.color
+                    .alpha / 255.0});`
+            );
+        } else {
+            builder.append(`fill:rgba(0,0,0,0);`);
+        }
+
+        if (font) {
+            // 字体
+            // builder.append(`font-family:"${this.state.font.name}";`);
+            builder.append(`font-size:${this.state.font.size}px;`);
+            // builder.append(`text-align:${this.state.font.textAlign};`);
+        }
+        builder.append('"');
+        return builder.toString("");
+    } // Get penStyle
+} // Class SSvgPaintEngine

+ 13 - 0
saga-web-draw/src/enums/SAspectRatioMode.ts

@@ -0,0 +1,13 @@
+/**
+ * 终横比模式
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export enum SAspectRatioMode {
+    /** 自由缩放放,不保存纵横比 */
+    IgnoreAspectRatio,
+    /** 自由缩放,保存纵横比 */
+    KeepAspectRatio,
+    /** 自由缩放,保存纵横比 */
+    KeepAspectRatioByExpanding
+} // Enum SAspectRatioMode

+ 11 - 0
saga-web-draw/src/enums/SBrushType.ts

@@ -0,0 +1,11 @@
+/**
+ * 线端端点样式
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export enum SBrushType {
+    /** 颜色 */
+    Color,
+    /** 渐变 */
+    Gradient
+} // Enum SBrushType

+ 13 - 0
saga-web-draw/src/enums/SLineCapStyle.ts

@@ -0,0 +1,13 @@
+/**
+ * 线端端点样式
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export enum SLineCapStyle {
+    /** 线段末端以方形结束。 */
+    Butt,
+    /** 线段末端以圆形结束。 */
+    Round,
+    /** 线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域。 */
+    Square
+} // Enum SLineCapStyle

+ 13 - 0
saga-web-draw/src/enums/SLineJoinStyle.ts

@@ -0,0 +1,13 @@
+/**
+ * 线段与线段间接合处的样式
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export enum SLineJoinStyle {
+    /** 默认。通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域。 */
+    Miter,
+    /** 通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。 圆角的半径是线段的宽度。 */
+    Round,
+    /** 在相连部分的末端填充一个额外的以三角形为底的区域, 每个部分都有各自独立的矩形拐角。 */
+    Bevel
+} // Enum SLineJoinStyle

+ 15 - 0
saga-web-draw/src/enums/SPaintEngineType.ts

@@ -0,0 +1,15 @@
+/**
+ * 绘制引擎类型
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export enum SPaintEngineType {
+    /** 线段末端以方形结束。 */
+    Canvas,
+    /** SVG绘制引擎 */
+    SVG,
+    /** OpenGL引擎 */
+    OpenGL,
+    /** 图片绘制引擎 */
+    Image
+} // Enum SPaintEngineType

+ 13 - 0
saga-web-draw/src/enums/STextAlign.ts

@@ -0,0 +1,13 @@
+/**
+ * 文本对齐选项
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export enum STextAlign {
+    /** 默认 */
+    Start,
+    End,
+    Left,
+    Right,
+    Center
+} // Enum STextAlign

+ 14 - 0
saga-web-draw/src/enums/STextBaseLine.ts

@@ -0,0 +1,14 @@
+/**
+ * 文本基线对齐选项
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export enum STextBaseLine {
+    /** 默认 */
+    Alphabetic,
+    Top,
+    Hanging,
+    Middle,
+    Ideographic,
+    Bottom
+} // Enum STextBaseLine

+ 11 - 0
saga-web-draw/src/enums/STextDirection.ts

@@ -0,0 +1,11 @@
+/**
+ * 文本方向
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export enum STextDirection {
+    /** 默认 */
+    Inherit,
+    LTR,
+    RTL
+} // Enum STextDirection

+ 58 - 0
saga-web-draw/src/index.ts

@@ -0,0 +1,58 @@
+import { SCanvasPaintEngine } from "./engines/SCanvasPaintEngine";
+import { SPaintEngine } from "./engines/SPaintEngine";
+import { SPaintState } from "./engines/SPaintState";
+import { SSvgPaintEngine } from "./engines/SSvgPaintEngine";
+export { SCanvasPaintEngine, SPaintEngine, SPaintState, SSvgPaintEngine };
+
+import { SAspectRatioMode } from "./enums/SAspectRatioMode";
+import { SBrushType } from "./enums/SBrushType";
+import { SLineCapStyle } from "./enums/SLineCapStyle";
+import { SLineJoinStyle } from "./enums/SLineJoinStyle";
+import { SPaintEngineType } from "./enums/SPaintEngineType";
+import { STextAlign } from "./enums/STextAlign";
+import { STextBaseLine } from "./enums/STextBaseLine";
+import { STextDirection } from "./enums/STextDirection";
+export {
+    SAspectRatioMode,
+    SBrushType,
+    SLineCapStyle,
+    SLineJoinStyle,
+    SPaintEngineType,
+    STextAlign,
+    STextBaseLine,
+    STextDirection
+};
+
+import { SLine } from "./types/SLine";
+import { SPoint } from "./types/SPoint";
+import { SRect } from "./types/SRect";
+import { SSize } from "./types/SSize";
+export { SLine, SPoint, SRect, SSize };
+
+import { SPolygonUtil } from "./utils/SPolygonUtil";
+export { SPolygonUtil };
+
+import { SBrush } from "./SBrush";
+import { SCanvasView } from "./SCanvasView";
+import { SColor } from "./SColor";
+import { SFont } from "./SFont";
+import { SGradient } from "./SGradient";
+import { SGradientStop } from "./SGradientStop";
+import { SLinearGradient } from "./SLinearGradient";
+import { SPainter } from "./SPainter";
+import { SPath2D } from "./SPath2D";
+import { SPen } from "./SPen";
+import { SRadialGradient } from "./SRadialGradient";
+export {
+    SBrush,
+    SCanvasView,
+    SColor,
+    SFont,
+    SGradient,
+    SGradientStop,
+    SLinearGradient,
+    SPainter,
+    SPath2D,
+    SPen,
+    SRadialGradient
+};

+ 170 - 0
saga-web-draw/src/types/SLine.ts

@@ -0,0 +1,170 @@
+import { SPoint } from "./SPoint";
+
+/**
+ * 线数据类型定义
+ *
+ * @author 庞利祥(sybotan@126.com)
+ */
+export class SLine {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** 端点1坐标 */
+    p1: SPoint;
+    /** 端点2坐标 */
+    p2: SPoint;
+
+    /** 属性X1 */
+    get x1(): number {
+        return this.p1.x;
+    } // Get x1
+    set x1(value: number) {
+        this.p1.x = value;
+    } // Set x1
+
+    /** 属性Y1 */
+    get y1(): number {
+        return this.p1.y;
+    } // Get y1
+    set y1(value: number) {
+        this.p1.y = value;
+    } // Set y1
+
+    /** 属性X2 */
+    get x2(): number {
+        return this.p2.x;
+    } // Get x2
+    set x2(v: number) {
+        this.p2.x = v;
+    } // Set x2
+
+    /** 属性Y2 */
+    get y2(): number {
+        return this.p2.y;
+    } // Get y2
+    set y2(value: number) {
+        this.p2.y = value;
+    } // Set y2
+
+    /** 属性dx */
+    get dx(): number {
+        return this.p2.x - this.p1.x;
+    } // Get dx
+    /** 属性dy */
+    get dy(): number {
+        return this.p2.y - this.p1.y;
+    } // Get dy()
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     */
+    constructor();
+
+    /**
+     * 构造函数
+     *
+     * @param   line    线段
+     */
+    constructor(line: SLine);
+
+    /**
+     * 构造函数
+     *
+     * @param   p1      启点坐标
+     * @param   p2      终点坐标
+     */
+    constructor(p1: SPoint, p2: SPoint);
+
+    /**
+     * 构造函数
+     *
+     * @param   x1      启点X坐标
+     * @param   y1      启点Y坐标
+     * @param   x2      终点X坐标
+     * @param   y2      终点Y坐标
+     */
+    constructor(x1: number, y1: number, x2: number, y2: number);
+
+    /**
+     * 构造函数(重载实现)
+     *
+     * @param   x1      启点X坐标 | 启点坐标 | 线段
+     * @param   y1      启点Y坐标 | 终点坐标
+     * @param   x2      终点X坐标
+     * @param   y2      终点Y坐标
+     */
+    constructor(
+        x1?: number | SPoint | SLine,
+        y1?: number | SPoint,
+        x2?: number,
+        y2?: number
+    ) {
+        if (x1 == undefined) {
+            this.p1 = new SPoint(0, 0);
+            this.p2 = new SPoint(0, 0);
+        } else if (x1 instanceof SLine) {
+            this.p1 = new SPoint(x1.p1.x, x1.p1.y);
+            this.p2 = new SPoint(x1.p2.x, x1.p2.y);
+        } else if (x1 instanceof SPoint && y1 instanceof SPoint) {
+            this.p1 = new SPoint(x1.x, x1.y);
+            this.p2 = new SPoint(y1.x, y1.y);
+        } else {
+            this.p1 = new SPoint(x1 as number, y1 as number);
+            this.p2 = new SPoint(x2 as number, y2 as number);
+        }
+    } // constructor
+
+    /**
+     * 判断线段是否为空
+     *
+     * @return  如果线段长度为零,返回true,否则返回false。
+     */
+    isNull(): boolean {
+        return (
+            Math.abs(this.p1.x - this.p2.x) + Math.abs(this.p1.y - this.p2.y) <
+            0.000001
+        );
+    } // Function isNull()
+
+    /**
+     * 返回线段的中点
+     *
+     * @return  线段的中点
+     */
+    center(): SPoint {
+        return new SPoint(
+            (this.p1.x + this.p2.x) / 2,
+            (this.p1.y + this.p2.y) / 2
+        );
+    } // Function center()
+
+    /**
+     * 平移线段
+     *
+     * @param   dx      X轴方向平衡量
+     * @param   dy      Y轴方向平衡量
+     */
+    translate(dx: number, dy: number): void {
+        this.p1.x += dx;
+        this.p2.x += dx;
+        this.p1.y += dy;
+        this.p2.y += dy;
+    } // Function translate()
+
+    /**
+     * 生成平移线段
+     *
+     * @param   dx      X轴方向平衡量
+     * @param   dy      Y轴方向平衡量
+     * @return  平移后的线段
+     */
+    translated(dx: number, dy: number): SLine {
+        return new SLine(
+            this.p1.x + dx,
+            this.p1.y + dy,
+            this.p2.x + dx,
+            this.p2.y + dy
+        );
+    } // Function translate()
+} // Class SLine

+ 92 - 0
saga-web-draw/src/types/SPoint.ts

@@ -0,0 +1,92 @@
+/**
+ * 点数据类型定义
+ *
+ * @author 庞利祥(sybotan@126.com)
+ */
+export class SPoint {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** X轴坐标 */
+    x: number;
+    /** Y轴坐标 */
+    y: number;
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     */
+    constructor();
+
+    /**
+     * 构造函数
+     *
+     * @param   p       点
+     */
+    constructor(p: SPoint);
+
+    /**
+     * 构造函数
+     *
+     * @param   x       X轴坐标
+     * @param   y       Y轴坐标
+     */
+    constructor(x: number, y: number);
+
+    /**
+     * 构造函数(重载实现)
+     *
+     * @param   x       X轴坐标 | 点 | undefined
+     * @param   y       Y轴坐标
+     */
+    constructor(x?: number | SPoint, y?: number) {
+        if (x == undefined) {
+            this.x = 0;
+            this.y = 0;
+        } else if (x instanceof SPoint) {
+            this.x = x.x;
+            this.y = x.y;
+        } else {
+            this.x = x as number;
+            this.y = y as number;
+        }
+    } // Function constructor()
+
+    /**
+     * 设置点
+     * @param   pos      点
+     */
+    setPoint(pos: SPoint): void;
+
+    /**
+     * 设置点
+     * @param   x       X轴坐标
+     * @param   y       Y轴坐标
+     */
+    setPoint(x: number, y: number): void;
+
+    /**
+     * 设置点(重载实现)
+     * @param   x       X轴坐标 | 点
+     * @param   y       Y轴坐标
+     */
+    setPoint(x: number | SPoint, y?: number): void {
+        if (x instanceof SPoint) {
+            this.x = x.x;
+            this.y = x.y;
+        } else {
+            this.x = x as number;
+            this.y = y as number;
+        }
+    } // Function setPoint()
+
+    /**
+     * 布线长度
+     *
+     * @return  返回 |X| + |Y|
+     */
+    manhattanLength(): number {
+        // noinspection JSSuspiciousNameCombination
+        return Math.abs(this.x) + Math.abs(this.y);
+    } // Function manhattanLength()
+} // Class SPoint

+ 287 - 0
saga-web-draw/src/types/SRect.ts

@@ -0,0 +1,287 @@
+import { SPoint } from "./SPoint";
+import { SSize } from "./SSize";
+
+/**
+ * 矩形数据类型定义
+ *
+ * @author 庞利祥(sybotan@126.com)
+ */
+export class SRect {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** 左上角坐标 */
+    leftTop: SPoint;
+    /** 大小 */
+    size: SSize;
+
+    /** 矩形的X轴坐标 */
+    get x(): number {
+        return this.leftTop.x;
+    } // Get x
+    set x(value: number) {
+        this.leftTop.x = value;
+    } // Set x
+
+    /** 矩形的Y轴坐标 */
+    get y(): number {
+        return this.leftTop.y;
+    } // Get x
+    set y(value: number) {
+        this.leftTop.y = value;
+    } // Set y
+
+    /** 矩形的宽 */
+    get width(): number {
+        return this.size.width;
+    } // Get width
+    set width(v: number) {
+        this.size.width = v;
+    } // Set width
+
+    /** 矩形的高 */
+    get height(): number {
+        return this.size.height;
+    } // Get height
+    set height(v: number) {
+        this.size.height = v;
+    } // Set height
+
+    /** 矩形的左 */
+    get left(): number {
+        return this.leftTop.x;
+    } // Get left
+    set left(v: number) {
+        this.leftTop.x = v;
+    } // Set left
+
+    /** 矩形的上 */
+    get top(): number {
+        return this.y;
+    } // Get top
+    set top(v: number) {
+        this.y = v;
+    } // Set top
+
+    /** 矩形的右 */
+    get right(): number {
+        return this.x + this.width;
+    } // get right
+    set right(right: number) {
+        this.width = right - this.x;
+    } // Set right
+
+    /** 矩形的下 */
+    get bottom(): number {
+        return this.y + this.height;
+    } // Get bottom
+    set bottom(value: number) {
+        this.height = value - this.y;
+    } // Set bottom
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     */
+    constructor();
+
+    /**
+     *构造函数
+     *
+     * @param   leftTop         左上角坐标
+     * @param   rightBottom     右下角坐标
+     */
+    constructor(leftTop: SPoint, rightBottom: SPoint);
+
+    /**
+     * 构造函数
+     *
+     * @param   leftTop         左上角坐标
+     * @param   size            大小
+     */
+    constructor(leftTop: SPoint, size: SSize);
+
+    /**
+     * 构造函数
+     *
+     * @param   x           X轴坐标
+     * @param   y           Y轴坐标
+     * @param   width       宽度
+     * @param   height      高度
+     */
+    constructor(x: number, y: number, width: number, height: number);
+
+    /**
+     * 构造函数
+     *
+     * @param   x           X轴坐标 | 左上角坐标
+     * @param   y           Y轴坐标 | 右下角坐标 | 大小
+     * @param   width       宽度
+     * @param   height      高度
+     */
+    constructor(
+        x?: number | SPoint,
+        y?: number | SPoint | SSize,
+        width?: number,
+        height?: number
+    ) {
+        if (x == undefined) {
+            // constructor()
+            this.leftTop = new SPoint(0, 0);
+            this.size = new SSize(0, 0);
+        } else if (x instanceof SPoint && y instanceof SPoint) {
+            // constructor(leftTop: SPoint, rightBottom: SPoint)
+            this.leftTop = new SPoint(x.x, x.y);
+            this.size = new SSize(y.x - x.x, y.y - x.y);
+        } else if (x instanceof SPoint && y instanceof SSize) {
+            // constructor(leftTop: SPoint, size: SSize)
+            this.leftTop = new SPoint(x.x, x.y);
+            this.size = new SSize(y.width, y.height);
+        } else {
+            // constructor(x: number, y: number, width: number, height: number)
+            this.leftTop = new SPoint(x as number, y as number);
+            this.size = new SSize(width as number, height as number);
+        }
+    } // constructor
+
+    /**
+     * 是否为空
+     *
+     * @return  width或height小于等于0,返回true,否则返回false。
+     *
+     * @see isNull(), isValid()
+     */
+    isEmpty(): boolean {
+        return this.size.isEmpty();
+    } // Function isEmpty()
+
+    /**
+     * 是否为Null
+     *
+     * @return  width与height都等于0,返回true,否则返回false。
+     *
+     * @see isEmpty(), isValid()
+     */
+    isNull(): boolean {
+        return this.size.isNull();
+    } // Function isNull()
+
+    /**
+     * 是否有效
+     *
+     * @return  width与height都大于0,返回true,否则返回false。
+     * @see isEmpty(), isNull()
+     */
+    isValid(): boolean {
+        return this.size.isValid();
+    } // Function isValid()
+
+    /**
+     * 判断矩形空间是否包含点x,y
+     *
+     * @param   x       X坐标
+     * @param   y       Y坐标
+     * @return  如果包含返回true, 否则返回false
+     */
+    contains(x: number, y: number): boolean {
+        return (
+            x >= this.left &&
+            x <= this.right &&
+            (y >= this.top && y <= this.bottom)
+        );
+    } // Function contains()
+
+    /**
+     * 计算中心点
+     *
+     * @return  中心点坐标
+     */
+    center(): SPoint {
+        return new SPoint(
+            this.x + this.width / 2.0,
+            this.y + this.height / 2.0
+        );
+    } // Function centerPos()
+
+    /**
+     * 平移矩形
+     *
+     * @param   dx      X轴位移
+     * @param   dy      Y轴位移
+     */
+    translate(dx: number, dy: number): void {
+        this.x += dx;
+        this.y += dy;
+    } // Function translate()
+
+    /**
+     * 生成平移矩形
+     *
+     * @param   dx      X轴位移
+     * @param   dy      Y轴位移
+     * @return  移动后的矩形
+     */
+    translated(dx: number, dy: number): SRect {
+        return new SRect(this.x + dx, this.y + dy, this.width, this.height);
+    } // Function translated()
+
+    /**
+     * 调整Rect位置
+     *
+     * @param   dx          X轴位移
+     * @param   dy          Y轴位移
+     * @param   dw          宽度调整
+     * @param   dh          高度调整
+     */
+    adjust(dx: number, dy: number, dw: number, dh: number): void {
+        this.x += dx;
+        this.y += dy;
+        this.width += dw;
+        this.height += dh;
+    } // Function adjusted()
+
+    /**
+     * 调整Rect位置
+     *
+     * @param   dx          X轴位移
+     * @param   dy          Y轴位移
+     * @param   dw          宽度调整
+     * @param   dh          高度调整
+     * @return  SRect       调整后的矩形
+     */
+    adjusted(dx: number, dy: number, dw: number, dh: number): SRect {
+        return new SRect(
+            this.x + dx,
+            this.y + dy,
+            this.width + dw,
+            this.height + dh
+        );
+    } // Function adjusted()
+
+    /**
+     * 合并矩形
+     *
+     * @param   rect    合并的矩形
+     */
+    union(rect: SRect): void {
+        let r = this.unioned(rect);
+        this.x = r.x;
+        this.y = r.y;
+        this.width = r.width;
+        this.height = r.height;
+    } // Function union()
+
+    /**
+     * 生成合并矩形
+     *
+     * @param   rect    合并的矩形
+     * @return  返回合并后的矩形
+     */
+    unioned(rect: SRect): SRect {
+        let left = Math.min(this.left, rect.left);
+        let top = Math.min(this.top, rect.top);
+        let right = Math.max(this.right, rect.right);
+        let bottom = Math.max(this.bottom, rect.bottom);
+        return new SRect(left, top, right - left, bottom - top);
+    } // Function union()
+} // Class SRect

+ 160 - 0
saga-web-draw/src/types/SSize.ts

@@ -0,0 +1,160 @@
+import {SAspectRatioMode} from "..";
+
+/**
+ * 大小数据类型定义
+ *
+ * @author 庞利祥(sybotan@126.com)
+ */
+export class SSize {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性定义
+    /** 宽度 */
+    width: number;
+    /** 高度 */
+    height: number;
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 构造函数
+    /**
+     * 构造函数
+     */
+    constructor();
+
+    /**
+     * 构造函数
+     *
+     * @param   size    大小
+     */
+    constructor(size: SSize);
+
+    /**
+     * 构造函数(重载实现)
+     *
+     * @param   width   宽度
+     * @param   height  高度
+     */
+    constructor(width: number, height: number);
+
+    /**
+     * 构造函数(重载实现)
+     *
+     * @param   width   宽度
+     * @param   height  高度
+     */
+    constructor(width?: number | SSize, height?: number) {
+        if (width == undefined) {
+            this.width = 0;
+            this.height = 0;
+        } else if (width instanceof SSize) {
+            this.width = width.width;
+            this.height = width.height;
+        } else {
+            this.width = width as number;
+            this.height = height as number;
+        }
+    } // Function constructor()
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 方法
+    /**
+     * 是否为空
+     *
+     * @return  width或height小于等于0,返回true,否则返回false。
+     *
+     * @see isNull(), isValid()
+     */
+    isEmpty(): boolean {
+        return this.width <= 0 || this.height <= 0;
+    } // Function isEmpty()
+
+    /**
+     * 是否为Null
+     *
+     * @return  width与height都等于0,返回true,否则返回false。
+     *
+     * @see isEmpty(), isValid()
+     */
+    isNull(): boolean {
+        return this.width == 0 && this.height == 0;
+    } // Function isNull()
+
+    /**
+     * 是否有效
+     *
+     * @return  width与height都大于0,返回true,否则返回false。
+     * @see isEmpty(), isNull()
+     */
+    isValid(): boolean {
+        return this.width > 0 && this.height > 0;
+    } // Function isValid()
+
+    /**
+     * 缩放
+     *
+     * @param   width       宽度
+     * @param   height      高度
+     * @param   mode        缩放模式
+     */
+    scale(
+        width: number,
+        height: number,
+        mode: SAspectRatioMode = SAspectRatioMode.IgnoreAspectRatio
+    ): void {
+        let s = this.scaled(width, height, mode);
+        this.width = s.width;
+        this.height = s.height;
+    } // Function scale()
+
+    /**
+     * 生成缩放大小
+     *
+     * @param   width       宽度
+     * @param   height      高度
+     * @param   mode        缩放模式
+     * @return  缩放大小
+     */
+    scaled(
+        width: number,
+        height: number,
+        mode: SAspectRatioMode = SAspectRatioMode.IgnoreAspectRatio
+    ): SSize {
+        let w = 0;
+        let h = 0;
+        if (mode == SAspectRatioMode.KeepAspectRatio) {
+            let s = Math.min(width / this.width, height / this.height);
+            w = this.width * s;
+            h = this.height * s;
+        } else if (mode == SAspectRatioMode.KeepAspectRatioByExpanding) {
+            let s = Math.max(width / this.width, height / this.height);
+            w = this.width * s;
+            h = this.height * s;
+        } else {
+            // 自由模式
+            w = width;
+            h = height;
+        }
+        return new SSize(w, h);
+    } // Function scaled()
+
+    /**
+     * 互换width与height
+     *
+     * @see transposed()
+     */
+    transpose(): void {
+        let size = this.transposed();
+        this.width = size.width;
+        this.height = size.height;
+    } // Function transpose()
+
+    /**
+     * 生成互换width与height的SSize
+     *
+     * @return  返回互换width与height的SSize。
+     *
+     * @see transpose()
+     */
+    transposed(): SSize {
+        return new SSize(this.height, this.width);
+    } // Function transposed()
+} // Class SSize

+ 70 - 0
saga-web-draw/src/utils/SPolygonUtil.ts

@@ -0,0 +1,70 @@
+import { SPoint } from "..";
+
+/**
+ * 多边形工具
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SPolygonUtil {
+    /**
+     * 判断点是否在多边形内
+     *
+     * @param   x           点的x坐标
+     * @param   y           点的y坐标
+     * @param   pointList   多边形顶点
+     */
+    static pointIn(x: number, y: number, pointList: SPoint[]): boolean {
+        let crossNum = 0;
+        for (let i = 0; i < pointList.length; i++) {
+            let p1 = pointList[i];
+            let p2 = pointList[(i + 1) % pointList.length];
+
+            // p1p2 与 y=p0.y平行
+            if (p1.y == p2.y) {
+                continue;
+            }
+
+            // 交点在p1p2延长线上
+            if (y < Math.min(p1.y, p2.y) || y >= Math.max(p1.y, p2.y)) {
+                continue;
+            }
+
+            // 求交点的 x 坐标
+            let x0 = ((y - p1.y) * (p2.x - p1.x)) / (p2.y - p1.y) + p1.x;
+
+            if (x0 > x) {
+                // 只统计单边交点
+                crossNum++;
+            }
+        }
+
+        // 单边交点为奇数,点在多边形之内
+        return crossNum % 2 == 1;
+    } // Function pointIn()
+
+    /**
+     * 判断多边型顶点的时针方向(计算基于数学坐标系统,在绘图坐标系由于y轴方向相反结果也要取反)
+     *
+     * @param   pointList   多边形顶点
+     * @return  为负,顺时针;为正,逆时针;0,点在直线上。
+     */
+    static clockDir(pointList: SPoint[]): number {
+        if (pointList.length < 3) {
+            return 0;
+        }
+
+        // 找出x坐标最大的值
+        let index = 0;
+        let len = pointList.length;
+        for (let i = 1; i < len; i++) {
+            if (pointList[i].x > pointList[index].x) {
+                index = i;
+            }
+        }
+
+        let p1 = pointList[index == 0 ? len - 1 : index - 1];
+        let p2 = pointList[index];
+        let p3 = pointList[(index + 1) % len];
+        return (p2.x - p1.x) * (p3.y - p2.y) - (p2.y - p1.y) * (p3.x - p2.x);
+    } // Function clockDir()
+} // Class SPolygonUtil

+ 16 - 0
saga-web-draw/tsconfig.json

@@ -0,0 +1,16 @@
+{
+    "compilerOptions": {
+        "target": "es5",                            // Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'.
+        "module": "commonjs",                       // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
+        "outDir": "./lib",                          // 编译后生成的文件目录
+        "strict": true,                             // 开启严格的类型检测
+        "declaration": true,                        // 生成 `.d.ts` 文件
+        "experimentalDecorators": true,             // 开启装饰器
+        "removeComments": true,                     // 去除注释
+        "noImplicitAny": true,                      // 在表达式和声明上有隐含的 any类型时报错。
+        "esModuleInterop": true,                    // 支持别名导入
+        "moduleResolution": "node"                  // 此处设置为node,才能解析import xx from 'xx'
+    },
+    "include": ["./src"],
+    "exclude": ["node_modules"]
+}

+ 6 - 0
saga-web-draw/typedoc.json

@@ -0,0 +1,6 @@
+{
+    "name": "上格云绘图引擎",
+    "mode": "file",
+    "out": "doc",
+    "exclude": ["**/*+(index|.test).ts"]
+}