diff --git a/index.d.ts b/index.d.ts index 811a2c6cf..047e18390 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,2 +1,2 @@ -export * from './src/zrender'; -export * from './src/export'; \ No newline at end of file +export * from './lib/zrender'; +export * from './lib/export'; diff --git a/index.js b/index.js index 903454279..a95f37b0a 100644 --- a/index.js +++ b/index.js @@ -5,4 +5,4 @@ import {registerPainter} from './lib/zrender'; import CanvasPainter from './lib/canvas/Painter'; import SVGPainter from './lib/svg/Painter'; registerPainter('canvas', CanvasPainter); -registerPainter('svg', SVGPainter); \ No newline at end of file +registerPainter('svg', SVGPainter); diff --git a/package-lock.json b/package-lock.json index 370ad6e64..609711f3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -712,6 +712,32 @@ "integrity": "sha512-518yewjSga1jLdiLrcmpMFlaba5P+50b0TWNFUpC+SL9Yzf0kMi57qw+bMl+rQ08cGqH1vLx4eg9YFUbZXgZ0Q==", "dev": true }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz", + "integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@sinonjs/commons": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.0.tgz", @@ -774,12 +800,6 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, "@types/estree": { "version": "0.0.42", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.42.tgz", @@ -822,9 +842,9 @@ } }, "@types/json-schema": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", - "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, "@types/node": { @@ -855,99 +875,186 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", - "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz", + "integrity": "sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/experimental-utils": "4.28.0", + "@typescript-eslint/scope-manager": "4.28.0", + "debug": "^4.3.1", "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "tsutils": "^3.17.1" + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "@typescript-eslint/experimental-utils": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", - "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz", + "integrity": "sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==", "dev": true, "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.34.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.28.0", + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/typescript-estree": "4.28.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" }, "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + }, + "eslint-visitor-keys": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } } } }, "@typescript-eslint/parser": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.24.0.tgz", - "integrity": "sha512-H2Y7uacwSSg8IbVxdYExSI3T7uM1DzmOn2COGtCahCC3g8YtM1xYAPi2MAHyfPs61VKxP/J/UiSctcRgw4G8aw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.0.tgz", + "integrity": "sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==", "dev": true, "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.24.0", - "@typescript-eslint/typescript-estree": "2.24.0", - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/scope-manager": "4.28.0", + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/typescript-estree": "4.28.0", + "debug": "^4.3.1" }, "dependencies": { - "@typescript-eslint/experimental-utils": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.24.0.tgz", - "integrity": "sha512-DXrwuXTdVh3ycNCMYmWhUzn/gfqu9N0VzNnahjiDJvcyhfBy4gb59ncVZVxdp5XzBC77dCncu0daQgOkbvPwBw==", + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz", + "integrity": "sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/visitor-keys": "4.28.0" + } + }, + "@typescript-eslint/types": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.0.tgz", + "integrity": "sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz", + "integrity": "sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/visitor-keys": "4.28.0", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.24.0", - "eslint-scope": "^5.0.0" + "ms": "2.1.2" } }, - "@typescript-eslint/typescript-estree": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.24.0.tgz", - "integrity": "sha512-RJ0yMe5owMSix55qX7Mi9V6z2FDuuDpN6eR5fzRJrp+8in9UF41IGNQHbg5aMK4/PjVaEQksLvz0IA8n+Mr/FA==", + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^6.3.0", - "tsutils": "^3.17.1" + "lru-cache": "^6.0.0" } } } }, - "@typescript-eslint/typescript-estree": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", - "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", + "@typescript-eslint/visitor-keys": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz", + "integrity": "sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==", "dev": true, "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "@typescript-eslint/types": "4.28.0", + "eslint-visitor-keys": "^2.0.0" }, "dependencies": { - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true } } @@ -1070,6 +1177,12 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -1763,6 +1876,15 @@ "integrity": "sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2247,6 +2369,20 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, + "fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2259,6 +2395,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -2466,6 +2611,28 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "dependencies": { + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + } + } + }, "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", @@ -3851,6 +4018,15 @@ "@sinonjs/commons": "^1.7.0" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "make-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", @@ -3896,6 +4072,12 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -4270,6 +4452,12 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -4407,6 +4595,12 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "react-is": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", @@ -4433,9 +4627,9 @@ } }, "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "remove-trailing-separator": { @@ -4600,6 +4794,12 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -4688,6 +4888,15 @@ "is-promise": "^2.1.0" } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "rxjs": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", @@ -5540,14 +5749,14 @@ } }, "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" }, "tsutils": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", - "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "requires": { "tslib": "^1.8.1" @@ -5607,9 +5816,9 @@ } }, "typescript": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", - "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true }, "uglify-js": { @@ -5964,6 +6173,12 @@ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "yargs": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", diff --git a/package.json b/package.json index a25b582fa..d6b543639 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "module": "index.js", "main": "dist/zrender.js", "dependencies": { - "tslib": "2.0.3" + "tslib": "2.3.0" }, "sideEffects": [ "lib/canvas/canvas.js", @@ -35,8 +35,8 @@ "devDependencies": { "@microsoft/api-extractor": "^7.7.2", "@types/jest": "^25.1.2", - "@typescript-eslint/eslint-plugin": "^2.24.0", - "@typescript-eslint/parser": "^2.24.0", + "@typescript-eslint/eslint-plugin": "^4.9.1", + "@typescript-eslint/parser": "^4.9.1", "chalk": "^3.0.0", "commander": "2.11.0", "eslint": "6.3.0", @@ -47,7 +47,7 @@ "rollup-plugin-typescript2": "^0.25.3", "rollup-plugin-uglify": "^6.0.4", "ts-jest": "^25.2.0", - "typescript": "^4.1.2", + "typescript": "4.3.5", "uglify-js": "^3.10.0" } } diff --git a/src/Element.ts b/src/Element.ts index 1922a498f..fd5324e38 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -8,7 +8,7 @@ import { } from './core/types'; import Path from './graphic/Path'; import BoundingRect, { RectLike } from './core/BoundingRect'; -import Eventful, {EventQuery, EventCallback} from './core/Eventful'; +import Eventful from './core/Eventful'; import ZRText, { DefaultTextStyle } from './graphic/Text'; import { calculateTextPosition, TextPositionCalculationResult, parsePercent } from './contain/text'; import { @@ -28,7 +28,7 @@ import Point from './core/Point'; import { LIGHT_LABEL_COLOR, DARK_LABEL_COLOR } from './config'; import { parse, stringify } from './tool/color'; import env from './core/env'; -import { REDARAW_BIT, STYLE_CHANGED_BIT } from './graphic/constants'; +import { REDARAW_BIT } from './graphic/constants'; export interface ElementAnimateConfig { duration?: number @@ -284,16 +284,22 @@ export type ElementCommonState = { hoverLayer?: boolean } +export type ElementCalculateTextPosition = ( + out: TextPositionCalculationResult, + style: ElementTextConfig, + rect: RectLike +) => TextPositionCalculationResult; + let tmpTextPosCalcRes = {} as TextPositionCalculationResult; let tmpBoundingRect = new BoundingRect(0, 0, 0, 0); -interface Element extends Transformable, Eventful, ElementEventHandlerProps { - // Provide more typed event callback params for mouse events. - on(event: ElementEventName, handler: ElementEventCallback, context?: Ctx): this - on(event: string, handler: EventCallback, context?: Ctx): this - - on(event: ElementEventName, query: EventQuery, handler: ElementEventCallback, context?: Ctx): this - on(event: string, query: EventQuery, handler: EventCallback, context?: Ctx): this +interface Element extends Transformable, + Eventful<{ + [key in ElementEventName]: (e: ElementEvent) => void | boolean + } & { + [key in string]: (...args: any) => void | boolean + }>, + ElementEventHandlerProps { } class Element { @@ -487,6 +493,7 @@ class Element { */ update() { this.updateTransform(); + if (this.__dirty) { this.updateInnerText(); } @@ -501,35 +508,21 @@ class Element { } const textConfig = this.textConfig; const isLocal = textConfig.local; - const attachedTransform = textEl.attachedTransform; + const innerTransformable = textEl.innerTransformable; let textAlign: TextAlign; let textVerticalAlign: TextVerticalAlign; let textStyleChanged = false; - // TODO Restore the element after textConfig changed. - - // NOTE: Can't be used both as normal element and as textContent. - if (isLocal) { - // Apply host's transform. - // TODO parent is always be group for developers. But can be displayble inside. - attachedTransform.parent = this as unknown as Group; - } - else { - attachedTransform.parent = null; - } + // Apply host's transform. + innerTransformable.parent = isLocal ? this as unknown as Group : null; let innerOrigin = false; // Reset x/y/rotation - attachedTransform.x = textEl.x; - attachedTransform.y = textEl.y; - attachedTransform.originX = textEl.originX; - attachedTransform.originY = textEl.originY; - attachedTransform.rotation = textEl.rotation; - attachedTransform.scaleX = textEl.scaleX; - attachedTransform.scaleY = textEl.scaleY; + innerTransformable.copyTransform(textEl); + // Force set attached text's position if `position` is in config. if (textConfig.position != null) { let layoutRect = tmpBoundingRect; @@ -552,8 +545,8 @@ class Element { // TODO Should modify back if textConfig.position is set to null again. // Or textContent is detached. - attachedTransform.x = tmpTextPosCalcRes.x; - attachedTransform.y = tmpTextPosCalcRes.y; + innerTransformable.x = tmpTextPosCalcRes.x; + innerTransformable.y = tmpTextPosCalcRes.y; // User specified align/verticalAlign has higher priority, which is // useful in the case that attached text is rotated 90 degree. @@ -574,26 +567,26 @@ class Element { } innerOrigin = true; - attachedTransform.originX = -attachedTransform.x + relOriginX + (isLocal ? 0 : layoutRect.x); - attachedTransform.originY = -attachedTransform.y + relOriginY + (isLocal ? 0 : layoutRect.y); + innerTransformable.originX = -innerTransformable.x + relOriginX + (isLocal ? 0 : layoutRect.x); + innerTransformable.originY = -innerTransformable.y + relOriginY + (isLocal ? 0 : layoutRect.y); } } if (textConfig.rotation != null) { - attachedTransform.rotation = textConfig.rotation; + innerTransformable.rotation = textConfig.rotation; } // TODO const textOffset = textConfig.offset; if (textOffset) { - attachedTransform.x += textOffset[0]; - attachedTransform.y += textOffset[1]; + innerTransformable.x += textOffset[0]; + innerTransformable.y += textOffset[1]; // Not change the user set origin. if (!innerOrigin) { - attachedTransform.originX = -textOffset[0]; - attachedTransform.originY = -textOffset[1]; + innerTransformable.originX = -textOffset[0]; + innerTransformable.originY = -textOffset[1]; } } @@ -1288,7 +1281,7 @@ class Element { throw new Error('Text element has been added to zrender.'); } - textEl.attachedTransform = new Transformable(); + textEl.innerTransformable = new Transformable(); this._attachComponent(textEl); @@ -1323,7 +1316,7 @@ class Element { removeTextContent() { const textEl = this._textContent; if (textEl) { - textEl.attachedTransform = null; + textEl.innerTransformable = null; this._detachComponent(textEl); this._textContent = null; this._innerTextDefaultStyle = null; @@ -1402,6 +1395,10 @@ class Element { * Not recursively because it will be invoked when element added to storage. */ addSelfToZr(zr: ZRenderType) { + if (this.__zr === zr) { + return; + } + this.__zr = zr; // 添加动画 const animators = this.animators; @@ -1427,6 +1424,10 @@ class Element { * Not recursively because it will be invoked when element added to storage. */ removeSelfFromZr(zr: ZRenderType) { + if (!this.__zr) { + return; + } + this.__zr = null; // Remove animation const animators = this.animators; @@ -1564,7 +1565,7 @@ class Element { // Overload definitions animateFrom( - target: Props, cfg: Omit, animationProps?: MapToType + target: Props, cfg: ElementAnimateConfig, animationProps?: MapToType ) { animateTo(this, target, cfg, animationProps, true); } @@ -1608,9 +1609,7 @@ class Element { * verticalAlign: string. optional. use style.textVerticalAlign by default. * } */ - calculateTextPosition: ( - out: TextPositionCalculationResult, style: ElementTextConfig, rect: RectLike - ) => TextPositionCalculationResult + calculateTextPosition: ElementCalculateTextPosition; protected static initDefaultProps = (function () { const elProto = Element.prototype; diff --git a/src/Storage.ts b/src/Storage.ts index f7f9ff928..7c094f9fb 100644 --- a/src/Storage.ts +++ b/src/Storage.ts @@ -1,6 +1,6 @@ import * as util from './core/util'; import env from './core/env'; -import Group from './graphic/Group'; +import Group, { GroupLike } from './graphic/Group'; import Element from './Element'; // Use timsort because in most case elements are partially sorted @@ -133,8 +133,8 @@ export default class Storage { } // ZRText and Group and combining morphing Path may use children - if ((el as Group).childrenRef) { - const children = (el as Group).childrenRef(); + if ((el as GroupLike).childrenRef) { + const children = (el as GroupLike).childrenRef(); for (let i = 0; i < children.length; i++) { const child = children[i]; diff --git a/src/animation/Animator.ts b/src/animation/Animator.ts index 5e02c0044..1fd3a12e9 100644 --- a/src/animation/Animator.ts +++ b/src/animation/Animator.ts @@ -145,21 +145,6 @@ function is1DArraySame(arr0: NumberArray, arr1: NumberArray) { return true; } -function is2DArraySame(arr0: NumberArray[], arr1: NumberArray[]) { - const len = arr0.length; - if (len !== arr1.length) { - return false; - } - const len2 = arr0[0].length; - for (let i = 0; i < len; i++) { - for (let j = 0; j < len2; j++) { - if (arr0[i][j] !== arr1[i][j]) { - return false; - } - } - } - return true; -} /** * Catmull Rom interpolate number @@ -311,8 +296,10 @@ class Track { } needsAnimate() { - // return this.keyframes.length >= 2; - return !this._isAllValueEqual && this.keyframes.length >= 2 && this.interpolable; + return !this._isAllValueEqual + && this.keyframes.length >= 2 + && this.interpolable + && this.maxTime > 0; } getAdditiveTrack() { @@ -706,10 +693,10 @@ export default class Animator { private _additiveAnimators: Animator[] - private _doneList: DoneCallback[] - private _onframeList: OnframeCallback[] + private _doneCbs: DoneCallback[] + private _onframeCbs: OnframeCallback[] - private _abortedList: AbortCallback[] + private _abortedCbs: AbortCallback[] private _clip: Clip = null @@ -811,7 +798,7 @@ export default class Animator { // Clear clip this._clip = null; - const doneList = this._doneList; + const doneList = this._doneCbs; if (doneList) { const len = doneList.length; for (let i = 0; i < len; i++) { @@ -823,7 +810,7 @@ export default class Animator { this._setTracksFinished(); const animation = this.animation; - const abortedList = this._abortedList; + const abortedList = this._abortedCbs; if (animation) { animation.removeClip(this._clip); @@ -877,7 +864,7 @@ export default class Animator { for (let i = 0; i < this._trackKeys.length; i++) { const propName = this._trackKeys[i]; const track = this._tracks[propName]; - const additiveTrack = this._getAdditiveTrack(propName) + const additiveTrack = this._getAdditiveTrack(propName); const kfs = track.keyframes; track.prepare(additiveTrack); if (track.needsAnimate()) { @@ -920,7 +907,7 @@ export default class Animator { // Because target may be changed. tracks[i].step(self._target, percent); } - const onframeList = self._onframeList; + const onframeList = self._onframeCbs; if (onframeList) { for (let i = 0; i < onframeList.length; i++) { onframeList[i](self._target, percent); @@ -980,10 +967,10 @@ export default class Animator { */ during(cb: OnframeCallback) { if (cb) { - if (!this._onframeList) { - this._onframeList = []; + if (!this._onframeCbs) { + this._onframeCbs = []; } - this._onframeList.push(cb); + this._onframeCbs.push(cb); } return this; } @@ -993,20 +980,20 @@ export default class Animator { */ done(cb: DoneCallback) { if (cb) { - if (!this._doneList) { - this._doneList = []; + if (!this._doneCbs) { + this._doneCbs = []; } - this._doneList.push(cb); + this._doneCbs.push(cb); } return this; } aborted(cb: AbortCallback) { if (cb) { - if (!this._abortedList) { - this._abortedList = []; + if (!this._abortedCbs) { + this._abortedCbs = []; } - this._abortedList.push(cb); + this._abortedCbs.push(cb); } return this; } diff --git a/src/canvas/graphic.ts b/src/canvas/graphic.ts index 834725cef..b65f494f1 100644 --- a/src/canvas/graphic.ts +++ b/src/canvas/graphic.ts @@ -25,6 +25,14 @@ function styleHasStroke(style: PathStyleProps) { return !(stroke == null || stroke === 'none' || !(style.lineWidth > 0)); } +// ignore lineWidth and must be string +// Expected color but found '[' when color is gradient +function isValidStrokeFillStyle( + strokeOrFill: PathStyleProps['stroke'] | PathStyleProps['fill'] +): strokeOrFill is string { + return typeof strokeOrFill === 'string' && strokeOrFill !== 'none'; +} + function styleHasFill(style: PathStyleProps) { const fill = style.fill; return fill != null && fill !== 'none'; @@ -459,14 +467,14 @@ function bindPathAndTextCommonStyle( flushPathDrawn(ctx, scope); styleChanged = true; } - ctx.fillStyle = style.fill as string; + isValidStrokeFillStyle(style.fill) && (ctx.fillStyle = style.fill); } if (forceSetAll || style.stroke !== prevStyle.stroke) { if (!styleChanged) { flushPathDrawn(ctx, scope); styleChanged = true; } - ctx.strokeStyle = style.stroke as string; + isValidStrokeFillStyle(style.stroke) && (ctx.strokeStyle = style.stroke); } if (forceSetAll || style.opacity !== prevStyle.opacity) { if (!styleChanged) { diff --git a/src/contain/path.ts b/src/contain/path.ts index ef0d855eb..16a37aa1f 100644 --- a/src/contain/path.ts +++ b/src/contain/path.ts @@ -11,8 +11,6 @@ const PI2 = Math.PI * 2; const EPSILON = 1e-4; -type PathData = Float32Array | number[]; - function isAroundEqual(a: number, b: number) { return Math.abs(a - b) < EPSILON; } diff --git a/src/core/Eventful.ts b/src/core/Eventful.ts index 700573dc9..d66ce7675 100644 --- a/src/core/Eventful.ts +++ b/src/core/Eventful.ts @@ -1,20 +1,26 @@ +import { Dictionary, WithThisType } from './types'; + // Return true to cancel bubble -export type EventCallback = ( - this: CbThis, eventParam: EvtParam, ...args: unknown[] -) => boolean | void +export type EventCallbackSingleParam = EvtParam extends any + ? (params: EvtParam) => boolean | void + : never + +export type EventCallback = EvtParams extends any[] + ? (...args: EvtParams) => boolean | void + : never export type EventQuery = string | Object type CbThis = unknown extends Ctx ? Impl : Ctx; -type EventHandler = { - h: EventCallback +type EventHandler = { + h: EventCallback ctx: CbThis query: EventQuery callAtLast: boolean } -type DefaultEventDefinition = {[eventName: string]: unknown}; +type DefaultEventDefinition = Dictionary>; export interface EventProcessor { normalizeQuery?: (query: EventQuery) => EventQuery @@ -58,9 +64,9 @@ export interface EventProcessor { * @param eventProcessor.afterTrigger Called after all handlers called. * param: {string} eventType */ -export default class Eventful { +export default class Eventful { - private _$handlers: {[key: string]: EventHandler[]} + private _$handlers: Dictionary[]> protected _$eventProcessor: EventProcessor @@ -72,13 +78,13 @@ export default class Eventful { on( event: EvtNm, - handler: EventCallback, + handler: WithThisType>, context?: Ctx ): this on( event: EvtNm, query: EventQuery, - handler: EventCallback, + handler: WithThisType>, context?: Ctx ): this /** @@ -91,8 +97,8 @@ export default class Eventful { */ on( event: EvtNm, - query: EventQuery | EventCallback, - handler?: EventCallback | Ctx, + query: EventQuery | WithThisType, CbThis>, + handler?: WithThisType, CbThis> | Ctx, context?: Ctx ): this { if (!this._$handlers) { @@ -103,7 +109,7 @@ export default class Eventful { if (typeof query === 'function') { context = handler as Ctx; - handler = query as EventCallback; + handler = query as (...args: any) => any; query = null; } @@ -126,8 +132,8 @@ export default class Eventful { } } - const wrap: EventHandler = { - h: handler as EventCallback, + const wrap: EventHandler = { + h: handler as EventCallback, query: query, ctx: (context || this) as CbThis, // FIXME @@ -199,8 +205,10 @@ export default class Eventful { * * @param {string} eventType The event name. */ - trigger(eventType: keyof EvtDef, eventParam?: EvtDef[keyof EvtDef], ...args: any[]): this; - trigger(eventType: keyof EvtDef, ...args: any[]): this { + trigger( + eventType: EvtNm, + ...args: Parameters + ): this { if (!this._$handlers) { return this; } @@ -225,7 +233,6 @@ export default class Eventful { // Optimize advise from backbone switch (argLen) { case 0: - // @ts-ignore hItem.h.call(hItem.ctx); break; case 1: @@ -253,7 +260,7 @@ export default class Eventful { * * @param {string} type The event name. */ - triggerWithContext(type: keyof EvtDef) { + triggerWithContext(type: keyof EvtDef, ...args: any[]): this { if (!this._$handlers) { return this; } @@ -262,7 +269,6 @@ export default class Eventful { const eventProcessor = this._$eventProcessor; if (_h) { - const args: any = arguments; const argLen = args.length; const ctx = args[argLen - 1]; @@ -280,7 +286,6 @@ export default class Eventful { // Optimize advise from backbone switch (argLen) { case 0: - // @ts-ignore hItem.h.call(ctx); break; case 1: diff --git a/src/core/PathProxy.ts b/src/core/PathProxy.ts index dded51acf..b28e832de 100644 --- a/src/core/PathProxy.ts +++ b/src/core/PathProxy.ts @@ -281,6 +281,8 @@ export default class PathProxy { } bezierCurveTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) { + this._drawPendingPt(); + this.addData(CMD.C, x1, y1, x2, y2, x3, y3); if (this._ctx) { this._needsDash ? this._dashedBezierTo(x1, y1, x2, y2, x3, y3) @@ -292,6 +294,8 @@ export default class PathProxy { } quadraticCurveTo(x1: number, y1: number, x2: number, y2: number) { + this._drawPendingPt(); + this.addData(CMD.Q, x1, y1, x2, y2); if (this._ctx) { this._needsDash ? this._dashedQuadraticTo(x1, y1, x2, y2) @@ -303,6 +307,8 @@ export default class PathProxy { } arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise?: boolean) { + this._drawPendingPt(); + tmpAngles[0] = startAngle; tmpAngles[1] = endAngle; normalizeArcAngles(tmpAngles, anticlockwise); @@ -312,10 +318,10 @@ export default class PathProxy { let delta = endAngle - startAngle; - this.addData( CMD.A, cx, cy, r, r, startAngle, delta, 0, anticlockwise ? 0 : 1 ); + this._ctx && this._ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise); this._xi = mathCos(endAngle) * r + cx; @@ -325,6 +331,8 @@ export default class PathProxy { // TODO arcTo(x1: number, y1: number, x2: number, y2: number, radius: number) { + this._drawPendingPt(); + if (this._ctx) { this._ctx.arcTo(x1, y1, x2, y2, radius); } @@ -333,6 +341,8 @@ export default class PathProxy { // TODO rect(x: number, y: number, w: number, h: number) { + this._drawPendingPt(); + this._ctx && this._ctx.rect(x, y, w, h); this.addData(CMD.R, x, y, w, h); return this; @@ -947,12 +957,14 @@ export default class PathProxy { x0 = xi; y0 = yi; } + // Only lineTo support ignoring small segments. + // Otherwise if the pending point should always been flushed. + if (cmd !== CMD.L && pendingPtDist > 0) { + ctx.lineTo(pendingPtX, pendingPtY); + pendingPtDist = 0; + } switch (cmd) { case CMD.M: - if (pendingPtDist > 0) { - ctx.lineTo(pendingPtX, pendingPtY); - pendingPtDist = 0; - } x0 = xi = d[i++]; y0 = yi = d[i++]; ctx.moveTo(xi, yi); @@ -1115,11 +1127,6 @@ export default class PathProxy { ctx.rect(x, y, width, height); break; case CMD.Z: - if (pendingPtDist > 0) { - ctx.lineTo(pendingPtX, pendingPtY); - pendingPtDist = 0; - } - if (drawPart) { const l = pathSegLen[segCount++]; if (accumLength + l > displayedLength) { @@ -1137,6 +1144,15 @@ export default class PathProxy { } } + clone() { + const newProxy = new PathProxy(); + const data = this.data; + newProxy.data = data.slice ? data.slice() + : Array.prototype.slice.call(data); + newProxy._len = this._len; + return newProxy; + } + private static initDefaultProps = (function () { const proto = PathProxy.prototype; proto._saveData = true; diff --git a/src/core/Point.ts b/src/core/Point.ts index 7d013f506..7dd767efa 100644 --- a/src/core/Point.ts +++ b/src/core/Point.ts @@ -1,4 +1,4 @@ -import { MatrixArray } from "./matrix"; +import { MatrixArray } from './matrix'; export interface PointLike { x: number diff --git a/src/core/Transformable.ts b/src/core/Transformable.ts index 3fd74016a..b8d7b63ba 100644 --- a/src/core/Transformable.ts +++ b/src/core/Transformable.ts @@ -42,6 +42,13 @@ class Transformable { transform: matrix.MatrixArray invTransform: matrix.MatrixArray + /** + * Get computed local transform + */ + getLocalTransform(m?: matrix.MatrixArray) { + return Transformable.getLocalTransform(this, m); + } + /** * Set position from array */ @@ -88,12 +95,11 @@ class Transformable { * Update global transform */ updateTransform() { - const parent = this.parent; - const parentHasTransform = parent && parent.transform; + const parentTransform = this.parent && this.parent.transform; const needLocalTransform = this.needLocalTransform(); let m = this.transform; - if (!(needLocalTransform || parentHasTransform)) { + if (!(needLocalTransform || parentTransform)) { m && mIdentity(m); return; } @@ -108,12 +114,12 @@ class Transformable { } // 应用父节点变换 - if (parentHasTransform) { + if (parentTransform) { if (needLocalTransform) { - matrix.mul(m, parent.transform, m); + matrix.mul(m, parentTransform, m); } else { - matrix.copy(m, parent.transform); + matrix.copy(m, parentTransform); } } // 保存这个变换矩阵 @@ -140,12 +146,6 @@ class Transformable { this.invTransform = this.invTransform || matrix.create(); matrix.invert(this.invTransform, m); } - /** - * Get computed local transform - */ - getLocalTransform(m?: matrix.MatrixArray) { - return Transformable.getLocalTransform(this, m); - } /** * Get computed global transform @@ -178,8 +178,6 @@ class Transformable { const rotation = Math.atan2(m[1], m[0]); - - const shearX = Math.PI / 2 + rotation - Math.atan2(m[3], m[2]); sy = Math.sqrt(sy) * Math.cos(shearX); sx = Math.sqrt(sx); @@ -281,6 +279,15 @@ class Transformable { : 1; } + copyTransform(source: Transformable) { + const target = this; + + for (let i = 0; i < TRANSFORMABLE_PROPS.length; i++) { + const propName = TRANSFORMABLE_PROPS[i]; + target[propName] = source[propName]; + } + } + static getLocalTransform(target: Transformable, m?: matrix.MatrixArray): matrix.MatrixArray { m = m || []; @@ -338,4 +345,8 @@ class Transformable { })() }; +export const TRANSFORMABLE_PROPS = [ + 'x', 'y', 'originX', 'originY', 'rotation', 'scaleX', 'scaleY', 'skewX', 'skewY' +] as const; + export default Transformable; \ No newline at end of file diff --git a/src/core/WeakMap.ts b/src/core/WeakMap.ts index 0caa83765..d4d00b1ad 100644 --- a/src/core/WeakMap.ts +++ b/src/core/WeakMap.ts @@ -1,5 +1,7 @@ let wmUniqueIndex = Math.round(Math.random() * 9); +const supportDefineProperty = typeof Object.defineProperty === 'function'; + export default class WeakMap { protected _id: string; @@ -14,7 +16,7 @@ export default class WeakMap { set(key: K, value: V): WeakMap { const target = this._guard(key) as any; - if (typeof Object.defineProperty === 'function') { + if (supportDefineProperty) { Object.defineProperty(target, this._id, { value: value, enumerable: false, diff --git a/src/core/curve.ts b/src/core/curve.ts index 054e164b3..9e3580723 100644 --- a/src/core/curve.ts +++ b/src/core/curve.ts @@ -480,7 +480,7 @@ export function quadraticLength( let d = 0; - const step = 1 / iteration; + const step = 1 / iteration; for (let i = 1; i <= iteration; i++) { let t = i * step; diff --git a/src/core/matrix.ts b/src/core/matrix.ts index 248c892fb..78009f815 100644 --- a/src/core/matrix.ts +++ b/src/core/matrix.ts @@ -116,7 +116,7 @@ export function scale(out: MatrixArray, a: MatrixArray, v: VectorArray): MatrixA /** * 求逆矩阵 */ -export function invert(out: MatrixArray, a: MatrixArray): MatrixArray { +export function invert(out: MatrixArray, a: MatrixArray): MatrixArray | null { const aa = a[0]; const ac = a[2]; @@ -147,4 +147,4 @@ export function clone(a: MatrixArray): MatrixArray { const b = create(); copy(b, a); return b; -} \ No newline at end of file +} diff --git a/src/core/types.ts b/src/core/types.ts index fb2c6c7b2..41b035d65 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -64,7 +64,7 @@ export type ZRPinchEvent = ZRRawEvent & { export type ElementEventName = 'click' | 'dblclick' | 'mousewheel' | 'mouseout' | 'mouseover' | 'mouseup' | 'mousedown' | 'mousemove' | 'contextmenu' | - 'drag' | 'dragstart' | 'dragend' | 'dragenter' | 'dragleave' | 'dragover' | 'drop'; + 'drag' | 'dragstart' | 'dragend' | 'dragenter' | 'dragleave' | 'dragover' | 'drop' | 'globalout'; export type ElementEventNameWithOn = 'onclick' | 'ondblclick' | 'onmousewheel' | 'onmouseout' | 'onmouseup' | 'onmousedown' | 'onmousemove' | 'oncontextmenu' | @@ -90,3 +90,6 @@ export type MapToType, S> = { // `keyof A | B` does not equals to `Keyof A | Keyof B` // KeyOfDistributive equals to `KeyOfDistributive | KeyOfDistributive` export type KeyOfDistributive = T extends unknown ? keyof T : never; + +export type WithThisType any, This> = + (this: This, ...args: Parameters) => ReturnType; diff --git a/src/core/util.ts b/src/core/util.ts index b2f77082a..7eae0db24 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -728,26 +728,6 @@ export function concatArray(a: ArrayLike, b: ArrayLike): ArrayLike(obj: T, proto: object): T { -// if (Object.setPrototypeOf) { -// Object.setPrototypeOf(obj, proto); -// return obj; -// } -// else { -// const StyleCtor = function () {}; -// StyleCtor.prototype = proto; -// const newObj = new (StyleCtor as any)(); -// extend(newObj, obj); -// return newObj; -// } -// } - - export function createObject(proto?: object, properties?: T): T { // Performance of Object.create // https://jsperf.com/style-strategy-proto-or-others diff --git a/src/export.ts b/src/export.ts index 0e68eee69..1b7dc35ed 100644 --- a/src/export.ts +++ b/src/export.ts @@ -8,7 +8,8 @@ import * as vector from './core/vector'; import * as colorTool from './tool/color'; import * as pathTool from './tool/path'; import {parseSVG} from './tool/parseSVG'; -import {morphPath} from './tool/morphPath'; + +import * as morphPathTool from './tool/morphPath'; export {default as Point, PointLike} from './core/Point'; @@ -65,7 +66,8 @@ export {colorTool as color}; export {pathTool as path}; export {zrUtil as util}; +export {morphPathTool as morph}; + export {parseSVG}; -export {morphPath}; export {default as showDebugDirtyRect} from './debug/showDebugDirtyRect'; \ No newline at end of file diff --git a/src/graphic/Group.ts b/src/graphic/Group.ts index efd400cad..337a8822b 100644 --- a/src/graphic/Group.ts +++ b/src/graphic/Group.ts @@ -113,6 +113,14 @@ class Group extends Element { return this; } + replace(oldChild: Element, newChild: Element) { + const idx = zrUtil.indexOf(this._children, oldChild); + if (idx >= 0) { + this.replaceAt(newChild, idx); + } + return this; + } + replaceAt(child: Element, index: number) { const children = this._children; const old = children[index]; @@ -212,6 +220,7 @@ class Group extends Element { * Visit all descendants. * Return false in callback to stop visit descendants of current node */ + // TODO Group itself should also invoke the callback. traverse( cb: (this: T, el: Element) => boolean | void, context?: T @@ -281,5 +290,9 @@ class Group extends Element { } Group.prototype.type = 'group'; +// Storage will use childrenRef to get children to render. +export interface GroupLike extends Element { + childrenRef(): Element[] +} export default Group; \ No newline at end of file diff --git a/src/graphic/Path.ts b/src/graphic/Path.ts index 7c2bfbbca..34885002d 100644 --- a/src/graphic/Path.ts +++ b/src/graphic/Path.ts @@ -99,7 +99,7 @@ export interface PathProps extends DisplayableProps { buildPath?: ( ctx: PathProxy | CanvasRenderingContext2D, shapeCfg: Dictionary, - inBundle?: boolean + inBatch?: boolean ) => void } @@ -302,13 +302,21 @@ class Path extends Displayable { buildPath( ctx: PathProxy | CanvasRenderingContext2D, shapeCfg: Dictionary, - inBundle?: boolean + inBatch?: boolean ) {} pathUpdated() { this.__dirty &= ~SHAPE_CHANGED_BIT; } + getUpdatedPathProxy(inBatch?: boolean) { + // Update path proxy data to latest. + !this.path && this.createPathProxy(); + this.path.beginPath(); + this.buildPath(this.path, this.shape, inBatch); + return this.path; + } + createPathProxy() { this.path = new PathProxy(false); } @@ -614,7 +622,7 @@ class Path extends Displayable { getBoundingRect?: Displayable['getBoundingRect'] calculateTextPosition?: Element['calculateTextPosition'] - buildPath(this: Path, ctx: CanvasRenderingContext2D | PathProxy, shape: Shape, inBundle?: boolean): void + buildPath(this: Path, ctx: CanvasRenderingContext2D | PathProxy, shape: Shape, inBatch?: boolean): void init?(this: Path, opts: PathProps): void // TODO Should be SubPathOption }): { new(opts?: PathProps & {shape: Shape}): Path diff --git a/src/graphic/Text.ts b/src/graphic/Text.ts index 93ba480a6..eb4709e3d 100644 --- a/src/graphic/Text.ts +++ b/src/graphic/Text.ts @@ -10,12 +10,17 @@ import { DEFAULT_FONT, adjustTextX, adjustTextY } from '../contain/text'; import ZRImage from './Image'; import Rect from './shape/Rect'; import BoundingRect from '../core/BoundingRect'; -import { MatrixArray, copy } from '../core/matrix'; -import Displayable, { DisplayableStatePropNames, DisplayableProps, DEFAULT_COMMON_ANIMATION_PROPS } from './Displayable'; +import { MatrixArray } from '../core/matrix'; +import Displayable, { + DisplayableStatePropNames, + DisplayableProps, + DEFAULT_COMMON_ANIMATION_PROPS +} from './Displayable'; import { ZRenderType } from '../zrender'; import Animator from '../animation/Animator'; import Transformable from '../core/Transformable'; import { ElementCommonState } from '../Element'; +import { GroupLike } from './Group'; type TextContentBlock = ReturnType type TextLine = TextContentBlock['lines'][0] @@ -248,7 +253,7 @@ interface ZRText { stateProxy: (stateName: string) => TextState } -class ZRText extends Displayable { +class ZRText extends Displayable implements GroupLike { type = 'text' @@ -262,10 +267,11 @@ class ZRText extends Displayable { overlap: 'hidden' | 'show' | 'blur' /** - * Calculated transform after the text is attached on some element. - * Will override the default transform. + * Will use this to calculate transform matrix + * instead of Element itseelf if it's give. + * Not exposed to developers */ - attachedTransform: Transformable + innerTransformable: Transformable private _children: (ZRImage | Rect | TSpan)[] = [] @@ -283,12 +289,14 @@ class ZRText extends Displayable { } update() { + + super.update(); + // Update children if (this.styleChanged()) { this._updateSubTexts(); } - for (let i = 0; i < this._children.length; i++) { const child = this._children[i]; // Set common properties. @@ -299,25 +307,29 @@ class ZRText extends Displayable { child.cursor = this.cursor; child.invisible = this.invisible; } + } - const attachedTransform = this.attachedTransform; - if (attachedTransform) { - attachedTransform.updateTransform(); - const m = attachedTransform.transform; - if (m) { - this.transform = this.transform || []; - // Copy to the transform will be actually used. - copy(this.transform, m); - } - else { - this.transform = null; + updateTransform() { + const innerTransformable = this.innerTransformable; + if (innerTransformable) { + innerTransformable.updateTransform(); + if (innerTransformable.transform) { + this.transform = innerTransformable.transform; } } else { - super.update(); + super.updateTransform(); } } + getLocalTransform(m?: MatrixArray): MatrixArray { + const innerTransformable = this.innerTransformable; + return innerTransformable + ? innerTransformable.getLocalTransform(m) + : super.getLocalTransform(m); + } + + // TODO override setLocalTransform? getComputedTransform() { if (this.__hostTarget) { // Update host target transform @@ -326,8 +338,7 @@ class ZRText extends Displayable { this.__hostTarget.updateInnerText(true); } - return this.attachedTransform ? this.attachedTransform.getComputedTransform() - : super.getComputedTransform(); + return super.getComputedTransform(); } private _updateSubTexts() { @@ -734,7 +745,7 @@ class ZRText extends Displayable { const defaultStyle = this._defaultStyle; let useDefaultFill = false; let defaultLineWidth = 0; - const textFill = getStroke( + const textFill = getFill( 'fill' in tokenStyle ? tokenStyle.fill : 'fill' in style ? style.fill : (useDefaultFill = true, defaultStyle.fill) @@ -810,7 +821,7 @@ class ZRText extends Displayable { let rectEl: Rect; let imgEl: ZRImage; - if (isPlainOrGradientBg || (textBorderWidth && textBorderColor)) { + if (isPlainOrGradientBg || style.lineHeight || (textBorderWidth && textBorderColor)) { // Background is color rectEl = this._getOrCreateChild(Rect); rectEl.useStyle(rectEl.createStyle()); // Create an empty style. @@ -985,6 +996,7 @@ function getStyleText(style: TextStylePropsPart): string { function needDrawBackground(style: TextStylePropsPart): boolean { return !!( style.backgroundColor + || style.lineHeight || (style.borderWidth && style.borderColor) ); } diff --git a/src/svg/Painter.ts b/src/svg/Painter.ts index fe61daa63..8cb19150d 100644 --- a/src/svg/Painter.ts +++ b/src/svg/Painter.ts @@ -3,7 +3,7 @@ * @module zrender/svg/Painter */ -import {createElement} from './core'; +import {createElement, normalizeColor} from './core'; import * as util from '../core/util'; import Path from '../graphic/Path'; import ZRImage from '../graphic/Image'; @@ -194,7 +194,10 @@ class SVGPainter implements PainterBase { bgNode.setAttribute('x', 0 as any); bgNode.setAttribute('y', 0 as any); bgNode.setAttribute('id', 0 as any); - bgNode.style.fill = backgroundColor; + const { color, opacity } = normalizeColor(backgroundColor); + bgNode.setAttribute('fill', color); + bgNode.setAttribute('fill-opacity', opacity as any); + this._backgroundRoot.appendChild(bgNode); this._backgroundNode = bgNode; } diff --git a/src/svg/core.ts b/src/svg/core.ts index cef8e2093..7ddc66b0f 100644 --- a/src/svg/core.ts +++ b/src/svg/core.ts @@ -1,3 +1,23 @@ +import { parse } from '../tool/color'; + export function createElement(name: string) { return document.createElementNS('http://www.w3.org/2000/svg', name); -} \ No newline at end of file +} + +export function normalizeColor(color: string): { color: string, opacity: number } { + let opacity; + if (!color || color === 'transparent') { + color = 'none'; + } + else if (typeof color === 'string' && color.indexOf('rgba') > -1) { + const arr = parse(color); + if (arr) { + color = 'rgb(' + arr[0] + ',' + arr[1] + ',' + arr[2] + ')'; + opacity = arr[3]; + } + } + return { + color, + opacity: opacity == null ? 1 : opacity + }; +} diff --git a/src/svg/graphic.ts b/src/svg/graphic.ts index ed3921bb2..709b0a544 100644 --- a/src/svg/graphic.ts +++ b/src/svg/graphic.ts @@ -2,7 +2,7 @@ // 1. shadow // 2. Image: sx, sy, sw, sh -import {createElement} from './core'; +import {createElement, normalizeColor} from './core'; import { PathRebuilder } from '../core/PathProxy'; import * as matrix from '../core/matrix'; import Path, { PathStyleProps } from '../graphic/Path'; @@ -92,12 +92,14 @@ function bindStyle(svgEl: SVGElement, style: AllStyleOption, el?: Path | TSpan | } if (pathHasFill(style)) { - let fill = style.fill; - fill = fill === 'transparent' ? NONE : fill; - attr(svgEl, 'fill', fill as string); + const fill = normalizeColor(style.fill as string); + attr(svgEl, 'fill', fill.color); attr(svgEl, 'fill-opacity', - (style.fillOpacity != null ? style.fillOpacity * opacity : opacity) + '' + (style.fillOpacity != null + ? style.fillOpacity * fill.opacity * opacity + : fill.opacity * opacity + ) + '' ); } else { @@ -105,9 +107,8 @@ function bindStyle(svgEl: SVGElement, style: AllStyleOption, el?: Path | TSpan | } if (pathHasStroke(style)) { - let stroke = style.stroke; - stroke = stroke === 'transparent' ? NONE : stroke; - attr(svgEl, 'stroke', stroke as string); + const stroke = normalizeColor(style.stroke as string); + attr(svgEl, 'stroke', stroke.color); const strokeWidth = style.lineWidth; const strokeScale = style.strokeNoScale ? (el as Path).getLineScale() @@ -115,7 +116,11 @@ function bindStyle(svgEl: SVGElement, style: AllStyleOption, el?: Path | TSpan | attr(svgEl, 'stroke-width', (strokeScale ? strokeWidth / strokeScale : 0) + ''); // stroke then fill for text; fill then stroke for others attr(svgEl, 'paint-order', style.strokeFirst ? 'stroke' : 'fill'); - attr(svgEl, 'stroke-opacity', (style.strokeOpacity != null ? style.strokeOpacity * opacity : opacity) + ''); + attr(svgEl, 'stroke-opacity', ( + style.strokeOpacity != null + ? style.strokeOpacity * stroke.opacity * opacity + : stroke.opacity * opacity + ) + ''); let lineDash = style.lineDash && strokeWidth > 0 && normalizeLineDash(style.lineDash, strokeWidth); if (lineDash) { let lineDashOffset = style.lineDashOffset; @@ -169,7 +174,14 @@ class SVGPathRebuilder implements PathRebuilder { arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean) { this.ellipse(cx, cy, r, r, 0, startAngle, endAngle, anticlockwise); } - ellipse(cx: number, cy: number, rx: number, ry: number, psi: number, startAngle: number, endAngle: number, anticlockwise: boolean) { + ellipse( + cx: number, cy: number, + rx: number, ry: number, + psi: number, + startAngle: number, + endAngle: number, + anticlockwise: boolean + ) { const firstCmd = this._d.length === 0; @@ -237,6 +249,7 @@ class SVGPathRebuilder implements PathRebuilder { this._add('L', x + w, y + h); this._add('L', x, y + h); this._add('L', x, y); + this._add('Z'); } closePath() { // Not use Z as first command diff --git a/src/svg/helper/ClippathManager.ts b/src/svg/helper/ClippathManager.ts index 774878aeb..2e2fe3345 100644 --- a/src/svg/helper/ClippathManager.ts +++ b/src/svg/helper/ClippathManager.ts @@ -44,8 +44,11 @@ export default class ClippathManager extends Definable { markAllUnused() { super.markAllUnused(); - for (let key in this._refGroups) { - this.markDomUnused(this._refGroups[key]); + const refGroups = this._refGroups; + for (let key in refGroups) { + if (refGroups.hasOwnProperty(key)) { + this.markDomUnused(refGroups[key]); + } } this._keyDuplicateCount = {}; } @@ -163,13 +166,16 @@ export default class ClippathManager extends Definable { super.removeUnused(); const newRefGroupsMap: Dictionary = {}; - for (let key in this._refGroups) { - const group = this._refGroups[key]; - if (!this.isDomUnused(group)) { - newRefGroupsMap[key] = group; - } - else if (group.parentNode) { - group.parentNode.removeChild(group); + const refGroups = this._refGroups; + for (let key in refGroups) { + if (refGroups.hasOwnProperty(key)) { + const group = refGroups[key]; + if (!this.isDomUnused(group)) { + newRefGroupsMap[key] = group; + } + else if (group.parentNode) { + group.parentNode.removeChild(group); + } } } this._refGroups = newRefGroupsMap; diff --git a/src/svg/helper/ShadowManager.ts b/src/svg/helper/ShadowManager.ts index 3baf3345d..16814c0b6 100644 --- a/src/svg/helper/ShadowManager.ts +++ b/src/svg/helper/ShadowManager.ts @@ -7,6 +7,7 @@ import Definable from './Definable'; import Displayable from '../../graphic/Displayable'; import { PathStyleProps } from '../../graphic/Path'; import { Dictionary } from '../../core/types'; +import { normalizeColor } from '../core'; type DisplayableExtended = Displayable & { _shadowDom: SVGElement @@ -35,7 +36,7 @@ export default class ShadowManager extends Definable { if (!shadowDom) { shadowDom = this.createElement('filter') as SVGFilterElement; shadowDom.setAttribute('id', 'zr' + this._zrId + '-shadow-' + this.nextId++); - const domChild = this.createElement('feDropShadow') + const domChild = this.createElement('feDropShadow'); shadowDom.appendChild(domChild); this.addDom(shadowDom); } @@ -98,11 +99,12 @@ export default class ShadowManager extends Definable { let offsetX = style.shadowOffsetX || 0; let offsetY = style.shadowOffsetY || 0; let blur = style.shadowBlur; - let color = style.shadowColor; + const normalizedColor = normalizeColor(style.shadowColor); domChild.setAttribute('dx', offsetX / scaleX + ''); domChild.setAttribute('dy', offsetY / scaleY + ''); - domChild.setAttribute('flood-color', color); + domChild.setAttribute('flood-color', normalizedColor.color); + domChild.setAttribute('flood-opacity', normalizedColor.opacity + ''); // Divide by two here so that it looks the same as in canvas // See: https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-shadowblur @@ -134,9 +136,11 @@ export default class ShadowManager extends Definable { let shadowDomsPool = this._shadowDomPool; // let currentUsedShadow = 0; - for (let key in this._shadowDomMap) { - const dom = this._shadowDomMap[key]; - shadowDomsPool.push(dom); + const shadowDomMap = this._shadowDomMap; + for (let key in shadowDomMap) { + if (shadowDomMap.hasOwnProperty(key)) { + shadowDomsPool.push(shadowDomMap[key]); + } // currentUsedShadow++; } diff --git a/src/tool/convertPath.ts b/src/tool/convertPath.ts new file mode 100644 index 000000000..d0848de20 --- /dev/null +++ b/src/tool/convertPath.ts @@ -0,0 +1,295 @@ +import { cubicSubdivide } from '../core/curve'; +import PathProxy from '../core/PathProxy'; + +const CMD = PathProxy.CMD; + +function aroundEqual(a: number, b: number) { + return Math.abs(a - b) < 1e-5; +} + +export function pathToBezierCurves(path: PathProxy) { + + const data = path.data; + const len = path.len(); + + const bezierArrayGroups: number[][] = []; + let currentSubpath: number[]; + + let xi = 0; + let yi = 0; + let x0 = 0; + let y0 = 0; + + function createNewSubpath(x: number, y: number) { + // More than one M command + if (currentSubpath && currentSubpath.length > 2) { + bezierArrayGroups.push(currentSubpath); + } + currentSubpath = [x, y]; + } + + function addLine(x0: number, y0: number, x1: number, y1: number) { + if (!(aroundEqual(x0, x1) && aroundEqual(y0, y1))) { + currentSubpath.push(x0, y0, x1, y1, x1, y1); + } + } + + function addArc(startAngle: number, endAngle: number, cx: number, cy: number, rx: number, ry: number) { + // https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves + const delta = Math.abs(endAngle - startAngle); + const len = Math.tan(delta / 4) * 4 / 3; + const dir = endAngle < startAngle ? -1 : 1; + + const c1 = Math.cos(startAngle); + const s1 = Math.sin(startAngle); + const c2 = Math.cos(endAngle); + const s2 = Math.sin(endAngle); + + const x1 = c1 * rx + cx; + const y1 = s1 * ry + cy; + + const x4 = c2 * rx + cx; + const y4 = s2 * ry + cy; + + const hx = rx * len * dir; + const hy = ry * len * dir; + currentSubpath.push( + // Move control points on tangent. + x1 - hx * s1, y1 + hy * c1, + x4 + hx * s2, y4 - hy * c2, + x4, y4 + ); + } + + let x1; + let y1; + let x2; + let y2; + + for (let i = 0; i < len;) { + const cmd = data[i++]; + const isFirst = i === 1; + + if (isFirst) { + // 如果第一个命令是 L, C, Q + // 则 previous point 同绘制命令的第一个 point + // 第一个命令为 Arc 的情况下会在后面特殊处理 + xi = data[i]; + yi = data[i + 1]; + + x0 = xi; + y0 = yi; + + if (cmd === CMD.L || cmd === CMD.C || cmd === CMD.Q) { + // Start point + currentSubpath = [x0, y0]; + } + } + + switch (cmd) { + case CMD.M: + // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 + // 在 closePath 的时候使用 + xi = x0 = data[i++]; + yi = y0 = data[i++]; + + createNewSubpath(x0, y0); + break; + case CMD.L: + x1 = data[i++]; + y1 = data[i++]; + addLine(xi, yi, x1, y1); + xi = x1; + yi = y1; + break; + case CMD.C: + currentSubpath.push( + data[i++], data[i++], data[i++], data[i++], + xi = data[i++], yi = data[i++] + ); + break; + case CMD.Q: + x1 = data[i++]; + y1 = data[i++]; + x2 = data[i++]; + y2 = data[i++]; + currentSubpath.push( + // Convert quadratic to cubic + xi + 2 / 3 * (x1 - xi), yi + 2 / 3 * (y1 - yi), + x2 + 2 / 3 * (x1 - x2), y2 + 2 / 3 * (y1 - y2), + x2, y2 + ); + xi = x2; + yi = y2; + break; + case CMD.A: + const cx = data[i++]; + const cy = data[i++]; + const rx = data[i++]; + const ry = data[i++]; + const startAngle = data[i++]; + const endAngle = data[i++] + startAngle; + + // TODO Arc rotation + i += 1; + const anticlockwise = !data[i++]; + + x1 = Math.cos(startAngle) * rx + cx; + y1 = Math.sin(startAngle) * ry + cy; + if (isFirst) { + // 直接使用 arc 命令 + // 第一个命令起点还未定义 + x0 = x1; + y0 = y1; + createNewSubpath(x0, y0); + } + else { + // Connect a line between current point to arc start point. + addLine(xi, yi, x1, y1); + } + + xi = Math.cos(endAngle) * rx + cx; + yi = Math.sin(endAngle) * ry + cy; + + const step = (anticlockwise ? -1 : 1) * Math.PI / 2; + + for (let angle = startAngle; anticlockwise ? angle > endAngle : angle < endAngle; angle += step) { + const nextAngle = anticlockwise ? Math.max(angle + step, endAngle) + : Math.min(angle + step, endAngle); + addArc(angle, nextAngle, cx, cy, rx, ry); + } + + break; + case CMD.R: + x0 = xi = data[i++]; + y0 = yi = data[i++]; + x1 = x0 + data[i++]; + y1 = y0 + data[i++]; + + // rect is an individual path. + createNewSubpath(x1, y0); + addLine(x1, y0, x1, y1); + addLine(x1, y1, x0, y1); + addLine(x0, y1, x0, y0); + addLine(x0, y0, x1, y0); + break; + case CMD.Z: + currentSubpath && addLine(xi, yi, x0, y0); + xi = x0; + yi = y0; + break; + } + } + + if (currentSubpath && currentSubpath.length > 2) { + bezierArrayGroups.push(currentSubpath); + } + + return bezierArrayGroups; +} + +function adpativeBezier( + x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, + out: number[], scale: number +) { + // This bezier is used to simulates a line when converting path to beziers. + if (aroundEqual(x0, x1) && aroundEqual(y0, y1) && aroundEqual(x2, x3) && aroundEqual(y2, y3)) { + out.push(x3, y3); + return; + } + + const PIXEL_DISTANCE = 2 / scale; + const PIXEL_DISTANCE_SQR = PIXEL_DISTANCE * PIXEL_DISTANCE; + + // Determine if curve is straight enough + let dx = x3 - x0; + let dy = y3 - y0; + const d = Math.sqrt(dx * dx + dy * dy); + dx /= d; + dy /= d; + + const dx1 = x1 - x0; + const dy1 = y1 - y0; + const dx2 = x2 - x3; + const dy2 = y2 - y3; + + const cp1LenSqr = dx1 * dx1 + dy1 * dy1; + const cp2LenSqr = dx2 * dx2 + dy2 * dy2; + + if (cp1LenSqr < PIXEL_DISTANCE_SQR && cp2LenSqr < PIXEL_DISTANCE_SQR) { + // Add small segment + out.push(x3, y3); + return; + } + + // Project length of cp1 + const projLen1 = dx * dx1 + dy * dy1; + // Project length of cp2 + const projLen2 = -dx * dx2 - dy * dy2; + + // Distance from cp1 to start-end line. + const d1Sqr = cp1LenSqr - projLen1 * projLen1; + // Distance from cp2 to start-end line. + const d2Sqr = cp2LenSqr - projLen2 * projLen2; + + // IF the cp1 and cp2 is near to the start-line enough + // We treat it straight enough + if (d1Sqr < PIXEL_DISTANCE_SQR && projLen1 >= 0 + && d2Sqr < PIXEL_DISTANCE_SQR && projLen2 >= 0 + ) { + out.push(x3, y3); + return; + } + + + const tmpSegX: number[] = []; + const tmpSegY: number[] = []; + // Subdivide + cubicSubdivide(x0, x1, x2, x3, 0.5, tmpSegX); + cubicSubdivide(y0, y1, y2, y3, 0.5, tmpSegY); + + adpativeBezier( + tmpSegX[0], tmpSegY[0], tmpSegX[1], tmpSegY[1], tmpSegX[2], tmpSegY[2], tmpSegX[3], tmpSegY[3], + out, scale + ); + adpativeBezier( + tmpSegX[4], tmpSegY[4], tmpSegX[5], tmpSegY[5], tmpSegX[6], tmpSegY[6], tmpSegX[7], tmpSegY[7], + out, scale + ); +} + +export function pathToPolygons(path: PathProxy, scale?: number) { + // TODO Optimize simple case like path is polygon and rect? + const bezierArrayGroups = pathToBezierCurves(path); + + const polygons: number[][] = []; + + scale = scale || 1; + + for (let i = 0; i < bezierArrayGroups.length; i++) { + const beziers = bezierArrayGroups[i]; + const polygon: number[] = []; + let x0 = beziers[0]; + let y0 = beziers[1]; + + polygon.push(x0, y0); + + for (let k = 2; k < beziers.length;) { + + const x1 = beziers[k++]; + const y1 = beziers[k++]; + const x2 = beziers[k++]; + const y2 = beziers[k++]; + const x3 = beziers[k++]; + const y3 = beziers[k++]; + + adpativeBezier(x0, y0, x1, y1, x2, y2, x3, y3, polygon, scale); + + x0 = x3; + y0 = y3; + } + + polygons.push(polygon); + } + return polygons; +} diff --git a/src/tool/dividePath.ts b/src/tool/dividePath.ts new file mode 100644 index 000000000..a7fc8a38c --- /dev/null +++ b/src/tool/dividePath.ts @@ -0,0 +1,406 @@ +import { fromPoints } from '../core/bbox'; +import BoundingRect from '../core/BoundingRect'; +import Point from '../core/Point'; +import { each, map } from '../core/util'; +import Path from '../graphic/Path'; +import Polygon from '../graphic/shape/Polygon'; +import Rect from '../graphic/shape/Rect'; +import Sector from '../graphic/shape/Sector'; +import { pathToPolygons } from './convertPath'; +import { clonePath } from './path'; + +// Default shape dividers +// TODO divide polygon by grids. +interface BinaryDivide { + (shape: Path['shape']): Path['shape'][] +} + +/** + * Calculating a grid to divide the shape. + */ +function getDividingGrids(dimSize: number[], rowDim: number, count: number) { + const rowSize = dimSize[rowDim]; + const columnSize = dimSize[1 - rowDim]; + + const ratio = Math.abs(rowSize / columnSize); + let rowCount = Math.ceil(Math.sqrt(ratio * count)); + let columnCount = Math.floor(count / rowCount); + if (columnCount === 0) { + columnCount = 1; + rowCount = count; + } + + const grids: number[] = []; + for (let i = 0; i < rowCount; i++) { + grids.push(columnCount); + } + const currentCount = rowCount * columnCount; + // Distribute the remaind grid evenly on each row. + const remained = count - currentCount; + if (remained > 0) { + // const stride = Math.max(Math.floor(rowCount / remained), 1); + for (let i = 0; i < remained; i++) { + grids[i % rowCount] += 1; + } + } + return grids; +} + + +// TODO cornerRadius +function divideSector(sectorShape: Sector['shape'], count: number, outShapes: Sector['shape'][]) { + const r0 = sectorShape.r0; + const r = sectorShape.r; + const startAngle = sectorShape.startAngle; + const endAngle = sectorShape.endAngle; + const angle = Math.abs(endAngle - startAngle); + const arcLen = angle * r; + const deltaR = r - r0; + + const isAngleRow = arcLen > Math.abs(deltaR); + const grids = getDividingGrids([arcLen, deltaR], isAngleRow ? 0 : 1, count); + + const rowSize = (isAngleRow ? angle : deltaR) / grids.length; + + for (let row = 0; row < grids.length; row++) { + const columnSize = (isAngleRow ? deltaR : angle) / grids[row]; + for (let column = 0; column < grids[row]; column++) { + const newShape = {} as Sector['shape']; + + if (isAngleRow) { + newShape.startAngle = startAngle + rowSize * row; + newShape.endAngle = startAngle + rowSize * (row + 1); + newShape.r0 = r0 + columnSize * column; + newShape.r = r0 + columnSize * (column + 1); + } + else { + newShape.startAngle = startAngle + columnSize * column; + newShape.endAngle = startAngle + columnSize * (column + 1); + newShape.r0 = r0 + rowSize * row; + newShape.r = r0 + rowSize * (row + 1); + } + + newShape.clockwise = sectorShape.clockwise; + newShape.cx = sectorShape.cx; + newShape.cy = sectorShape.cy; + + outShapes.push(newShape); + } + } +} + +function divideRect(rectShape: Rect['shape'], count: number, outShapes: Rect['shape'][]) { + const width = rectShape.width; + const height = rectShape.height; + + const isHorizontalRow = width > height; + const grids = getDividingGrids([width, height], isHorizontalRow ? 0 : 1, count); + const rowSizeDim = isHorizontalRow ? 'width' : 'height'; + const columnSizeDim = isHorizontalRow ? 'height' : 'width'; + const rowDim = isHorizontalRow ? 'x' : 'y'; + const columnDim = isHorizontalRow ? 'y' : 'x'; + const rowSize = rectShape[rowSizeDim] / grids.length; + + for (let row = 0; row < grids.length; row++) { + const columnSize = rectShape[columnSizeDim] / grids[row]; + for (let column = 0; column < grids[row]; column++) { + const newShape = {} as Rect['shape']; + newShape[rowDim] = row * rowSize; + newShape[columnDim] = column * columnSize; + newShape[rowSizeDim] = rowSize; + newShape[columnSizeDim] = columnSize; + + newShape.x += rectShape.x; + newShape.y += rectShape.y; + + outShapes.push(newShape); + } + } +} + +function crossProduct2d(x1: number, y1: number, x2: number, y2: number) { + return x1 * y2 - x2 * y1; +} + +function lineLineIntersect( + a1x: number, a1y: number, a2x: number, a2y: number, // p1 + b1x: number, b1y: number, b2x: number, b2y: number // p2 +): Point { + const mx = a2x - a1x; + const my = a2y - a1y; + const nx = b2x - b1x; + const ny = b2y - b1y; + + const nmCrossProduct = crossProduct2d(nx, ny, mx, my); + if (Math.abs(nmCrossProduct) < 1e-6) { + return null; + } + + const b1a1x = a1x - b1x; + const b1a1y = a1y - b1y; + + const p = crossProduct2d(b1a1x, b1a1y, nx, ny) / nmCrossProduct; + if (p < 0 || p > 1) { + return null; + } + // p2 is an infinite line + return new Point( + p * mx + a1x, + p * my + a1y + ); +} + +function projPtOnLine(pt: Point, lineA: Point, lineB: Point): number { + const dir = new Point(); + Point.sub(dir, lineB, lineA); + dir.normalize(); + const dir2 = new Point(); + Point.sub(dir2, pt, lineA); + const len = dir2.dot(dir); + return len; +} + +function addToPoly(poly: number[][], pt: number[]) { + const last = poly[poly.length - 1]; + if (last && last[0] === pt[0] && last[1] === pt[1]) { + return; + } + poly.push(pt); +} + +function splitPolygonByLine(points: number[][], lineA: Point, lineB: Point) { + const len = points.length; + const intersections: { + projPt: number, + pt: Point + idx: number + }[] = []; + for (let i = 0; i < len; i++) { + const p0 = points[i]; + const p1 = points[(i + 1) % len]; + const intersectionPt = lineLineIntersect( + p0[0], p0[1], p1[0], p1[1], + lineA.x, lineA.y, lineB.x, lineB.y + ); + if (intersectionPt) { + intersections.push({ + projPt: projPtOnLine(intersectionPt, lineA, lineB), + pt: intersectionPt, + idx: i + }); + } + } + + // TODO No intersection? + if (intersections.length < 2) { + // Do clone + return [ { points}, {points} ]; + } + + // Find two farthest points. + intersections.sort((a, b) => { + return a.projPt - b.projPt; + }); + let splitPt0 = intersections[0]; + let splitPt1 = intersections[intersections.length - 1]; + if (splitPt1.idx < splitPt0.idx) { + const tmp = splitPt0; + splitPt0 = splitPt1; + splitPt1 = tmp; + } + + const splitPt0Arr = [splitPt0.pt.x, splitPt0.pt.y]; + const splitPt1Arr = [splitPt1.pt.x, splitPt1.pt.y]; + + const newPolyA: number[][] = [splitPt0Arr]; + const newPolyB: number[][] = [splitPt1Arr]; + + for (let i = splitPt0.idx + 1; i <= splitPt1.idx; i++) { + addToPoly(newPolyA, points[i].slice()); + } + addToPoly(newPolyA, splitPt1Arr); + // Close the path + addToPoly(newPolyA, splitPt0Arr); + + for (let i = splitPt1.idx + 1; i <= splitPt0.idx + len; i++) { + addToPoly(newPolyB, points[i % len].slice()); + } + addToPoly(newPolyB, splitPt0Arr); + // Close the path + addToPoly(newPolyB, splitPt1Arr); + + return [{ + points: newPolyA + }, { + points: newPolyB + }]; +} + +function binaryDividePolygon( + polygonShape: Pick +) { + const points = polygonShape.points; + const min: number[] = []; + const max: number[] = []; + fromPoints(points, min, max); + const boundingRect = new BoundingRect( + min[0], min[1], max[0] - min[0], max[1] - min[1] + ); + + const width = boundingRect.width; + const height = boundingRect.height; + const x = boundingRect.x; + const y = boundingRect.y; + + const pt0 = new Point(); + const pt1 = new Point(); + if (width > height) { + pt0.x = pt1.x = x + width / 2; + pt0.y = y; + pt1.y = y + height; + } + else { + pt0.y = pt1.y = y + height / 2; + pt0.x = x; + pt1.x = x + width; + } + return splitPolygonByLine(points, pt0, pt1); +} + + +function binaryDivideRecursive( + divider: BinaryDivide, shape: T, count: number, out: T[] +): T[] { + if (count === 1) { + out.push(shape); + } + else { + const mid = Math.floor(count / 2); + const sub = divider(shape); + binaryDivideRecursive(divider, sub[0], mid, out); + binaryDivideRecursive(divider, sub[1], count - mid, out); + } + + return out; +} + +export function clone(path: Path, count: number) { + const paths = []; + for (let i = 0; i < count; i++) { + paths.push(clonePath(path)); + } + return paths; +} + +function copyPathProps(source: Path, target: Path) { + target.setStyle(source.style); + target.z = source.z; + target.z2 = source.z2; + target.zlevel = source.zlevel; +} + +function polygonConvert(points: number[]): number[][] { + const out = []; + for (let i = 0; i < points.length;) { + out.push([points[i++], points[i++]]); + } + return out; +} + +export function split( + path: Path, count: number +) { + const outShapes: Path['shape'][] = []; + const shape = path.shape; + let OutShapeCtor: new() => Path; + // TODO Use clone when shape size is small + switch (path.type) { + case 'rect': + divideRect(shape as Rect['shape'], count, outShapes as Rect['shape'][]); + OutShapeCtor = Rect; + break; + case 'sector': + divideSector(shape as Sector['shape'], count, outShapes as Sector['shape'][]); + OutShapeCtor = Sector; + break; + case 'circle': + divideSector({ + r0: 0, r: shape.r, startAngle: 0, endAngle: Math.PI * 2, + cx: shape.cx, cy: shape.cy + } as Sector['shape'], count, outShapes as Sector['shape'][]); + OutShapeCtor = Sector; + break; + default: + const m = path.getComputedTransform(); + const scale = m ? Math.sqrt(Math.max(m[0] * m[0] + m[1] * m[1], m[2] * m[2] + m[3] * m[3])) : 1; + const polygons = map( + pathToPolygons(path.getUpdatedPathProxy(), scale), + poly => polygonConvert(poly) + ); + const polygonCount = polygons.length; + if (polygonCount === 0) { + binaryDivideRecursive(binaryDividePolygon, { + points: polygons[0] + }, count, outShapes); + } + else if (polygonCount === count) { // In case we only split batched paths to non-batched paths. No need to split. + for (let i = 0; i < polygonCount; i++) { + outShapes.push({ + points: polygons[i] + } as Polygon['shape']); + } + } + else { + // Most complex case. Assign multiple subpath to each polygon based on it's area. + let totalArea = 0; + const items = map(polygons, poly => { + const min: number[] = []; + const max: number[] = []; + fromPoints(poly, min, max); + // TODO: polygon area? + const area = (max[1] - min[1]) * (max[0] - min[0]); + totalArea += area; + return { poly, area }; + }); + items.sort((a, b) => b.area - a.area); + + let left = count; + for (let i = 0; i < polygonCount; i++) { + const item = items[i]; + if (left <= 0) { + break; + } + + const selfCount = i === polygonCount - 1 + ? left // Use the last piece directly + : Math.ceil(item.area / totalArea * count); + + if (selfCount < 0) { + continue; + } + + binaryDivideRecursive(binaryDividePolygon, { + points: item.poly + }, selfCount, outShapes); + left -= selfCount; + }; + } + OutShapeCtor = Polygon; + break; + } + + if (!OutShapeCtor) { + // Unkown split algorithm. Use clone instead + return clone(path, count); + } + const out: Path[] = []; + + for (let i = 0; i < outShapes.length; i++) { + const subPath = new OutShapeCtor(); + subPath.setShape(outShapes[i]); + copyPathProps(path, subPath); + out.push(subPath); + } + + return out; +} \ No newline at end of file diff --git a/src/tool/morphPath.ts b/src/tool/morphPath.ts index c1a9e2137..6c6fd205e 100644 --- a/src/tool/morphPath.ts +++ b/src/tool/morphPath.ts @@ -2,229 +2,15 @@ import PathProxy from '../core/PathProxy'; import { cubicSubdivide } from '../core/curve'; import Path from '../graphic/Path'; import Element, { ElementAnimateConfig } from '../Element'; -import { defaults, assert, noop, clone } from '../core/util'; +import { defaults, extend, map } from '../core/util'; import { lerp } from '../core/vector'; -import Rect from '../graphic/shape/Rect'; -import Sector from '../graphic/shape/Sector'; +import Group, { GroupLike } from '../graphic/Group'; +import { clonePath } from './path'; +import { MatrixArray } from '../core/matrix'; +import Transformable from '../core/Transformable'; import { ZRenderType } from '../zrender'; -import Group from '../graphic/Group'; - -const CMD = PathProxy.CMD; -const PI2 = Math.PI * 2; - -const PROP_XY = ['x', 'y'] as const; -const PROP_WH = ['width', 'height'] as const; - -const tmpArr: number[] = []; - - -interface CombiningPath extends Path { - __combiningSubList: Path[]; - __oldAddSelfToZr: Element['addSelfToZr']; - __oldRemoveSelfFromZr: Element['removeSelfFromZr']; - __oldBuildPath: Path['buildPath']; - // See `Stroage['_updateAndAddDisplayable']` - childrenRef(): Path[]; -} - -export type MorphDividingMethod = 'split' | 'duplicate'; - -export interface CombineSeparateConfig extends ElementAnimateConfig { - dividingMethod?: MorphDividingMethod; -} - -export interface CombineSeparateResult { - // The length of `fromIndividuals`, `toIndividuals` - // are the same as `count`. - fromIndividuals: Path[]; - toIndividuals: Path[]; - count: number; -} - -function aroundEqual(a: number, b: number) { - return Math.abs(a - b) < 1e-5; -} - -export function pathToBezierCurves(path: PathProxy) { - - const data = path.data; - const len = path.len(); - - const bezierArray: number[][] = []; - let currentSubpath: number[]; - - let xi = 0; - let yi = 0; - let x0 = 0; - let y0 = 0; - - function createNewSubpath(x: number, y: number) { - // More than one M command - if (currentSubpath && currentSubpath.length > 2) { - bezierArray.push(currentSubpath); - } - currentSubpath = [x, y]; - } - - function addLine(x0: number, y0: number, x1: number, y1: number) { - if (!(aroundEqual(x0, x1) && aroundEqual(y0, y1))) { - currentSubpath.push(x0, y0, x1, y1, x1, y1); - } - } - - function addArc(startAngle: number, endAngle: number, cx: number, cy: number, rx: number, ry: number) { - // https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves - const delta = Math.abs(endAngle - startAngle); - const len = Math.tan(delta / 4) * 4 / 3; - const dir = endAngle < startAngle ? -1 : 1; - - const c1 = Math.cos(startAngle); - const s1 = Math.sin(startAngle); - const c2 = Math.cos(endAngle); - const s2 = Math.sin(endAngle); - - const x1 = c1 * rx + cx; - const y1 = s1 * ry + cy; - - const x4 = c2 * rx + cx; - const y4 = s2 * ry + cy; - - const hx = rx * len * dir; - const hy = ry * len * dir; - currentSubpath.push( - // Move control points on tangent. - x1 - hx * s1, y1 + hy * c1, - x4 + hx * s2, y4 - hy * c2, - x4, y4 - ); - } - - let x1; - let y1; - let x2; - let y2; - - for (let i = 0; i < len;) { - const cmd = data[i++]; - const isFirst = i === 1; - - if (isFirst) { - // 如果第一个命令是 L, C, Q - // 则 previous point 同绘制命令的第一个 point - // 第一个命令为 Arc 的情况下会在后面特殊处理 - xi = data[i]; - yi = data[i + 1]; - - x0 = xi; - y0 = yi; - - if (cmd === CMD.L || cmd === CMD.C || cmd === CMD.Q) { - // Start point - currentSubpath = [x0, y0]; - } - } - - switch (cmd) { - case CMD.M: - // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 - // 在 closePath 的时候使用 - xi = x0 = data[i++]; - yi = y0 = data[i++]; - - createNewSubpath(x0, y0); - break; - case CMD.L: - x1 = data[i++]; - y1 = data[i++]; - addLine(xi, yi, x1, y1); - xi = x1; - yi = y1; - break; - case CMD.C: - currentSubpath.push( - data[i++], data[i++], data[i++], data[i++], - xi = data[i++], yi = data[i++] - ); - break; - case CMD.Q: - x1 = data[i++]; - y1 = data[i++]; - x2 = data[i++]; - y2 = data[i++]; - currentSubpath.push( - // Convert quadratic to cubic - xi + 2 / 3 * (x1 - xi), yi + 2 / 3 * (y1 - yi), - x2 + 2 / 3 * (x1 - x2), y2 + 2 / 3 * (y1 - y2), - x2, y2 - ); - xi = x2; - yi = y2; - break; - case CMD.A: - const cx = data[i++]; - const cy = data[i++]; - const rx = data[i++]; - const ry = data[i++]; - const startAngle = data[i++]; - const endAngle = data[i++] + startAngle; - - // TODO Arc rotation - i += 1; - const anticlockwise = !data[i++]; - - x1 = Math.cos(startAngle) * rx + cx; - y1 = Math.sin(startAngle) * ry + cy; - if (isFirst) { - // 直接使用 arc 命令 - // 第一个命令起点还未定义 - x0 = x1; - y0 = y1; - createNewSubpath(x0, y0); - } - else { - // Connect a line between current point to arc start point. - addLine(xi, yi, x1, y1); - } - - xi = Math.cos(endAngle) * rx + cx; - yi = Math.sin(endAngle) * ry + cy; - - const step = (anticlockwise ? -1 : 1) * Math.PI / 2; - - for (let angle = startAngle; anticlockwise ? angle > endAngle : angle < endAngle; angle += step) { - const nextAngle = anticlockwise ? Math.max(angle + step, endAngle) - : Math.min(angle + step, endAngle); - addArc(angle, nextAngle, cx, cy, rx, ry); - } - - break; - case CMD.R: - x0 = xi = data[i++]; - y0 = yi = data[i++]; - x1 = x0 + data[i++]; - y1 = y0 + data[i++]; - - // rect is an individual path. - createNewSubpath(x1, y0); - addLine(x1, y0, x1, y1); - addLine(x1, y1, x0, y1); - addLine(x0, y1, x0, y0); - addLine(x0, y0, x1, y0); - break; - case CMD.Z: - currentSubpath && addLine(xi, yi, x0, y0); - xi = x0; - yi = y0; - break; - } - } - - if (currentSubpath && currentSubpath.length > 2) { - bezierArray.push(currentSubpath); - } - - return bezierArray; -} +import { split } from './dividePath'; +import { pathToBezierCurves } from './convertPath'; function alignSubpath(subpath1: number[], subpath2: number[]): [number[], number[]] { const len1 = subpath1.length; @@ -232,6 +18,8 @@ function alignSubpath(subpath1: number[], subpath2: number[]): [number[], number if (len1 === len2) { return [subpath1, subpath2]; } + const tmpSegX: number[] = []; + const tmpSegY: number[] = []; const shorterPath = len1 < len2 ? subpath1 : subpath2; const shorterLen = Math.min(len1, len2); @@ -244,9 +32,6 @@ function alignSubpath(subpath1: number[], subpath2: number[]): [number[], number const newSubpath = [shorterPath[0], shorterPath[1]]; let remained = diff; - const tmpSegX: number[] = []; - const tmpSegY: number[] = []; - for (let i = 2; i < shorterLen;) { let x0 = shorterPath[i - 2]; let y0 = shorterPath[i - 1]; @@ -346,8 +131,11 @@ export function alignBezierCurves(array1: number[][], array2: number[][]) { interface MorphingPath extends Path { __morphT: number; - __oldBuildPath: Path['buildPath']; - __morphingData: MorphingData; +} + +export interface CombineMorphingPath extends Path { + childrenRef(): (CombineMorphingPath | Path)[] + __isCombineMorphing: boolean; } export function centroid(array: number[]) { @@ -543,59 +331,180 @@ function findBestMorphingRotation( return result; } +export function isCombineMorphing(path: Element): path is CombineMorphingPath { + return (path as CombineMorphingPath).__isCombineMorphing; +} + +export function isMorphing(el: Element) { + return (el as MorphingPath).__morphT >= 0; +} + +const SAVED_METHOD_PREFIX = '__mOriginal_'; +function saveAndModifyMethod( + obj: T, + methodName: M, + modifiers: { replace?: T[M], after?: T[M], before?: T[M] } +) { + const savedMethodName = SAVED_METHOD_PREFIX + methodName; + const originalMethod = (obj as any)[savedMethodName] || obj[methodName]; + if (!(obj as any)[savedMethodName]) { + (obj as any)[savedMethodName] = obj[methodName]; + } + const replace = modifiers.replace; + const after = modifiers.after; + const before = modifiers.before; + + (obj as any)[methodName] = function () { + const args = arguments; + let res; + before && (before as unknown as Function).apply(this, args); + // Still call the original method if not replacement. + if (replace) { + res = (replace as unknown as Function).apply(this, args); + } + else { + res = originalMethod.apply(this, args); + } + after && (after as unknown as Function).apply(this, args); + return res; + }; +} +function restoreMethod( + obj: T, + methodName: keyof T +) { + const savedMethodName = SAVED_METHOD_PREFIX + methodName; + if ((obj as any)[savedMethodName]) { + obj[methodName] = (obj as any)[savedMethodName]; + (obj as any)[savedMethodName] = null; + } +} + +function applyTransformOnBeziers(bezierCurves: number[][], mm: MatrixArray) { + for (let i = 0; i < bezierCurves.length; i++) { + const subBeziers = bezierCurves[i]; + for (let k = 0; k < subBeziers.length;) { + const x = subBeziers[k]; + const y = subBeziers[k + 1]; + + subBeziers[k++] = mm[0] * x + mm[2] * y + mm[4]; + subBeziers[k++] = mm[1] * x + mm[3] * y + mm[5]; + } + } +} + +function prepareMorphPath( + fromPath: Path, + toPath: Path +) { + const fromPathProxy = fromPath.getUpdatedPathProxy(); + const toPathProxy = toPath.getUpdatedPathProxy(); + + const [fromBezierCurves, toBezierCurves] = + alignBezierCurves(pathToBezierCurves(fromPathProxy), pathToBezierCurves(toPathProxy)); + + const fromPathTransform = fromPath.getComputedTransform(); + const toPathTransform = toPath.getComputedTransform(); + function updateIdentityTransform(this: Transformable) { + this.transform = null; + } + fromPathTransform && applyTransformOnBeziers(fromBezierCurves, fromPathTransform); + toPathTransform && applyTransformOnBeziers(toBezierCurves, toPathTransform); + // Just ignore transform + saveAndModifyMethod(toPath, 'updateTransform', { replace: updateIdentityTransform }); + toPath.transform = null; + + const morphingData = findBestMorphingRotation(fromBezierCurves, toBezierCurves, 10, Math.PI); + + const tmpArr: number[] = []; + + saveAndModifyMethod(toPath, 'buildPath', { replace(path: PathProxy) { + const t = (toPath as MorphingPath).__morphT; + const onet = 1 - t; + + const newCp: number[] = []; + + for (let i = 0; i < morphingData.length; i++) { + const item = morphingData[i]; + const from = item.from; + const to = item.to; + const angle = item.rotation * t; + const fromCp = item.fromCp; + const toCp = item.toCp; + const sa = Math.sin(angle); + const ca = Math.cos(angle); + + lerp(newCp, fromCp, toCp, t); + + for (let m = 0; m < from.length; m += 2) { + const x0 = from[m]; + const y0 = from[m + 1]; + const x1 = to[m]; + const y1 = to[m + 1]; + + const x = x0 * onet + x1 * t; + const y = y0 * onet + y1 * t; + + tmpArr[m] = (x * ca - y * sa) + newCp[0]; + tmpArr[m + 1] = (x * sa + y * ca) + newCp[1]; + } + + let x0 = tmpArr[0]; + let y0 = tmpArr[1]; + + path.moveTo(x0, y0); + + for (let m = 2; m < from.length;) { + const x1 = tmpArr[m++]; + const y1 = tmpArr[m++]; + const x2 = tmpArr[m++]; + const y2 = tmpArr[m++]; + const x3 = tmpArr[m++]; + const y3 = tmpArr[m++]; + + // Is a line. + if (x0 === x1 && y0 === y1 && x2 === x3 && y2 === y3) { + path.lineTo(x3, y3); + } + else { + path.bezierCurveTo(x1, y1, x2, y2, x3, y3); + } + x0 = x3; + y0 = y3; + } + } + } }); +} + /** * Morphing from old path to new path. */ export function morphPath( - // `fromPath` only provides the current path state, which will - // not be rendered or kept. - // Note: - // should be able to handle `isIndividualMorphingPath(fromPath)` is `ture`. fromPath: Path, - // `toPath` is the target path that will be rendered and kept. - // Note: - // (1) `toPath` and `fromPath` might be the same. - // e.g., when triggering the same transition repeatly. - // (2) should be able to handle `isIndividualMorphingPath(toPath)` is `ture`. toPath: Path, animationOpts: ElementAnimateConfig ): Path { - let fromPathProxy: PathProxy; - let toPathProxy: PathProxy; - if (!fromPath || !toPath) { return toPath; } - // Calculate the current path into `fromPathProxy` from `fromPathInput`. - !fromPath.path && fromPath.createPathProxy(); - fromPathProxy = fromPath.path; - fromPathProxy.beginPath(); - fromPath.buildPath(fromPathProxy, fromPath.shape); - - // Calculate the target path into `toPathProxy` from `toPath`. - !toPath.path && toPath.createPathProxy(); - toPathProxy = toPath.path; - // From and to might be the same path. - toPathProxy === fromPathProxy && (toPathProxy = new PathProxy(false)); - toPathProxy.beginPath(); - // toPath should always calculate the final state rather than morphing state. - if (isIndividualMorphingPath(toPath)) { - toPath.__oldBuildPath(toPathProxy, toPath.shape); - } - else { - toPath.buildPath(toPathProxy, toPath.shape); - } + const oldDone = animationOpts.done; + // const oldAborted = animationOpts.aborted; + const oldDuring = animationOpts.during; - const [fromBezierCurves, toBezierCurves] = - alignBezierCurves(pathToBezierCurves(fromPathProxy), pathToBezierCurves(toPathProxy)); + prepareMorphPath(fromPath, toPath); - const morphingData = findBestMorphingRotation(fromBezierCurves, toBezierCurves, 10, Math.PI); - becomeIndividualMorphingPath(toPath, morphingData, 0); + (toPath as MorphingPath).__morphT = 0; - const oldDone = animationOpts && animationOpts.done; - const oldAborted = animationOpts && animationOpts.aborted; - const oldDuring = animationOpts && animationOpts.during; + function restoreToPath() { + restoreMethod(toPath, 'buildPath'); + restoreMethod(toPath, 'updateTransform'); + // Mark as not in morphing + (toPath as MorphingPath).__morphT = -1; + // Cleanup. + toPath.createPathProxy(); + toPath.dirtyShape(); + } toPath.animateTo({ __morphT: 1 @@ -605,458 +514,376 @@ export function morphPath( oldDuring && oldDuring(p); }, done() { - restoreIndividualMorphingPath(toPath); - // Cleanup. - toPath.createPathProxy(); - toPath.dirtyShape(); + restoreToPath(); oldDone && oldDone(); - }, - aborted() { - oldAborted && oldAborted(); } + // NOTE: Don't do restore if aborted. + // Because all status was just set when animation started. + // aborted() { + // oldAborted && oldAborted(); + // } } as ElementAnimateConfig, animationOpts)); return toPath; } -function morphingPathBuildPath( - this: Pick, - path: PathProxy -): void { - const morphingData = this.__morphingData; - const t = this.__morphT; - const onet = 1 - t; - - const newCp: number[] = []; - for (let i = 0; i < morphingData.length; i++) { - const item = morphingData[i]; - const from = item.from; - const to = item.to; - const angle = item.rotation * t; - const fromCp = item.fromCp; - const toCp = item.toCp; - const sa = Math.sin(angle); - const ca = Math.cos(angle); - - lerp(newCp, fromCp, toCp, t); - - for (let m = 0; m < from.length; m += 2) { - const x0 = from[m]; - const y0 = from[m + 1]; - const x1 = to[m]; - const y1 = to[m + 1]; - - const x = x0 * onet + x1 * t; - const y = y0 * onet + y1 * t; - - tmpArr[m] = (x * ca - y * sa) + newCp[0]; - tmpArr[m + 1] = (x * sa + y * ca) + newCp[1]; - } - - for (let m = 0; m < from.length;) { - if (m === 0) { - path.moveTo(tmpArr[m++], tmpArr[m++]); +// https://github.com/mapbox/earcut/blob/master/src/earcut.js#L437 +// https://jsfiddle.net/pissang/2jk7x145/ +// function zOrder(x: number, y: number, minX: number, minY: number, maxX: number, maxY: number) { +// // Normalize coords to 0 - 1 +// // The transformed into non-negative 15-bit integer range +// x = (maxX === minX) ? 0 : Math.round(32767 * (x - minX) / (maxX - minX)); +// y = (maxY === minY) ? 0 : Math.round(32767 * (y - minY) / (maxY - minY)); + +// x = (x | (x << 8)) & 0x00FF00FF; +// x = (x | (x << 4)) & 0x0F0F0F0F; +// x = (x | (x << 2)) & 0x33333333; +// x = (x | (x << 1)) & 0x55555555; + +// y = (y | (y << 8)) & 0x00FF00FF; +// y = (y | (y << 4)) & 0x0F0F0F0F; +// y = (y | (y << 2)) & 0x33333333; +// y = (y | (y << 1)) & 0x55555555; + +// return x | (y << 1); +// } + +// https://github.com/w8r/hilbert/blob/master/hilbert.js#L30 +// https://jsfiddle.net/pissang/xdnbzg6v/ +function hilbert(x: number, y: number, minX: number, minY: number, maxX: number, maxY: number) { + const bits = 16; + x = (maxX === minX) ? 0 : Math.round(32767 * (x - minX) / (maxX - minX)); + y = (maxY === minY) ? 0 : Math.round(32767 * (y - minY) / (maxY - minY)); + + let d = 0; + let tmp: number; + for (let s = (1 << bits) / 2; s > 0; s /= 2) { + let rx = 0, ry = 0; + + if ((x & s) > 0) rx = 1; + if ((y & s) > 0) ry = 1; + + d += s * s * ((3 * rx) ^ ry); + + if (ry === 0) { + if (rx === 1) { + x = s - 1 - x; + y = s - 1 - y; } - path.bezierCurveTo( - tmpArr[m++], tmpArr[m++], - tmpArr[m++], tmpArr[m++], - tmpArr[m++], tmpArr[m++] - ); + tmp = x; + x = y; + y = tmp; } } -}; - -function becomeIndividualMorphingPath( - path: Path, - morphingData: MorphingData, - morphT: number -): void { - if (isIndividualMorphingPath(path)) { - updateIndividualMorphingPath(path, morphingData, morphT); - return; - } + return d; +} + +// Sort paths on hilbert. Not using z-order because it may still have large cross. +// So the left most source can animate to the left most target, not right most target. +// Hope in this way. We can make sure each element is animated to the proper target. Not the farthest. +function sortPaths(pathList: Path[]): Path[] { + let xMin = Infinity; + let yMin = Infinity; + let xMax = -Infinity; + let yMax = -Infinity; + const cps = map(pathList, path => { + const rect = path.getBoundingRect(); + const m = path.getComputedTransform(); + const x = rect.x + rect.width / 2 + (m ? m[4] : 0); + const y = rect.y + rect.height / 2 + (m ? m[5] : 0); + xMin = Math.min(x, xMin); + yMin = Math.min(y, yMin); + xMax = Math.max(x, xMax); + yMax = Math.max(y, yMax); + return [x, y]; + }); + + const items = map(cps, (cp, idx) => { + return { + cp, + z: hilbert(cp[0], cp[1], xMin, yMin, xMax, yMax), + path: pathList[idx] + } + }); - const morphingPath = path as MorphingPath; - morphingPath.__oldBuildPath = morphingPath.buildPath; - morphingPath.buildPath = morphingPathBuildPath; - updateIndividualMorphingPath(morphingPath, morphingData, morphT); + return items.sort((a, b) => a.z - b.z).map(item => item.path); } -function updateIndividualMorphingPath( - morphingPath: MorphingPath, - morphingData: MorphingData, - morphT: number -): void { - morphingPath.__morphingData = morphingData; - morphingPath.__morphT = morphT; +export interface DividePathParams { + path: Path, + count: number +}; +export interface DividePath { + (params: DividePathParams): Path[] } -function restoreIndividualMorphingPath(path: Path): void { - if (isIndividualMorphingPath(path)) { - path.buildPath = path.__oldBuildPath; - path.__oldBuildPath = path.__morphingData = null; - } +export interface IndividualDelay { + (index: number, count: number, fromPath: Path, toPath: Path): number } -function isIndividualMorphingPath(path: Path): path is MorphingPath { - return (path as MorphingPath).__oldBuildPath != null; +function defaultDividePath(param: DividePathParams) { + return split(param.path, param.count); } - -export function isCombiningPath(path: Path): path is CombiningPath { - return !!(path as CombiningPath).__combiningSubList; +export interface CombineConfig extends ElementAnimateConfig { + /** + * Transform of returned will be ignored. + */ + dividePath?: DividePath + /** + * delay of each individual. + * Because individual are sorted on z-order. The index is also sorted top-left / right-down. + */ + individualDelay?: IndividualDelay + /** + * If sort splitted paths so the movement between them can be more natural + */ + // sort?: boolean } -export function isInAnyMorphing(path: Path): boolean { - return isIndividualMorphingPath(path) || isCombiningPath(path); +function createEmptyReturn() { + return { + fromIndividuals: [] as Path[], + toIndividuals: [] as Path[], + count: 0 + } } - - /** - * Make combining morphing from many paths to one. - * Make the MorphingKind of `toPath` become `'COMBINING'`. + * Make combine morphing from many paths to one. + * Will return a group to replace the original path. */ -export function combine( - fromPathList: Path[], +export function combineMorph( + fromList: (CombineMorphingPath | Path)[], toPath: Path, - animationOpts: CombineSeparateConfig, - copyPropsIfDivided?: (srcPath: Path, tarPath: Path, needClone: boolean) => void -): CombineSeparateResult { - - const fromIndividuals: Path[] = []; - let separateCount = 0; - for (let i = 0; i < fromPathList.length; i++) { - const fromPath = fromPathList[i]; - if (isCombiningPath(fromPath)) { - // If fromPath is combining, use the combineFromList as the from. - const fromCombiningSubList = fromPath.__combiningSubList; - for (let j = 0; j < fromCombiningSubList.length; j++) { - fromIndividuals.push(fromCombiningSubList[j]); - } - separateCount += fromCombiningSubList.length; - } - else { - fromIndividuals.push(fromPath); - separateCount++; - } - } - - // fromPathList.length is 0. - if (!separateCount) { - return; - } - - // PENDING: more separate strategies other than `divideShape`? - const dividingMethod = animationOpts ? animationOpts.dividingMethod : null; - const toPathSplittedList = divideShape(toPath, separateCount, dividingMethod); - assert(toPathSplittedList.length === separateCount); - - const oldDone = animationOpts && animationOpts.done; - const oldAborted = animationOpts && animationOpts.aborted; - const oldDuring = animationOpts && animationOpts.during; + animationOpts: CombineConfig +) { + let fromPathList: Path[] = []; - let doneCount = 0; - let abortedCalled = false; - const morphAnimationOpts = defaults({ - during(p) { - oldDuring && oldDuring(p); - }, - done() { - doneCount++; - if (doneCount === toPathSplittedList.length) { - restoreCombiningPath(toPath); - oldDone && oldDone(); + function addFromPath(fromList: Element[]) { + for (let i = 0; i < fromList.length; i++) { + const from = fromList[i]; + if (isCombineMorphing(from)) { + addFromPath((from as GroupLike).childrenRef()); } - }, - aborted() { - // PENDING: is it logically correct? - if (!abortedCalled) { - abortedCalled = true; - oldAborted && oldAborted(); + else if (from instanceof Path) { + fromPathList.push(from); } } - } as ElementAnimateConfig, animationOpts); - - for (let i = 0; i < separateCount; i++) { - const from = fromIndividuals[i]; - const to = toPathSplittedList[i]; - copyPropsIfDivided && copyPropsIfDivided(toPath, to, true); - morphPath(from, to, morphAnimationOpts); } + addFromPath(fromList); - becomeCombiningPath(toPath, toPathSplittedList); - - return { - fromIndividuals: fromIndividuals, - toIndividuals: toPathSplittedList, - count: separateCount - }; -} - + const separateCount = fromPathList.length; -// PENDING: This is NOT a good implementation to decorate path methods. -// Potential flaw: when get path by `group.childAt(i)`, -// it might return the `combiningSubList` group, which is not expected. -// Probably this feature should be implemented same as the way of rich text? -function becomeCombiningPath(path: Path, combiningSubList: Path[]): void { - if (isCombiningPath(path)) { - updateCombiningPathSubList(path, combiningSubList); - return; + // fromPathList.length is 0. + if (!separateCount) { + return createEmptyReturn(); } - const combiningPath = path as CombiningPath; + const dividePath = animationOpts.dividePath || defaultDividePath; - updateCombiningPathSubList(combiningPath, combiningSubList); + let toSubPathList = dividePath({ + path: toPath, count: separateCount + }); + if (toSubPathList.length !== separateCount) { + console.error('Invalid morphing: unmatched splitted path'); + return createEmptyReturn(); + } - // PENDING: Too tricky. error-prone. - // Decorate methods. Do not do it repeatly. - combiningPath.__oldAddSelfToZr = path.addSelfToZr; - combiningPath.__oldRemoveSelfFromZr = path.removeSelfFromZr; - combiningPath.addSelfToZr = combiningAddSelfToZr; - combiningPath.removeSelfFromZr = combiningRemoveSelfFromZr; - combiningPath.__oldBuildPath = combiningPath.buildPath; - combiningPath.buildPath = noop; - combiningPath.childrenRef = combiningChildrenRef; + fromPathList = sortPaths(fromPathList); + toSubPathList = sortPaths(toSubPathList); - // PENDING: bounding rect? -} + const oldDone = animationOpts.done; + // const oldAborted = animationOpts.aborted; + const oldDuring = animationOpts.during; + const individualDelay = animationOpts.individualDelay; -function restoreCombiningPath(path: Path): void { - if (!isCombiningPath(path)) { - return; - } + const identityTransform = new Transformable(); - const combiningPath = path as CombiningPath; + for (let i = 0; i < separateCount; i++) { + const from = fromPathList[i]; + const to = toSubPathList[i]; - updateCombiningPathSubList(combiningPath, null); + to.parent = toPath as unknown as Group; - combiningPath.addSelfToZr = combiningPath.__oldAddSelfToZr; - combiningPath.removeSelfFromZr = combiningPath.__oldRemoveSelfFromZr; - combiningPath.buildPath = combiningPath.__oldBuildPath; - combiningPath.childrenRef = - combiningPath.__combiningSubList = - combiningPath.__oldAddSelfToZr = - combiningPath.__oldRemoveSelfFromZr = - combiningPath.__oldBuildPath = null; -} + // Ignore transform in each subpath. + to.copyTransform(identityTransform); -function updateCombiningPathSubList( - combiningPath: CombiningPath, - // Especially, `combiningSubList` is null/undefined means that remove sub list. - combiningSubList: Path[] -): void { - if (combiningPath.__combiningSubList !== combiningSubList) { - combiningPathSubListAddRemoveWithZr(combiningPath, 'removeSelfFromZr'); - combiningPath.__combiningSubList = combiningSubList; - if (combiningSubList) { - for (let i = 0; i < combiningSubList.length; i++) { - // Tricky: make `updateTransform` work in `Transformable`. The parent can only be Group. - combiningSubList[i].parent = combiningPath as unknown as Group; - } + // Will do morphPath for each individual if individualDelay is set. + if (!individualDelay) { + prepareMorphPath(from, to); } - combiningPathSubListAddRemoveWithZr(combiningPath, 'addSelfToZr'); } -} -function combiningAddSelfToZr(this: CombiningPath, zr: ZRenderType): void { - this.__oldAddSelfToZr(zr); - combiningPathSubListAddRemoveWithZr(this, 'addSelfToZr'); -} + (toPath as CombineMorphingPath).__isCombineMorphing = true; + (toPath as CombineMorphingPath).childrenRef = function () { + return toSubPathList; + }; -function combiningPathSubListAddRemoveWithZr( - path: CombiningPath, - method: 'addSelfToZr' | 'removeSelfFromZr' -): void { - const combiningSubList = path.__combiningSubList; - const zr = path.__zr; - if (combiningSubList && zr) { - for (let i = 0; i < combiningSubList.length; i++) { - const child = combiningSubList[i]; - child[method](zr); + function addToSubPathListToZr(zr: ZRenderType) { + for (let i = 0; i < toSubPathList.length; i++) { + toSubPathList[i].addSelfToZr(zr); } } -} - -function combiningRemoveSelfFromZr(this: CombiningPath, zr: ZRenderType): void { - this.__oldRemoveSelfFromZr(zr); - const combiningSubList = this.__combiningSubList; - for (let i = 0; i < combiningSubList.length; i++) { - const child = combiningSubList[i]; - child.removeSelfFromZr(zr); - } -} + saveAndModifyMethod(toPath, 'addSelfToZr', { + after(zr) { + addToSubPathListToZr(zr); + } + }); + saveAndModifyMethod(toPath, 'removeSelfFromZr', { + after(zr) { + for (let i = 0; i < toSubPathList.length; i++) { + toSubPathList[i].removeSelfFromZr(zr); + } + } + }); -function combiningChildrenRef(this: CombiningPath): Path[] { - return this.__combiningSubList; -} + function restoreToPath() { + (toPath as CombineMorphingPath).__isCombineMorphing = false; + // Mark as not in morphing + (toPath as MorphingPath).__morphT = -1; + (toPath as CombineMorphingPath).childrenRef = null; + restoreMethod(toPath, 'addSelfToZr'); + restoreMethod(toPath, 'removeSelfFromZr'); + } -/** - * Make separate morphing from one path to many paths. - * Make the MorphingKind of `toPath` become `'ONE_ONE'`. - */ -export function separate( - fromPath: Path, - toPathList: Path[], - animationOpts: CombineSeparateConfig, - copyPropsIfDivided?: (srcPath: Path, tarPath: Path, needClone: boolean) => void -): CombineSeparateResult { - const toPathListLen = toPathList.length; - let fromPathList: Path[]; - const dividingMethod = animationOpts ? animationOpts.dividingMethod : null; - let copyProps = false; + const toLen = toSubPathList.length; - // This case most happen when a combining path is called to reverse the animation - // to its original separated state. - if (isCombiningPath(fromPath)) { - // [CATEAT]: - // do not `restoreCombiningPath`, because it will cause the sub paths been removed - // from its host, so that the original "global transform" can not be gotten any more. - - const fromCombiningSubList = fromPath.__combiningSubList; - if (fromCombiningSubList.length === toPathListLen) { - fromPathList = fromCombiningSubList; + if (individualDelay) { + let animating = toLen; + const eachDone = () => { + animating--; + if (animating === 0) { + restoreToPath(); + oldDone && oldDone(); + } } - // The fromPath is a `CombiningPath` and its combiningSubCount is different from toPathList.length - // At present we do not make "continuous" animation for this case. It's might bring complicated logic. - else { - fromPathList = divideShape(fromPath, toPathListLen, dividingMethod); - copyProps = true; + // Animate each element individually. + for (let i = 0; i < toLen; i++) { + // TODO only call during once? + const indivdualAnimationOpts = individualDelay ? defaults({ + delay: (animationOpts.delay || 0) + individualDelay(i, toLen, fromPathList[i], toSubPathList[i]), + done: eachDone + } as ElementAnimateConfig, animationOpts) : animationOpts; + morphPath(fromPathList[i], toSubPathList[i], indivdualAnimationOpts); } } else { - fromPathList = divideShape(fromPath, toPathListLen, dividingMethod); - copyProps = true; + (toPath as MorphingPath).__morphT = 0; + toPath.animateTo({ + __morphT: 1 + } as any, defaults({ + during(p) { + for (let i = 0; i < toLen; i++) { + const child = toSubPathList[i] as MorphingPath; + child.__morphT = (toPath as MorphingPath).__morphT; + child.dirtyShape(); + } + oldDuring && oldDuring(p); + }, + done() { + restoreToPath(); + for (let i = 0; i < fromList.length; i++) { + restoreMethod(fromList[i], 'updateTransform'); + } + oldDone && oldDone(); + } + } as ElementAnimateConfig, animationOpts)); } - assert(fromPathList.length === toPathListLen); - for (let i = 0; i < toPathListLen; i++) { - if (copyProps && copyPropsIfDivided) { - copyPropsIfDivided(fromPath, fromPathList[i], false); - } - morphPath(fromPathList[i], toPathList[i], animationOpts); + if (toPath.__zr) { + addToSubPathListToZr(toPath.__zr); } return { fromIndividuals: fromPathList, - toIndividuals: toPathList, - count: toPathListLen + toIndividuals: toSubPathList, + count: toLen }; } - - -/** - * TODO: triangulate separate - * - * @return Never be null/undefined, may empty []. - */ -function divideShape( - path: Path, - separateCount: number, - // By default 'split'. - dividingMethod?: MorphDividingMethod -): Path[] { - return dividingMethod === 'duplicate' - ? duplicateShape(path, separateCount) - : splitShape(path, separateCount); +export interface SeparateConfig extends ElementAnimateConfig { + dividePath?: DividePath + individualDelay?: IndividualDelay + /** + * If sort splitted paths so the movement between them can be more natural + */ + // sort?: boolean + // // If the from path of separate animation is doing combine animation. + // // And the paths number is not same with toPathList. We need to do enter/leave animation + // // on the missing/spare paths. + // enter?: (el: Path) => void + // leave?: (el: Path) => void } /** - * @return Never be null/undefined, may empty []. + * Make separate morphing from one path to many paths. + * Make the MorphingKind of `toPath` become `'ONE_ONE'`. */ -function splitShape( - path: Path, - separateCount: number -): Path[] { - const resultPaths: Path[] = []; - if (separateCount <= 0) { - return resultPaths; - } - if (separateCount === 1) { - return duplicateShape(path, separateCount); - } +export function separateMorph( + fromPath: Path, + toPathList: Path[], + animationOpts: SeparateConfig +) { + const toLen = toPathList.length; + let fromPathList: Path[] = []; - if (path instanceof Rect) { - const toPathShape = path.shape; - const splitPropIdx = toPathShape.height > toPathShape.width ? 1 : 0; - const propWH = PROP_WH[splitPropIdx]; - const propXY = PROP_XY[splitPropIdx]; - const subWH = toPathShape[propWH] / separateCount; - let xyCurr = toPathShape[propXY]; - - for (let i = 0; i < separateCount; i++, xyCurr += subWH) { - const subShape = { - x: toPathShape.x, - y: toPathShape.y, - width: toPathShape.width, - height: toPathShape.height - }; - subShape[propXY] = xyCurr; - subShape[propWH] = i < separateCount - 1 - ? subWH - : toPathShape[propXY] + toPathShape[propWH] - xyCurr; - const splitted = new Rect({ shape: subShape }); - resultPaths.push(splitted); + const dividePath = animationOpts.dividePath || defaultDividePath; + + function addFromPath(fromList: Element[]) { + for (let i = 0; i < fromList.length; i++) { + const from = fromList[i]; + if (isCombineMorphing(from)) { + addFromPath((from as GroupLike).childrenRef()); + } + else if (from instanceof Path) { + fromPathList.push(from); + } } } - else if (path instanceof Sector) { - const toPathShape = path.shape; - const clockwise = toPathShape.clockwise; - const startAngle = toPathShape.startAngle; - const endAngle = toPathShape.endAngle; - const endAngleNormalized = normalizeRadian(startAngle, toPathShape.endAngle, clockwise); - const step = (endAngleNormalized - startAngle) / separateCount; - let angleCurr = startAngle; - for (let i = 0; i < separateCount; i++, angleCurr += step) { - const splitted = new Sector({ - shape: { - cx: toPathShape.cx, - cy: toPathShape.cy, - r: toPathShape.r, - r0: toPathShape.r0, - clockwise: clockwise, - startAngle: angleCurr, - endAngle: i === separateCount - 1 ? endAngle : angleCurr + step - } - }); - resultPaths.push(splitted); + // This case most happen when a combining path is called to reverse the animation + // to its original separated state. + if (isCombineMorphing(fromPath)) { + addFromPath(fromPath.childrenRef()); + + const fromLen = fromPathList.length; + if (fromLen < toLen) { + let k = 0; + for (let i = fromLen; i < toLen; i++) { + // Create a clone + fromPathList.push(clonePath(fromPathList[k++ % fromLen])); + } } + // Else simply remove if fromLen > toLen + fromPathList.length = toLen; } - // TODO: triangulate path and split. - // And should consider path is morphing. else { - return duplicateShape(path, separateCount); + fromPathList = dividePath({ path: fromPath, count: toLen }); + const fromPathTransform = fromPath.getComputedTransform(); + for (let i = 0; i < fromPathList.length; i++) { + // Force use transform of source path. + fromPathList[i].setLocalTransform(fromPathTransform); + } + if (fromPathList.length !== toLen) { + console.error('Invalid morphing: unmatched splitted path'); + return createEmptyReturn(); + } } - return resultPaths; -} + fromPathList = sortPaths(fromPathList); + toPathList = sortPaths(toPathList); -/** - * @return Never be null/undefined, may empty []. - */ -function duplicateShape( - path: Path, - separateCount: number -): Path[] { - const resultPaths: Path[] = []; - if (separateCount <= 0) { - return resultPaths; + const individualDelay = animationOpts.individualDelay; + for (let i = 0; i < toLen; i++) { + const indivdualAnimationOpts = individualDelay ? defaults({ + delay: (animationOpts.delay || 0) + individualDelay(i, toLen, fromPathList[i], toPathList[i]) + } as ElementAnimateConfig, animationOpts) : animationOpts; + morphPath(fromPathList[i], toPathList[i], indivdualAnimationOpts); } - const ctor = path.constructor; - for (let i = 0; i < separateCount; i++) { - const sub = new (ctor as any)({ - shape: clone(path.shape) - }); - resultPaths.push(sub); - } - return resultPaths; -} -/** - * If `clockwise`, normalize the `end` to the interval `[start, start + 2 * PI)` and return. - * else, normalize the `end` to the interval `(start - 2 * PI, start]` and return. - */ -function normalizeRadian(start: number, end: number, clockwise: boolean): number { - return end + PI2 * ( - Math[clockwise ? 'ceil' : 'floor']((start - end) / PI2) - ); + return { + fromIndividuals: fromPathList, + toIndividuals: toPathList, + count: toPathList.length + }; } + +export { split as defaultDividePath } \ No newline at end of file diff --git a/src/tool/path.ts b/src/tool/path.ts index fefb3dfd0..76c4d099b 100644 --- a/src/tool/path.ts +++ b/src/tool/path.ts @@ -448,13 +448,7 @@ export function mergePath(pathEls: Path[], opts: PathProps) { const len = pathEls.length; for (let i = 0; i < len; i++) { const pathEl = pathEls[i]; - if (!pathEl.path) { - pathEl.createPathProxy(); - } - if (pathEl.shapeChanged()) { - pathEl.buildPath(pathEl.path, pathEl.shape, true); - } - pathList.push(pathEl.path); + pathList.push(pathEl.getUpdatedPathProxy(true)); } const pathBundle = new Path(opts); @@ -473,4 +467,48 @@ export function mergePath(pathEls: Path[], opts: PathProps) { }; return pathBundle; -} \ No newline at end of file +} + +/** + * Clone a path. + */ +export function clonePath(sourcePath: Path, opts?: { + /** + * If bake global transform to path. + */ + bakeTransform?: boolean + /** + * Convert global transform to local. + */ + toLocal?: boolean +}) { + opts = opts || {}; + const path = new Path(); + if (sourcePath.shape) { + path.setShape(sourcePath.shape); + } + path.setStyle(sourcePath.style); + + if (opts.bakeTransform) { + transformPath(path.path, sourcePath.getComputedTransform()); + } + else { + // TODO Copy getLocalTransform, updateTransform since they can be changed. + if (opts.toLocal) { + path.setLocalTransform(sourcePath.getComputedTransform()); + } + else { + path.copyTransform(sourcePath); + } + } + + // These methods may be overridden + path.buildPath = sourcePath.buildPath; + (path as SVGPath).applyTransform = (path as SVGPath).applyTransform; + + path.z = sourcePath.z; + path.z2 = sourcePath.z2; + path.zlevel = sourcePath.zlevel; + + return path; +} diff --git a/src/tool/transformPath.ts b/src/tool/transformPath.ts index 3245e8f64..e6ff2f433 100644 --- a/src/tool/transformPath.ts +++ b/src/tool/transformPath.ts @@ -9,6 +9,10 @@ const mathSqrt = Math.sqrt; const mathAtan2 = Math.atan2; export default function transformPath(path: PathProxy, m: MatrixArray) { + if (!m) { + return; + } + let data = path.data; const len = path.len(); let cmd; diff --git a/src/zrender.ts b/src/zrender.ts index 9ca502a8c..a1d02b6e9 100644 --- a/src/zrender.ts +++ b/src/zrender.ts @@ -16,7 +16,7 @@ import {PainterBase} from './PainterBase'; import Animation from './animation/Animation'; import HandlerProxy from './dom/HandlerProxy'; import Element, {ElementEventCallback, ElementEvent} from './Element'; -import { Dictionary, ElementEventName, RenderedEvent } from './core/types'; +import { Dictionary, ElementEventName, RenderedEvent, WithThisType } from './core/types'; import { LayerConfig } from './canvas/Layer'; import { GradientObject } from './graphic/Gradient'; import { PatternObject } from './graphic/Pattern'; @@ -406,10 +406,10 @@ class ZRender { return this.handler.findHover(x, y); } - on(eventName: ElementEventName, eventHandler: ElementEventCallback, context?: Ctx): this - on(eventName: string, eventHandler: EventCallback, context?: Ctx): this + on(eventName: ElementEventName, eventHandler: ElementEventCallback, context?: Ctx): this + on(eventName: string, eventHandler: WithThisType, unknown extends Ctx ? ZRenderType : Ctx>, context?: Ctx): this // eslint-disable-next-line max-len - on(eventName: string, eventHandler: EventCallback | EventCallback, context?: Ctx): this { + on(eventName: string, eventHandler: (...args: any) => any, context?: Ctx): this { this.handler.on(eventName, eventHandler, context); return this; } @@ -420,7 +420,7 @@ class ZRender { * @param eventHandler Handler function */ // eslint-disable-next-line max-len - off(eventName?: string, eventHandler?: EventCallback | EventCallback) { + off(eventName?: string, eventHandler?: EventCallback) { this.handler.off(eventName, eventHandler); } diff --git a/test/boundingbox.html b/test/boundingbox.html index a6a7f3f30..61e7b4298 100644 --- a/test/boundingbox.html +++ b/test/boundingbox.html @@ -12,13 +12,13 @@ + + + +
+ + + \ No newline at end of file diff --git a/test/dragOrigin.html b/test/dragOrigin.html index f902b61db..ec67f0184 100644 --- a/test/dragOrigin.html +++ b/test/dragOrigin.html @@ -14,7 +14,7 @@ rotation: 2, origin: [50, 50], style: { - image: './test.png', + image: './asset/test.png', height: 100, width: 100 } diff --git a/test/morphPath.html b/test/morphPath.html index ebd1c26ee..c5238c114 100644 --- a/test/morphPath.html +++ b/test/morphPath.html @@ -100,8 +100,9 @@ let nextShape = shapes[currentShapeIdx]; zr.remove(currentShape); zr.add(nextShape); - zrender.morphPath(currentShape, nextShape, { + zrender.morph.morphPath(currentShape, nextShape, { duration: 1000, + easing: 'cubicInOut', done() { setTimeout(morphShape, 100); } diff --git a/test/pathContain.html b/test/pathContain.html index 8411b690e..78ac9d399 100644 --- a/test/pathContain.html +++ b/test/pathContain.html @@ -11,12 +11,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test/pathToBezier.html b/test/pathToBezier.html deleted file mode 100644 index 877a76964..000000000 --- a/test/pathToBezier.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - Path to Bezier - - - - - - - - - - \ No newline at end of file diff --git a/test/splitAnimation.html b/test/splitAnimation.html new file mode 100644 index 000000000..a739a4cde --- /dev/null +++ b/test/splitAnimation.html @@ -0,0 +1,185 @@ + + + + + Split Animation + + + + +
+ + + \ No newline at end of file