From 7f6e1f420e55bb6c6b112accc5dc39deef64605d Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Sun, 19 Jan 2020 13:21:33 -0800 Subject: [PATCH] Pure node rendering (#443) --- package-lock.json | 438 ++++++++++++++++++++++++++++ package.json | 2 + scripts/build-node.js | 40 +++ src/actions/actionSelectAll.ts | 4 +- src/actions/actionStyles.ts | 6 +- src/actions/actionZindex.tsx | 10 +- src/components/ExportDialog.tsx | 2 +- src/index-node.ts | 74 +++++ src/index.tsx | 4 +- src/keys.ts | 11 +- src/scene/data.ts | 69 +---- src/scene/getExportCanvasPreview.ts | 71 +++++ 12 files changed, 647 insertions(+), 84 deletions(-) create mode 100755 scripts/build-node.js create mode 100644 src/index-node.ts create mode 100644 src/scene/getExportCanvasPreview.ts diff --git a/package-lock.json b/package-lock.json index 4d4fd570..984e3217 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3607,6 +3607,12 @@ "safe-buffer": "^5.0.1" } }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -10903,6 +10909,12 @@ "semver-compare": "^1.0.0" } }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -12744,6 +12756,39 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + }, + "dependencies": { + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + } + } + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -12842,6 +12887,384 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "rewire": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rewire/-/rewire-4.0.1.tgz", + "integrity": "sha512-+7RQ/BYwTieHVXetpKhT11UbfF6v1kGhKFrtZN7UDL2PybMsSt/rpLWeEUGF5Ndsl1D5BxiCB14VDJyoX+noYw==", + "dev": true, + "requires": { + "eslint": "^4.19.1" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "eslint": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", + "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", + "dev": true, + "requires": { + "ajv": "^5.3.0", + "babel-code-frame": "^6.22.0", + "chalk": "^2.1.0", + "concat-stream": "^1.6.0", + "cross-spawn": "^5.1.0", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^3.7.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^3.5.4", + "esquery": "^1.0.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.0.1", + "ignore": "^3.3.3", + "imurmurhash": "^0.1.4", + "inquirer": "^3.0.6", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.9.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^1.0.1", + "require-uncached": "^1.0.3", + "semver": "^5.3.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "~2.0.1", + "table": "4.0.2", + "text-table": "~0.2.0" + } + }, + "eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + } + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "regexpp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", + "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "dev": true, + "requires": { + "ajv": "^5.2.3", + "ajv-keywords": "^2.1.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, "rework": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz", @@ -12922,6 +13345,21 @@ "aproba": "^1.1.1" } }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "*" + } + }, "rxjs": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", diff --git a/package.json b/package.json index 468ba5c1..c9b75b99 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "lint-staged": "9.5.0", "node-sass": "4.13.1", "prettier": "1.19.1", + "rewire": "^4.0.1", "typescript": "3.7.5" }, "eslintConfig": { @@ -47,6 +48,7 @@ "name": "excalidraw", "scripts": { "build": "react-scripts build", + "build-node": "./scripts/build-node.js", "eject": "react-scripts eject", "fix": "npm run prettier -- --write", "prettier": "prettier \"**/*.{js,css,scss,json,md,ts,tsx,html,yml}\"", diff --git a/scripts/build-node.js b/scripts/build-node.js new file mode 100755 index 00000000..3a4a4155 --- /dev/null +++ b/scripts/build-node.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +// In order to use this, you need to install Cairo on your machine. See +// instructions here: https://github.com/Automattic/node-canvas#compiling + +// In order to run: +// npm install canvas # please do not check it in +// npm run build-node +// node build/static/js/build-node.js +// open test.png + +var rewire = require("rewire"); +var defaults = rewire("react-scripts/scripts/build.js"); +var config = defaults.__get__("config"); + +// Disable multiple chunks +config.optimization.runtimeChunk = false; +config.optimization.splitChunks = { + cacheGroups: { + default: false + } +}; +// Set the filename to be deterministic +config.output.filename = "static/js/build-node.js"; +// Don't choke on node-specific requires +config.target = "node"; +// Set the node entrypoint +config.entry = "./src/index-node"; +// By default, webpack is going to replace the require of the canvas.node file +// to just a string with the path of the canvas.node file. We need to tell +// webpack to avoid rewriting that dependency. +config.externals = function(context, request, callback) { + if (/\.node$/.test(request)) { + return callback( + null, + "commonjs ../../../node_modules/canvas/build/Release/canvas.node" + ); + } + callback(); +}; diff --git a/src/actions/actionSelectAll.ts b/src/actions/actionSelectAll.ts index ddcd5e00..f6dc242e 100644 --- a/src/actions/actionSelectAll.ts +++ b/src/actions/actionSelectAll.ts @@ -1,5 +1,5 @@ import { Action } from "./types"; -import { META_KEY } from "../keys"; +import { KEYS } from "../keys"; export const actionSelectAll: Action = { name: "selectAll", @@ -9,5 +9,5 @@ export const actionSelectAll: Action = { }; }, contextItemLabel: "Select All", - keyTest: event => event[META_KEY] && event.code === "KeyA" + keyTest: event => event[KEYS.META] && event.code === "KeyA" }; diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts index f927c534..ca49e3ab 100644 --- a/src/actions/actionStyles.ts +++ b/src/actions/actionStyles.ts @@ -1,6 +1,6 @@ import { Action } from "./types"; import { isTextElement, redrawTextBoundingBox } from "../element"; -import { META_KEY } from "../keys"; +import { KEYS } from "../keys"; let copiedStyles: string = "{}"; @@ -14,7 +14,7 @@ export const actionCopyStyles: Action = { return {}; }, contextItemLabel: "Copy Styles", - keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyC", + keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyC", contextMenuOrder: 0 }; @@ -46,6 +46,6 @@ export const actionPasteStyles: Action = { }; }, contextItemLabel: "Paste Styles", - keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyV", + keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyV", contextMenuOrder: 1 }; diff --git a/src/actions/actionZindex.tsx b/src/actions/actionZindex.tsx index 352dccbf..668cb975 100644 --- a/src/actions/actionZindex.tsx +++ b/src/actions/actionZindex.tsx @@ -6,7 +6,7 @@ import { moveAllRight } from "../zindex"; import { getSelectedIndices } from "../scene"; -import { META_KEY } from "../keys"; +import { KEYS } from "../keys"; export const actionSendBackward: Action = { name: "sendBackward", @@ -19,7 +19,7 @@ export const actionSendBackward: Action = { contextItemLabel: "Send Backward", keyPriority: 40, keyTest: event => - event[META_KEY] && event.shiftKey && event.altKey && event.code === "KeyB" + event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyB" }; export const actionBringForward: Action = { @@ -33,7 +33,7 @@ export const actionBringForward: Action = { contextItemLabel: "Bring Forward", keyPriority: 40, keyTest: event => - event[META_KEY] && event.shiftKey && event.altKey && event.code === "KeyF" + event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyF" }; export const actionSendToBack: Action = { @@ -45,7 +45,7 @@ export const actionSendToBack: Action = { }; }, contextItemLabel: "Send to Back", - keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyB" + keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyB" }; export const actionBringToFront: Action = { @@ -57,5 +57,5 @@ export const actionBringToFront: Action = { }; }, contextItemLabel: "Bring to Front", - keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyF" + keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyF" }; diff --git a/src/components/ExportDialog.tsx b/src/components/ExportDialog.tsx index 022585e8..1028dda2 100644 --- a/src/components/ExportDialog.tsx +++ b/src/components/ExportDialog.tsx @@ -8,7 +8,7 @@ import { clipboard, exportFile, downloadFile } from "./icons"; import { Island } from "./Island"; import { ExcalidrawElement } from "../element/types"; import { AppState } from "../types"; -import { getExportCanvasPreview } from "../scene/data"; +import { getExportCanvasPreview } from "../scene/getExportCanvasPreview"; import { ActionsManagerInterface, UpdaterFn } from "../actions/types"; import Stack from "./Stack"; diff --git a/src/index-node.ts b/src/index-node.ts new file mode 100644 index 00000000..3a296ce8 --- /dev/null +++ b/src/index-node.ts @@ -0,0 +1,74 @@ +import { getExportCanvasPreview } from "../src/scene/getExportCanvasPreview"; + +const { registerFont, createCanvas } = require("canvas"); + +const elements = [ + { + id: "eVzaxG3YnHhqjEmD7NdYo", + type: "diamond", + x: 519, + y: 199, + width: 113, + height: 115, + strokeColor: "#000000", + backgroundColor: "transparent", + fillStyle: "hachure", + strokeWidth: 1, + roughness: 1, + opacity: 100, + isSelected: false, + seed: 749612521 + }, + { + id: "7W-iw5pEBPTU3eaCaLtFo", + type: "ellipse", + x: 552, + y: 238, + width: 49, + height: 44, + strokeColor: "#000000", + backgroundColor: "transparent", + fillStyle: "hachure", + strokeWidth: 1, + roughness: 1, + opacity: 100, + isSelected: false, + seed: 952056308 + }, + { + id: "kqKI231mvTrcsYo2DkUsR", + type: "text", + x: 557.5, + y: 317.5, + width: 43, + height: 31, + strokeColor: "#000000", + backgroundColor: "transparent", + fillStyle: "hachure", + strokeWidth: 1, + roughness: 1, + opacity: 100, + isSelected: false, + seed: 1683771448, + text: "test", + font: "20px Virgil", + baseline: 22 + } +]; + +registerFont("./public/FG_Virgil.ttf", { family: "Virgil" }); +const canvas = getExportCanvasPreview( + elements as any, + { + exportBackground: true, + viewBackgroundColor: "#ffffff", + scale: 1 + }, + createCanvas +); + +const fs = require("fs"); +const out = fs.createWriteStream("test.png"); +const stream = canvas.createPNGStream(); +stream.pipe(out); +out.on("finish", () => console.log("test.png was created.")); diff --git a/src/index.tsx b/src/index.tsx index 3fad4a24..bc56890b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -35,7 +35,7 @@ import { AppState } from "./types"; import { ExcalidrawElement, ExcalidrawTextElement } from "./element/types"; import { isInputLike, measureText, debounce, capitalizeString } from "./utils"; -import { KEYS, META_KEY, isArrowKey } from "./keys"; +import { KEYS, isArrowKey } from "./keys"; import { findShapeByKey, shapesShortcutKeys, SHAPES } from "./shapes"; import { createHistory } from "./history"; @@ -303,7 +303,7 @@ export class App extends React.Component<{}, AppState> { this.state.elementType !== "selection") ) { this.setState({ elementType: findShapeByKey(event.key) }); - } else if (event[META_KEY] && event.code === "KeyZ") { + } else if (event[KEYS.META] && event.code === "KeyZ") { if (event.shiftKey) { // Redo action const data = history.redoOnce(); diff --git a/src/keys.ts b/src/keys.ts index 8147b56b..665488ba 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -6,13 +6,14 @@ export const KEYS = { ENTER: "Enter", ESCAPE: "Escape", DELETE: "Delete", - BACKSPACE: "Backspace" + BACKSPACE: "Backspace", + get META() { + return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform) + ? "metaKey" + : "ctrlKey"; + } }; -export const META_KEY = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform) - ? "metaKey" - : "ctrlKey"; - export function isArrowKey(keyCode: string) { return ( keyCode === KEYS.ARROW_LEFT || diff --git a/src/scene/data.ts b/src/scene/data.ts index fe1602c4..697ad957 100644 --- a/src/scene/data.ts +++ b/src/scene/data.ts @@ -1,13 +1,10 @@ -import rough from "roughjs/bin/rough"; - import { ExcalidrawElement } from "../element/types"; -import { getElementAbsoluteCoords } from "../element"; import { getDefaultAppState } from "../appState"; -import { renderScene } from "../renderer"; import { AppState } from "../types"; import { ExportType } from "./types"; +import { getExportCanvasPreview } from "./getExportCanvasPreview"; import nanoid from "nanoid"; const LOCAL_STORAGE_KEY = "excalidraw"; @@ -169,66 +166,6 @@ export async function loadFromJSON() { } } -export function getExportCanvasPreview( - elements: readonly ExcalidrawElement[], - { - exportBackground, - exportPadding = 10, - viewBackgroundColor, - scale = 1 - }: { - exportBackground: boolean; - exportPadding?: number; - scale?: number; - viewBackgroundColor: string; - } -) { - // calculate smallest area to fit the contents in - let subCanvasX1 = Infinity; - let subCanvasX2 = 0; - let subCanvasY1 = Infinity; - let subCanvasY2 = 0; - - elements.forEach(element => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); - subCanvasX1 = Math.min(subCanvasX1, x1); - subCanvasY1 = Math.min(subCanvasY1, y1); - subCanvasX2 = Math.max(subCanvasX2, x2); - subCanvasY2 = Math.max(subCanvasY2, y2); - }); - - function distance(x: number, y: number) { - return Math.abs(x > y ? x - y : y - x); - } - - const tempCanvas = document.createElement("canvas"); - const width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2; - const height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2; - tempCanvas.style.width = width + "px"; - tempCanvas.style.height = height + "px"; - tempCanvas.width = width * scale; - tempCanvas.height = height * scale; - tempCanvas.getContext("2d")?.scale(scale, scale); - - renderScene( - elements, - rough.canvas(tempCanvas), - tempCanvas, - { - viewBackgroundColor: exportBackground ? viewBackgroundColor : null, - scrollX: 0, - scrollY: 0 - }, - { - offsetX: -subCanvasX1 + exportPadding, - offsetY: -subCanvasY1 + exportPadding, - renderScrollbars: false, - renderSelection: false - } - ); - return tempCanvas; -} - export async function exportCanvas( type: ExportType, elements: readonly ExcalidrawElement[], @@ -262,7 +199,7 @@ export async function exportCanvas( if (type === "png") { const fileName = `${name}.png`; if ("chooseFileSystemEntries" in window) { - tempCanvas.toBlob(async blob => { + tempCanvas.toBlob(async (blob: any) => { if (blob) { await saveFileNative(fileName, blob); } @@ -272,7 +209,7 @@ export async function exportCanvas( } } else if (type === "clipboard") { try { - tempCanvas.toBlob(async function(blob) { + tempCanvas.toBlob(async function(blob: any) { try { await navigator.clipboard.write([ new window.ClipboardItem({ "image/png": blob }) diff --git a/src/scene/getExportCanvasPreview.ts b/src/scene/getExportCanvasPreview.ts new file mode 100644 index 00000000..75b66f03 --- /dev/null +++ b/src/scene/getExportCanvasPreview.ts @@ -0,0 +1,71 @@ +import rough from "roughjs/bin/rough"; +import { ExcalidrawElement } from "../element/types"; +import { getElementAbsoluteCoords } from "../element/bounds"; +import { renderScene } from "../renderer/renderScene"; + +export function getExportCanvasPreview( + elements: readonly ExcalidrawElement[], + { + exportBackground, + exportPadding = 10, + viewBackgroundColor, + scale = 1 + }: { + exportBackground: boolean; + exportPadding?: number; + scale?: number; + viewBackgroundColor: string; + }, + createCanvas: (width: number, height: number) => any = function( + width, + height + ) { + const tempCanvas = document.createElement("canvas"); + tempCanvas.style.width = width + "px"; + tempCanvas.style.height = height + "px"; + tempCanvas.width = width * scale; + tempCanvas.height = height * scale; + return tempCanvas; + } +) { + // calculate smallest area to fit the contents in + let subCanvasX1 = Infinity; + let subCanvasX2 = 0; + let subCanvasY1 = Infinity; + let subCanvasY2 = 0; + + elements.forEach(element => { + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + subCanvasX1 = Math.min(subCanvasX1, x1); + subCanvasY1 = Math.min(subCanvasY1, y1); + subCanvasX2 = Math.max(subCanvasX2, x2); + subCanvasY2 = Math.max(subCanvasY2, y2); + }); + + function distance(x: number, y: number) { + return Math.abs(x > y ? x - y : y - x); + } + + const width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2; + const height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2; + const tempCanvas: any = createCanvas(width, height); + tempCanvas.getContext("2d")?.scale(scale, scale); + + renderScene( + elements, + rough.canvas(tempCanvas), + tempCanvas, + { + viewBackgroundColor: exportBackground ? viewBackgroundColor : null, + scrollX: 0, + scrollY: 0 + }, + { + offsetX: -subCanvasX1 + exportPadding, + offsetY: -subCanvasY1 + exportPadding, + renderScrollbars: false, + renderSelection: false + } + ); + return tempCanvas; +}